First working attack in tutorial combat system

This commit is contained in:
Griatch 2022-07-14 20:29:09 +02:00
parent 29ffd5fd06
commit f298de0585
6 changed files with 260 additions and 129 deletions

View file

@ -9,7 +9,7 @@ from evennia.utils.utils import int2str, lazy_property
from . import rules from . import rules
from .enums import Ability, WieldLocation from .enums import Ability, WieldLocation
from .objects import EvAdventureObject from .objects import EvAdventureObject, WeaponEmptyHand
class EquipmentError(TypeError): class EquipmentError(TypeError):
@ -134,7 +134,7 @@ class EquipmentHandler:
@property @property
def weapon(self): def weapon(self):
""" """
Conveniently get the currently active weapon. Conveniently get the currently active weapon or rune stone.
Returns: Returns:
obj or None: The weapon. None if unarmored. obj or None: The weapon. None if unarmored.
@ -146,6 +146,8 @@ class EquipmentHandler:
weapon = slots[WieldLocation.TWO_HANDS] weapon = slots[WieldLocation.TWO_HANDS]
if not weapon: if not weapon:
weapon = slots[WieldLocation.WEAPON_HAND] weapon = slots[WieldLocation.WEAPON_HAND]
if not weapon:
weapon = WeaponEmptyHand()
return weapon return weapon
def display_loadout(self): def display_loadout(self):
@ -370,6 +372,13 @@ class LivingMixin:
else: else:
self.msg(f"|g{healer.key} heals you for {healed} health.|n") self.msg(f"|g{healer.key} heals you for {healed} health.|n")
def at_damage(self, damage, attacker=None):
"""
Called when attacked and taking damage.
"""
pass
class EvAdventureCharacter(LivingMixin, DefaultCharacter): class EvAdventureCharacter(LivingMixin, DefaultCharacter):
""" """
@ -401,23 +410,6 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
"""Allows to access equipment like char.equipment.worn""" """Allows to access equipment like char.equipment.worn"""
return EquipmentHandler(self) return EquipmentHandler(self)
@property
def weapon(self):
"""
Quick access to the character's currently wielded weapon.
"""
self.equipment.weapon
@property
def armor(self):
"""
Quick access to the character's current armor.
Will return the "Unarmored" armor level (11) if none other are found.
"""
self.equipment.armor or 11
def at_pre_object_receive(self, moved_object, source_location, **kwargs): def at_pre_object_receive(self, moved_object, source_location, **kwargs):
""" """
Hook called by Evennia before moving an object here. Return False to abort move. Hook called by Evennia before moving an object here. Return False to abort move.
@ -467,21 +459,25 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
""" """
self.equipment.remove(moved_object) self.equipment.remove(moved_object)
def at_damage(self, dmg, attacker=None): def at_defeat(self):
""" """
Called when receiving damage for whatever reason. This This happens when character drops <= 0 HP. For Characters, this means rolling on
is called *before* hp is evaluated for defeat/death. the death table.
""" """
rules.dice.roll_death(self)
if hp <= 0:
# this means we rolled death on the table
self.handle_death()
else:
# still alive, but lost in some stats
self.location.msg_contents(
f"|y$You() $conj(stagger) back, weakened but still alive.|n", from_obj=self
)
def defeat_message(self, attacker, dmg): def defeat_message(self, attacker, dmg):
return f"After {attacker.key}'s attack, {self.key} collapses in a heap."
def at_defeat(self, attacker, dmg):
""" """
At this point, character has been defeated but is not killed (their Sent out to everyone in the location by the combathandler.
hp >= 0 but they lost ability bonuses). Called after being defeated in combat or
other situation where health is lost below or equal to 0.
""" """
@ -490,3 +486,6 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
Called when character dies. Called when character dies.
""" """
self.location.msg_contents(
f"|r$You() $conj(collapse) in a heap. No getting back from that.|n", from_obj=self
)

View file

