mirror of
https://github.com/evennia/evennia.git
synced 2026-03-29 12:07:17 +02:00
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.
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 "<WeaponEmptyHand>"
|
||||
|
||||
|
||||
class EvAdventureRunestone(EvAdventureWeapon):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue