Add combat summary stat

This commit is contained in:
Griatch 2023-03-13 22:13:32 +01:00
parent 7971e6c2ff
commit ceeebbdd79
5 changed files with 123 additions and 40 deletions

View file

@ -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:
```
```

View file

@ -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: <enemy 1>
2: <enemy 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 = ""

View file

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

View file

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

View file

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