@ -104,7 +104,7 @@ from datetime import datetime
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 from evennia.utils import dbserialize, delay, evmenu, evtable, logger
from evennia.utils.utils import make_iter from evennia.utils.utils import make_iter
from . import rules from . import rules
@ -121,6 +121,11 @@ class CombatFailure(RuntimeError):
""" """
# -----------------------------------------------------------------------------------
# Combat Actions
# -----------------------------------------------------------------------------------
class CombatAction: class CombatAction:
""" """
This is the base of a combat-action, like 'attack' Inherit from this to make new actions. This is the base of a combat-action, like 'attack' Inherit from this to make new actions.
@ -141,8 +146,6 @@ class CombatAction:
# use None to do nothing (jump directly to registering the action) # use None to do nothing (jump directly to registering the action)
next_menu_node = "node_select_target" next_menu_node = "node_select_target"
# action to echo to everyone.
post_action_text = "{combatant} performed an action."
max_uses = None # None for unlimited max_uses = None # None for unlimited
# in which order (highest first) to perform the action. If identical, use random order # in which order (highest first) to perform the action. If identical, use random order
priority = 0 priority = 0
@ -153,12 +156,15 @@ class CombatAction:
self.uses = 0 self.uses = 0
def msg(self, message, broadcast=False): def msg(self, message, broadcast=False):
if broadcast: """
# send to everyone in combat. Convenience route to the combathandler msg-sender mechanism.
self.combathandler.msg(message)
else: Args:
# send only to the combatant. message (str): Message to send; use `$You()` and `$You(other.key)`
self.combatant.msg(message) to refer to the combatant doing the action and other combatants,
respectively.
"""
self.combathandler.msg(message, combatant=self.combatant, broadcast=broadcast)
def __serialize_dbobjs__(self): def __serialize_dbobjs__(self):
""" """
@ -207,14 +213,26 @@ class CombatAction:
return True if self.max_uses is None else self.uses < (self.max_uses or 0) return True if self.max_uses is None else self.uses < (self.max_uses or 0)
def pre_use(self, *args, **kwargs): def pre_use(self, *args, **kwargs):
"""
Called just before the main action.
"""
pass pass
def use(self, *args, **kwargs): def use(self, *args, **kwargs):
"""
Main activation of the action. This happens simultaneously to other actions.
"""
pass pass
def post_use(self, *args, **kwargs): def post_use(self, *args, **kwargs):
self.uses += 1 """
self.combathandler.msg(self.post_action_text.format(**kwargs)) Called just after the action has been taken.
"""
pass
class CombatActionAttack(CombatAction): class CombatActionAttack(CombatAction):
@ -237,27 +255,53 @@ class CombatActionAttack(CombatAction):
""" """
attacker = self.combatant attacker = self.combatant
weapon = self.combatant.equipment.weapon
# figure out advantage (gained by previous stunts) # figure out advantage (gained by previous stunts)
advantage = bool(self.combathandler.advantage_matrix[attacker].pop(defender, False)) advantage = bool(self.combathandler.advantage_matrix[attacker].pop(defender, False))
# figure out disadvantage (gained by enemy stunts/actions) # figure out disadvantage (gained by enemy stunts/actions)
disadvantage = bool(self.combathandler.disadvantage_matrix[attacker].pop(defender, False)) disadvantage = bool(self.combathandler.disadvantage_matrix[attacker].pop(defender, False))
is_hit, quality = rules.dice.opposed_saving_throw( is_hit, quality, txt = rules.dice.opposed_saving_throw(
attacker, attacker,
defender, defender,
attack_type=attacker.weapon.attack_type, attack_type=weapon.attack_type,
defense_type=attacker.weapon.defense_type, defense_type=attacker.equipment.weapon.defense_type,
advantage=advantage, advantage=advantage,
disadvantage=disadvantage, disadvantage=disadvantage,
) )
self.msg(f"$You() $conj(attack) $You({defender.key}) with {weapon.key}: {txt}")
if is_hit: if is_hit:
self.combathandler.resolve_damage( # enemy hit, calculate damage
attacker, defender, critical=quality == "critical success" weapon_dmg_roll = attacker.equipment.weapon.damage_roll
)
# TODO messaging here dmg = rules.dice.roll(weapon_dmg_roll)
if quality is Ability.CRITICAL_SUCCESS:
dmg += rules.dice.roll(weapon_dmg_roll)
message = (
f" $You() |ycritically|n $conj(hit) $You({defender.key}) for |r{dmg}|n damage!"
)
else:
message = f" $You() $conj(hit) $You({defender.key}) for |r{dmg}|n damage!"
self.msg(message)
defender.hp -= dmg
# call hook
defender.at_damage(dmg, attacker=attacker)
# note that we mustn't remove anyone from combat yet, because this is
# happening simultaneously. So checking of the final hp
# and rolling of death etc happens in the combathandler at the end of the turn.
else:
# a miss
message = f" $You() $conj(miss) $You({defender.key})."
if quality is Ability.CRITICAL_FAILURE:
attacker.equipment.weapon.quality -= 1
message += ".. it's a |rcritical miss!|n, damaging the weapon."
self.msg(message)
class CombatActionStunt(CombatAction): class CombatActionStunt(CombatAction):
@ -299,7 +343,7 @@ class CombatActionStunt(CombatAction):
attacker = self.combatant attacker = self.combatant
advantage, disadvantage = False, False advantage, disadvantage = False, False
is_success, _ = rules.dice.opposed_saving_throw( is_success, _, txt = rules.dice.opposed_saving_throw(
attacker, attacker,
defender, defender,
attack_type=self.attack_type, attack_type=self.attack_type,
@ -307,13 +351,13 @@ class CombatActionStunt(CombatAction):
advantage=advantage, advantage=advantage,
disadvantage=disadvantage, disadvantage=disadvantage,
) )
self.msg(f"$You() $conj(attempt) stunt on $You(defender.key). {txt}")
if is_success: if is_success:
if advantage: if advantage:
self.combathandler.gain_advantage(attacker, defender) self.combathandler.gain_advantage(attacker, defender)
else: else:
self.combathandler.gain_disadvantage(defender, attacker) self.combathandler.gain_disadvantage(defender, attacker)
self.msg
# only spend a use after being successful # only spend a use after being successful
self.uses += 1 self.uses += 1
@ -376,11 +420,14 @@ class CombatActionFlee(CombatAction):
"Disengage from combat. Use successfully two times in a row to leave combat at the " "Disengage from combat. Use successfully two times in a row to leave combat at the "
"end of the second round. If someone Blocks you successfully, this counter is reset." "end of the second round. If someone Blocks you successfully, this counter is reset."
) )
priority = -5 # checked last priority = -5 # checked last
def use(self, *args, **kwargs): def use(self, *args, **kwargs):
# it's safe to do this twice # it's safe to do this twice
self.msg(
"$You() retreats, and will leave combat next round unless someone successfully "
"blocks them."
)
self.combathandler.flee(self.combatant) self.combathandler.flee(self.combatant)
@ -409,7 +456,7 @@ class CombatActionBlock(CombatAction):
advantage = bool(self.advantage_matrix[combatant].pop(fleeing_target, False)) advantage = bool(self.advantage_matrix[combatant].pop(fleeing_target, False))
disadvantage = bool(self.disadvantage_matrix[combatant].pop(fleeing_target, False)) disadvantage = bool(self.disadvantage_matrix[combatant].pop(fleeing_target, False))
is_success, _ = rules.dice.opposed_saving_throw( is_success, _, txt = rules.dice.opposed_saving_throw(
combatant, combatant,
fleeing_target, fleeing_target,
attack_type=self.attack_type, attack_type=self.attack_type,
@ -417,12 +464,14 @@ class CombatActionBlock(CombatAction):
advantage=advantage, advantage=advantage,
disadvantage=disadvantage, disadvantage=disadvantage,
) )
self.msg(f"$You() tries to block the retreat of $You({fleeing_target.key}). {txt}")
if is_success: if is_success:
# managed to stop the target from fleeing/disengaging # managed to stop the target from fleeing/disengaging
self.combatant.unflee(fleeing_target) self.combatant.unflee(fleeing_target)
self.msg("$You() blocks the retreat of $You({fleeing_target.key})")
else: else:
pass # they are getting away! self.msg("$You({fleeing_target.key}) dodges away from you $You()!")
class CombatActionSwapWieldedWeaponOrSpell(CombatAction): class CombatActionSwapWieldedWeaponOrSpell(CombatAction):
@ -450,8 +499,6 @@ class CombatActionSwapWieldedWeaponOrSpell(CombatAction):
next_menu_node = "node_select_wield_from_inventory" next_menu_node = "node_select_wield_from_inventory"
post_action_text = "{combatant} switches weapons."
def use(self, combatant, item, *args, **kwargs): def use(self, combatant, item, *args, **kwargs):
# this will make use of the item # this will make use of the item
combatant.inventory.use(item) combatant.inventory.use(item)
@ -470,10 +517,9 @@ class CombatActionUseItem(CombatAction):
next_menu_node = "node_select_use_item_from_inventory" next_menu_node = "node_select_use_item_from_inventory"
post_action_text = "{combatant} used an item."
def use(self, combatant, item, *args, **kwargs): def use(self, combatant, item, *args, **kwargs):
item.use(combatant, *args, **kwargs) item.use(combatant, *args, **kwargs)
self.msg("$You() $conj(use) an item.")
class CombatActionDoNothing(CombatAction): class CombatActionDoNothing(CombatAction):
@ -492,6 +538,14 @@ class CombatActionDoNothing(CombatAction):
post_action_text = "{combatant} does nothing this turn." post_action_text = "{combatant} does nothing this turn."
def use(self, *args, **kwargs):
self.msg("$You() $conj(hesitate), accomplishing nothing.")
# -----------------------------------------------------------------------------------
# Combat handler
# -----------------------------------------------------------------------------------
class EvAdventureCombatHandler(DefaultScript): class EvAdventureCombatHandler(DefaultScript):
""" """
@ -618,6 +672,8 @@ class EvAdventureCombatHandler(DefaultScript):
self.interval - warning_time, self._warn_time, warning_time self.interval - warning_time, self._warn_time, warning_time
) )
self.msg(f"|y_______________________ start turn {self.turn} ___________________________|n")
for combatant in self.combatants: for combatant in self.combatants:
# cycle combat menu # cycle combat menu
self._init_menu(combatant) self._init_menu(combatant)
@ -628,10 +684,15 @@ class EvAdventureCombatHandler(DefaultScript):
End of turn operations. End of turn operations.
1. Do all regular actions 1. Do all regular actions
2. Roll for any death events
2. Remove combatants that disengaged successfully 2. Remove combatants that disengaged successfully
3. Timeout advantages/disadvantages 3. Timeout advantages/disadvantages
""" """
self.msg(
f"|y__________________ turn resolution (turn {self.turn}) ____________________|n\n"
)
# do all actions # do all actions
for combatant in self.combatants: for combatant in self.combatants:
# read the current action type selected by the player # read the current action type selected by the player
@ -639,7 +700,16 @@ class EvAdventureCombatHandler(DefaultScript):
combatant, (CombatActionDoNothing(self, combatant), (), {}) combatant, (CombatActionDoNothing(self, combatant), (), {})
) )
# perform the action on the CombatAction instance # perform the action on the CombatAction instance
try:
action.pre_use(*args, **kwargs)
action.use(*args, **kwargs) action.use(*args, **kwargs)
action.post_use(*args, **kwargs)
except Exception as err:
combatant.msg(
f"An error ({err}) occurred when performing this action.\n"
"Please report the problem to an admin."
)
logger.log_trace()
# handle disengaging combatants # handle disengaging combatants
@ -647,14 +717,37 @@ class EvAdventureCombatHandler(DefaultScript):
for combatant in self.combatants: for combatant in self.combatants:
# check disengaging combatants (these are combatants that managed # check disengaging combatants (these are combatants that managed
# to stay at disengaging distance for a turn) # not get their escape blocked last turn
if combatant in self.fleeing_combatants: if combatant in self.fleeing_combatants:
self.fleeing_combatants.remove(combatant) self.fleeing_combatants.remove(combatant)
if combatant.hp <= 0:
# characters roll on the death table here, npcs usually just die
combatant.at_defeat()
# tell everyone
self.msg(combatant.defeat_message(attacker, dmg), combatant=combatant)
if defender.hp > 0:
# death roll didn't kill them - they are weakened, but with hp
self.msg(
"You are alive, but out of the fight. If you want to press your luck, "
"you need to rejoin the combat.",
combatant=combatant,
broadcast=False,
)
defender.at_defeat() # note - NPC monsters may still 'die' here
else:
# outright killed
defender.at_death()
# no matter the result, the combatant is out
to_remove.append(combatant)
for combatant in to_remove: for combatant in to_remove:
# for clarity, we remove here rather than modifying the combatant list # for clarity, we remove here rather than modifying the combatant list
# inside the previous loop # inside the previous loop
self.msg(f"{combatant.key} disengaged and left combat.") self.msg(f"|y$You() $conj(are) out of combat.|n", combatant=combatant)
self.remove_combatant(combatant) self.remove_combatant(combatant)
# refresh stunt timeouts (note - self.stunt_duration is the same for # refresh stunt timeouts (note - self.stunt_duration is the same for
@ -788,7 +881,7 @@ class EvAdventureCombatHandler(DefaultScript):
if comb is combatant: if comb is combatant:
continue continue
name = combatant.key name = comb.key
health = f"{comb.hurt_level}" health = f"{comb.hurt_level}"
fleeing = "" fleeing = ""
if comb in self.fleeing_combatants: if comb in self.fleeing_combatants:
@ -798,24 +891,37 @@ class EvAdventureCombatHandler(DefaultScript):
return str(table) return str(table)
def msg(self, message, targets=None): 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
for adding any combat-specific text-decoration in one place. for adding any combat-specific text-decoration in one place.
Args: Args:
message (str): The message to send. message (str): The message to send.
targets (Object or list, optional): Sends message only to combatant (Object): The 'You' in the message, if any.
one or more particular combatants. If unset, send to broadcast (bool): If `False`, `combatant` must be included and
everyone in the combat. will be the only one to see the message. If `True`, send to
everyone in the location.
Notes:
If `combatant` is given, use `$You/you()` markup to create
a message that looks different depending on who sees it. Use
`$You(combatant_key)` to refer to other combatants.
""" """
if targets: location = self.obj
for target in make_iter(targets): location_objs = location.contents
target.msg(message)
else: exclude = []
for target in self.combatants: if not broadcast and combatant:
target.msg(message) exclude = [obj for obj in location_objs if obj is not combatant]
location.msg_contents(
message,
exclude=exclude,
from_obj=combatant,
mapping={locobj.key: locobj for locobj in location_objs},
)
def gain_advantage(self, combatant, target): def gain_advantage(self, combatant, target):
""" """
@ -839,48 +945,6 @@ class EvAdventureCombatHandler(DefaultScript):
if combatant in self.fleeing_combatants: if combatant in self.fleeing_combatants:
self.fleeing_combatants.remove(combatant) self.fleeing_combatants.remove(combatant)
def resolve_damage(self, attacker, defender, critical=False):
"""
Apply damage to defender. On a critical hit, the damage die
is rolled twice.
"""
weapon_dmg_roll = attacker.weapon.damage_roll
dmg = rules.dice.roll(weapon_dmg_roll)
if critical:
dmg += rules.dice.roll(weapon_dmg_roll)
defender.hp -= dmg
# call hook
defender.at_damage(dmg, attacker=attacker)
if defender.hp <= 0:
# roll on death table. This may or may not kill you
rules.dice.roll_death(self)
# tell everyone
self.msg(defender.defeat_message(attacker, dmg))
if defender.hp > 0:
# they are weakened, but with hp
self.msg(
"You are alive, but out of the fight. If you want to press your luck, "
"you need to rejoin the combat.",
targets=defender,
)
defender.at_defeat() # note - NPC monsters may still 'die' here
else:
# outright killed
defender.at_death()
# no matter the result, the combatant is out
self.remove_combatant(defender)
else:
# defender still alive
self.msg(defender)
def register_action(self, combatant, action_key, *args, **kwargs): def register_action(self, combatant, action_key, *args, **kwargs):
""" """
Register an action based on its `.key`. Register an action based on its `.key`.
@ -927,7 +991,9 @@ class EvAdventureCombatHandler(DefaultScript):
return list(self.combatant_actions[combatant].values()) return list(self.combatant_actions[combatant].values())
# ------------ start combat menu definitions # -----------------------------------------------------------------------------------
# Combat Menu definitions
# -----------------------------------------------------------------------------------
def _register_action(caller, raw_string, **kwargs): def _register_action(caller, raw_string, **kwargs):
@ -1165,7 +1231,9 @@ def node_wait_start(caller, raw_string, **kwargs):
return text, options return text, options
# -------------- end of combat menu definitions # -----------------------------------------------------------------------------------
# Access function
# -----------------------------------------------------------------------------------
def join_combat(caller, *targets, session=None): def join_combat(caller, *targets, session=None):

