From ceeebbdd792b37a5a3ce2508fd623489befd4b1d Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 13 Mar 2023 22:13:32 +0100 Subject: [PATCH] Add combat summary stat --- ....tutorials.evadventure.combat_turnbased.md | 4 +- .../{combat_turnbased.py => combat.py} | 117 ++++++++++++++---- .../contrib/tutorials/evadventure/commands.py | 2 +- evennia/contrib/tutorials/evadventure/npcs.py | 2 +- .../evadventure/tests/test_combat.py | 38 ++++-- 5 files changed, 123 insertions(+), 40 deletions(-) rename evennia/contrib/tutorials/evadventure/{combat_turnbased.py => combat.py} (97%) diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.combat_turnbased.md b/docs/source/api/evennia.contrib.tutorials.evadventure.combat_turnbased.md index ffccb7f735..d46e70a590 100644 --- a/docs/source/api/evennia.contrib.tutorials.evadventure.combat_turnbased.md +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.combat_turnbased.md @@ -1,5 +1,5 @@ ```{eval-rst} -evennia.contrib.tutorials.evadventure.combat\_turnbased +evennia.contrib.tutorials.evadventure.combat\_turnbased ============================================================== .. automodule:: evennia.contrib.tutorials.evadventure.combat_turnbased @@ -7,4 +7,4 @@ evennia.contrib.tutorials.evadventure.combat\_turnbased :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat.py similarity index 97% rename from evennia/contrib/tutorials/evadventure/combat_turnbased.py rename to evennia/contrib/tutorials/evadventure/combat.py index efdc99acc4..1b8b897fbf 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat.py @@ -83,17 +83,8 @@ You hang back, passively defending. ------------------- Disengage You retreat, getting ready to get out of combat. Use two times in a row to -leave combat. You flee last in a round. If anyone Blocks your retreat, this counter resets. +leave combat. You flee last in a round. -------------------- Block Fleeing - -You move to block the escape route of an opponent. If you win a DEX challenge, -you'll negate the target's disengage action(s). - -Choose who to block: -1: -2: -3: ... """ @@ -101,7 +92,7 @@ Choose who to block: import random from collections import defaultdict, deque -from evennia import CmdSet, Command, create_script +from evennia import CmdSet, Command, create_script, default_cmds from evennia.commands.command import InterruptCommand from evennia.scripts.scripts import DefaultScript from evennia.typeclasses.attributes import AttributeProperty @@ -571,7 +562,7 @@ class EvAdventureCombatHandler(DefaultScript): self.combatants[combatant].append(action_dict) # track who inserted actions this turn (non-persistent) - did_action = set(self.nbd.did_action or ()) + did_action = set(self.ndb.did_action or ()) did_action.add(combatant) if len(did_action) >= len(self.combatants): # everyone has inserted an action. Start next turn without waiting! @@ -668,6 +659,15 @@ class EvAdventureCombatHandler(DefaultScript): """ 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) @@ -676,9 +676,40 @@ class EvAdventureCombatHandler(DefaultScript): """ 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, + ) def get_or_create_combathandler(combatant, combathandler_name="combathandler", combat_tick=5): @@ -767,24 +798,35 @@ class _CmdCombatBase(Command): raise InterruptCommand() -class CombatCmdSet(CmdSet): - """ - Commands to make available while in combat. Note that - the 'attack' command should also be added to the CharacterCmdSet, - in order for the user to attack things. +class CmdLook(default_cmds.CmdLook): - """ + key = "look" + aliases = ["l"] - priority = 1 - mergetype = "Union" # use Replace to lock down all other commands - no_exits = True # don't allow combatants to walk away + template = """ +|c{room_name} |r(In Combat!)|n +{room_desc} +⚔ ⚔ ⚔ ⚔ ⚔ +{combat_summary} + """.strip() - def at_cmdset_creation(self): - self.add(CmdAttack()) - self.add(CmdStunt()) - self.add(CmdUseItem()) - self.add(CmdWield()) - self.add(CmdUseFlee()) + 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), + ) + ) + else: + # use regular look to look at things + super().func() class CmdAttack(_CmdCombatBase): @@ -987,6 +1029,26 @@ class CmdFlee(_CmdCombatBase): self.msg("You prepare to flee!") +class CombatCmdSet(CmdSet): + """ + Commands to make available while in combat. Note that + the 'attack' command should also be added to the CharacterCmdSet, + in order for the user to attack things. + + """ + + 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(CmdAttack()) + self.add(CmdStunt()) + self.add(CmdUseItem()) + self.add(CmdWield()) + self.add(CmdUseFlee()) + + # ----------------------------------------------------------------------------------- # # Turn-based combat (Final Fantasy style), using a menu @@ -1038,6 +1100,7 @@ def node_choose_target(caller, raw_string, **kwargs): def node_combat(caller, raw_string, **kwargs): """Base combat menu""" + text = "" diff --git a/evennia/contrib/tutorials/evadventure/commands.py b/evennia/contrib/tutorials/evadventure/commands.py index 95cb71ca5e..01383fb20c 100644 --- a/evennia/contrib/tutorials/evadventure/commands.py +++ b/evennia/contrib/tutorials/evadventure/commands.py @@ -33,7 +33,7 @@ from evennia import CmdSet, Command, InterruptCommand from evennia.utils.evmenu import EvMenu from evennia.utils.utils import inherits_from -from .combat_turnbased import CombatFailure, join_combat +from .combat import CombatFailure, join_combat from .enums import WieldLocation from .equipment import EquipmentError from .npcs import EvAdventureTalkativeNPC diff --git a/evennia/contrib/tutorials/evadventure/npcs.py b/evennia/contrib/tutorials/evadventure/npcs.py index 2b717d5287..70113836ce 100644 --- a/evennia/contrib/tutorials/evadventure/npcs.py +++ b/evennia/contrib/tutorials/evadventure/npcs.py @@ -256,7 +256,7 @@ class EvAdventureMob(EvAdventureNPC): combatant in the current combat handler. """ - from .combat_turnbased import CombatActionAttack, CombatActionDoNothing + from .combat import CombatActionAttack, CombatActionDoNothing if self.is_idle: # mob just stands around diff --git a/evennia/contrib/tutorials/evadventure/tests/test_combat.py b/evennia/contrib/tutorials/evadventure/tests/test_combat.py index 58569349ab..e280f91ba8 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_combat.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_combat.py @@ -7,9 +7,10 @@ from collections import deque from unittest.mock import Mock, call, patch from evennia.utils import create +from evennia.utils.ansi import strip_ansi from evennia.utils.test_resources import BaseEvenniaTest -from .. import combat_turnbased as combat +from .. import combat from ..characters import EvAdventureCharacter from ..enums import Ability, WieldLocation from ..npcs import EvAdventureMob @@ -28,11 +29,11 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): # make sure to mock away all time-keeping elements @patch( - "evennia.contrib.tutorials.evadventure.combat_turnbased.EvAdventureCombatHandler.interval", + "evennia.contrib.tutorials.evadventure.combat.EvAdventureCombatHandler.interval", new=-1, ) @patch( - "evennia.contrib.tutorials.evadventure.combat_turnbased.delay", + "evennia.contrib.tutorials.evadventure.combat.delay", new=Mock(return_value=None), ) def setUp(self): @@ -153,6 +154,25 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): allies, enemies = self.combathandler.get_sides(self.target) self.assertEqual((allies, enemies), ([target2], [self.combatant, combatant2])) + def test_get_combat_summary(self): + """Test combat summary""" + + # as seen from one side + result = str(self.combathandler.get_combat_summary(self.combatant)) + + self.assertEqual( + strip_ansi(result), + " testchar (Perfect) vs testmonster (Perfect) ", + ) + + # as seen from other side + result = str(self.combathandler.get_combat_summary(self.target)) + + self.assertEqual( + strip_ansi(result), + " testmonster (Perfect) vs testchar (Perfect) ", + ) + def test_queue_and_execute_action(self): """Queue actions and execute""" @@ -224,7 +244,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): self.combatant.msg.assert_not_called() - @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint") + @patch("evennia.contrib.tutorials.evadventure.combat.rules.randint") def test_attack__miss(self, mock_randint): actiondict = {"key": "attack", "target": self.target} @@ -233,7 +253,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): self._run_actions(actiondict) self.assertEqual(self.target.hp, 4) - @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint") + @patch("evennia.contrib.tutorials.evadventure.combat.rules.randint") def test_attack__success__still_alive(self, mock_randint): actiondict = {"key": "attack", "target": self.target} @@ -243,7 +263,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): self._run_actions(actiondict) self.assertEqual(self.target.hp, 9) - @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint") + @patch("evennia.contrib.tutorials.evadventure.combat.rules.randint") def test_attack__success__kill(self, mock_randint): actiondict = {"key": "attack", "target": self.target} @@ -253,7 +273,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): # 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.rules.randint") def test_stunt_fail(self, mock_randint): action_dict = { "key": "stunt", @@ -268,7 +288,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): 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") + @patch("evennia.contrib.tutorials.evadventure.combat.rules.randint") def test_stunt_advantage__success(self, mock_randint): action_dict = { "key": "stunt", @@ -284,7 +304,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest): bool(self.combathandler.advantage_matrix[self.combatant][self.target]), True ) - @patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint") + @patch("evennia.contrib.tutorials.evadventure.combat.rules.randint") def test_stunt_disadvantage__success(self, mock_randint): action_dict = { "key": "stunt",