More tutorial combat tests
This commit is contained in:
parent
e6ac8d347e
commit
2daadca999
6 changed files with 431 additions and 124 deletions
|
|
@ -119,13 +119,17 @@ class EquipmentHandler:
|
||||||
method.
|
method.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int: Armor from equipment.
|
int: Armor from equipment. Note that this is the +bonus of Armor, not the
|
||||||
|
'defense' (to get that one adds 10).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
slots = self.slots
|
slots = self.slots
|
||||||
return sum(
|
return sum(
|
||||||
(
|
(
|
||||||
getattr(slots[WieldLocation.BODY], "armor", 0),
|
# armor is listed using its defense, so we remove 10 from it
|
||||||
|
# (11 is base no-armor value in Knave)
|
||||||
|
getattr(slots[WieldLocation.BODY], "armor", 11) - 10,
|
||||||
|
# shields and helmets are listed by their bonus to armor
|
||||||
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
|
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
|
||||||
getattr(slots[WieldLocation.HEAD], "armor", 0),
|
getattr(slots[WieldLocation.HEAD], "armor", 0),
|
||||||
)
|
)
|
||||||
|
|
@ -333,7 +337,8 @@ class EquipmentHandler:
|
||||||
list: A list of objects that are usable.
|
list: A list of objects that are usable.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return [obj for obj in slots[WieldLocation.BACKPACK] if obj.uses > 0]
|
character = self.obj
|
||||||
|
return [obj for obj in slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)]
|
||||||
|
|
||||||
|
|
||||||
class LivingMixin:
|
class LivingMixin:
|
||||||
|
|
@ -386,6 +391,21 @@ class LivingMixin:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def at_defeat(self):
|
||||||
|
"""
|
||||||
|
Called when this living thing reaches HP 0.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# by default, defeat means death
|
||||||
|
self.at_death()
|
||||||
|
|
||||||
|
def at_death(self):
|
||||||
|
"""
|
||||||
|
Called when this living thing dies.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
"""
|
"""
|
||||||
|
|
@ -417,6 +437,14 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
"""Allows to access equipment like char.equipment.worn"""
|
"""Allows to access equipment like char.equipment.worn"""
|
||||||
return EquipmentHandler(self)
|
return EquipmentHandler(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weapon(self):
|
||||||
|
return self.equipment.weapon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def armor(self):
|
||||||
|
return self.equipment.armor
|
||||||
|
|
||||||
def at_pre_object_receive(self, moved_object, source_location, **kwargs):
|
def at_pre_object_receive(self, moved_object, source_location, **kwargs):
|
||||||
"""
|
"""
|
||||||
Hook called by Evennia before moving an object here. Return False to abort move.
|
Hook called by Evennia before moving an object here. Return False to abort move.
|
||||||
|
|
@ -475,20 +503,14 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
rules.dice.roll_death(self)
|
rules.dice.roll_death(self)
|
||||||
if hp <= 0:
|
if hp <= 0:
|
||||||
# this means we rolled death on the table
|
# this means we rolled death on the table
|
||||||
self.handle_death()
|
self.at_death()
|
||||||
else:
|
else:
|
||||||
# still alive, but lost in some stats
|
# still alive, but lost in some stats
|
||||||
self.location.msg_contents(
|
self.location.msg_contents(
|
||||||
f"|y$You() $conj(stagger) back, weakened but still alive.|n", from_obj=self
|
f"|y$You() $conj(stagger) back, weakened but still alive.|n", from_obj=self
|
||||||
)
|
)
|
||||||
|
|
||||||
def defeat_message(self, attacker, dmg):
|
def at_death(self):
|
||||||
"""
|
|
||||||
Sent out to everyone in the location by the combathandler.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def handle_death(self):
|
|
||||||
"""
|
"""
|
||||||
Called when character dies.
|
Called when character dies.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -326,8 +326,7 @@ class CombatActionStunt(CombatAction):
|
||||||
"actions. The effect needs to be used up within 5 turns."
|
"actions. The effect needs to be used up within 5 turns."
|
||||||
)
|
)
|
||||||
|
|
||||||
give_advantage = True
|
give_advantage = True # if False, give_disadvantage
|
||||||
give_disadvantage = False
|
|
||||||
max_uses = 1
|
max_uses = 1
|
||||||
priority = -1
|
priority = -1
|
||||||
attack_type = Ability.DEX
|
attack_type = Ability.DEX
|
||||||
|
|
@ -353,10 +352,19 @@ class CombatActionStunt(CombatAction):
|
||||||
)
|
)
|
||||||
self.msg(f"$You() $conj(attempt) stunt on $You(defender.key). {txt}")
|
self.msg(f"$You() $conj(attempt) stunt on $You(defender.key). {txt}")
|
||||||
if is_success:
|
if is_success:
|
||||||
if advantage:
|
stunt_duration = self.combathandler.stunt_duration
|
||||||
|
if self.give_advantage:
|
||||||
self.combathandler.gain_advantage(attacker, defender)
|
self.combathandler.gain_advantage(attacker, defender)
|
||||||
|
self.msg(
|
||||||
|
f"%You() $conj(gain) advantage against $You(defender.key! "
|
||||||
|
f"You must use it within {stunt_duration} turns."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.combathandler.gain_disadvantage(defender, attacker)
|
self.combathandler.gain_disadvantage(defender, attacker)
|
||||||
|
self.msg(
|
||||||
|
f"%You(defender.key) $conj(suffer) disadvantage against $You(). "
|
||||||
|
f"Lasts next attack, or until 3 turns passed."
|
||||||
|
)
|
||||||
|
|
||||||
# only spend a use after being successful
|
# only spend a use after being successful
|
||||||
self.uses += 1
|
self.uses += 1
|
||||||
|
|
@ -386,19 +394,53 @@ class CombatActionUseItem(CombatAction):
|
||||||
help_text = "Use an item from your inventory."
|
help_text = "Use an item from your inventory."
|
||||||
|
|
||||||
def get_help(self, item, *args):
|
def get_help(self, item, *args):
|
||||||
return item.combat_get_help(*args)
|
return item.get_help(*args)
|
||||||
|
|
||||||
def can_use(self, item, *args, **kwargs):
|
|
||||||
return item.combat_can_use(self.combatant, self.combathandler, *args, **kwargs)
|
|
||||||
|
|
||||||
def pre_use(self, item, *args, **kwargs):
|
def pre_use(self, item, *args, **kwargs):
|
||||||
item.combat_pre_use(self.combatant, *args, **kwargs)
|
"""
|
||||||
|
We tie into the `item.at_pre_use` hook here, which returns False if
|
||||||
|
the item is not usable (that is, has .uses > 0).
|
||||||
|
|
||||||
|
"""
|
||||||
|
if item.at_pre_use(self.combatant, *args, **kwargs):
|
||||||
|
item.at_use(self.combatant, *args, **kwargs)
|
||||||
|
|
||||||
def use(self, item, target, *args, **kwargs):
|
def use(self, item, target, *args, **kwargs):
|
||||||
item.combat_use(self.combatant, target, *args, **kwargs)
|
item.at_use(self.combatant, target, *args, **kwargs)
|
||||||
|
|
||||||
def post_use(self, item, *args, **kwargs):
|
def post_use(self, item, *args, **kwargs):
|
||||||
item.combat_post_use(self.combatant, *args, **kwargs)
|
item.at_post_use(self.combatant, *args, **kwargs)
|
||||||
|
self.msg("$You() $conj(use) an item.")
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionSwapWieldedWeaponOrSpell(CombatAction):
|
||||||
|
"""
|
||||||
|
Swap Wielded weapon or spell.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "Swap weapon/rune/shield"
|
||||||
|
desc = "Swap currently wielded weapon, shield or spell-rune."
|
||||||
|
aliases = (
|
||||||
|
"s",
|
||||||
|
"swap",
|
||||||
|
"draw",
|
||||||
|
"swap weapon",
|
||||||
|
"draw weapon",
|
||||||
|
"swap rune",
|
||||||
|
"draw rune",
|
||||||
|
"swap spell",
|
||||||
|
"draw spell",
|
||||||
|
)
|
||||||
|
help_text = (
|
||||||
|
"Draw a new weapon or spell-rune from your inventory, replacing your current loadout"
|
||||||
|
)
|
||||||
|
|
||||||
|
next_menu_node = "node_select_wield_from_inventory"
|
||||||
|
|
||||||
|
def use(self, _, item, *args, **kwargs):
|
||||||
|
# this will make use of the item
|
||||||
|
self.combatant.equipment.use(item)
|
||||||
|
|
||||||
|
|
||||||
class CombatActionFlee(CombatAction):
|
class CombatActionFlee(CombatAction):
|
||||||
|
|
@ -451,13 +493,17 @@ class CombatActionBlock(CombatAction):
|
||||||
attack_type = Ability.DEX
|
attack_type = Ability.DEX
|
||||||
defense_type = Ability.DEX
|
defense_type = Ability.DEX
|
||||||
|
|
||||||
def use(self, combatant, fleeing_target, *args, **kwargs):
|
def use(self, fleeing_target, *args, **kwargs):
|
||||||
|
|
||||||
advantage = bool(self.advantage_matrix[combatant].pop(fleeing_target, False))
|
advantage = bool(
|
||||||
disadvantage = bool(self.disadvantage_matrix[combatant].pop(fleeing_target, False))
|
self.combathandler.advantage_matrix[self.combatant].pop(fleeing_target, False)
|
||||||
|
)
|
||||||
|
disadvantage = bool(
|
||||||
|
self.combathandler.disadvantage_matrix[self.combatant].pop(fleeing_target, False)
|
||||||
|
)
|
||||||
|
|
||||||
is_success, _, txt = rules.dice.opposed_saving_throw(
|
is_success, _, txt = rules.dice.opposed_saving_throw(
|
||||||
combatant,
|
self.combatant,
|
||||||
fleeing_target,
|
fleeing_target,
|
||||||
attack_type=self.attack_type,
|
attack_type=self.attack_type,
|
||||||
defense_type=self.defense_type,
|
defense_type=self.defense_type,
|
||||||
|
|
@ -468,60 +514,12 @@ class CombatActionBlock(CombatAction):
|
||||||
|
|
||||||
if is_success:
|
if is_success:
|
||||||
# managed to stop the target from fleeing/disengaging
|
# managed to stop the target from fleeing/disengaging
|
||||||
self.combatant.unflee(fleeing_target)
|
self.combathandler.unflee(fleeing_target)
|
||||||
self.msg("$You() blocks the retreat of $You({fleeing_target.key})")
|
self.msg("$You() blocks the retreat of $You({fleeing_target.key})")
|
||||||
else:
|
else:
|
||||||
self.msg("$You({fleeing_target.key}) dodges away from you $You()!")
|
self.msg("$You({fleeing_target.key}) dodges away from you $You()!")
|
||||||
|
|
||||||
|
|
||||||
class CombatActionSwapWieldedWeaponOrSpell(CombatAction):
|
|
||||||
"""
|
|
||||||
Swap Wielded weapon or spell.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "Swap weapon/rune/shield"
|
|
||||||
desc = "Swap currently wielded weapon, shield or spell-rune."
|
|
||||||
aliases = (
|
|
||||||
"s",
|
|
||||||
"swap",
|
|
||||||
"draw",
|
|
||||||
"swap weapon",
|
|
||||||
"draw weapon",
|
|
||||||
"swap rune",
|
|
||||||
"draw rune",
|
|
||||||
"swap spell",
|
|
||||||
"draw spell",
|
|
||||||
)
|
|
||||||
help_text = (
|
|
||||||
"Draw a new weapon or spell-rune from your inventory, replacing your current loadout"
|
|
||||||
)
|
|
||||||
|
|
||||||
next_menu_node = "node_select_wield_from_inventory"
|
|
||||||
|
|
||||||
def use(self, combatant, item, *args, **kwargs):
|
|
||||||
# this will make use of the item
|
|
||||||
combatant.inventory.use(item)
|
|
||||||
|
|
||||||
|
|
||||||
class CombatActionUseItem(CombatAction):
|
|
||||||
"""
|
|
||||||
Use an item from inventory.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "Use an item from backpack"
|
|
||||||
desc = "Use an item from your inventory."
|
|
||||||
aliases = ("u", "use", "use item")
|
|
||||||
help_text = "Choose an item from your inventory to use."
|
|
||||||
|
|
||||||
next_menu_node = "node_select_use_item_from_inventory"
|
|
||||||
|
|
||||||
def use(self, combatant, item, *args, **kwargs):
|
|
||||||
item.use(combatant, *args, **kwargs)
|
|
||||||
self.msg("$You() $conj(use) an item.")
|
|
||||||
|
|
||||||
|
|
||||||
class CombatActionDoNothing(CombatAction):
|
class CombatActionDoNothing(CombatAction):
|
||||||
"""
|
"""
|
||||||
Do nothing this turn.
|
Do nothing this turn.
|
||||||
|
|
@ -635,19 +633,6 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
combathandler=self, # makes this available as combatant.ndb._evmenu.combathandler
|
combathandler=self, # makes this available as combatant.ndb._evmenu.combathandler
|
||||||
)
|
)
|
||||||
|
|
||||||
def _reset_menu(self):
|
|
||||||
"""
|
|
||||||
Move menu to the action-selection node.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _update_turn_stats(self, combatant, message):
|
|
||||||
"""
|
|
||||||
Store combat messages to display at the end of turn.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.turn_stats[combatant].append(message)
|
|
||||||
|
|
||||||
def _warn_time(self, time_remaining):
|
def _warn_time(self, time_remaining):
|
||||||
"""
|
"""
|
||||||
Send a warning message when time is about to run out.
|
Send a warning message when time is about to run out.
|
||||||
|
|
@ -693,6 +678,9 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
f"|y__________________ turn resolution (turn {self.turn}) ____________________|n\n"
|
f"|y__________________ turn resolution (turn {self.turn}) ____________________|n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# store those in the process of fleeing
|
||||||
|
already_fleeing = self.fleeing_combatants[:]
|
||||||
|
|
||||||
# do all actions
|
# do all actions
|
||||||
for combatant in self.combatants:
|
for combatant in self.combatants:
|
||||||
# read the current action type selected by the player
|
# read the current action type selected by the player
|
||||||
|
|
@ -710,39 +698,26 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
"Please report the problem to an admin."
|
"Please report the problem to an admin."
|
||||||
)
|
)
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
|
raise
|
||||||
|
|
||||||
# handle disengaging combatants
|
# handle disengaging combatants
|
||||||
|
|
||||||
to_remove = []
|
to_remove = []
|
||||||
|
|
||||||
for combatant in self.combatants:
|
for combatant in self.combatants:
|
||||||
# check disengaging combatants (these are combatants that managed
|
# see if fleeing characters managed to do two flee actions in a row.
|
||||||
# not get their escape blocked last turn
|
if (combatant in self.fleeing_combatants) and (combatant in already_fleeing):
|
||||||
if combatant in self.fleeing_combatants:
|
|
||||||
self.fleeing_combatants.remove(combatant)
|
self.fleeing_combatants.remove(combatant)
|
||||||
|
to_remove.append(combatant)
|
||||||
|
|
||||||
if combatant.hp <= 0:
|
if combatant.hp <= 0:
|
||||||
|
# check characters that are beaten down.
|
||||||
# characters roll on the death table here, npcs usually just die
|
# characters roll on the death table here, npcs usually just die
|
||||||
combatant.at_defeat()
|
combatant.at_defeat()
|
||||||
|
if combatant.hp <= 0:
|
||||||
# tell everyone
|
# if character still < 0 after at_defeat, it means they are dead.
|
||||||
self.msg(combatant.defeat_message(attacker, dmg), combatant=combatant)
|
# force-remove from combat.
|
||||||
|
to_remove.append(combatant)
|
||||||
if defender.hp > 0:
|
|
||||||
# death roll didn't kill them - they are weakened, but with hp
|
|
||||||
self.msg(
|
|
||||||
"You are alive, but out of the fight. If you want to press your luck, "
|
|
||||||
"you need to rejoin the combat.",
|
|
||||||
combatant=combatant,
|
|
||||||
broadcast=False,
|
|
||||||
)
|
|
||||||
defender.at_defeat() # note - NPC monsters may still 'die' here
|
|
||||||
else:
|
|
||||||
# outright killed
|
|
||||||
defender.at_death()
|
|
||||||
|
|
||||||
# no matter the result, the combatant is out
|
|
||||||
to_remove.append(combatant)
|
|
||||||
|
|
||||||
for combatant in to_remove:
|
for combatant in to_remove:
|
||||||
# for clarity, we remove here rather than modifying the combatant list
|
# for clarity, we remove here rather than modifying the combatant list
|
||||||
|
|
@ -1050,7 +1025,7 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
combat = caller.ndb._evmenu.combathandler
|
combat = caller.ndb._evmenu.combathandler
|
||||||
loadout = caller.inventory.display_loadout()
|
loadout = caller.equipment.display_loadout()
|
||||||
text = (
|
text = (
|
||||||
f"{loadout}\nSelect weapon, spell or shield to draw. It will swap out "
|
f"{loadout}\nSelect weapon, spell or shield to draw. It will swap out "
|
||||||
"anything already in the same hand (you can't change armor or helmet in combat)."
|
"anything already in the same hand (you can't change armor or helmet in combat)."
|
||||||
|
|
@ -1058,7 +1033,7 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs):
|
||||||
|
|
||||||
# get a list of all suitable weapons/spells/shields
|
# get a list of all suitable weapons/spells/shields
|
||||||
options = []
|
options = []
|
||||||
for obj in caller.inventory.get_wieldable_objects_from_backpack():
|
for obj in caller.equipment.get_wieldable_objects_from_backpack():
|
||||||
if obj.quality <= 0:
|
if obj.quality <= 0:
|
||||||
# object is broken
|
# object is broken
|
||||||
options.append(
|
options.append(
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
hit_dice = AttributeProperty(default=1)
|
hit_dice = AttributeProperty(default=1)
|
||||||
armor = AttributeProperty(default=11)
|
armor = AttributeProperty(default=1) # +10 to get armor defense
|
||||||
morale = AttributeProperty(default=9)
|
morale = AttributeProperty(default=9)
|
||||||
hp = AttributeProperty(default=8)
|
hp = AttributeProperty(default=8)
|
||||||
|
|
||||||
|
|
@ -92,8 +92,15 @@ class EvAdventureQuestGiver(EvAdventureNPC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class EvadventureMob(EvAdventureNPC):
|
class EvAdventureMob(EvAdventureNPC):
|
||||||
"""
|
"""
|
||||||
Mob (mobile) NPC; this is usually an enemy.
|
Mob (mobile) NPC; this is usually an enemy.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def at_defeat(self):
|
||||||
|
"""
|
||||||
|
Mobs die right away when defeated, no death-table rolls.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.at_death()
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,54 @@ class EvAdventureObject(DefaultObject):
|
||||||
quality = AttributeProperty(1)
|
quality = AttributeProperty(1)
|
||||||
value = AttributeProperty(0)
|
value = AttributeProperty(0)
|
||||||
|
|
||||||
|
help_text = AttributeProperty("")
|
||||||
|
|
||||||
|
def get_help(self):
|
||||||
|
"""
|
||||||
|
Get help text for the item.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The help text, by default taken from the `.help_text` property.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.help_text
|
||||||
|
|
||||||
|
def at_pre_use(self, user, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Called before this item is used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Object): The one using the item.
|
||||||
|
*args, **kwargs: Optional arguments.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
bool: False to stop usage.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.uses > 0
|
||||||
|
|
||||||
|
def at_use(self, user, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Called when this item is used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Object): The one using the item.
|
||||||
|
*args, **kwargs: Optional arguments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def at_post_use(self, user, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Called after this item was used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Object): The one using the item.
|
||||||
|
*args, **kwargs: Optional arguments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.uses -= 1
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureObjectFiller(EvAdventureObject):
|
class EvAdventureObjectFiller(EvAdventureObject):
|
||||||
"""
|
"""
|
||||||
|
|
@ -59,7 +107,7 @@ class EvAdventureConsumable(EvAdventureObject):
|
||||||
size = AttributeProperty(0.25)
|
size = AttributeProperty(0.25)
|
||||||
uses = AttributeProperty(1)
|
uses = AttributeProperty(1)
|
||||||
|
|
||||||
def use(self, user, *args, **kwargs):
|
def at_use(self, user, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Consume a 'use' of this item. Once it reaches 0 uses, it should normally
|
Consume a 'use' of this item. Once it reaches 0 uses, it should normally
|
||||||
not be usable anymore and probably be deleted.
|
not be usable anymore and probably be deleted.
|
||||||
|
|
@ -71,6 +119,20 @@ class EvAdventureConsumable(EvAdventureObject):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def at_post_use(self, user, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Called after this item was used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Object): The one using the item.
|
||||||
|
*args, **kwargs: Optional arguments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.uses -= 1
|
||||||
|
if self.uses <= 0:
|
||||||
|
user.msg(f"{self.key} was used up.")
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureWeapon(EvAdventureObject):
|
class EvAdventureWeapon(EvAdventureObject):
|
||||||
"""
|
"""
|
||||||
|
|
@ -98,6 +160,9 @@ class WeaponEmptyHand:
|
||||||
damage_roll = "1d4"
|
damage_roll = "1d4"
|
||||||
quality = 100000 # let's assume fists are always available ...
|
quality = 100000 # let's assume fists are always available ...
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<WeaponEmptyHand>"
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureRunestone(EvAdventureWeapon):
|
class EvAdventureRunestone(EvAdventureWeapon):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -200,8 +200,9 @@ class EvAdventureRollEngine:
|
||||||
Advantage and disadvantage cancel each other out.
|
Advantage and disadvantage cancel each other out.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# what is stored on the character/npc is the bonus; we add 10 to get the defense target
|
||||||
|
defender_defense = getattr(defender, defense_type.value, 1) + 10
|
||||||
|
|
||||||
defender_defense = getattr(defender, defense_type.value, 1)
|
|
||||||
result, quality, txt = self.saving_throw(
|
result, quality, txt = self.saving_throw(
|
||||||
attacker,
|
attacker,
|
||||||
bonus_type=attack_type,
|
bonus_type=attack_type,
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,63 @@ Test EvAdventure combat.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
from evennia.utils.test_resources import BaseEvenniaTest
|
|
||||||
|
from anything import Something
|
||||||
from evennia.utils import create
|
from evennia.utils import create
|
||||||
from .mixins import EvAdventureMixin
|
from evennia.utils.test_resources import BaseEvenniaTest
|
||||||
|
|
||||||
from .. import combat_turnbased
|
from .. import combat_turnbased
|
||||||
from ..characters import EvAdventureCharacter
|
from ..characters import EvAdventureCharacter
|
||||||
|
from ..enums import WieldLocation
|
||||||
|
from ..npcs import EvAdventureMob
|
||||||
|
from ..objects import (
|
||||||
|
EvAdventureConsumable,
|
||||||
|
EvAdventureRunestone,
|
||||||
|
EvAdventureWeapon,
|
||||||
|
WeaponEmptyHand,
|
||||||
|
)
|
||||||
|
from ..rooms import EvAdventureRoom
|
||||||
|
from .mixins import EvAdventureMixin
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
"""
|
"""
|
||||||
Test the turn-based combat-handler implementation.
|
Test methods on the turn-based combat handler.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
|
||||||
|
# make sure to mock away all time-keeping elements
|
||||||
@patch(
|
@patch(
|
||||||
"evennia.contrib.tutorials.evadventure.combat_turnbased"
|
"evennia.contrib.tutorials.evadventure.combat_turnbased"
|
||||||
".EvAdventureCombatHandler.interval",
|
".EvAdventureCombatHandler.interval",
|
||||||
new=-1,
|
new=-1,
|
||||||
)
|
)
|
||||||
|
@patch(
|
||||||
|
"evennia.contrib.tutorials.evadventure.combat_turnbased.delay",
|
||||||
|
new=MagicMock(return_value=None),
|
||||||
|
)
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.combatant = self.character
|
self.combatant = self.character
|
||||||
self.target = create.create_object(EvAdventureCharacter, key="testchar2")
|
self.target = create.create_object(
|
||||||
|
EvAdventureMob, key="testmonster", location=self.location
|
||||||
|
)
|
||||||
|
|
||||||
# this already starts turn 1
|
# this already starts turn 1
|
||||||
self.combathandler = combat_turnbased.join_combat(self.combatant, self.target)
|
self.combathandler = combat_turnbased.join_combat(self.combatant, self.target)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.combathandler.delete()
|
self.combathandler.delete()
|
||||||
|
self.target.delete()
|
||||||
|
|
||||||
def test_remove_combatant(self):
|
def test_remove_combatant(self):
|
||||||
self.combathandler.remove_combatant(self.character)
|
self.assertTrue(bool(self.combatant.db.turnbased_combathandler))
|
||||||
|
self.combathandler.remove_combatant(self.combatant)
|
||||||
|
self.assertFalse(self.combatant in self.combathandler.combatants)
|
||||||
|
self.assertFalse(bool(self.combatant.db.turnbased_combathandler))
|
||||||
|
|
||||||
def test_start_turn(self):
|
def test_start_turn(self):
|
||||||
self.combathandler._start_turn()
|
self.combathandler._start_turn()
|
||||||
|
|
@ -47,6 +70,52 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
def test_end_of_turn__empty(self):
|
def test_end_of_turn__empty(self):
|
||||||
self.combathandler._end_turn()
|
self.combathandler._end_turn()
|
||||||
|
|
||||||
|
def test_add_combatant(self):
|
||||||
|
self.combathandler._init_menu = MagicMock()
|
||||||
|
combatant3 = create.create_object(EvAdventureCharacter, key="testcharacter3")
|
||||||
|
self.combathandler.add_combatant(combatant3)
|
||||||
|
|
||||||
|
self.assertTrue(combatant3 in self.combathandler.combatants)
|
||||||
|
self.combathandler._init_menu.assert_called_once()
|
||||||
|
|
||||||
|
def test_start_combat(self):
|
||||||
|
self.combathandler._start_turn = MagicMock()
|
||||||
|
self.combathandler.start = MagicMock()
|
||||||
|
self.combathandler.start_combat()
|
||||||
|
self.combathandler._start_turn.assert_called_once()
|
||||||
|
self.combathandler.start.assert_called_once()
|
||||||
|
|
||||||
|
def test_combat_summary(self):
|
||||||
|
result = self.combathandler.get_combat_summary(self.combatant)
|
||||||
|
self.assertTrue("You (4 / 4 health)" in result)
|
||||||
|
self.assertTrue("testmonster" in result)
|
||||||
|
|
||||||
|
def test_msg(self):
|
||||||
|
self.location.msg_contents = MagicMock()
|
||||||
|
self.combathandler.msg("You hurt the target", combatant=self.combatant)
|
||||||
|
self.location.msg_contents.assert_called_with(
|
||||||
|
"You hurt the target",
|
||||||
|
from_obj=self.combatant,
|
||||||
|
exclude=[],
|
||||||
|
mapping={"testchar": self.combatant, "testmonster": self.target},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_gain_advantage(self):
|
||||||
|
self.combathandler.gain_advantage(self.combatant, self.target)
|
||||||
|
self.assertTrue(bool(self.combathandler.advantage_matrix[self.combatant][self.target]))
|
||||||
|
|
||||||
|
def test_gain_disadvantage(self):
|
||||||
|
self.combathandler.gain_disadvantage(self.combatant, self.target)
|
||||||
|
self.assertTrue(bool(self.combathandler.disadvantage_matrix[self.combatant][self.target]))
|
||||||
|
|
||||||
|
def test_flee(self):
|
||||||
|
self.combathandler.flee(self.combatant)
|
||||||
|
self.assertTrue(self.combatant in self.combathandler.fleeing_combatants)
|
||||||
|
|
||||||
|
def test_unflee(self):
|
||||||
|
self.combathandler.unflee(self.combatant)
|
||||||
|
self.assertFalse(self.combatant in self.combathandler.fleeing_combatants)
|
||||||
|
|
||||||
def test_register_and_run_action(self):
|
def test_register_and_run_action(self):
|
||||||
action_class = combat_turnbased.CombatActionAttack
|
action_class = combat_turnbased.CombatActionAttack
|
||||||
action = self.combathandler.combatant_actions[self.combatant][action_class.key]
|
action = self.combathandler.combatant_actions[self.combatant][action_class.key]
|
||||||
|
|
@ -60,10 +129,178 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
self.combathandler._end_turn()
|
self.combathandler._end_turn()
|
||||||
action.use.assert_called_once()
|
action.use.assert_called_once()
|
||||||
|
|
||||||
|
def test_get_available_actions(self):
|
||||||
|
result = self.combathandler.get_available_actions(self.combatant)
|
||||||
|
self.assertTrue(len(result), 7)
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
|
"""
|
||||||
|
Test actions in turn_based combat.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"evennia.contrib.tutorials.evadventure.combat_turnbased"
|
||||||
|
".EvAdventureCombatHandler.interval",
|
||||||
|
new=-1,
|
||||||
|
)
|
||||||
|
@patch(
|
||||||
|
"evennia.contrib.tutorials.evadventure.combat_turnbased.delay",
|
||||||
|
new=MagicMock(return_value=None),
|
||||||
|
)
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.combatant = self.character
|
||||||
|
self.combatant2 = create.create_object(EvAdventureCharacter, key="testcharacter2")
|
||||||
|
self.target = create.create_object(EvAdventureMob, key="testmonster")
|
||||||
|
self.target.hp = 4
|
||||||
|
|
||||||
|
# this already starts turn 1
|
||||||
|
self.combathandler = combat_turnbased.join_combat(self.combatant, self.target)
|
||||||
|
|
||||||
|
def _run_action(self, action, *args, **kwargs):
|
||||||
|
self.combathandler.register_action(self.combatant, action.key, *args, **kwargs)
|
||||||
|
self.combathandler._end_turn()
|
||||||
|
|
||||||
|
def test_do_nothing(self):
|
||||||
|
self.combathandler.msg = MagicMock()
|
||||||
|
self._run_action(combat_turnbased.CombatActionDoNothing, None)
|
||||||
|
self.combathandler.msg.assert_called()
|
||||||
|
|
||||||
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
def test_attack(self, mock_randint):
|
def test_attack__miss(self, mock_randint):
|
||||||
mock_randint.return_value = 8
|
mock_randint.return_value = 8 # target has default armor 11, so 8+1 str will miss
|
||||||
|
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
||||||
|
self.assertEqual(self.target.hp, 4)
|
||||||
|
|
||||||
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
|
def test_attack__success__still_alive(self, mock_randint):
|
||||||
|
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
|
||||||
|
# make sure target survives
|
||||||
|
self.target.hp = 20
|
||||||
|
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
||||||
|
self.assertEqual(self.target.hp, 9)
|
||||||
|
|
||||||
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
|
def test_attack__success__kill(self, mock_randint):
|
||||||
|
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
|
||||||
|
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
||||||
|
self.assertEqual(self.target.hp, -7)
|
||||||
|
|
||||||
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
|
def test_stunt_fail(self, mock_randint):
|
||||||
|
mock_randint.return_value = 8 # fails 8+1 dex vs DEX 11 defence
|
||||||
|
self._run_action(combat_turnbased.CombatActionStunt, self.target)
|
||||||
|
self.assertEqual(self.combathandler.advantage_matrix[self.combatant], {})
|
||||||
|
self.assertEqual(self.combathandler.disadvantage_matrix[self.combatant], {})
|
||||||
|
|
||||||
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
|
def test_stunt_advantage__success(self, mock_randint):
|
||||||
|
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
|
||||||
|
self._run_action(combat_turnbased.CombatActionStunt, self.target)
|
||||||
|
self.assertEqual(
|
||||||
|
bool(self.combathandler.advantage_matrix[self.combatant][self.target]), True
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
|
def test_stunt_disadvantage__success(self, mock_randint):
|
||||||
|
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
|
||||||
|
action = combat_turnbased.CombatActionStunt
|
||||||
|
action.give_advantage = False
|
||||||
|
self._run_action(
|
||||||
|
action,
|
||||||
|
self.target,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
bool(self.combathandler.disadvantage_matrix[self.target][self.combatant]), True
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_use_item(self):
|
||||||
|
"""
|
||||||
|
Use up a potion during combat.
|
||||||
|
|
||||||
|
"""
|
||||||
|
item = create.create_object(
|
||||||
|
EvAdventureConsumable, key="Healing potion", attributes=[("uses", 2)]
|
||||||
|
)
|
||||||
|
self.assertEqual(item.uses, 2)
|
||||||
|
self._run_action(combat_turnbased.CombatActionUseItem, item, self.combatant)
|
||||||
|
self.assertEqual(item.uses, 1)
|
||||||
|
self._run_action(combat_turnbased.CombatActionUseItem, item, self.combatant)
|
||||||
|
self.assertEqual(item.pk, None) # deleted, it was used up
|
||||||
|
|
||||||
|
def test_swap_wielded_weapon_or_spell(self):
|
||||||
|
"""
|
||||||
|
First draw a weapon (from empty fists), then swap that out to another weapon, then
|
||||||
|
swap to a spell rune.
|
||||||
|
|
||||||
|
"""
|
||||||
|
sword = create.create_object(EvAdventureWeapon, key="sword")
|
||||||
|
zweihander = create.create_object(
|
||||||
|
EvAdventureWeapon,
|
||||||
|
key="zweihander",
|
||||||
|
attributes=(("inventory_use_slot", WieldLocation.TWO_HANDS),),
|
||||||
|
)
|
||||||
|
runestone = create.create_object(EvAdventureRunestone, key="ice rune")
|
||||||
|
|
||||||
|
# check hands are empty
|
||||||
|
self.assertEqual(self.combatant.weapon.key, "Empty Fists")
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
|
||||||
|
|
||||||
|
# swap to sword
|
||||||
|
self._run_action(combat_turnbased.CombatActionSwapWieldedWeaponOrSpell, None, sword)
|
||||||
|
self.assertEqual(self.combatant.weapon, sword)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], sword)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
|
||||||
|
|
||||||
|
# swap to zweihander (two-handed sword)
|
||||||
|
self._run_action(combat_turnbased.CombatActionSwapWieldedWeaponOrSpell, None, zweihander)
|
||||||
|
self.assertEqual(self.combatant.weapon, zweihander)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], zweihander)
|
||||||
|
|
||||||
|
# swap to runestone (also using two hands)
|
||||||
|
self._run_action(combat_turnbased.CombatActionSwapWieldedWeaponOrSpell, None, runestone)
|
||||||
|
self.assertEqual(self.combatant.weapon, runestone)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], runestone)
|
||||||
|
|
||||||
|
# swap back to normal one-handed sword
|
||||||
|
self._run_action(combat_turnbased.CombatActionSwapWieldedWeaponOrSpell, None, sword)
|
||||||
|
self.assertEqual(self.combatant.weapon, sword)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], sword)
|
||||||
|
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
|
||||||
|
|
||||||
|
def test_flee__success(self):
|
||||||
|
"""
|
||||||
|
Test fleeing twice, leading to leaving combat.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# first flee records the fleeing state
|
||||||
|
self._run_action(combat_turnbased.CombatActionFlee, None)
|
||||||
|
self.assertTrue(self.combatant in self.combathandler.fleeing_combatants)
|
||||||
|
|
||||||
|
# second flee should remove combatant
|
||||||
|
self._run_action(combat_turnbased.CombatActionFlee, None)
|
||||||
|
self.assertTrue(self.combatant not in self.combathandler.combatants)
|
||||||
|
|
||||||
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
|
def test_flee__blocked(self, mock_randint):
|
||||||
|
""" """
|
||||||
|
mock_randint.return_value = 11 # means block will succeed
|
||||||
|
|
||||||
|
self._run_action(combat_turnbased.CombatActionFlee, None)
|
||||||
|
self.assertTrue(self.combatant in self.combathandler.fleeing_combatants)
|
||||||
|
|
||||||
|
# other combatant blocks in the same turn
|
||||||
self.combathandler.register_action(
|
self.combathandler.register_action(
|
||||||
combat_turnbased.CombatActionAttack.key, self.combatant, self.target
|
self.combatant, combat_turnbased.CombatActionFlee.key, None
|
||||||
|
)
|
||||||
|
self.combathandler.register_action(
|
||||||
|
self.target, combat_turnbased.CombatActionBlock.key, self.combatant
|
||||||
)
|
)
|
||||||
self.combathandler._end_turn()
|
self.combathandler._end_turn()
|
||||||
|
# the fleeing combatant should remain now
|
||||||
|
self.assertTrue(self.combatant not in self.combathandler.fleeing_combatants)
|
||||||
|
self.assertTrue(self.combatant in self.combathandler.combatants)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue