Testing out combat

This commit is contained in:
Griatch 2023-03-25 16:56:57 +01:00
parent 97b11ccea7
commit 5c914eb8b0
7 changed files with 311 additions and 154 deletions

View file

@ -19,6 +19,10 @@ py self.cmdset.add("evennia.contrib.tutorials.evadventure.combat.TwitchAttackCmd
create sword:evennia.contrib.tutorials.evadventure.objects.EvAdventureWeapon 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 a combat arena
dig arena:evennia.contrib.tutorials.evadventure.rooms.EvAdventureRoom = arena,back dig arena:evennia.contrib.tutorials.evadventure.rooms.EvAdventureRoom = arena,back

View file

@ -97,7 +97,7 @@ from evennia.commands.command import InterruptCommand
from evennia.scripts.scripts import DefaultScript from evennia.scripts.scripts import DefaultScript
from evennia.typeclasses.attributes import AttributeProperty from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils import dbserialize, delay, evmenu, evtable, logger 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 . import rules
from .characters import EvAdventureCharacter from .characters import EvAdventureCharacter
@ -209,15 +209,15 @@ class CombatAction:
self.combathandler.fleeing_combatants.pop(self.combatant, None) self.combathandler.fleeing_combatants.pop(self.combatant, None)
class CombatActionDoNothing(CombatAction): class CombatActionHold(CombatAction):
""" """
Action that does nothing. Action that does nothing.
Note: Note:
Refer to as 'nothing' Refer to as 'hold'
action_dict = { action_dict = {
"key": "nothing" "key": "hold"
} }
""" """
@ -287,9 +287,6 @@ class CombatActionStunt(CombatAction):
# to give. # to give.
defender = target if self.advantage else recipient 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: if not is_success:
# trying to give advantage to recipient against target. Target defends against caller # trying to give advantage to recipient against target. Target defends against caller
is_success, _, txt = rules.dice.opposed_saving_throw( is_success, _, txt = rules.dice.opposed_saving_throw(
@ -302,19 +299,19 @@ class CombatActionStunt(CombatAction):
) )
# deal with results # 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 is_success:
if self.advantage: if self.advantage:
self.give_advantage(recipient, target) self.give_advantage(recipient, target)
else: else:
self.give_disadvantage(recipient, target) self.give_disadvantage(recipient, target)
self.msg( 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"to gain {'advantage' if self.advantage else 'disadvantage'} "
f"against $You({target.key})!" f"against $You({target.key})!"
) )
else: 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): class CombatActionUseItem(CombatAction):
@ -384,15 +381,23 @@ class CombatActionFlee(CombatAction):
def execute(self): def execute(self):
if self.combatant not in self.combathandler.fleeing_combatants: combathandler = self.combathandler
# we record the turn on which we started fleeing
self.combathandler.fleeing_combatants[self.combatant] = self.combathandler.turn
flee_timeout = self.combathandler.flee_timeout if self.combatant not in combathandler.fleeing_combatants:
self.msg( # we record the turn on which we started fleeing
"$You() $conj(retreat), leaving yourself exposed while doing so (will escape in " combathandler.fleeing_combatants[self.combatant] = self.combathandler.turn
f"{flee_timeout} $pluralize(turn, {flee_timeout}))."
) # 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): def post_execute(self):
""" """
@ -410,7 +415,7 @@ class EvAdventureCombatHandler(DefaultScript):
# available actions in combat # available actions in combat
action_classes = { action_classes = {
"nothing": CombatActionDoNothing, "hold": CombatActionHold,
"attack": CombatActionAttack, "attack": CombatActionAttack,
"stunt": CombatActionStunt, "stunt": CombatActionStunt,
"use": CombatActionUseItem, "use": CombatActionUseItem,
@ -422,10 +427,10 @@ class EvAdventureCombatHandler(DefaultScript):
max_action_queue_size = 1 max_action_queue_size = 1
# fallback action if not selecting anything # 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 # how many turns you must be fleeing before escaping
flee_timeout = 1 flee_timeout = 5
# persistent storage # persistent storage
@ -441,6 +446,9 @@ class EvAdventureCombatHandler(DefaultScript):
fleeing_combatants = AttributeProperty(dict) fleeing_combatants = AttributeProperty(dict)
defeated_combatants = AttributeProperty(list) defeated_combatants = AttributeProperty(list)
# usable script properties
# .is_active - show if timer is running
def msg(self, message, combatant=None, broadcast=True): def msg(self, message, combatant=None, broadcast=True):
""" """
Central place for sending messages to combatants. This allows Central place for sending messages to combatants. This allows
@ -475,11 +483,14 @@ class EvAdventureCombatHandler(DefaultScript):
def add_combatant(self, combatant): 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: Args:
*combatants (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to *combatants (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to
the combat. the combat.
Returns:
bool: If this combatant was newly added or not (it was already in combat).
""" """
if combatant not in self.combatants: if combatant not in self.combatants:
self.combatants[combatant] = deque((), maxlen=self.max_action_queue_size) self.combatants[combatant] = deque((), maxlen=self.max_action_queue_size)
@ -496,6 +507,18 @@ class EvAdventureCombatHandler(DefaultScript):
""" """
self.combatants.pop(combatant, None) 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): def stop_combat(self):
""" """
@ -543,6 +566,69 @@ class EvAdventureCombatHandler(DefaultScript):
enemies = pcs enemies = pcs
return allies, enemies 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): def queue_action(self, combatant, action_dict):
""" """
Queue an action by adding the new actiondict to the back of the queue. If the 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! # everyone has inserted an action. Start next turn without waiting!
self.force_repeat() 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): def execute_next_action(self, combatant):
""" """
Perform a combatant's next queued action. Note that there is _always_ an action queued, 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 rotating the queue. When the queue has a length of one, this means just repeating the
same action over and over. 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). queue will be rotated to the left and be `[b, c, a]` (so next time, `b` will be used).
""" """
action_queue = self.combatants[combatant] # this gets the next dict and rotates the queue
action_dict = action_queue[0] if action_queue else self.fallback_action_dict action_dict = self.get_next_action_dict(combatant)
# rotate the queue to the left so that the first element is now the last one
action_queue.rotate(-1)
# use the action-dict to select and create an action from an action class # use the action-dict to select and create an action from an action class
action_class = self.action_classes[action_dict["key"]] action_class = self.action_classes[action_dict["key"]]
@ -655,82 +758,38 @@ class EvAdventureCombatHandler(DefaultScript):
self.msg(txt) self.msg(txt)
self.stop_combat() 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. This is called every time the script ticks (how fast depends on if this handler runs a
twitch- or turn-based 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
""" """
allies, enemies = self.get_sides(combatant) self.execute_full_turn()
# 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): 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 Joins or continues combat. This is a access function that will either get the
combathandler on the current room or create a new one. combathandler on the current room or create a new one.
Args: 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: Returns:
CombatHandler: The new or created combathandler. 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: if not location:
raise CombatFailure("Cannot start combat without a 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: if not combathandler:
combathandler = create_script( combathandler = create_script(
EvAdventureCombatHandler, EvAdventureCombatHandler,
@ -738,8 +797,8 @@ def get_or_create_combathandler(combatant, combathandler_name="combathandler", c
obj=location, obj=location,
interval=combat_tick, interval=combat_tick,
persistent=True, persistent=True,
autostart=False,
) )
combathandler.add_combatant(combatant)
return combathandler return combathandler
@ -766,9 +825,10 @@ Examples of commands:
- |yuse <item>|n - use/consume an item in your inventory - |yuse <item>|n - use/consume an item in your inventory
- |yuse <item> on <target>|n - use an item on an enemy or ally - |yuse <item> on <target>|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 - |yflee|n - start to flee or disengage from combat
Use |yhelp <command>|n for more info.""" Use |yhelp <command>|n for more info. Use |yhelp combat|n to re-show this list."""
class _CmdCombatBase(Command): class _CmdCombatBase(Command):
@ -779,14 +839,16 @@ class _CmdCombatBase(Command):
""" """
combathandler_name = "combathandler" combathandler_name = "combathandler"
combat_tick = 2 combat_tick = 3
flee_timeout = 5 flee_timeout = 5
@property @property
def combathandler(self): def combathandler(self):
combathandler = getattr(self, "combathandler", None) combathandler = getattr(self, "_combathandler", None)
if not combathandler: 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 return combathandler
def parse(self): def parse(self):
@ -805,16 +867,20 @@ class TwitchCombatCmdSet(CmdSet):
""" """
name = "Twitchcombat cmdset"
priority = 1 priority = 1
mergetype = "Union" # use Replace to lock down all other commands mergetype = "Union" # use Replace to lock down all other commands
no_exits = True # don't allow combatants to walk away no_exits = True # don't allow combatants to walk away
def at_cmdset_creation(self): def at_cmdset_creation(self):
self.add(CmdTwitchAttack()) self.add(CmdTwitchAttack())
self.add(CmdLook())
self.add(CmdHelpCombat())
self.add(CmdHold())
self.add(CmdStunt()) self.add(CmdStunt())
self.add(CmdUseItem()) self.add(CmdUseItem())
self.add(CmdWield()) self.add(CmdWield())
self.add(CmdUseFlee()) self.add(CmdFlee())
class CmdTwitchAttack(_CmdCombatBase): class CmdTwitchAttack(_CmdCombatBase):
@ -852,47 +918,71 @@ class CmdTwitchAttack(_CmdCombatBase):
self.msg(f"{target.get_display_name(self.caller)} is already down.") self.msg(f"{target.get_display_name(self.caller)} is already down.")
return 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) is_new = self.combathandler.add_combatant(self.caller)
if is_new: if is_new:
# just joined combat - add the combat cmdset # just joined combat - add the combat cmdset
self.caller.cmdset.add(CombatCmdSet) self.caller.cmdset.add(TwitchCombatCmdSet, persistent=True)
self.msg(_COMBAT_HELP) 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.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): 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): def func(self):
if not self.args: if not self.args:
# when looking around with no argument, show the room description followed by the combathandler = get_or_create_combathandler(self.caller.location)
# current combat state. txt = str(combathandler.get_combat_summary(self.caller))
location = self.caller.location maxwidth = max(display_len(line) for line in txt.strip().split("\n"))
combathandler = get_or_create_combathandler(self.caller) self.msg(f"|r{pad(' Combat Status ', width=maxwidth, fillchar='-')}|n\n{txt}")
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: else:
# use regular look to look at things # use regular look to look at things
super().func() 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): class CmdStunt(_CmdCombatBase):
""" """
Perform a combat stunt, that boosts an ally against a target, or Perform a combat stunt, that boosts an ally against a target, or
@ -923,26 +1013,46 @@ class CmdStunt(_CmdCombatBase):
def parse(self): def parse(self):
super().parse() super().parse()
args = self.args args = self.args
if not args:
self.msg("Usage: [ability] of <recipient> vs <target>")
raise InterruptCommand()
if "of" in args: if "of" in args:
self.stunt_type, args = (part.strip() for part in args.split("of", 1)) self.stunt_type, args = (part.strip() for part in args.split("of", 1))
else: else:
self.stunt_type, args = (part.strip() for part in args.split(None, 1)) 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: if " vs " in args:
self.recipient, self.target = (part.strip() for part in args.split(" vs ")) self.recipient, self.target = (part.strip() for part in args.split(" vs "))
elif self.cmdname == "foil": elif self.cmdname == "foil":
self.recipient, self.target = "me", args.strip() self.recipient, self.target = "me", args.strip()
else: else:
self.recipient, self.target = args.strip(), "me" self.recipient, self.target = args.strip(), "me"
self.advantage = self.cmdname == "boost" self.advantage = self.cmdname != "foil"
def func(self): 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.combathandler.queue_action(
self.caller, self.caller,
{ {
"key": "stunt", "key": "stunt",
"recipient": self.recipient, "recipient": recipient,
"target": self.target, "target": target,
"advantage": self.advantage, "advantage": self.advantage,
"stunt_type": self.stunt_type, "stunt_type": self.stunt_type,
"defense_type": self.stunt_type, "defense_type": self.stunt_type,
@ -973,7 +1083,10 @@ class CmdUseItem(_CmdCombatBase):
super().parse() super().parse()
args = self.args 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)) self.item, self.target = (part.strip() for part in args.split("on", 1))
else: else:
self.item, *target = args.split(None, 1) self.item, *target = args.split(None, 1)
@ -1017,6 +1130,12 @@ class CmdWield(_CmdCombatBase):
key = "wield" key = "wield"
help_category = "combat" 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): def func(self):
item = self.caller.search( item = self.caller.search(
@ -1070,7 +1189,7 @@ class TwitchAttackCmdSet(CmdSet):
def _get_combathandler(caller): def _get_combathandler(caller):
evmenu = caller.ndb._evmenu evmenu = caller.ndb._evmenu
if not hasattr(evmenu, "combathandler"): if not hasattr(evmenu, "combathandler"):
evmenu.combathandler = get_or_create_combathandler(caller) evmenu.combathandler = get_or_create_combathandler(caller.location)
return evmenu.combathandler return evmenu.combathandler
@ -1330,8 +1449,8 @@ def node_combat(caller, raw_string, **kwargs):
"goto": (_queue_action, {"flee": {"key": "flee"}}), "goto": (_queue_action, {"flee": {"key": "flee"}}),
}, },
{ {
"desc": "do nothing", "desc": "hold, doing nothing",
"goto": (_queue_action, {"action_dict": {"key": "nothing"}}), "goto": (_queue_action, {"action_dict": {"key": "hold"}}),
}, },
] ]
@ -1364,7 +1483,7 @@ class CmdTurnAttack(Command):
if not target: if not target:
return 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) combathandler.add_combatant(self.caller)
# build and start the menu # build and start the menu

