Cleanup combat syntax, add flake8 config for legacy compat
This commit is contained in:
parent
049e4fbb35
commit
e88e6d1b1b
5 changed files with 209 additions and 252 deletions
2
.flake8
Normal file
2
.flake8
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 100
|
||||||
|
|
@ -12,24 +12,12 @@ This establishes the basic building blocks for combat:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
from evennia import create_script
|
||||||
from collections import defaultdict, deque
|
|
||||||
|
|
||||||
from evennia import CmdSet, Command, create_script, default_cmds
|
|
||||||
from evennia.commands.command import InterruptCommand
|
|
||||||
from evennia.scripts.scripts import DefaultScript
|
from evennia.scripts.scripts import DefaultScript
|
||||||
from evennia.typeclasses.attributes import AttributeProperty
|
from evennia.typeclasses.attributes import AttributeProperty
|
||||||
from evennia.utils import dbserialize, delay, evmenu, evtable, logger
|
from evennia.utils import evtable
|
||||||
from evennia.utils.utils import display_len, inherits_from, list_to_string, pad
|
|
||||||
|
|
||||||
from . import rules
|
from . import rules
|
||||||
from .characters import EvAdventureCharacter
|
|
||||||
from .enums import ABILITY_REVERSE_MAP, Ability, ObjType
|
|
||||||
from .npcs import EvAdventureNPC
|
|
||||||
from .objects import EvAdventureObject
|
|
||||||
|
|
||||||
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
|
||||||
COMBAT_HANDLER_INTERVAL = 30
|
|
||||||
|
|
||||||
|
|
||||||
class CombatFailure(RuntimeError):
|
class CombatFailure(RuntimeError):
|
||||||
|
|
@ -39,7 +27,7 @@ class CombatFailure(RuntimeError):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Combat action classes
|
# Combaw action classes
|
||||||
|
|
||||||
|
|
||||||
class CombatAction:
|
class CombatAction:
|
||||||
|
|
@ -149,9 +137,9 @@ class CombatActionAttack(CombatAction):
|
||||||
class CombatActionStunt(CombatAction):
|
class CombatActionStunt(CombatAction):
|
||||||
"""
|
"""
|
||||||
Perform a stunt the grants a beneficiary (can be self) advantage on their next action against a
|
Perform a stunt the grants a beneficiary (can be self) advantage on their next action against a
|
||||||
target. Whenever performing a stunt that would affect another negatively (giving them disadvantage
|
target. Whenever performing a stunt that would affect another negatively (giving them
|
||||||
against an ally, or granting an advantage against them, we need to make a check first. We don't
|
disadvantage against an ally, or granting an advantage against them, we need to make a check
|
||||||
do a check if giving an advantage to an ally or ourselves.
|
first. We don't do a check if giving an advantage to an ally or ourselves.
|
||||||
|
|
||||||
action_dict = {
|
action_dict = {
|
||||||
"key": "stunt",
|
"key": "stunt",
|
||||||
|
|
@ -159,8 +147,8 @@ class CombatActionStunt(CombatAction):
|
||||||
"target": Character/NPC,
|
"target": Character/NPC,
|
||||||
"advantage": bool, # if False, it's a disadvantage
|
"advantage": bool, # if False, it's a disadvantage
|
||||||
"stunt_type": Ability, # what ability (like STR, DEX etc) to use to perform this stunt.
|
"stunt_type": Ability, # what ability (like STR, DEX etc) to use to perform this stunt.
|
||||||
"defense_type": Ability, # what ability to use to defend against (negative) effects of this
|
"defense_type": Ability, # what ability to use to defend against (negative) effects of
|
||||||
stunt.
|
this stunt.
|
||||||
}
|
}
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
|
|
@ -298,7 +286,7 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
fallback_action_dict = AttributeProperty({"key": "hold"}, autocreate=False)
|
fallback_action_dict = AttributeProperty({"key": "hold"}, autocreate=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_or_create_combathandler(cls, obj, combathandler_key="combathandler", **kwargs):
|
def get_or_create_combathandler(cls, obj, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get or create a combathandler on `obj`.
|
Get or create a combathandler on `obj`.
|
||||||
|
|
||||||
|
|
@ -306,23 +294,29 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
obj (any): The Typeclassed entity to store the CombatHandler Script on. This could be
|
obj (any): The Typeclassed entity to store the CombatHandler Script on. This could be
|
||||||
a location (for turn-based combat) or a Character (for twitch-based combat).
|
a location (for turn-based combat) or a Character (for twitch-based combat).
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
combathandler_key (str): They key name for the script. Will be 'combathandler' by default.
|
combathandler_key (str): They key name for the script. Will be 'combathandler' by
|
||||||
|
default.
|
||||||
**kwargs: Arguments to the Script, if it is created.
|
**kwargs: Arguments to the Script, if it is created.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not obj:
|
if not obj:
|
||||||
raise CombatFailure("Cannot start combat without a place to do it!")
|
raise CombatFailure("Cannot start combat without a place to do it!")
|
||||||
|
|
||||||
|
combathandler_key = kwargs.pop("key", "combathandler")
|
||||||
combathandler = obj.ndb.combathandler
|
combathandler = obj.ndb.combathandler
|
||||||
if not combathandler:
|
if not combathandler:
|
||||||
combathandler = obj.scripts.get(combathandler_name).first()
|
combathandler = obj.scripts.get(combathandler_key).first()
|
||||||
if not combathandler:
|
if not combathandler:
|
||||||
# have to create from scratch
|
# have to create from scratch
|
||||||
persistent = kwargs.pop("persistent", True)
|
persistent = kwargs.pop("persistent", True)
|
||||||
combathandler = create_script(
|
combathandler = create_script(
|
||||||
cls, key=combathandler_key, obj=obj, persistent=persistent, **kwargs
|
cls,
|
||||||
|
key=combathandler_key,
|
||||||
|
obj=obj,
|
||||||
|
persistent=persistent,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
self.caller.ndb.combathandler = combathandler
|
obj.ndb.combathandler = combathandler
|
||||||
return combathandler
|
return combathandler
|
||||||
|
|
||||||
def msg(self, message, combatant=None, broadcast=True):
|
def msg(self, message, combatant=None, broadcast=True):
|
||||||
|
|
@ -438,7 +432,7 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
this with group mechanics).
|
this with group mechanics).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def give_advantage(self, recipient, target):
|
def give_advantage(self, recipient, target):
|
||||||
"""
|
"""
|
||||||
|
|
@ -452,7 +446,7 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
some future boost)
|
some future boost)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def give_disadvantage(self, recipient, target):
|
def give_disadvantage(self, recipient, target):
|
||||||
"""
|
"""
|
||||||
|
|
@ -460,10 +454,11 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
recipient (Character or NPC): The one to get the disadvantage.
|
recipient (Character or NPC): The one to get the disadvantage.
|
||||||
target (Character or NPC): The one against which the target gains disadvantage, usually an enemy.
|
target (Character or NPC): The one against which the target gains disadvantage, usually
|
||||||
|
an enemy.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def has_advantage(self, combatant, target):
|
def has_advantage(self, combatant, target):
|
||||||
"""
|
"""
|
||||||
|
|
@ -474,7 +469,7 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
target (Character or NPC): The target to check advantage against.
|
target (Character or NPC): The target to check advantage against.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def has_disadvantage(self, combatant, target):
|
def has_disadvantage(self, combatant, target):
|
||||||
"""
|
"""
|
||||||
|
|
@ -485,7 +480,7 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
target (Character or NPC): The target to check disadvantage against.
|
target (Character or NPC): The target to check disadvantage against.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def queue_action(self, combatant, action_dict):
|
def queue_action(self, combatant, action_dict):
|
||||||
"""
|
"""
|
||||||
|
|
@ -503,7 +498,7 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
to make room.
|
to make room.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def execute_next_action(self, combatant):
|
def execute_next_action(self, combatant):
|
||||||
"""
|
"""
|
||||||
|
|
@ -514,14 +509,14 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def start_combat(self):
|
def start_combat(self):
|
||||||
"""
|
"""
|
||||||
Start combat.
|
Start combat.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def check_stop_combat(self):
|
def check_stop_combat(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -537,10 +532,10 @@ class EvAdventureCombatHandlerBase(DefaultScript):
|
||||||
bool: If `True`, the `stop_combat` method sho
|
bool: If `True`, the `stop_combat` method sho
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def stop_combat(self):
|
def stop_combat(self):
|
||||||
"""
|
"""
|
||||||
Stop combat. This should also do all cleanup.
|
Stop combat. This should also do all cleanup.
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplementedError
|
||||||
|
|
|
||||||
|
|
@ -3,98 +3,35 @@ EvAdventure Turn-based combat
|
||||||
|
|
||||||
This implements a turn-based (Final Fantasy, etc) style of MUD combat.
|
This implements a turn-based (Final Fantasy, etc) style of MUD combat.
|
||||||
|
|
||||||
choose their next action. If they don't react before a timer runs out, the previous action
|
In this variation, all combatants are sharing the same combat handler, sitting on the current room.
|
||||||
will be repeated. This means that a 'twitch' style combat can be created using the same
|
The user will receive a menu of combat options and each combatat has a certain time time (e.g. 30s)
|
||||||
mechanism, by just speeding up each 'turn'.
|
to select their next action or do nothing. To speed up play, as soon as everyone in combat selected
|
||||||
|
their next action, the next turn runs immediately, regardless of the timeout.
|
||||||
|
|
||||||
The combat is handled with a `Script` shared between all combatants; this tracks the state
|
With this example, all chosen combat actions are considered to happen at the same time (so you are
|
||||||
of combat and handles all timing elements.
|
able to kill and be killed in the same turn).
|
||||||
|
|
||||||
Unlike in base _Knave_, the MUD version's combat is simultaneous; everyone plans and executes
|
Unlike in twitch-like combat, there is no movement while in turn-based combat. Fleeing is a select
|
||||||
their turns simultaneously with minimum downtime.
|
action that takes several vulnerable turns to complete.
|
||||||
|
|
||||||
This version is simplified to not worry about things like optimal range etc. So a bow can be used
|
|
||||||
the same as a sword in battle. One could add a 1D range mechanism to add more strategy by requiring
|
|
||||||
optimizal positioning.
|
|
||||||
|
|
||||||
The combat is controlled through a menu:
|
|
||||||
|
|
||||||
------------------- main menu
|
|
||||||
Combat
|
|
||||||
|
|
||||||
You have 30 seconds to choose your next action. If you don't decide, you will hesitate and do
|
|
||||||
nothing. Available actions:
|
|
||||||
|
|
||||||
1. [A]ttack/[C]ast spell at <target> using your equipped weapon/spell
|
|
||||||
3. Make [S]tunt <target/yourself> (gain/give advantage/disadvantage for future attacks)
|
|
||||||
4. S[W]ap weapon / spell rune
|
|
||||||
5. [U]se <item>
|
|
||||||
6. [F]lee/disengage (takes one turn, during which attacks have advantage against you)
|
|
||||||
8. [H]esitate/Do nothing
|
|
||||||
|
|
||||||
You can also use say/emote between rounds.
|
|
||||||
As soon as all combatants have made their choice (or time out), the round will be resolved
|
|
||||||
simultaneusly.
|
|
||||||
|
|
||||||
-------------------- attack/cast spell submenu
|
|
||||||
|
|
||||||
Choose the target of your attack/spell:
|
|
||||||
0: Yourself 3: <enemy 3> (wounded)
|
|
||||||
1: <enemy 1> (hurt)
|
|
||||||
2: <enemy 2> (unharmed)
|
|
||||||
|
|
||||||
------------------- make stunt submenu
|
|
||||||
|
|
||||||
Stunts are special actions that don't cause damage but grant advantage for you or
|
|
||||||
an ally for future attacks - or grant disadvantage to your enemy's future attacks.
|
|
||||||
The effects of stunts start to apply *next* round. The effect does not stack, can only
|
|
||||||
be used once and must be taken advantage of within 5 rounds.
|
|
||||||
|
|
||||||
Choose stunt:
|
|
||||||
1: Trip <target> (give disadvantage DEX)
|
|
||||||
2: Feint <target> (get advantage DEX against target)
|
|
||||||
3: ...
|
|
||||||
|
|
||||||
-------------------- make stunt target submenu
|
|
||||||
|
|
||||||
Choose the target of your stunt:
|
|
||||||
0: Yourself 3: <combatant 3> (wounded)
|
|
||||||
1: <combatant 1> (hurt)
|
|
||||||
2: <combatant 2> (unharmed)
|
|
||||||
|
|
||||||
------------------- swap weapon or spell run
|
|
||||||
|
|
||||||
Choose the item to wield.
|
|
||||||
1: <item1>
|
|
||||||
2: <item2> (two hands)
|
|
||||||
3: <item3>
|
|
||||||
4: ...
|
|
||||||
|
|
||||||
------------------- use item
|
|
||||||
|
|
||||||
Choose item to use.
|
|
||||||
1: Healing potion (+1d6 HP)
|
|
||||||
2: Magic pebble (gain advantage, 1 use)
|
|
||||||
3: Potion of glue (give disadvantage to target)
|
|
||||||
|
|
||||||
------------------- Hesitate/Do nothing
|
|
||||||
|
|
||||||
You hang back, passively defending.
|
|
||||||
|
|
||||||
------------------- Disengage
|
|
||||||
|
|
||||||
You retreat, getting ready to get out of combat. Use two times in a row to
|
|
||||||
leave combat. You flee last in a round.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import random
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from evennia import AttributeProperty, CmdSet, Command, EvMenu
|
||||||
|
from evennia.utils import inherits_from, list_to_string
|
||||||
|
|
||||||
|
from .characters import EvAdventureCharacter
|
||||||
from .combat_base import (
|
from .combat_base import (
|
||||||
CombatAction,
|
CombatAction,
|
||||||
|
CombatActionAttack,
|
||||||
CombatActionHold,
|
CombatActionHold,
|
||||||
CombatActionStunt,
|
CombatActionStunt,
|
||||||
CombatActionUserItem,
|
CombatActionUseItem,
|
||||||
CombatActionWield,
|
CombatActionWield,
|
||||||
EvAdventureCombatHandler,
|
EvAdventureCombatHandlerBase,
|
||||||
)
|
)
|
||||||
from .enums import Ability
|
from .enums import Ability
|
||||||
|
|
||||||
|
|
@ -133,7 +70,7 @@ class CombatActionFlee(CombatAction):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureTurnbasedCombatHandler(EvAdventureCombatHandler):
|
class EvAdventureTurnbasedCombatHandler(EvAdventureCombatHandlerBase):
|
||||||
"""
|
"""
|
||||||
A version of the combathandler, handling turn-based combat.
|
A version of the combathandler, handling turn-based combat.
|
||||||
|
|
||||||
|
|
@ -175,31 +112,32 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatHandler):
|
||||||
# usable script properties
|
# usable script properties
|
||||||
# .is_active - show if timer is running
|
# .is_active - show if timer is running
|
||||||
|
|
||||||
def give_advantage(self, recipient, target):
|
def give_advantage(self, combatant, target):
|
||||||
"""
|
"""
|
||||||
Let a benefiter gain advantage against the target.
|
Let a benefiter gain advantage against the target.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
recipient (Character or NPC): The one to gain the advantage. This may or may not
|
combatant (Character or NPC): The one to gain the advantage. This may or may not
|
||||||
be the same entity that creates the advantage in the first place.
|
be the same entity that creates the advantage in the first place.
|
||||||
target (Character or NPC): The one against which the target gains advantage. This
|
target (Character or NPC): The one against which the target gains advantage. This
|
||||||
could (in principle) be the same as the benefiter (e.g. gaining advantage on
|
could (in principle) be the same as the benefiter (e.g. gaining advantage on
|
||||||
some future boost)
|
some future boost)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.advantage_matrix[recipient][target] = True
|
self.advantage_matrix[combatant][target] = True
|
||||||
|
|
||||||
def give_disadvantage(self, recipient, target, **kwargs):
|
def give_disadvantage(self, combatant, target, **kwargs):
|
||||||
"""
|
"""
|
||||||
Let an affected party gain disadvantage against a target.
|
Let an affected party gain disadvantage against a target.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
recipient (Character or NPC): The one to get the disadvantage.
|
recipient (Character or NPC): The one to get the disadvantage.
|
||||||
target (Character or NPC): The one against which the target gains disadvantage, usually an enemy.
|
target (Character or NPC): The one against which the target gains disadvantage, usually
|
||||||
|
an enemy.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.disadvantage_matrix[recipient][target] = True
|
self.disadvantage_matrix[combatant][target] = True
|
||||||
self.combathandler.advantage_matrix[recipient][target] = False
|
self.combathandler.advantage_matrix[combatant][target] = False
|
||||||
|
|
||||||
def has_advantage(self, combatant, target, **kwargs):
|
def has_advantage(self, combatant, target, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -210,7 +148,7 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatHandler):
|
||||||
target (Character or NPC): The target to check advantage against.
|
target (Character or NPC): The target to check advantage against.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return bool(self.combathandler.advantage_matrix[recipient].pop(target, False)) or (
|
return bool(self.combathandler.advantage_matrix[combatant].pop(target, False)) or (
|
||||||
target in self.combathandler.fleeing_combatants
|
target in self.combathandler.fleeing_combatants
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -223,11 +161,9 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatHandler):
|
||||||
target (Character or NPC): The target to check disadvantage against.
|
target (Character or NPC): The target to check disadvantage against.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
return bool(self.combathandler.disadvantage_matrix[combatant].pop(target, False)) or (
|
||||||
def has_disadvantage(self, recipient, target):
|
combatant in self.combathandler.fleeing_combatants
|
||||||
return bool(self.combathandler.disadvantage_matrix[recipient].pop(target, False)) or (
|
)
|
||||||
recipient in self.combathandler.fleeing_combatants
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_combatant(self, combatant):
|
def add_combatant(self, combatant):
|
||||||
"""
|
"""
|
||||||
|
|
@ -370,7 +306,7 @@ class EvAdventureTurnbasedCombatHandler(EvAdventureCombatHandler):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# this gets the next dict and rotates the queue
|
# this gets the next dict and rotates the queue
|
||||||
action_dict = self.combatants.get(combatants, self.fallback_action_dict)
|
action_dict = self.combatants.get(combatant, self.fallback_action_dict)
|
||||||
|
|
||||||
# use the action-dict to select and create an action from an action class
|
# use the action-dict to select and create an action from an action class
|
||||||
action_class = self.action_classes[action_dict["key"]]
|
action_class = self.action_classes[action_dict["key"]]
|
||||||
|
|
@ -763,18 +699,7 @@ def node_combat(caller, raw_string, **kwargs):
|
||||||
# Add this command to the Character cmdset to make turn-based combat available.
|
# Add this command to the Character cmdset to make turn-based combat available.
|
||||||
|
|
||||||
|
|
||||||
class _CmdTurnCombatBase(_CmdCombatBase):
|
class CmdTurnAttack(Command):
|
||||||
"""
|
|
||||||
Override parent class to slow down the tick for more clearly turn-based play.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
combathandler_name = "combathandler"
|
|
||||||
combat_tick = 30
|
|
||||||
flee_timeout = 2
|
|
||||||
|
|
||||||
|
|
||||||
class CmdTurnAttack(_CmdTurnCombatBase):
|
|
||||||
"""
|
"""
|
||||||
Start or join combat.
|
Start or join combat.
|
||||||
|
|
||||||
|
|
@ -800,7 +725,7 @@ class CmdTurnAttack(_CmdTurnCombatBase):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not hasattr(target, "hp"):
|
if not hasattr(target, "hp"):
|
||||||
self.msg(f"You can't attack that.")
|
self.msg("You can't attack that.")
|
||||||
return
|
return
|
||||||
elif target.hp <= 0:
|
elif target.hp <= 0:
|
||||||
self.msg(f"{target.get_display_name(self.caller)} is already down.")
|
self.msg(f"{target.get_display_name(self.caller)} is already down.")
|
||||||
|
|
@ -810,14 +735,18 @@ class CmdTurnAttack(_CmdTurnCombatBase):
|
||||||
self.msg("PvP combat is not allowed here!")
|
self.msg("PvP combat is not allowed here!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
combathandler = EvAdventureTurnbasedCombatHandler.get_or_create_combathandler(
|
||||||
|
self.caller.location
|
||||||
|
)
|
||||||
|
|
||||||
# add combatants to combathandler. this can be done safely over and over
|
# add combatants to combathandler. this can be done safely over and over
|
||||||
self.combathandler.add_combatant(self.caller)
|
combathandler.add_combatant(self.caller)
|
||||||
self.combathandler.queue_action(self.caller, {"key": "attack", "target": target})
|
combathandler.queue_action(self.caller, {"key": "attack", "target": target})
|
||||||
self.combathandler.add_combatant(target)
|
combathandler.add_combatant(target)
|
||||||
self.combathandler.start_combat()
|
combathandler.start_combat()
|
||||||
|
|
||||||
# build and start the menu
|
# build and start the menu
|
||||||
evmenu.EvMenu(
|
EvMenu(
|
||||||
self.caller,
|
self.caller,
|
||||||
{
|
{
|
||||||
"node_choose_enemy_target": node_choose_enemy_target,
|
"node_choose_enemy_target": node_choose_enemy_target,
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,20 @@ EvAdventure Twitch-based combat
|
||||||
This implements a 'twitch' (aka DIKU or other traditional muds) style of MUD combat.
|
This implements a 'twitch' (aka DIKU or other traditional muds) style of MUD combat.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from evennia import AttributeProperty
|
from evennia import AttributeProperty, CmdSet, default_cmds
|
||||||
from evennia.commands.command import Command, InterruptCommand
|
from evennia.commands.command import Command, InterruptCommand
|
||||||
from evennia.scripts.scripts import DefaultScript
|
from evennia.utils.utils import display_len, inherits_from, list_to_string, pad, repeat, unrepeat
|
||||||
from evennia.utils.create import create_script
|
|
||||||
from evennia.utils.utils import repeat, unrepeat
|
|
||||||
|
|
||||||
from .combat import (
|
from .characters import EvAdventureCharacter
|
||||||
|
from .combat_base import (
|
||||||
CombatActionAttack,
|
CombatActionAttack,
|
||||||
CombatActionHold,
|
CombatActionHold,
|
||||||
CombatActionStunt,
|
CombatActionStunt,
|
||||||
CombatActionUserItem,
|
CombatActionUseItem,
|
||||||
CombatActionWield,
|
CombatActionWield,
|
||||||
EvAdventureCombatHandlerBase,
|
EvAdventureCombatHandlerBase,
|
||||||
)
|
)
|
||||||
from .enums import ABILITY_REVERSE_MAP, Ability, ObjType
|
from .enums import ABILITY_REVERSE_MAP
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
|
class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
|
||||||
|
|
@ -44,7 +43,7 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
|
||||||
disadvantages_against = AttributeProperty(dict)
|
disadvantages_against = AttributeProperty(dict)
|
||||||
|
|
||||||
action_dict = AttributeProperty(dict)
|
action_dict = AttributeProperty(dict)
|
||||||
fallback_action_dict = AttributePropety({"key": "hold", "dt": 0})
|
fallback_action_dict = AttributeProperty({"key": "hold", "dt": 0})
|
||||||
|
|
||||||
# stores the current ticker reference, so we can manipulate it later
|
# stores the current ticker reference, so we can manipulate it later
|
||||||
current_ticker_ref = AttributeProperty(None)
|
current_ticker_ref = AttributeProperty(None)
|
||||||
|
|
@ -107,7 +106,8 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
recipient (Character or NPC): The one to get the disadvantage.
|
recipient (Character or NPC): The one to get the disadvantage.
|
||||||
target (Character or NPC): The one against which the target gains disadvantage, usually an enemy.
|
target (Character or NPC): The one against which the target gains disadvantage, usually
|
||||||
|
an enemy.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.disadvantages_against[target] = True
|
self.disadvantages_against[target] = True
|
||||||
|
|
@ -166,6 +166,7 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
|
||||||
"""
|
"""
|
||||||
Triggered after a delay by the command
|
Triggered after a delay by the command
|
||||||
"""
|
"""
|
||||||
|
combatant = self.obj
|
||||||
action_dict = self.action_dict
|
action_dict = self.action_dict
|
||||||
action_class = self.action_classes[action_dict["key"]]
|
action_class = self.action_classes[action_dict["key"]]
|
||||||
action = action_class(self, combatant, action_dict)
|
action = action_class(self, combatant, action_dict)
|
||||||
|
|
@ -176,8 +177,8 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
|
||||||
|
|
||||||
if not action_dict.get("repeat", True):
|
if not action_dict.get("repeat", True):
|
||||||
# not a repeating action, use the fallback (normally the original attack)
|
# not a repeating action, use the fallback (normally the original attack)
|
||||||
self.action_dict = fallback_action_dict
|
self.action_dict = self.fallback_action_dict
|
||||||
self.queue_action(fallback_action_dict.get("dt", 0))
|
self.queue_action(self.fallback_action_dict.get("dt", 0))
|
||||||
|
|
||||||
def check_stop_combat(self):
|
def check_stop_combat(self):
|
||||||
# check if one side won the battle.
|
# check if one side won the battle.
|
||||||
|
|
@ -243,7 +244,7 @@ class _BaseTwitchCombatCommand(Command):
|
||||||
Get or create the combathandler assigned to this combatant.
|
Get or create the combathandler assigned to this combatant.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return EvAdventureCombatHandlerBase.get_or_create_combathandler(self.caller)
|
return EvAdventureCombatTwitchHandler.get_or_create_combathandler(self.caller)
|
||||||
|
|
||||||
|
|
||||||
class CmdAttack(_BaseTwitchCombatCommand):
|
class CmdAttack(_BaseTwitchCombatCommand):
|
||||||
|
|
@ -261,7 +262,7 @@ class CmdAttack(_BaseTwitchCombatCommand):
|
||||||
help_category = "combat"
|
help_category = "combat"
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
target = self.search(lhs)
|
target = self.search(self.lhs)
|
||||||
if not target:
|
if not target:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -448,7 +449,7 @@ class CmdUseItem(_BaseTwitchCombatCommand):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CmdWield(_CmdCombatBase):
|
class CmdWield(_BaseTwitchCombatCommand):
|
||||||
"""
|
"""
|
||||||
Wield a weapon or spell-rune. You will the wield the item, swapping with any other item(s) you
|
Wield a weapon or spell-rune. You will the wield the item, swapping with any other item(s) you
|
||||||
were wielded before.
|
were wielded before.
|
||||||
|
|
|
||||||
|
|
@ -8,37 +8,30 @@ from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
from evennia.utils import create
|
from evennia.utils import create
|
||||||
from evennia.utils.ansi import strip_ansi
|
from evennia.utils.ansi import strip_ansi
|
||||||
from evennia.utils.test_resources import BaseEvenniaTest
|
from evennia.utils.test_resources import EvenniaCommandTestMixin, EvenniaTestCase
|
||||||
|
|
||||||
from .. import combat
|
from .. import combat_base, combat_turnbased, combat_twitch
|
||||||
from ..characters import EvAdventureCharacter
|
from ..characters import EvAdventureCharacter
|
||||||
from ..enums import Ability, WieldLocation
|
from ..enums import Ability, WieldLocation
|
||||||
from ..npcs import EvAdventureMob
|
from ..npcs import EvAdventureMob
|
||||||
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon
|
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon
|
||||||
from ..rooms import EvAdventureRoom
|
from ..rooms import EvAdventureRoom
|
||||||
from .mixins import EvAdventureMixin
|
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
class _CombatTestBase(EvenniaTestCase):
|
||||||
"""
|
"""
|
||||||
Test methods on the turn-based combat handler
|
Set up common entities for testing combat:
|
||||||
|
|
||||||
|
- `location` (key=testroom)
|
||||||
|
- `combatant` (key=testchar)
|
||||||
|
- `target` (key=testmonster)`
|
||||||
|
|
||||||
|
We also mock the `.msg` method of both `combatant` and `target` so we can
|
||||||
|
see what was sent.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
maxDiff = None
|
|
||||||
|
|
||||||
# make sure to mock away all time-keeping elements
|
|
||||||
@patch(
|
|
||||||
"evennia.contrib.tutorials.evadventure.combat.EvAdventureCombatHandler.interval",
|
|
||||||
new=-1,
|
|
||||||
)
|
|
||||||
@patch(
|
|
||||||
"evennia.contrib.tutorials.evadventure.combat.delay",
|
|
||||||
new=Mock(return_value=None),
|
|
||||||
)
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
self.location = create.create_object(EvAdventureRoom, key="testroom")
|
self.location = create.create_object(EvAdventureRoom, key="testroom")
|
||||||
self.combatant = create.create_object(
|
self.combatant = create.create_object(
|
||||||
EvAdventureCharacter, key="testchar", location=self.location
|
EvAdventureCharacter, key="testchar", location=self.location
|
||||||
|
|
@ -58,8 +51,82 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
self.combatant.msg = Mock()
|
self.combatant.msg = Mock()
|
||||||
self.target.msg = Mock()
|
self.target.msg = Mock()
|
||||||
|
|
||||||
self.combathandler = combat.get_or_create_combathandler(self.combatant)
|
|
||||||
|
class TestEvAdventureCombatHandlerBase(_CombatTestBase):
|
||||||
|
"""
|
||||||
|
Test the base functionality of the base combat handler.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""This also tests the `get_or_create_combathandler` classfunc"""
|
||||||
|
super().setUp()
|
||||||
|
self.combathandler = combat_base.EvAdventureCombatHandlerBase.get_or_create_combathandler(
|
||||||
|
self.location, key="combathandler"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_combathandler_msg(self):
|
||||||
|
"""Test sending messages to all in handler"""
|
||||||
|
|
||||||
|
self.location.msg_contents = Mock()
|
||||||
|
|
||||||
|
self.combathandler.msg("test_message")
|
||||||
|
|
||||||
|
self.location.msg_contents.assert_called_with(
|
||||||
|
"test_message",
|
||||||
|
exclude=[],
|
||||||
|
from_obj=None,
|
||||||
|
mapping={"testchar": self.combatant, "testmonster": self.target},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_combat_summary(self):
|
||||||
|
"""Test combat summary"""
|
||||||
|
|
||||||
|
self.combathandler.get_sides = Mock(return_value=(self.combatant, self.target))
|
||||||
|
|
||||||
|
# as seen from one side
|
||||||
|
result = str(self.combathandler.get_combat_summary(self.combatant))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
strip_ansi(result),
|
||||||
|
" testchar (Perfect) vs testmonster (Perfect) ",
|
||||||
|
)
|
||||||
|
|
||||||
|
# as seen from other side
|
||||||
|
result = str(self.combathandler.get_combat_summary(self.target))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
strip_ansi(result),
|
||||||
|
" testmonster (Perfect) vs testchar (Perfect) ",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureTurnbasedCombatHandlerTest(_CombatTestBase):
|
||||||
|
"""
|
||||||
|
Test methods on the turn-based combat handler and actions
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
|
# make sure to mock away all time-keeping elements
|
||||||
|
@patch(
|
||||||
|
"evennia.contrib.tutorials.evadventure.combat_turnbased.EvAdventureTurnbasedCombatHandler.interval", # noqa
|
||||||
|
new=-1,
|
||||||
|
)
|
||||||
|
@patch(
|
||||||
|
"evennia.contrib.tutorials.evadventure.combat_turnbased.delay",
|
||||||
|
new=Mock(return_value=None),
|
||||||
|
)
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
# add target to combat
|
# add target to combat
|
||||||
|
self.combathandler = (
|
||||||
|
combat_turnbased.EvAdventureTurnebasedCombatHandler.get_or_create_combathandler(
|
||||||
|
self.location, key="combathandler"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.combathandler.add_combatant(self.combatant)
|
||||||
self.combathandler.add_combatant(self.target)
|
self.combathandler.add_combatant(self.target)
|
||||||
|
|
||||||
def _get_action(self, action_dict={"key": "hold"}):
|
def _get_action(self, action_dict={"key": "hold"}):
|
||||||
|
|
@ -86,16 +153,16 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
"""Testing all is set up correctly in the combathandler"""
|
"""Testing all is set up correctly in the combathandler"""
|
||||||
|
|
||||||
chandler = self.combathandler
|
chandler = self.combathandler
|
||||||
self.assertEqual(dict(chandler.combatants), {self.combatant: deque(), self.target: deque()})
|
self.assertEqual(dict(chandler.combatants), {self.combatant: {}, self.target: {}})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
dict(chandler.action_classes),
|
dict(chandler.action_classes),
|
||||||
{
|
{
|
||||||
"hold": combat.CombatActionHold,
|
"hold": combat_turnbased.CombatActionHold,
|
||||||
"attack": combat.CombatActionAttack,
|
"attack": combat_turnbased.CombatActionAttack,
|
||||||
"stunt": combat.CombatActionStunt,
|
"stunt": combat_turnbased.CombatActionStunt,
|
||||||
"use": combat.CombatActionUseItem,
|
"use": combat_turnbased.CombatActionUseItem,
|
||||||
"wield": combat.CombatActionWield,
|
"wield": combat_turnbased.CombatActionWield,
|
||||||
"flee": combat.CombatActionFlee,
|
"flee": combat_turnbased.CombatActionFlee,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(chandler.flee_timeout, 1)
|
self.assertEqual(chandler.flee_timeout, 1)
|
||||||
|
|
@ -104,20 +171,6 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
self.assertEqual(dict(chandler.fleeing_combatants), {})
|
self.assertEqual(dict(chandler.fleeing_combatants), {})
|
||||||
self.assertEqual(dict(chandler.defeated_combatants), {})
|
self.assertEqual(dict(chandler.defeated_combatants), {})
|
||||||
|
|
||||||
def test_combathandler_msg(self):
|
|
||||||
"""Test sending messages to all in handler"""
|
|
||||||
|
|
||||||
self.location.msg_contents = Mock()
|
|
||||||
|
|
||||||
self.combathandler.msg("test_message")
|
|
||||||
|
|
||||||
self.location.msg_contents.assert_called_with(
|
|
||||||
"test_message",
|
|
||||||
exclude=[],
|
|
||||||
from_obj=None,
|
|
||||||
mapping={"testchar": self.combatant, "testmonster": self.target},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_remove_combatant(self):
|
def test_remove_combatant(self):
|
||||||
"""Remove a combatant."""
|
"""Remove a combatant."""
|
||||||
|
|
||||||
|
|
@ -154,25 +207,6 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
allies, enemies = self.combathandler.get_sides(self.target)
|
allies, enemies = self.combathandler.get_sides(self.target)
|
||||||
self.assertEqual((allies, enemies), ([target2], [self.combatant, combatant2]))
|
self.assertEqual((allies, enemies), ([target2], [self.combatant, combatant2]))
|
||||||
|
|
||||||
def test_get_combat_summary(self):
|
|
||||||
"""Test combat summary"""
|
|
||||||
|
|
||||||
# as seen from one side
|
|
||||||
result = str(self.combathandler.get_combat_summary(self.combatant))
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
strip_ansi(result),
|
|
||||||
" testchar (Perfect) vs testmonster (Perfect) ",
|
|
||||||
)
|
|
||||||
|
|
||||||
# as seen from other side
|
|
||||||
result = str(self.combathandler.get_combat_summary(self.target))
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
strip_ansi(result),
|
|
||||||
" testmonster (Perfect) vs testchar (Perfect) ",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_queue_and_execute_action(self):
|
def test_queue_and_execute_action(self):
|
||||||
"""Queue actions and execute"""
|
"""Queue actions and execute"""
|
||||||
|
|
||||||
|
|
@ -210,31 +244,6 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
[call(self.combatant), call(self.target)], any_order=True
|
[call(self.combatant), call(self.target)], any_order=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_combat_action(self):
|
|
||||||
"""General tests of action functionality"""
|
|
||||||
|
|
||||||
combatant = self.combatant
|
|
||||||
target = self.target
|
|
||||||
|
|
||||||
action = self._get_action({"key": "hold"})
|
|
||||||
|
|
||||||
self.assertTrue(action.can_use())
|
|
||||||
|
|
||||||
action.give_advantage(combatant, target)
|
|
||||||
action.give_disadvantage(combatant, target)
|
|
||||||
|
|
||||||
self.assertTrue(action.has_advantage(combatant, target))
|
|
||||||
self.assertTrue(action.has_disadvantage(combatant, target))
|
|
||||||
|
|
||||||
action.lose_advantage(combatant, target)
|
|
||||||
action.lose_disadvantage(combatant, target)
|
|
||||||
|
|
||||||
self.assertFalse(action.has_advantage(combatant, target))
|
|
||||||
self.assertFalse(action.has_disadvantage(combatant, target))
|
|
||||||
|
|
||||||
action.msg(f"$You() attack $You({target.key}).")
|
|
||||||
combatant.msg.assert_called_with(text=("You attack testmonster.", {}), from_obj=combatant)
|
|
||||||
|
|
||||||
def test_action__hold(self):
|
def test_action__hold(self):
|
||||||
"""Hold, doing nothing"""
|
"""Hold, doing nothing"""
|
||||||
|
|
||||||
|
|
@ -246,7 +255,6 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
|
|
||||||
@patch("evennia.contrib.tutorials.evadventure.combat.rules.randint")
|
@patch("evennia.contrib.tutorials.evadventure.combat.rules.randint")
|
||||||
def test_attack__miss(self, mock_randint):
|
def test_attack__miss(self, mock_randint):
|
||||||
|
|
||||||
actiondict = {"key": "attack", "target": self.target}
|
actiondict = {"key": "attack", "target": self.target}
|
||||||
|
|
||||||
mock_randint.return_value = 8 # target has default armor 11, so 8+1 str will miss
|
mock_randint.return_value = 8 # target has default armor 11, so 8+1 str will miss
|
||||||
|
|
@ -298,7 +306,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
"stunt_type": Ability.STR,
|
"stunt_type": Ability.STR,
|
||||||
"defense_type": Ability.DEX,
|
"defense_type": Ability.DEX,
|
||||||
}
|
}
|
||||||
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
|
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
|
||||||
self._run_actions(action_dict)
|
self._run_actions(action_dict)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bool(self.combathandler.advantage_matrix[self.combatant][self.target]), True
|
bool(self.combathandler.advantage_matrix[self.combatant][self.target]), True
|
||||||
|
|
@ -314,7 +322,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
"stunt_type": Ability.STR,
|
"stunt_type": Ability.STR,
|
||||||
"defense_type": Ability.DEX,
|
"defense_type": Ability.DEX,
|
||||||
}
|
}
|
||||||
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
|
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
|
||||||
self._run_actions(action_dict)
|
self._run_actions(action_dict)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bool(self.combathandler.disadvantage_matrix[self.target][self.combatant]), True
|
bool(self.combathandler.disadvantage_matrix[self.target][self.combatant]), True
|
||||||
|
|
@ -417,10 +425,32 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
|
||||||
from_obj=self.combatant,
|
from_obj=self.combatant,
|
||||||
)
|
)
|
||||||
# Check that enemies have advantage against you now
|
# Check that enemies have advantage against you now
|
||||||
action = combat.CombatAction(self.combathandler, self.target, {"key": "hold"})
|
action = combat_turnbased.CombatAction(self.combathandler, self.target, {"key": "hold"})
|
||||||
self.assertTrue(action.has_advantage(self.target, self.combatant))
|
self.assertTrue(action.has_advantage(self.target, self.combatant))
|
||||||
|
|
||||||
# second flee should remove combatant
|
# second flee should remove combatant
|
||||||
self._run_actions(action_dict)
|
self._run_actions(action_dict)
|
||||||
# this ends combat, so combathandler should be gone
|
# this ends combat, so combathandler should be gone
|
||||||
self.assertIsNone(self.combathandler.pk)
|
self.assertIsNone(self.combathandler.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvAdventureTwitchCombatHandler(EvenniaCommandTestMixin, _CombatTestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.combatant_combathandler = (
|
||||||
|
combat_twitch.EvAdventureCombatTwitchHandler.get_or_create_combathandler(
|
||||||
|
self.combatant, key="combathandler"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.target_combathandler = (
|
||||||
|
combat_twitch.EvAdventureCombatTwitchHandler.get_or_create_combathandler(
|
||||||
|
self.target, key="combathandler"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_sides(self):
|
||||||
|
""" """
|
||||||
|
|
||||||
|
def test_queue_and_execute_action(self):
|
||||||
|
""" """
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue