More tutorial combat tests

This commit is contained in:
Griatch 2022-07-16 18:01:09 +02:00
parent e6ac8d347e
commit 2daadca999
6 changed files with 431 additions and 124 deletions

View file

@ -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.

View file

@ -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(

View file

@ -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()

View file

@ -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):
""" """

View file

@ -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,

View file

@ -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)