View file

@ -69,6 +69,14 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
""" """
self.hp = self.hp_max self.hp = self.hp_max
def ai_combat_next_action(self):
"""
The combat engine should ask this method in order to
get the next action the npc should perform in combat.
"""
pass
class EvAdventureShopKeeper(EvAdventureNPC): class EvAdventureShopKeeper(EvAdventureNPC):
""" """

View file

@ -85,6 +85,20 @@ class EvAdventureWeapon(EvAdventureObject):
damage_roll = AttributeProperty("1d6") damage_roll = AttributeProperty("1d6")
class WeaponEmptyHand:
"""
This is used when you wield no weapons. We won't create any db-object for it.
"""
key = "Empty Fists"
inventory_use_slot = WieldLocation.WEAPON_HAND
attack_type = Ability.STR
defense_type = Ability.ARMOR
damage_roll = "1d4"
quality = 100000 # let's assume fists are always available ...
class EvAdventureRunestone(EvAdventureWeapon): class EvAdventureRunestone(EvAdventureWeapon):
""" """
Base class for magic runestones. In _Knave_, every spell is represented by a rune stone Base class for magic runestones. In _Knave_, every spell is represented by a rune stone

View file

@ -81,7 +81,7 @@ class EvAdventureRollEngine:
if 0 < diesize > max_diesize: if 0 < diesize > max_diesize:
raise TypeError(f"Invalid die-size used (must be between 1 and {max_diesize} sides)") raise TypeError(f"Invalid die-size used (must be between 1 and {max_diesize} sides)")
# At this point we know we have valid input - roll and all dice together # At this point we know we have valid input - roll and add dice together
return sum(randint(1, diesize) for _ in range(number)) return sum(randint(1, diesize) for _ in range(number))
def roll_with_advantage_or_disadvantage(self, advantage=False, disadvantage=False): def roll_with_advantage_or_disadvantage(self, advantage=False, disadvantage=False):
@ -98,7 +98,7 @@ class EvAdventureRollEngine:
""" """
if not (advantage or disadvantage) or (advantage and disadvantage): if not (advantage or disadvantage) or (advantage and disadvantage):
# normal roll # normal roll, or advantage cancels disadvantage
return self.roll("1d20") return self.roll("1d20")
elif advantage: elif advantage:
return max(self.roll("1d20"), self.roll("1d20")) return max(self.roll("1d20"), self.roll("1d20"))
@ -129,9 +129,10 @@ class EvAdventureRollEngine:
modifier (int, optional): An additional +/- modifier to the roll. modifier (int, optional): An additional +/- modifier to the roll.
Returns: Returns:
tuple: (bool, str): If the save was passed or not. The second element is the tuple: A tuple `(bool, str, str)`. The bool indicates if the save was passed or not.
quality of the roll - None (normal), "critical fail" and "critical success". The second element is the quality of the roll - None (normal),
"critical fail" and "critical success". Last element is a text detailing
the roll, for display purposes.
Notes: Notes:
Advantage and disadvantage cancel each other out. Advantage and disadvantage cancel each other out.
@ -147,7 +148,25 @@ class EvAdventureRollEngine:
quality = Ability.CRITICAL_SUCCESS quality = Ability.CRITICAL_SUCCESS
else: else:
quality = None quality = None
return (dice_roll + bonus + modifier) > target, quality result = dice_roll + bonus + modifier > target
# determine text output
rolltxt = "d20 "
if advantage and disadvantage:
rolltxt = "d20 (advantage canceled by disadvantage)"
elif advantage:
rolltxt = "|g2d20|n (advantage: picking highest) "
elif disadvantage:
rolltxt = "|r2d20|n (disadvantage: picking lowest) "
bontxt = f"(+{bonus})"
modtxt = ""
if modifier:
modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}"
qualtxt = f" ({quality.value}!)" if quality else ""
txt = f"{dice_roll} + {bonus_type.value}{bontxt}{modtxt} -> |w{result}{qualtxt}|n"
return (dice_roll + bonus + modifier) > target, quality, txt
def opposed_saving_throw( def opposed_saving_throw(
self, self,
@ -174,14 +193,16 @@ class EvAdventureRollEngine:
modifier (int): An additional +/- modifier to the roll. modifier (int): An additional +/- modifier to the roll.
Returns: Returns:
tuple: (bool, str): If the attack succeed or not. The second element is the tuple: (bool, str, str): If the attack succeed or not. The second element is the
quality of the roll - None (normal), "critical fail" and "critical success". quality of the roll - None (normal), "critical fail" and "critical success". Last
element is a text that summarizes the details of the roll.
Notes: Notes:
Advantage and disadvantage cancel each other out. Advantage and disadvantage cancel each other out.
""" """
defender_defense = getattr(defender, defense_type.value, 1) + 10
return self.saving_throw( defender_defense = getattr(defender, defense_type.value, 1)
result, quality, txt = self.saving_throw(
attacker, attacker,
bonus_type=attack_type, bonus_type=attack_type,
target=defender_defense, target=defender_defense,
@ -189,6 +210,9 @@ class EvAdventureRollEngine:
disadvantage=disadvantage, disadvantage=disadvantage,
modifier=modifier, modifier=modifier,
) )
txt = f"Roll vs {defense_type.value}({defender_defense}):\n{txt}"
return result, quality, txt
def roll_random_table(self, dieroll, table_choices): def roll_random_table(self, dieroll, table_choices):
""" """

View file

@ -21,9 +21,15 @@ from evennia.scripts.scripthandler import ScriptHandler
from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler
from evennia.typeclasses.models import TypeclassBase from evennia.typeclasses.models import TypeclassBase
from evennia.utils import ansi, create, funcparser, logger, search from evennia.utils import ansi, create, funcparser, logger, search
from evennia.utils.utils import (class_from_module, is_iter, lazy_property, from evennia.utils.utils import (
list_to_string, make_iter, to_str, class_from_module,
variable_from_module) is_iter,
lazy_property,
list_to_string,
make_iter,
to_str,
variable_from_module,
)
_INFLECT = inflect.engine() _INFLECT = inflect.engine()
_MULTISESSION_MODE = settings.MULTISESSION_MODE _MULTISESSION_MODE = settings.MULTISESSION_MODE
@ -714,7 +720,15 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
for obj in contents: for obj in contents:
func(obj, **kwargs) func(obj, **kwargs)
def msg_contents(self, text=None, exclude=None, from_obj=None, mapping=None, **kwargs): def msg_contents(
self,
text=None,
exclude=None,
from_obj=None,
mapping=None,
raise_funcparse_errors=False,
**kwargs,
):
""" """
Emits a message to all objects inside this object. Emits a message to all objects inside this object.
@ -738,6 +752,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
in the `text` string. If `<object>` doesn't have a `get_display_name` in the `text` string. If `<object>` doesn't have a `get_display_name`
method, it will be returned as a string. If not set, a key `you` will method, it will be returned as a string. If not set, a key `you` will
be auto-added to point to `from_obj` if given, otherwise to `self`. be auto-added to point to `from_obj` if given, otherwise to `self`.
raise_funcparse_errors (bool, optional): If set, a failing `$func()` will
lead to an outright error. If unset (default), the failing `$func()`
will instead appear in output unparsed.
**kwargs: Keyword arguments will be passed on to `obj.msg()` for all **kwargs: Keyword arguments will be passed on to `obj.msg()` for all
messaged objects. messaged objects.
@ -802,7 +820,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# actor-stance replacements # actor-stance replacements
inmessage = _MSG_CONTENTS_PARSER.parse( inmessage = _MSG_CONTENTS_PARSER.parse(
inmessage, inmessage,
raise_errors=True, raise_errors=raise_funcparse_errors,
return_string=True, return_string=True,
caller=you, caller=you,
receiver=receiver, receiver=receiver,