View file

@ -6,6 +6,7 @@ from random import choice
from evennia import DefaultCharacter from evennia import DefaultCharacter
from evennia.typeclasses.attributes import AttributeProperty from evennia.typeclasses.attributes import AttributeProperty
from evennia.typeclasses.tags import TagProperty
from evennia.utils.evmenu import EvMenu from evennia.utils.evmenu import EvMenu
from evennia.utils.utils import make_iter from evennia.utils.utils import make_iter
@ -53,6 +54,9 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
weapon = AttributeProperty(default=WeaponEmptyHand, autocreate=False) # instead of inventory weapon = AttributeProperty(default=WeaponEmptyHand, autocreate=False) # instead of inventory
coins = AttributeProperty(default=1, autocreate=False) # coin loot 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 @property
def strength(self): def strength(self):
return self.hit_dice return self.hit_dice
@ -88,7 +92,7 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
""" """
self.hp = self.hp_max 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 The combat engine should ask this method in order to
get the next action the npc should perform in combat. get the next action the npc should perform in combat.

View file

@ -177,9 +177,30 @@ class EvAdventureWeapon(EvAdventureObject):
defense_type = AttributeProperty(Ability.ARMOR) defense_type = AttributeProperty(Ability.ARMOR)
damage_roll = AttributeProperty("1d6") 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): def use(self, attacker, target, *args, advantage=False, disadvantage=False, **kwargs):
"""When a weapon is used, it attacks an opponent""" """When a weapon is used, it attacks an opponent"""
location = attacker.location
is_hit, quality, txt = rules.dice.opposed_saving_throw( is_hit, quality, txt = rules.dice.opposed_saving_throw(
attacker, attacker,
target, target,
@ -188,7 +209,11 @@ class EvAdventureWeapon(EvAdventureObject):
advantage=advantage, advantage=advantage,
disadvantage=disadvantage, 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: if is_hit:
# enemy hit, calculate damage # enemy hit, calculate damage
dmg = rules.dice.roll(self.damage_roll) dmg = rules.dice.roll(self.damage_roll)
@ -201,8 +226,8 @@ class EvAdventureWeapon(EvAdventureObject):
) )
else: else:
message = f" $You() $conj(hit) $You({target.key}) for |r{dmg}|n damage!" 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 # call hook
target.at_damage(dmg, attacker=attacker) target.at_damage(dmg, attacker=attacker)
@ -212,7 +237,11 @@ class EvAdventureWeapon(EvAdventureObject):
if quality is Ability.CRITICAL_FAILURE: if quality is Ability.CRITICAL_FAILURE:
self.quality -= 1 self.quality -= 1
message += ".. it's a |rcritical miss!|n, damaging the weapon." 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): class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable):

