From 5c914eb8b0e4f12ef4d4c0260644652e312093b0 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 25 Mar 2023 16:56:57 +0100 Subject: [PATCH] Testing out combat --- .../evadventure/batchscripts/combat_demo.ev | 4 + .../contrib/tutorials/evadventure/combat.py | 371 ++++++++++++------ evennia/contrib/tutorials/evadventure/npcs.py | 6 +- .../contrib/tutorials/evadventure/objects.py | 35 +- .../contrib/tutorials/evadventure/rules.py | 12 +- .../evadventure/tests/test_combat.py | 34 +- evennia/utils/utils.py | 3 +- 7 files changed, 311 insertions(+), 154 deletions(-) diff --git a/evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev b/evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev index f51f409193..6f80ec9b7e 100644 --- a/evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev +++ b/evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev @@ -19,6 +19,10 @@ py self.cmdset.add("evennia.contrib.tutorials.evadventure.combat.TwitchAttackCmd create sword:evennia.contrib.tutorials.evadventure.objects.EvAdventureWeapon +# create a consumable to use + +create potion:evennia.contrib.tutorials.evadventure.objects.EvAdventureConsumable + # dig a combat arena dig arena:evennia.contrib.tutorials.evadventure.rooms.EvAdventureRoom = arena,back diff --git a/evennia/contrib/tutorials/evadventure/combat.py b/evennia/contrib/tutorials/evadventure/combat.py index eeb046f791..6d7e1242f9 100644 --- a/evennia/contrib/tutorials/evadventure/combat.py +++ b/evennia/contrib/tutorials/evadventure/combat.py @@ -97,7 +97,7 @@ from evennia.commands.command import InterruptCommand from evennia.scripts.scripts import DefaultScript from evennia.typeclasses.attributes import AttributeProperty from evennia.utils import dbserialize, delay, evmenu, evtable, logger -from evennia.utils.utils import inherits_from, list_to_string +from evennia.utils.utils import display_len, inherits_from, list_to_string, pad from . import rules from .characters import EvAdventureCharacter @@ -209,15 +209,15 @@ class CombatAction: self.combathandler.fleeing_combatants.pop(self.combatant, None) -class CombatActionDoNothing(CombatAction): +class CombatActionHold(CombatAction): """ Action that does nothing. Note: - Refer to as 'nothing' + Refer to as 'hold' action_dict = { - "key": "nothing" + "key": "hold" } """ @@ -287,9 +287,6 @@ class CombatActionStunt(CombatAction): # to give. defender = target if self.advantage else recipient - self.stunt_type = ABILITY_REVERSE_MAP.get(self.stunt_type, self.stunt_type) - self.defense_type = ABILITY_REVERSE_MAP.get(self.defense_type, self.defense_type) - if not is_success: # trying to give advantage to recipient against target. Target defends against caller is_success, _, txt = rules.dice.opposed_saving_throw( @@ -302,19 +299,19 @@ class CombatActionStunt(CombatAction): ) # deal with results - 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 self.advantage: self.give_advantage(recipient, target) else: self.give_disadvantage(recipient, target) self.msg( - f"%You() $conj(cause) $You({recipient.key}) " + f"$You() $conj(cause) $You({recipient.key}) " f"to gain {'advantage' if self.advantage else 'disadvantage'} " f"against $You({target.key})!" ) else: - self.msg(f"$You({target.key}) resists! $You() $conj(fail) the stunt.") + self.msg(f"$You({target.key}) $conj(resist)! $You() $conj(fail) the stunt.") class CombatActionUseItem(CombatAction): @@ -384,15 +381,23 @@ class CombatActionFlee(CombatAction): def execute(self): - if self.combatant not in self.combathandler.fleeing_combatants: - # we record the turn on which we started fleeing - self.combathandler.fleeing_combatants[self.combatant] = self.combathandler.turn + combathandler = self.combathandler - flee_timeout = self.combathandler.flee_timeout - self.msg( - "$You() $conj(retreat), leaving yourself exposed while doing so (will escape in " - f"{flee_timeout} $pluralize(turn, {flee_timeout}))." - ) + if self.combatant not in combathandler.fleeing_combatants: + # we record the turn on which we started fleeing + combathandler.fleeing_combatants[self.combatant] = self.combathandler.turn + + # show how many turns until successful flight + current_turn = combathandler.turn + started_fleeing = combathandler.fleeing_combatants[self.combatant] + flee_timeout = combathandler.flee_timeout + time_left = flee_timeout - (current_turn - started_fleeing) + + if time_left > 0: + self.msg( + "$You() $conj(retreat), being exposed to attack while doing so (will escape in " + f"{time_left} $pluralize(turn, {time_left}))." + ) def post_execute(self): """ @@ -410,7 +415,7 @@ class EvAdventureCombatHandler(DefaultScript): # available actions in combat action_classes = { - "nothing": CombatActionDoNothing, + "hold": CombatActionHold, "attack": CombatActionAttack, "stunt": CombatActionStunt, "use": CombatActionUseItem, @@ -422,10 +427,10 @@ class EvAdventureCombatHandler(DefaultScript): max_action_queue_size = 1 # fallback action if not selecting anything - fallback_action_dict = {"key": "nothing"} + fallback_action_dict = {"key": "hold"} # how many turns you must be fleeing before escaping - flee_timeout = 1 + flee_timeout = 5 # persistent storage @@ -441,6 +446,9 @@ class EvAdventureCombatHandler(DefaultScript): fleeing_combatants = AttributeProperty(dict) defeated_combatants = AttributeProperty(list) + # usable script properties + # .is_active - show if timer is running + def msg(self, message, combatant=None, broadcast=True): """ Central place for sending messages to combatants. This allows @@ -475,11 +483,14 @@ class EvAdventureCombatHandler(DefaultScript): def add_combatant(self, combatant): """ - Add a new combatant to the battle. + Add a new combatant to the battle. Can be called multiple times safely. Args: *combatants (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to the combat. + Returns: + bool: If this combatant was newly added or not (it was already in combat). + """ if combatant not in self.combatants: self.combatants[combatant] = deque((), maxlen=self.max_action_queue_size) @@ -496,6 +507,18 @@ class EvAdventureCombatHandler(DefaultScript): """ self.combatants.pop(combatant, None) + # clean up twitch cmdset if it exists + combatant.cmdset.remove(TwitchCombatCmdSet) + # clean up menu if it exists + + def start_combat(self, **kwargs): + """ + This actually starts the combat. It's safe to run this multiple times + since it will only start combat if it isn't already running. + + """ + if not self.is_active: + self.start(**kwargs) def stop_combat(self): """ @@ -543,6 +566,69 @@ class EvAdventureCombatHandler(DefaultScript): enemies = pcs return allies, enemies + def get_combat_summary(self, combatant): + """ + Get a 'battle report' - an overview of the current state of combat from the perspective + of one of the sides. + + Args: + combatant (EvAdventureCharacter, EvAdventureNPC): The combatant to get. + + Returns: + EvTable: A table representing the current state of combat. + + Example: + :: + + Goblin shaman (Perfect)[attack] + Gregor (Hurt)[attack] Goblin brawler(Hurt)[attack] + Bob (Perfect)[stunt] vs Goblin grunt 1 (Hurt)[attack] + Goblin grunt 2 (Perfect)[hold] + Goblin grunt 3 (Wounded)[flee] + + """ + allies, enemies = self.get_sides(combatant) + # we must include outselves at the top of the list (we are not returned from get_sides) + allies.insert(0, combatant) + nallies, nenemies = len(allies), len(enemies) + + # prepare colors and hurt-levels + allies = [ + f"{ally} ({ally.hurt_level})[{self.get_next_action_dict(ally)['key']}]" + for ally in allies + ] + enemies = [ + f"{enemy} ({enemy.hurt_level})[{self.get_next_action_dict(enemy)['key']}]" + for enemy in enemies + ] + + # the center column with the 'vs' + vs_column = ["" for _ in range(max(nallies, nenemies))] + vs_column[len(vs_column) // 2] = "|wvs|n" + + # the two allies / enemies columns should be centered vertically + diff = abs(nallies - nenemies) + top_empty = diff // 2 + bot_empty = diff - top_empty + topfill = ["" for _ in range(top_empty)] + botfill = ["" for _ in range(bot_empty)] + + if nallies >= nenemies: + enemies = topfill + enemies + botfill + else: + allies = topfill + allies + botfill + + # make a table with three columns + return evtable.EvTable( + table=[ + evtable.EvColumn(*allies, align="l"), + evtable.EvColumn(*vs_column, align="c"), + evtable.EvColumn(*enemies, align="r"), + ], + border=None, + maxwidth=78, + ) + def queue_action(self, combatant, action_dict): """ Queue an action by adding the new actiondict to the back of the queue. If the @@ -568,10 +654,29 @@ class EvAdventureCombatHandler(DefaultScript): # everyone has inserted an action. Start next turn without waiting! self.force_repeat() + def get_next_action_dict(self, combatant, rotate_queue=True): + """ + Give the action_dict for the next action that will be executed. + + Args: + combatant (EvAdventureCharacter, EvAdventureNPC): The combatant to get the action for. + rotate_queue (bool, optional): Rotate the queue after getting the action dict. + + Returns: + dict: The next action-dict in the queue. + + """ + action_queue = self.combatants[combatant] + action_dict = action_queue[0] if action_queue else self.fallback_action_dict + if rotate_queue: + # rotate the queue to the left so that the first element is now the last one + action_queue.rotate(-1) + return action_dict + def execute_next_action(self, combatant): """ Perform a combatant's next queued action. Note that there is _always_ an action queued, - even if this action is 'do nothing'. We don't pop anything from the queue, instead we keep + even if this action is 'hold'. We don't pop anything from the queue, instead we keep rotating the queue. When the queue has a length of one, this means just repeating the same action over and over. @@ -584,10 +689,8 @@ class EvAdventureCombatHandler(DefaultScript): queue will be rotated to the left and be `[b, c, a]` (so next time, `b` will be used). """ - action_queue = self.combatants[combatant] - action_dict = action_queue[0] if action_queue else self.fallback_action_dict - # rotate the queue to the left so that the first element is now the last one - action_queue.rotate(-1) + # this gets the next dict and rotates the queue + action_dict = self.get_next_action_dict(combatant) # use the action-dict to select and create an action from an action class action_class = self.action_classes[action_dict["key"]] @@ -655,82 +758,38 @@ class EvAdventureCombatHandler(DefaultScript): self.msg(txt) self.stop_combat() - def get_combat_summary(self, combatant): + def at_repeat(self, **kwargs): """ - Get a 'battle report' - an overview of the current state of combat. - - Args: - combatant (EvAdventureCharacter, EvAdventureNPC): The combatant to get. - - Returns: - EvTable: A table representing the current state of combat. - - Example: - :: - - Goblin shaman - Ally (hurt) Goblin brawler - Bob vs Goblin grunt 1 (hurt) - Goblin grunt 2 - Goblin grunt 3 - + This is called every time the script ticks (how fast depends on if this handler runs a + twitch- or turn-based combat). """ - allies, enemies = self.get_sides(combatant) - # we must include outselves at the top of the list (we are not returned from get_sides) - allies.insert(0, combatant) - nallies, nenemies = len(allies), len(enemies) - - # prepare colors and hurt-levels - allies = [f"{ally} ({ally.hurt_level})" for ally in allies] - enemies = [f"{enemy} ({enemy.hurt_level})" for enemy in enemies] - - # the center column with the 'vs' - vs_column = ["" for _ in range(max(nallies, nenemies))] - vs_column[len(vs_column) // 2] = "vs" - - # the two allies / enemies columns should be centered vertically - diff = abs(nallies - nenemies) - top_empty = diff // 2 - bot_empty = diff - top_empty - topfill = ["" for _ in range(top_empty)] - botfill = ["" for _ in range(bot_empty)] - - if nallies >= nenemies: - enemies = topfill + enemies + botfill - else: - allies = topfill + allies + botfill - - # make a table with three columns - return evtable.EvTable( - table=[ - evtable.EvColumn(*allies, align="l"), - evtable.EvColumn(*vs_column, align="c"), - evtable.EvColumn(*enemies, align="r"), - ], - border=None, - width=78, - ) + self.execute_full_turn() -def get_or_create_combathandler(combatant, combathandler_name="combathandler", combat_tick=5): +def get_or_create_combathandler(location, combat_tick=3, combathandler_name="combathandler"): """ Joins or continues combat. This is a access function that will either get the combathandler on the current room or create a new one. Args: - combatant (EvAdventureCharacter, EvAdventureNPC): The one to + location (EvAdventureRoom): Where to start the combat. + combat_tick (int): How often (in seconds) the combathandler will perform a tick. The + shorter this interval, the more 'twitch-like' the combat will be. E.g. + combathandler_name (str): If the combathandler should be stored with a different script + name. Changing this could allow multiple combats to coexist in the same location. Returns: CombatHandler: The new or created combathandler. + Notes: + The combathandler starts disabled; one needs to run `.start` on it once all + (initial) combatants are added. + """ - - location = combatant.location - if not location: raise CombatFailure("Cannot start combat without a location.") - combathandler = location.scripts.get(combathandler_name) + combathandler = location.scripts.get(combathandler_name).first() if not combathandler: combathandler = create_script( EvAdventureCombatHandler, @@ -738,8 +797,8 @@ def get_or_create_combathandler(combatant, combathandler_name="combathandler", c obj=location, interval=combat_tick, persistent=True, + autostart=False, ) - combathandler.add_combatant(combatant) return combathandler @@ -766,9 +825,10 @@ Examples of commands: - |yuse |n - use/consume an item in your inventory - |yuse on |n - use an item on an enemy or ally + - |yhold|n - hold your attack, doing nothing - |yflee|n - start to flee or disengage from combat -Use |yhelp |n for more info.""" +Use |yhelp |n for more info. Use |yhelp combat|n to re-show this list.""" class _CmdCombatBase(Command): @@ -779,14 +839,16 @@ class _CmdCombatBase(Command): """ combathandler_name = "combathandler" - combat_tick = 2 + combat_tick = 3 flee_timeout = 5 @property def combathandler(self): - combathandler = getattr(self, "combathandler", None) + combathandler = getattr(self, "_combathandler", None) if not combathandler: - self.combathandler = combathandler = get_or_create_combathandler(self.caller) + self._combathandler = combathandler = get_or_create_combathandler( + self.caller.location, combat_tick=2 + ) return combathandler def parse(self): @@ -805,16 +867,20 @@ class TwitchCombatCmdSet(CmdSet): """ + name = "Twitchcombat cmdset" priority = 1 mergetype = "Union" # use Replace to lock down all other commands no_exits = True # don't allow combatants to walk away def at_cmdset_creation(self): self.add(CmdTwitchAttack()) + self.add(CmdLook()) + self.add(CmdHelpCombat()) + self.add(CmdHold()) self.add(CmdStunt()) self.add(CmdUseItem()) self.add(CmdWield()) - self.add(CmdUseFlee()) + self.add(CmdFlee()) class CmdTwitchAttack(_CmdCombatBase): @@ -852,47 +918,71 @@ class CmdTwitchAttack(_CmdCombatBase): self.msg(f"{target.get_display_name(self.caller)} is already down.") return - # this can be done over and over + if target.is_pc and not target.location.allow_pvp: + self.msg("PvP combat is not allowed here!") + return + + # add combatants to combathandler. this can be done safely over and over is_new = self.combathandler.add_combatant(self.caller) if is_new: # just joined combat - add the combat cmdset - self.caller.cmdset.add(CombatCmdSet) + self.caller.cmdset.add(TwitchCombatCmdSet, persistent=True) self.msg(_COMBAT_HELP) + + is_new = self.combathandler.add_combatant(target) + if is_new and target.is_pc: + # a pvp battle + target.cmdset.add(TwitchCombatCmdSet, persistent=True) + target.msg(_COMBAT_HELP) + self.combathandler.queue_action(self.caller, {"key": "attack", "target": target}) - self.msg("You prepare to attack!") + self.combathandler.start_combat() + self.msg(f"You attack {target.get_display_name(self.caller)}!") class CmdLook(default_cmds.CmdLook): - - key = "look" - aliases = ["l"] - - template = """ -|c{room_name} |r(In Combat!)|n -{room_desc} -⚔ ⚔ ⚔ ⚔ ⚔ -{combat_summary} - """.strip() - def func(self): if not self.args: - # when looking around with no argument, show the room description followed by the - # current combat state. - location = self.caller.location - combathandler = get_or_create_combathandler(self.caller) - - self.caller.msg( - self.template.format( - room_name=location.get_display_name(self.caller), - room_desc=caller.at_look(location), - combat_summary=combathandler.get_combat_summary(self.caller), - ) - ) + combathandler = get_or_create_combathandler(self.caller.location) + txt = str(combathandler.get_combat_summary(self.caller)) + maxwidth = max(display_len(line) for line in txt.strip().split("\n")) + self.msg(f"|r{pad(' Combat Status ', width=maxwidth, fillchar='-')}|n\n{txt}") else: # use regular look to look at things super().func() +class CmdHelpCombat(_CmdCombatBase): + """ + Re-show the combat command summary. + + Usage: + help combat + + """ + + key = "help combat" + + def func(self): + self.msg(_COMBAT_HELP) + + +class CmdHold(_CmdCombatBase): + """ + Hold back your blows, doing nothing. + + Usage: + hold + + """ + + key = "hold" + + def func(self): + self.combathandler.queue_action(self.caller, {"key": "hold"}) + self.msg("You hold, doing nothing.") + + class CmdStunt(_CmdCombatBase): """ Perform a combat stunt, that boosts an ally against a target, or @@ -923,26 +1013,46 @@ class CmdStunt(_CmdCombatBase): def parse(self): super().parse() args = self.args + + if not args: + self.msg("Usage: [ability] of vs ") + raise InterruptCommand() + if "of" in args: self.stunt_type, args = (part.strip() for part in args.split("of", 1)) else: self.stunt_type, args = (part.strip() for part in args.split(None, 1)) + # convert stunt-type to an Ability, like Ability.STR etc + if not self.stunt_type in ABILITY_REVERSE_MAP: + self.msg("That's not a valid ability.") + raise InterruptCommand() + self.stunt_type = ABILITY_REVERSE_MAP[self.stunt_type] + if " vs " in args: self.recipient, self.target = (part.strip() for part in args.split(" vs ")) elif self.cmdname == "foil": self.recipient, self.target = "me", args.strip() else: self.recipient, self.target = args.strip(), "me" - self.advantage = self.cmdname == "boost" + self.advantage = self.cmdname != "foil" def func(self): + + combathandler = self.combathandler + target = self.caller.search(self.target, candidates=combathandler.combatants.keys()) + if not target: + return + recipient = self.caller.search(self.recipient, candidates=combathandler.combatants.keys()) + if not recipient: + return + self.combathandler.queue_action( self.caller, { "key": "stunt", - "recipient": self.recipient, - "target": self.target, + "recipient": recipient, + "target": target, "advantage": self.advantage, "stunt_type": self.stunt_type, "defense_type": self.stunt_type, @@ -973,7 +1083,10 @@ class CmdUseItem(_CmdCombatBase): super().parse() args = self.args - if "on" in args: + if not args: + self.msg("What do you want to use?") + raise InterruptCommand() + elif "on" in args: self.item, self.target = (part.strip() for part in args.split("on", 1)) else: self.item, *target = args.split(None, 1) @@ -1017,6 +1130,12 @@ class CmdWield(_CmdCombatBase): key = "wield" help_category = "combat" + def parse(self): + if not self.args: + self.msg("What do you want to wield?") + raise InterruptCommand() + super().parse() + def func(self): item = self.caller.search( @@ -1070,7 +1189,7 @@ class TwitchAttackCmdSet(CmdSet): def _get_combathandler(caller): evmenu = caller.ndb._evmenu if not hasattr(evmenu, "combathandler"): - evmenu.combathandler = get_or_create_combathandler(caller) + evmenu.combathandler = get_or_create_combathandler(caller.location) return evmenu.combathandler @@ -1330,8 +1449,8 @@ def node_combat(caller, raw_string, **kwargs): "goto": (_queue_action, {"flee": {"key": "flee"}}), }, { - "desc": "do nothing", - "goto": (_queue_action, {"action_dict": {"key": "nothing"}}), + "desc": "hold, doing nothing", + "goto": (_queue_action, {"action_dict": {"key": "hold"}}), }, ] @@ -1364,7 +1483,7 @@ class CmdTurnAttack(Command): if not target: return - combathandler = get_or_create_combathandler(self.caller, combat_tick=30) + combathandler = get_or_create_combathandler(self.caller.location, combat_tick=30) combathandler.add_combatant(self.caller) # build and start the menu diff --git a/evennia/contrib/tutorials/evadventure/npcs.py b/evennia/contrib/tutorials/evadventure/npcs.py index 70113836ce..2e68612577 100644 --- a/evennia/contrib/tutorials/evadventure/npcs.py +++ b/evennia/contrib/tutorials/evadventure/npcs.py @@ -6,6 +6,7 @@ from random import choice from evennia import DefaultCharacter from evennia.typeclasses.attributes import AttributeProperty +from evennia.typeclasses.tags import TagProperty from evennia.utils.evmenu import EvMenu from evennia.utils.utils import make_iter @@ -53,6 +54,9 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter): weapon = AttributeProperty(default=WeaponEmptyHand, autocreate=False) # instead of inventory coins = AttributeProperty(default=1, autocreate=False) # coin loot + # if this npc is attacked, everyone with the same tag in the current location will also be pulled into combat. + group = TagProperty("npcs") + @property def strength(self): return self.hit_dice @@ -88,7 +92,7 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter): """ self.hp = self.hp_max - def ai_combat_next_action(self): + def ai_combat_next_action(self, **kwargs): """ The combat engine should ask this method in order to get the next action the npc should perform in combat. diff --git a/evennia/contrib/tutorials/evadventure/objects.py b/evennia/contrib/tutorials/evadventure/objects.py index 083e06a885..b8882f9a22 100644 --- a/evennia/contrib/tutorials/evadventure/objects.py +++ b/evennia/contrib/tutorials/evadventure/objects.py @@ -177,9 +177,30 @@ class EvAdventureWeapon(EvAdventureObject): defense_type = AttributeProperty(Ability.ARMOR) damage_roll = AttributeProperty("1d6") + def get_display_name(self, looker=None, **kwargs): + quality = self.quality + + quality_txt = "" + if quality <= 0: + quality_txt = "|r(broken!)|n" + elif quality < 2: + quality_txt = "|y(damaged)|n" + elif quality < 3: + quality_txt = "|Y(chipped)|n" + + return super().get_display_name(looker=looker, **kwargs) + quality_txt + + def at_pre_use(self, user, *args, **kwargs): + if self.quality <= 0: + user.msg(f"{self.get_display_name(user)} is broken and can't be used!") + return False + return super().at_pre_use(user, *args, **kwargs) + def use(self, attacker, target, *args, advantage=False, disadvantage=False, **kwargs): """When a weapon is used, it attacks an opponent""" + location = attacker.location + is_hit, quality, txt = rules.dice.opposed_saving_throw( attacker, target, @@ -188,7 +209,11 @@ class EvAdventureWeapon(EvAdventureObject): advantage=advantage, disadvantage=disadvantage, ) - self.msg(f"$You() $conj(attack) $You({target.key}) with {self.key}: {txt}") + location.msg_contents( + f"$You() $conj(attack) $You({target.key}) with {self.key}: {txt}", + from_obj=attacker, + mapping={target.key: target}, + ) if is_hit: # enemy hit, calculate damage dmg = rules.dice.roll(self.damage_roll) @@ -201,8 +226,8 @@ class EvAdventureWeapon(EvAdventureObject): ) else: message = f" $You() $conj(hit) $You({target.key}) for |r{dmg}|n damage!" - self.msg(message) + location.msg_contents(message, from_obj=attacker, mapping={target.key: target}) # call hook target.at_damage(dmg, attacker=attacker) @@ -212,7 +237,11 @@ class EvAdventureWeapon(EvAdventureObject): if quality is Ability.CRITICAL_FAILURE: self.quality -= 1 message += ".. it's a |rcritical miss!|n, damaging the weapon." - self.msg(message) + location.msg_contents(message, from_obj=attacker, mapping={target.key: target}) + + def at_post_use(self, user, *args, **kwargs): + if self.quality <= 0: + user.msg(f"|r{self.get_display_name(user)} breaks and can no longer be used!") class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable): diff --git a/evennia/contrib/tutorials/evadventure/rules.py b/evennia/contrib/tutorials/evadventure/rules.py index 62ec4d0dd5..edac0df76b 100644 --- a/evennia/contrib/tutorials/evadventure/rules.py +++ b/evennia/contrib/tutorials/evadventure/rules.py @@ -157,13 +157,13 @@ class EvAdventureRollEngine: bontxt = f"(+{bonus})" modtxt = "" if modifier: - 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 "" txt = ( - f"rolled {dice_roll} on {rolltxt} " + f" rolled {dice_roll} on {rolltxt} " f"+ {bonus_type.value}{bontxt}{modtxt} vs " - f"{target} -> |w{result}{qualtxt}|n" + f"{target} -> |w{'|GSuccess|w' if result else '|RFail|w'}{qualtxt}|n" ) return (dice_roll + bonus + modifier) > target, quality, txt @@ -332,9 +332,11 @@ class EvAdventureRollEngine: setattr(character, abi, current_abi) character.msg( - "~" * 78 + "\n|yYou survive your brush with death, " + "~" * 78 + + "\n|yYou survive your brush with death, " f"but are |r{result.upper()}|y and permanently |rlose {loss} {abi}|y.|n\n" - f"|GYou recover |g{new_hp}|G health|.\n" + "~" * 78 + f"|GYou recover |g{new_hp}|G health|.\n" + + "~" * 78 ) diff --git a/evennia/contrib/tutorials/evadventure/tests/test_combat.py b/evennia/contrib/tutorials/evadventure/tests/test_combat.py index e280f91ba8..8a9e87787d 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_combat.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_combat.py @@ -62,12 +62,12 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): # add target to combat self.combathandler.add_combatant(self.target) - def _get_action(self, action_dict={"key": "nothing"}): + def _get_action(self, action_dict={"key": "hold"}): action_class = self.combathandler.action_classes[action_dict["key"]] return action_class(self.combathandler, self.combatant, action_dict) def _run_actions( - self, action_dict, action_dict2={"key": "nothing"}, combatant_msg=None, target_msg=None + self, action_dict, action_dict2={"key": "hold"}, combatant_msg=None, target_msg=None ): """ Helper method to run an action and check so combatant saw the expected message. @@ -90,7 +90,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): self.assertEqual( dict(chandler.action_classes), { - "nothing": combat.CombatActionDoNothing, + "hold": combat.CombatActionHold, "attack": combat.CombatActionAttack, "stunt": combat.CombatActionStunt, "use": combat.CombatActionUseItem, @@ -176,31 +176,31 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): def test_queue_and_execute_action(self): """Queue actions and execute""" - donothing = {"key": "nothing"} + hold = {"key": "hold"} - self.combathandler.queue_action(self.combatant, donothing) + self.combathandler.queue_action(self.combatant, hold) self.assertEqual( dict(self.combathandler.combatants), - {self.combatant: deque([donothing]), self.target: deque()}, + {self.combatant: deque([hold]), self.target: deque()}, ) mock_action = Mock() - self.combathandler.action_classes["nothing"] = Mock(return_value=mock_action) + self.combathandler.action_classes["hold"] = Mock(return_value=mock_action) self.combathandler.execute_next_action(self.combatant) - self.combathandler.action_classes["nothing"].assert_called_with( - self.combathandler, self.combatant, donothing + self.combathandler.action_classes["hold"].assert_called_with( + self.combathandler, self.combatant, hold ) mock_action.execute.assert_called_once() def test_execute_full_turn(self): """Run a full (passive) turn""" - donothing = {"key": "nothing"} + hold = {"key": "hold"} - self.combathandler.queue_action(self.combatant, donothing) - self.combathandler.queue_action(self.target, donothing) + self.combathandler.queue_action(self.combatant, hold) + self.combathandler.queue_action(self.target, hold) self.combathandler.execute_next_action = Mock() @@ -216,7 +216,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): combatant = self.combatant target = self.target - action = self._get_action({"key": "nothing"}) + action = self._get_action({"key": "hold"}) self.assertTrue(action.can_use()) @@ -235,10 +235,10 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): action.msg(f"$You() attack $You({target.key}).") combatant.msg.assert_called_with(text=("You attack testmonster.", {}), from_obj=combatant) - def test_action__do_nothing(self): - """Do nothing""" + def test_action__hold(self): + """Hold, doing nothing""" - actiondict = {"key": "nothing"} + actiondict = {"key": "hold"} self._run_actions(actiondict, actiondict) self.assertEqual(self.combathandler.turn, 1) @@ -417,7 +417,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): from_obj=self.combatant, ) # Check that enemies have advantage against you now - action = combat.CombatAction(self.combathandler, self.target, {"key": "nothing"}) + action = combat.CombatAction(self.combathandler, self.target, {"key": "hold"}) self.assertTrue(action.has_advantage(self.target, self.combatant)) # second flee should remove combatant diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 180f8e8b56..e48fe1f4ee 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -34,13 +34,12 @@ from django.core.validators import validate_email as django_validate_email from django.utils import timezone from django.utils.html import strip_tags from django.utils.translation import gettext as _ +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 -from evennia.utils import logger - _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE _EVENNIA_DIR = settings.EVENNIA_DIR _GAME_DIR = settings.GAME_DIR