Debugging of tutorial

This commit is contained in:
Griatch 2022-07-18 16:58:45 +02:00
parent fd4a1fb5ca
commit 091a13674d
8 changed files with 189 additions and 391 deletions

View file

@ -30,7 +30,7 @@ from evennia.contrib.tutorials.evadventure.objects import (
EvAdventureRunestone, EvAdventureRunestone,
EvAdventureWeapon, EvAdventureWeapon,
) )
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom from evennia.contrib.tutorials.evadventure.rooms import EvAdventurePvPRoom, EvAdventureRoom
# CODE # CODE
@ -65,7 +65,7 @@ create_object(
# A combat room evtechdemo#01 # A combat room evtechdemo#01
# with a static enemy # with a static enemy
combat_room = create_object(EvAdventureRoom, key="Combat Arena", aliases=("evtechdemo#01",)) combat_room = create_object(EvAdventurePvPRoom, key="Combat Arena", aliases=("evtechdemo#01",))
# link to/back to hub # link to/back to hub
hub_room = search_object("evtechdemo#00")[0] hub_room = search_object("evtechdemo#00")[0]
create_object( create_object(

View file

@ -1,5 +1,5 @@
""" """
Base Character and NPCs. Character class.
""" """
@ -8,336 +8,8 @@ from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.utils import lazy_property from evennia.utils.utils import lazy_property
from . import rules from . import rules
from .enums import Ability, WieldLocation from .equipment import EquipmentHandler
from .objects import WeaponEmptyHand from .quests import EvAdventureQuestHandler
class EquipmentError(TypeError):
pass
class EquipmentHandler:
"""
_Knave_ puts a lot of emphasis on the inventory. You have CON_DEFENSE inventory
slots. Some things, like torches can fit multiple in one slot, other (like
big weapons and armor) use more than one slot. The items carried and wielded has a big impact
on character customization - even magic requires carrying a runestone per spell.
The inventory also doubles as a measure of negative effects. Getting soaked in mud
or slime could gunk up some of your inventory slots and make the items there unusuable
until you clean them.
"""
save_attribute = "inventory_slots"
def __init__(self, obj):
self.obj = obj
self._load()
def _load(self):
"""
Load or create a new slot storage.
"""
self.slots = self.obj.attributes.get(
self.save_attribute,
category="inventory",
default={
WieldLocation.WEAPON_HAND: None,
WieldLocation.SHIELD_HAND: None,
WieldLocation.TWO_HANDS: None,
WieldLocation.BODY: None,
WieldLocation.HEAD: None,
WieldLocation.BACKPACK: [],
},
)
def _count_slots(self):
"""
Count slot usage. This is fetched from the .size Attribute of the
object. The size can also be partial slots.
"""
slots = self.slots
wield_usage = sum(
getattr(slotobj, "size", 0) or 0
for slot, slotobj in slots.items()
if slot is not WieldLocation.BACKPACK
)
backpack_usage = sum(
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
)
return wield_usage + backpack_usage
def _save(self):
"""
Save slot to storage.
"""
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
@property
def max_slots(self):
"""
The max amount of equipment slots ('carrying capacity') is based on
the constitution defense.
"""
return getattr(self.obj, Ability.CON.value, 1) + 10
def validate_slot_usage(self, obj):
"""
Check if obj can fit in equipment, based on its size.
Args:
obj (EvAdventureObject): The object to add.
Raise:
EquipmentError: If there's not enough room.
"""
size = getattr(obj, "size", 0)
max_slots = self.max_slots
current_slot_usage = self._count_slots()
if current_slot_usage + size > max_slots:
slots_left = max_slots - current_slot_usage
raise EquipmentError(
f"Equipment full ($int2str({slots_left}) slots "
f"remaining, {obj.key} needs $int2str({size}) "
f"$pluralize(slot, {size}))."
)
return True
@property
def armor(self):
"""
Armor provided by actually worn equipment/shield. For body armor
this is a base value, like 12, for shield/helmet, it's a bonus, like +1.
We treat values and bonuses equal and just add them up. This value
can thus be 0, the 'unarmored' default should be handled by the calling
method.
Returns:
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(
(
# 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),
)
)
@property
def weapon(self):
"""
Conveniently get the currently active weapon or rune stone.
Returns:
obj or None: The weapon. None if unarmored.
"""
# first checks two-handed wield, then one-handed; the two
# should never appear simultaneously anyhow (checked in `use` method).
slots = self.slots
weapon = slots[WieldLocation.TWO_HANDS]
if not weapon:
weapon = slots[WieldLocation.WEAPON_HAND]
if not weapon:
weapon = WeaponEmptyHand()
return weapon
def display_loadout(self):
"""
Get a visual representation of your current loadout.
Returns:
str: The current loadout.
"""
slots = self.slots
weapon_str = "You are fighting with your bare fists"
shield_str = " and have no shield."
armor_str = "You wear no armor"
helmet_str = " and no helmet."
two_hands = slots[WieldLocation.TWO_HANDS]
if two_hands:
weapon_str = f"You wield {two_hands} with both hands"
shield_str = " (you can't hold a shield at the same time)."
else:
one_hands = slots[WieldLocation.WEAPON_HAND]
if one_hands:
weapon_str = f"You are wielding {one_hands} in one hand."
shield = slots[WieldLocation.SHIELD_HAND]
if shield:
shield_str = f"You have {shield} in your off hand."
armor = slots[WieldLocation.BODY]
if armor:
armor_str = f"You are wearing {armor}"
helmet = slots[WieldLocation.BODY]
if helmet:
helmet_str = f" and {helmet} on your head."
return f"{weapon_str}{shield_str}\n{armor_str}{helmet_str}"
def use(self, obj):
"""
Make use of item - this makes use of the object's wield slot to decide where
it goes. If it doesn't have any, it goes into backpack.
Args:
obj (EvAdventureObject): Thing to use.
Raises:
EquipmentError: If there's no room in inventory. It will contains the details
of the error, suitable to echo to user.
Notes:
If using an item already in the backpack, it should first be `removed` from the
backpack, before applying here - otherwise, it will be added a second time!
this will cleanly move any 'colliding' items to the backpack to
make the use possible (such as moving sword + shield to backpack when wielding
a two-handed weapon). If wanting to warn the user about this, it needs to happen
before this call.
"""
# first check if we have room for this
self.validate_slot_usage(obj)
slots = self.slots
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
if use_slot is WieldLocation.TWO_HANDS:
# two-handed weapons can't co-exist with weapon/shield-hand used items
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
slots[use_slot] = obj
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
# can't keep a two-handed weapon if adding a one-handede weapon or shield
slots[WieldLocation.TWO_HANDS] = None
slots[use_slot] = obj
elif use_slot is WieldLocation.BACKPACK:
# backpack has multiple slots.
slots[use_slot].append(obj)
else:
# for others (body, head), just replace whatever's there
slots[use_slot] = obj
# store new state
self._save()
def add(self, obj):
"""
Put something in the backpack specifically (even if it could be wield/worn).
"""
# check if we have room
self.validate_slot_usage(obj)
self.slots[WieldLocation.BACKPACK].append(obj)
self._save()
def can_remove(self, leaving_object):
"""
Called to check if the object can be removed.
"""
return True # TODO - some things may not be so easy, like mud
def remove(self, obj_or_slot):
"""
Remove specific object or objects from a slot.
Args:
obj_or_slot (EvAdventureObject or WieldLocation): The specific object or
location to empty. If this is WieldLocation.BACKPACK, all items
in the backpack will be emptied and returned!
Returns:
list: A list of 0, 1 or more objects emptied from the inventory.
"""
slots = self.slots
ret = []
if isinstance(obj_or_slot, WieldLocation):
if obj_or_slot is WieldLocation.BACKPACK:
# empty entire backpack
ret.extend(slots[obj_or_slot])
slots[obj_or_slot] = []
else:
ret.append(slots[obj_or_slot])
slots[obj_or_slot] = None
elif obj_or_slot in self.slots.values():
# obj in use/wear slot
for slot, objslot in slots.items():
if objslot is obj_or_slot:
slots[slot] = None
ret.append(objslot)
elif obj_or_slot in slots[WieldLocation.BACKPACK]:
# obj in backpack slot
try:
slots[WieldLocation.BACKPACK].remove(obj_or_slot)
ret.append(obj_or_slot)
except ValueError:
pass
if ret:
self._save()
return ret
def get_wieldable_objects_from_backpack(self):
"""
Get all wieldable weapons (or spell runes) from backpack. This is useful in order to
have a list to select from when swapping your wielded loadout.
Returns:
list: A list of objects with a suitable `inventory_use_slot`. We don't check
quality, so this may include broken items (we may want to visually show them
in the list after all).
"""
return [
obj
for obj in self.slots[WieldLocation.BACKPACK]
if obj.inventory_use_slot
in (WieldLocation.WEAPON_HAND, WieldLocation.TWO_HANDS, WieldLocation.SHIELD_HAND)
]
def get_wearable_objects_from_backpack(self):
"""
Get all wearable items (armor or helmets) from backpack. This is useful in order to
have a list to select from when swapping your worn loadout.
Returns:
list: A list of objects with a suitable `inventory_use_slot`. We don't check
quality, so this may include broken items (we may want to visually show them
in the list after all).
"""
return [
obj
for obj in self.slots[WieldLocation.BACKPACK]
if obj.inventory_use_slot in (WieldLocation.BODY, WieldLocation.HEAD)
]
def get_usable_objects_from_backpack(self):
"""
Get all 'usable' items (like potions) from backpack. This is useful for getting a
list to select from.
Returns:
list: A list of objects that are usable.
"""
character = self.obj
return [obj for obj in self.slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)]
class LivingMixin: class LivingMixin:
@ -407,6 +79,16 @@ class LivingMixin:
""" """
pass pass
def at_loot(self, looted):
"""
Called when looting another entity.
Args:
looted: The thing to loot.
"""
looted.get_loot()
def get_loot(self, looter): def get_loot(self, looter):
""" """
Called when being looted (after defeat). Called when being looted (after defeat).
@ -418,8 +100,8 @@ class LivingMixin:
max_steal = rules.dice.roll("1d10") max_steal = rules.dice.roll("1d10")
owned = self.coin owned = self.coin
stolen = max(max_steal, owned) stolen = max(max_steal, owned)
self.coin -= stolen self.coins -= stolen
looter.coin += stolen looter.coins += stolen
self.location.msg_contents( self.location.msg_contents(
f"$You(looter) loots $You() for {stolen} coins!", f"$You(looter) loots $You() for {stolen} coins!",
@ -434,6 +116,9 @@ class LivingMixin:
Args: Args:
defeated_enemy (Object): The enemy soon to loot. defeated_enemy (Object): The enemy soon to loot.
Returns:
bool: If False, no looting is allowed.
""" """
pass pass
@ -481,6 +166,11 @@ 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)
@lazy_property
def quests(self):
"""Access and track quests"""
return EvAdventureQuestHandler(self)
@property @property
def weapon(self): def weapon(self):
return self.equipment.weapon return self.equipment.weapon
@ -544,14 +234,18 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
the death table. the death table.
""" """
rules.dice.roll_death(self) if self.location.allow_death:
if self.hp > 0: rules.dice.roll_death(self)
# still alive, but lost some stats if self.hp > 0:
self.location.msg_contents( # still alive, but lost some stats
"|y$You() $conj(stagger) back and fall to the ground - alive, " self.location.msg_contents(
"but unable to move.|n", "|y$You() $conj(stagger) back and fall to the ground - alive, "
from_obj=self, "but unable to move.|n",
) from_obj=self,
)
else:
self.location.msg_contents("|y$You() $conj(yield), beaten and out of the fight.|n")
self.hp = self.hp_max
def at_death(self): def at_death(self):
""" """
@ -562,3 +256,17 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
"|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n", "|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n",
from_obj=self, from_obj=self,
) )
def at_pre_loot(self):
"""
Called before allowing to loot. Return False to block enemy looting.
"""
# don't allow looting in pvp
return not self.location.allow_pvp
def get_loot(self, looter):
"""
Called when being looted.
"""
pass

View file

@ -111,7 +111,7 @@ from .enums import Ability
from .npcs import EvAdventureNPC from .npcs import EvAdventureNPC
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler" COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
COMBAT_HANDLER_INTERVAL = 60 COMBAT_HANDLER_INTERVAL = 30
class CombatFailure(RuntimeError): class CombatFailure(RuntimeError):
@ -156,7 +156,7 @@ class CombatAction:
self.combatant = combatant self.combatant = combatant
self.uses = 0 self.uses = 0
def msg(self, message, broadcast=False): def msg(self, message, broadcast=True):
""" """
Convenience route to the combathandler msg-sender mechanism. Convenience route to the combathandler msg-sender mechanism.
@ -520,9 +520,9 @@ 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.combathandler.unflee(fleeing_target) self.combathandler.unflee(fleeing_target)
self.msg("$You() blocks the retreat of $You({fleeing_target.key})") self.msg(f"$You() $conj(block) the retreat of $You({fleeing_target.key})")
else: else:
self.msg("$You({fleeing_target.key}) dodges away from you $You()!") self.msg(f"$You({fleeing_target.key}) dodges away from you $You()!")
class CombatActionDoNothing(CombatAction): class CombatActionDoNothing(CombatAction):
@ -660,7 +660,7 @@ class EvAdventureCombatHandler(DefaultScript):
# start a timer to echo a warning to everyone 15 seconds before end of round # start a timer to echo a warning to everyone 15 seconds before end of round
if self.interval >= 0: if self.interval >= 0:
# set -1 for unit tests # set -1 for unit tests
warning_time = 15 warning_time = 10
self._warn_time_task = delay( self._warn_time_task = delay(
self.interval - warning_time, self._warn_time, warning_time self.interval - warning_time, self._warn_time, warning_time
) )
@ -766,9 +766,9 @@ class EvAdventureCombatHandler(DefaultScript):
for ally in allies: for ally in allies:
for enemy in defeated_enemies: for enemy in defeated_enemies:
try: try:
ally.pre_loot(enemy) if ally.pre_loot(enemy):
enemy.get_loot(ally) enemy.get_loot(ally)
ally.post_loot(enemy) ally.post_loot(enemy)
except Exception: except Exception:
logger.log_trace() logger.log_trace()
self.stop_combat() self.stop_combat()
@ -844,7 +844,8 @@ class EvAdventureCombatHandler(DefaultScript):
if combatant in self.combatants: if combatant in self.combatants:
self.combatants.remove(combatant) self.combatants.remove(combatant)
self.combatant_actions.pop(combatant, None) self.combatant_actions.pop(combatant, None)
combatant.ndb._evmenu.close_menu() if combatant.ndb._evmenu:
combatant.ndb._evmenu.close_menu()
del combatant.db.combathandler del combatant.db.combathandler
def start_combat(self): def start_combat(self):
@ -867,6 +868,7 @@ class EvAdventureCombatHandler(DefaultScript):
""" """
for combatant in self.combatants: for combatant in self.combatants:
self.remove_combatant(combatant) self.remove_combatant(combatant)
self.delete()
def get_enemy_targets(self, combatant, excluded=None, all_combatants=None): def get_enemy_targets(self, combatant, excluded=None, all_combatants=None):
""" """
@ -1131,8 +1133,10 @@ def _select_target_helper(caller, raw_string, targets, **kwargs):
text = f"Select target for |w{action_key}|n." text = f"Select target for |w{action_key}|n."
# make the apply-self option always the first one, give it key 0 # make the apply-self option always the first one, give it key 0
kwargs["action_target"] = caller if caller in targets:
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}] targets.remove(caller)
kwargs["action_target"] = caller
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
# filter out ourselves and then make options for everyone else # filter out ourselves and then make options for everyone else
for inum, combatant in enumerate(targets): for inum, combatant in enumerate(targets):
kwargs["action_target"] = combatant kwargs["action_target"] = combatant
@ -1385,6 +1389,9 @@ def join_combat(caller, *targets, session=None):
if not location: if not location:
raise CombatFailure("Must have a location to start combat.") raise CombatFailure("Must have a location to start combat.")
if caller.hp <= 0:
raise CombatFailure("You can't start a fight in your current condition!")
if not getattr(location, "allow_combat", False): if not getattr(location, "allow_combat", False):
raise CombatFailure("This is not the time and place for picking a fight.") raise CombatFailure("This is not the time and place for picking a fight.")
@ -1402,6 +1409,9 @@ def join_combat(caller, *targets, session=None):
# it's safe to add a combatant to the same combat more than once # it's safe to add a combatant to the same combat more than once
combathandler.add_combatant(caller, session=session) combathandler.add_combatant(caller, session=session)
for target in targets: for target in targets:
if target.hp <= 0:
caller.msg(f"{target.get_display_name(caller)} is already out of it.")
continue
combathandler.add_combatant(target) combathandler.add_combatant(target)
if created: if created:

View file

@ -8,8 +8,9 @@ from evennia import DefaultCharacter
from evennia.typeclasses.attributes import AttributeProperty from evennia.typeclasses.attributes import AttributeProperty
from .characters import LivingMixin from .characters import LivingMixin
from .enums import Ability from .enums import Ability, WieldLocation
from .objects import WeaponEmptyHand from .objects import WeaponEmptyHand
from .rules import dice
class EvAdventureNPC(LivingMixin, DefaultCharacter): class EvAdventureNPC(LivingMixin, DefaultCharacter):
@ -114,6 +115,9 @@ class EvAdventureMob(EvAdventureNPC):
""" """
# chance (%) that this enemy will loot you when defeating you
loot_chance = AttributeProperty(75)
def ai_combat_next_action(self, combathandler): def ai_combat_next_action(self, combathandler):
""" """
Called to get the next action in combat. Called to get the next action in combat.
@ -150,3 +154,55 @@ class EvAdventureMob(EvAdventureNPC):
""" """
self.at_death() self.at_death()
def at_loot(self, looted):
"""
Called when mob gets to loot a PC.
"""
if dice.roll("1d100") > self.loot_chance:
# don't loot
return
if looted.coins:
# looter prefer coins
loot = dice.roll("1d20")
if looted.coins < loot:
self.location.msg_location(
"$You(looter) loots $You() for all coin!",
from_obj=looted,
mapping={"looter": self},
)
else:
self.location.msg_location(
"$You(looter) loots $You() for |y{loot}|n coins!",
from_obj=looted,
mapping={"looter": self},
)
elif hasattr(looted, "equipment"):
# go through backpack, first usable, then wieldable, wearable items
# and finally stuff wielded
stealable = looted.equipment.get_usable_objects_from_backpack()
if not stealable:
stealable = looted.equipment.get_wieldable_objects_from_backpack()
if not stealable:
stealable = looted.equipment.get_wearable_objects_from_backpack()
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.SHIELD_HAND]]
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.HEAD]]
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.ARMOR]]
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.WEAPON_HAND]]
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.TWO_HANDS]]
stolen = looted.equipment.remove(choice(stealable))
stolen.location = self
self.location.msg_location(
"$You(looter) steals {stolen.key} from $You()!",
from_obj=looted,
mapping={"looter": self},
)

View file

@ -38,6 +38,7 @@ class EvAdventureQuest:
key = "basequest" key = "basequest"
desc = "This is the base quest. It will just step through its steps immediately." desc = "This is the base quest. It will just step through its steps immediately."
start_step = "start" start_step = "start"
end_text = "This quest is completed!"
# help entries for quests # help entries for quests
help_start = "You need to start first" help_start = "You need to start first"
@ -49,6 +50,7 @@ class EvAdventureQuest:
self.questhandler = questhandler self.questhandler = questhandler
self.current_step = start_step self.current_step = start_step
self.completed = False
@property @property
def quester(self): def quester(self):
@ -59,17 +61,20 @@ class EvAdventureQuest:
Call this to end the quest. Call this to end the quest.
""" """
self.current_step self.completed = True
def progress(self): def progress(self, *args, **kwargs):
""" """
This is called whenever the environment expects a quest may be complete. This is called whenever the environment expects a quest may be complete.
This will determine which quest-step we are on, run check_<stepname>, and if it This will determine which quest-step we are on, run check_<stepname>, and if it
succeeds, continue with complete_<stepname> succeeds, continue with complete_<stepname>.
Args:
*args, **kwargs: Will be passed into the check/complete methods.
""" """
if getattr(self, f"check_{self.current_step}")(): if getattr(self, f"check_{self.current_step}")(*args, **kwargs):
getattr(self, f"complete_{self.current_step}")() getattr(self, f"complete_{self.current_step}")(*args, **kwargs)
def help(self): def help(self):
""" """
@ -93,7 +98,7 @@ class EvAdventureQuest:
# step methods # step methods
def check_start(self): def check_start(self, *args, **kwargs):
""" """
Check if the starting conditions are met. Check if the starting conditions are met.
@ -104,7 +109,7 @@ class EvAdventureQuest:
""" """
return True return True
def complete_start(self): def complete_start(self, *args, **kwargs):
""" """
Completed start. This should change `.current_step` to the next step to complete Completed start. This should change `.current_step` to the next step to complete
and call `self.progress()` just in case the next step is already completed too. and call `self.progress()` just in case the next step is already completed too.
@ -114,10 +119,10 @@ class EvAdventureQuest:
self.current_step = "end" self.current_step = "end"
self.progress() self.progress()
def check_end(self): def check_end(self, *args, **kwargs):
return True return True
def complete_end(self): def complete_end(self, *args, **kwargs):
self.quester.msg("Quest complete!") self.quester.msg("Quest complete!")
self.end_quest() self.end_quest()

View file

@ -164,7 +164,11 @@ class EvAdventureRollEngine:
modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}" modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}"
qualtxt = f" ({quality.value}!)" if quality else "" qualtxt = f" ({quality.value}!)" if quality else ""
txt = f"{rolltxt}={dice_roll} + {bonus_type.value}{bontxt}{modtxt} -> |w{result}{qualtxt}|n" txt = (
f"rolled {dice_roll} on {rolltxt} "
f"+ {bonus_type.value}{bontxt}{modtxt} vs "
f"{target} -> |w{result}{qualtxt}|n"
)
return (dice_roll + bonus + modifier) > target, quality, txt return (dice_roll + bonus + modifier) > target, quality, txt

View file

@ -195,7 +195,8 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11 mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
self._run_action(combat_turnbased.CombatActionAttack, self.target) self._run_action(combat_turnbased.CombatActionAttack, self.target)
self.assertEqual(self.target.hp, -7) self.assertEqual(self.target.hp, -7)
self.assertTrue(self.target not in self.combathandler.combatants) # after this the combat is over
self.assertIsNone(self.combathandler.pk)
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint") @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_stunt_fail(self, mock_randint): def test_stunt_fail(self, mock_randint):
@ -293,7 +294,7 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
# second flee should remove combatant # second flee should remove combatant
self._run_action(combat_turnbased.CombatActionFlee, None) self._run_action(combat_turnbased.CombatActionFlee, None)
self.assertTrue(self.combatant not in self.combathandler.combatants) self.assertIsNone(self.combathandler.pk)
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint") @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_flee__blocked(self, mock_randint): def test_flee__blocked(self, mock_randint):

View file

@ -6,38 +6,38 @@ They provide some useful string and conversion methods that might
be of use when designing your own game. be of use when designing your own game.
""" """
import os
import gc import gc
import sys
import types
import math
import threading
import re
import textwrap
import random
import inspect
import traceback
import importlib import importlib
import importlib.util
import importlib.machinery import importlib.machinery
import importlib.util
import inspect
import math
import os
import random
import re
import sys
import textwrap
import threading
import traceback
import types
from ast import literal_eval from ast import literal_eval
from simpleeval import simple_eval from collections import OrderedDict, defaultdict
from unicodedata import east_asian_width from inspect import getmembers, getmodule, getmro, ismodule, trace
from twisted.internet.task import deferLater
from twisted.internet.defer import returnValue # noqa - used as import target
from twisted.internet import threads, reactor
from os.path import join as osjoin from os.path import join as osjoin
from inspect import ismodule, trace, getmembers, getmodule, getmro from unicodedata import east_asian_width
from collections import defaultdict, OrderedDict
from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import validate_email as django_validate_email
from django.utils import timezone from django.utils import timezone
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.apps import apps
from django.core.validators import validate_email as django_validate_email
from django.core.exceptions import ValidationError as DjangoValidationError
from evennia.utils import logger from evennia.utils import logger
from simpleeval import simple_eval
from twisted.internet import reactor, threads
from twisted.internet.defer import returnValue # noqa - used as import target
from twisted.internet.task import deferLater
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR _EVENNIA_DIR = settings.EVENNIA_DIR
@ -2714,10 +2714,24 @@ def run_in_main_thread(function_or_method, *args, **kwargs):
return threads.blockingCallFromThread(reactor, function_or_method, *args, **kwargs) return threads.blockingCallFromThread(reactor, function_or_method, *args, **kwargs)
_INT2STR_MAP_NOUN = {0: "no", 1: "one", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six", _INT2STR_MAP_NOUN = {
7: "seven", 8: "eight", 9: "nine", 10: "ten", 11: "eleven", 12: "twelve"} 0: "no",
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
10: "ten",
11: "eleven",
12: "twelve",
}
_INT2STR_MAP_ADJ = {1: "1st", 2: "2nd", 3: "3rd"} # rest is Xth. _INT2STR_MAP_ADJ = {1: "1st", 2: "2nd", 3: "3rd"} # rest is Xth.
def int2str(self, number, adjective=False): def int2str(self, number, adjective=False):
""" """
Convert a number to an English string for better display; so 1 -> one, 2 -> two etc Convert a number to an English string for better display; so 1 -> one, 2 -> two etc