mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Testing out combat
This commit is contained in:
parent
97b11ccea7
commit
5c914eb8b0
7 changed files with 311 additions and 154 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <item>|n - use/consume an item in your inventory
|
||||
- |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
|
||||
|
||||
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):
|
||||
|
|
@ -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 <recipient> vs <target>")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue