From 2daadca999b897bcf0c1beb264274e9029cd76eb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 16 Jul 2022 18:01:09 +0200 Subject: [PATCH] More tutorial combat tests --- .../tutorials/evadventure/characters.py | 44 ++- .../tutorials/evadventure/combat_turnbased.py | 175 ++++++------ evennia/contrib/tutorials/evadventure/npcs.py | 11 +- .../contrib/tutorials/evadventure/objects.py | 67 ++++- .../contrib/tutorials/evadventure/rules.py | 3 +- .../evadventure/tests/test_combat.py | 255 +++++++++++++++++- 6 files changed, 431 insertions(+), 124 deletions(-) diff --git a/evennia/contrib/tutorials/evadventure/characters.py b/evennia/contrib/tutorials/evadventure/characters.py index 10f96f81c2..623977764b 100644 --- a/evennia/contrib/tutorials/evadventure/characters.py +++ b/evennia/contrib/tutorials/evadventure/characters.py @@ -119,13 +119,17 @@ class EquipmentHandler: method. 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 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.HEAD], "armor", 0), ) @@ -333,7 +337,8 @@ class EquipmentHandler: 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: @@ -386,6 +391,21 @@ class LivingMixin: """ 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): """ @@ -417,6 +437,14 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): """Allows to access equipment like char.equipment.worn""" 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): """ 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) if hp <= 0: # this means we rolled death on the table - self.handle_death() + self.at_death() else: # still alive, but lost in some stats self.location.msg_contents( f"|y$You() $conj(stagger) back, weakened but still alive.|n", from_obj=self ) - def defeat_message(self, attacker, dmg): - """ - Sent out to everyone in the location by the combathandler. - - """ - - def handle_death(self): + def at_death(self): """ Called when character dies. diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat_turnbased.py index e310757d13..1a23d43bc6 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat_turnbased.py @@ -326,8 +326,7 @@ class CombatActionStunt(CombatAction): "actions. The effect needs to be used up within 5 turns." ) - give_advantage = True - give_disadvantage = False + give_advantage = True # if False, give_disadvantage max_uses = 1 priority = -1 attack_type = Ability.DEX @@ -353,10 +352,19 @@ class CombatActionStunt(CombatAction): ) self.msg(f"$You() $conj(attempt) stunt on $You(defender.key). {txt}") if is_success: - if advantage: + stunt_duration = self.combathandler.stunt_duration + if self.give_advantage: 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: 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 self.uses += 1 @@ -386,19 +394,53 @@ class CombatActionUseItem(CombatAction): help_text = "Use an item from your inventory." def get_help(self, item, *args): - return item.combat_get_help(*args) - - def can_use(self, item, *args, **kwargs): - return item.combat_can_use(self.combatant, self.combathandler, *args, **kwargs) + return item.get_help(*args) 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): - item.combat_use(self.combatant, target, *args, **kwargs) + item.at_use(self.combatant, target, *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): @@ -451,13 +493,17 @@ class CombatActionBlock(CombatAction): attack_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)) - disadvantage = bool(self.disadvantage_matrix[combatant].pop(fleeing_target, False)) + advantage = bool( + 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( - combatant, + self.combatant, fleeing_target, attack_type=self.attack_type, defense_type=self.defense_type, @@ -468,60 +514,12 @@ class CombatActionBlock(CombatAction): if is_success: # 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})") else: 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): """ Do nothing this turn. @@ -635,19 +633,6 @@ class EvAdventureCombatHandler(DefaultScript): 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): """ 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" ) + # store those in the process of fleeing + already_fleeing = self.fleeing_combatants[:] + # do all actions for combatant in self.combatants: # read the current action type selected by the player @@ -710,39 +698,26 @@ class EvAdventureCombatHandler(DefaultScript): "Please report the problem to an admin." ) logger.log_trace() + raise # handle disengaging combatants to_remove = [] for combatant in self.combatants: - # check disengaging combatants (these are combatants that managed - # not get their escape blocked last turn - if combatant in self.fleeing_combatants: + # see if fleeing characters managed to do two flee actions in a row. + if (combatant in self.fleeing_combatants) and (combatant in already_fleeing): self.fleeing_combatants.remove(combatant) + to_remove.append(combatant) if combatant.hp <= 0: + # check characters that are beaten down. # characters roll on the death table here, npcs usually just die combatant.at_defeat() - - # tell everyone - self.msg(combatant.defeat_message(attacker, dmg), combatant=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) + if combatant.hp <= 0: + # if character still < 0 after at_defeat, it means they are dead. + # force-remove from combat. + to_remove.append(combatant) for combatant in to_remove: # 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 - loadout = caller.inventory.display_loadout() + loadout = caller.equipment.display_loadout() text = ( 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)." @@ -1058,7 +1033,7 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs): # get a list of all suitable weapons/spells/shields 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: # object is broken options.append( diff --git a/evennia/contrib/tutorials/evadventure/npcs.py b/evennia/contrib/tutorials/evadventure/npcs.py index 6b109187b5..8fd2ad6749 100644 --- a/evennia/contrib/tutorials/evadventure/npcs.py +++ b/evennia/contrib/tutorials/evadventure/npcs.py @@ -30,7 +30,7 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter): """ hit_dice = AttributeProperty(default=1) - armor = AttributeProperty(default=11) + armor = AttributeProperty(default=1) # +10 to get armor defense morale = AttributeProperty(default=9) 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. """ + + def at_defeat(self): + """ + Mobs die right away when defeated, no death-table rolls. + + """ + self.at_death() diff --git a/evennia/contrib/tutorials/evadventure/objects.py b/evennia/contrib/tutorials/evadventure/objects.py index 3b9caf1d6c..1e31c1600f 100644 --- a/evennia/contrib/tutorials/evadventure/objects.py +++ b/evennia/contrib/tutorials/evadventure/objects.py @@ -31,6 +31,54 @@ class EvAdventureObject(DefaultObject): quality = AttributeProperty(1) 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): """ @@ -59,7 +107,7 @@ class EvAdventureConsumable(EvAdventureObject): size = AttributeProperty(0.25) 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 not be usable anymore and probably be deleted. @@ -71,6 +119,20 @@ class EvAdventureConsumable(EvAdventureObject): """ 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): """ @@ -98,6 +160,9 @@ class WeaponEmptyHand: damage_roll = "1d4" quality = 100000 # let's assume fists are always available ... + def __repr__(self): + return "" + class EvAdventureRunestone(EvAdventureWeapon): """ diff --git a/evennia/contrib/tutorials/evadventure/rules.py b/evennia/contrib/tutorials/evadventure/rules.py index f9e6a05efe..1ceb216705 100644 --- a/evennia/contrib/tutorials/evadventure/rules.py +++ b/evennia/contrib/tutorials/evadventure/rules.py @@ -200,8 +200,9 @@ class EvAdventureRollEngine: 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( attacker, bonus_type=attack_type, diff --git a/evennia/contrib/tutorials/evadventure/tests/test_combat.py b/evennia/contrib/tutorials/evadventure/tests/test_combat.py index bbba30890a..e95c7576dc 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_combat.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_combat.py @@ -3,40 +3,63 @@ Test EvAdventure combat. """ -from unittest.mock import patch, MagicMock -from evennia.utils.test_resources import BaseEvenniaTest +from unittest.mock import MagicMock, patch + +from anything import Something from evennia.utils import create -from .mixins import EvAdventureMixin +from evennia.utils.test_resources import BaseEvenniaTest + from .. import combat_turnbased 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): """ - Test the turn-based combat-handler implementation. + Test methods on the turn-based combat handler. """ maxDiff = None + # make sure to mock away all time-keeping elements @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.target = create.create_object(EvAdventureCharacter, key="testchar2") + self.target = create.create_object( + EvAdventureMob, key="testmonster", location=self.location + ) # this already starts turn 1 self.combathandler = combat_turnbased.join_combat(self.combatant, self.target) def tearDown(self): self.combathandler.delete() + self.target.delete() 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): self.combathandler._start_turn() @@ -47,6 +70,52 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest): def test_end_of_turn__empty(self): 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): action_class = combat_turnbased.CombatActionAttack action = self.combathandler.combatant_actions[self.combatant][action_class.key] @@ -60,10 +129,178 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest): self.combathandler._end_turn() 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") - def test_attack(self, mock_randint): - mock_randint.return_value = 8 + def test_attack__miss(self, mock_randint): + 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( - 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() + # the fleeing combatant should remain now + self.assertTrue(self.combatant not in self.combathandler.fleeing_combatants) + self.assertTrue(self.combatant in self.combathandler.combatants)