View file

@ -157,13 +157,13 @@ class EvAdventureRollEngine:
bontxt = f"(+{bonus})" bontxt = f"(+{bonus})"
modtxt = "" modtxt = ""
if modifier: 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 "" qualtxt = f" ({quality.value}!)" if quality else ""
txt = ( txt = (
f"rolled {dice_roll} on {rolltxt} " f" rolled {dice_roll} on {rolltxt} "
f"+ {bonus_type.value}{bontxt}{modtxt} vs " 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 return (dice_roll + bonus + modifier) > target, quality, txt
@ -332,9 +332,11 @@ class EvAdventureRollEngine:
setattr(character, abi, current_abi) setattr(character, abi, current_abi)
character.msg( 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"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
) )

View file

@ -62,12 +62,12 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
# add target to combat # add target to combat
self.combathandler.add_combatant(self.target) 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"]] action_class = self.combathandler.action_classes[action_dict["key"]]
return action_class(self.combathandler, self.combatant, action_dict) return action_class(self.combathandler, self.combatant, action_dict)
def _run_actions( 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. Helper method to run an action and check so combatant saw the expected message.
@ -90,7 +90,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
self.assertEqual( self.assertEqual(
dict(chandler.action_classes), dict(chandler.action_classes),
{ {
"nothing": combat.CombatActionDoNothing, "hold": combat.CombatActionHold,
"attack": combat.CombatActionAttack, "attack": combat.CombatActionAttack,
"stunt": combat.CombatActionStunt, "stunt": combat.CombatActionStunt,
"use": combat.CombatActionUseItem, "use": combat.CombatActionUseItem,
@ -176,31 +176,31 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
def test_queue_and_execute_action(self): def test_queue_and_execute_action(self):
"""Queue actions and execute""" """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( self.assertEqual(
dict(self.combathandler.combatants), dict(self.combathandler.combatants),
{self.combatant: deque([donothing]), self.target: deque()}, {self.combatant: deque([hold]), self.target: deque()},
) )
mock_action = Mock() 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.execute_next_action(self.combatant)
self.combathandler.action_classes["nothing"].assert_called_with( self.combathandler.action_classes["hold"].assert_called_with(
self.combathandler, self.combatant, donothing self.combathandler, self.combatant, hold
) )
mock_action.execute.assert_called_once() mock_action.execute.assert_called_once()
def test_execute_full_turn(self): def test_execute_full_turn(self):
"""Run a full (passive) turn""" """Run a full (passive) turn"""
donothing = {"key": "nothing"} hold = {"key": "hold"}
self.combathandler.queue_action(self.combatant, donothing) self.combathandler.queue_action(self.combatant, hold)
self.combathandler.queue_action(self.target, donothing) self.combathandler.queue_action(self.target, hold)
self.combathandler.execute_next_action = Mock() self.combathandler.execute_next_action = Mock()
@ -216,7 +216,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
combatant = self.combatant combatant = self.combatant
target = self.target target = self.target
action = self._get_action({"key": "nothing"}) action = self._get_action({"key": "hold"})
self.assertTrue(action.can_use()) self.assertTrue(action.can_use())
@ -235,10 +235,10 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
action.msg(f"$You() attack $You({target.key}).") action.msg(f"$You() attack $You({target.key}).")
combatant.msg.assert_called_with(text=("You attack testmonster.", {}), from_obj=combatant) combatant.msg.assert_called_with(text=("You attack testmonster.", {}), from_obj=combatant)
def test_action__do_nothing(self): def test_action__hold(self):
"""Do nothing""" """Hold, doing nothing"""
actiondict = {"key": "nothing"} actiondict = {"key": "hold"}
self._run_actions(actiondict, actiondict) self._run_actions(actiondict, actiondict)
self.assertEqual(self.combathandler.turn, 1) self.assertEqual(self.combathandler.turn, 1)
@ -417,7 +417,7 @@ class EvAdventureCombatHandlerTest(BaseEvenniaTest):
from_obj=self.combatant, from_obj=self.combatant,
) )
# Check that enemies have advantage against you now # 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)) self.assertTrue(action.has_advantage(self.target, self.combatant))
# second flee should remove combatant # second flee should remove combatant

View file

@ -34,13 +34,12 @@ 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 evennia.utils import logger
from simpleeval import simple_eval from simpleeval import simple_eval
from twisted.internet import reactor, threads from twisted.internet import reactor, threads
from twisted.internet.defer import returnValue # noqa - used as import target from twisted.internet.defer import returnValue # noqa - used as import target
from twisted.internet.task import deferLater from twisted.internet.task import deferLater
from evennia.utils import logger
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR _EVENNIA_DIR = settings.EVENNIA_DIR
_GAME_DIR = settings.GAME_DIR _GAME_DIR = settings.GAME_DIR