mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
First working attack in tutorial combat system
This commit is contained in:
parent
29ffd5fd06
commit
f298de0585
6 changed files with 260 additions and 129 deletions
|
|
@ -9,7 +9,7 @@ from evennia.utils.utils import int2str, lazy_property
|
|||
|
||||
from . import rules
|
||||
from .enums import Ability, WieldLocation
|
||||
from .objects import EvAdventureObject
|
||||
from .objects import EvAdventureObject, WeaponEmptyHand
|
||||
|
||||
|
||||
class EquipmentError(TypeError):
|
||||
|
|
@ -134,7 +134,7 @@ class EquipmentHandler:
|
|||
@property
|
||||
def weapon(self):
|
||||
"""
|
||||
Conveniently get the currently active weapon.
|
||||
Conveniently get the currently active weapon or rune stone.
|
||||
|
||||
Returns:
|
||||
obj or None: The weapon. None if unarmored.
|
||||
|
|
@ -146,6 +146,8 @@ class EquipmentHandler:
|
|||
weapon = slots[WieldLocation.TWO_HANDS]
|
||||
if not weapon:
|
||||
weapon = slots[WieldLocation.WEAPON_HAND]
|
||||
if not weapon:
|
||||
weapon = WeaponEmptyHand()
|
||||
return weapon
|
||||
|
||||
def display_loadout(self):
|
||||
|
|
@ -370,6 +372,13 @@ class LivingMixin:
|
|||
else:
|
||||
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):
|
||||
"""
|
||||
|
|
@ -401,23 +410,6 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
"""Allows to access equipment like char.equipment.worn"""
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
|
||||
def at_damage(self, dmg, attacker=None):
|
||||
def at_defeat(self):
|
||||
"""
|
||||
Called when receiving damage for whatever reason. This
|
||||
is called *before* hp is evaluated for defeat/death.
|
||||
This happens when character drops <= 0 HP. For Characters, this means rolling on
|
||||
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):
|
||||
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
|
||||
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.
|
||||
Sent out to everyone in the location by the combathandler.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -490,3 +486,6 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ from datetime import datetime
|
|||
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
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 . import rules
|
||||
|
|
@ -121,6 +121,11 @@ class CombatFailure(RuntimeError):
|
|||
"""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Combat Actions
|
||||
# -----------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CombatAction:
|
||||
"""
|
||||
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)
|
||||
next_menu_node = "node_select_target"
|
||||
|
||||
# action to echo to everyone.
|
||||
post_action_text = "{combatant} performed an action."
|
||||
max_uses = None # None for unlimited
|
||||
# in which order (highest first) to perform the action. If identical, use random order
|
||||
priority = 0
|
||||
|
|
@ -153,12 +156,15 @@ class CombatAction:
|
|||
self.uses = 0
|
||||
|
||||
def msg(self, message, broadcast=False):
|
||||
if broadcast:
|
||||
# send to everyone in combat.
|
||||
self.combathandler.msg(message)
|
||||
else:
|
||||
# send only to the combatant.
|
||||
self.combatant.msg(message)
|
||||
"""
|
||||
Convenience route to the combathandler msg-sender mechanism.
|
||||
|
||||
Args:
|
||||
message (str): Message to send; use `$You()` and `$You(other.key)`
|
||||
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):
|
||||
"""
|
||||
|
|
@ -207,14 +213,26 @@ class CombatAction:
|
|||
return True if self.max_uses is None else self.uses < (self.max_uses or 0)
|
||||
|
||||
def pre_use(self, *args, **kwargs):
|
||||
"""
|
||||
Called just before the main action.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def use(self, *args, **kwargs):
|
||||
"""
|
||||
Main activation of the action. This happens simultaneously to other actions.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
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):
|
||||
|
|
@ -237,27 +255,53 @@ class CombatActionAttack(CombatAction):
|
|||
|
||||
"""
|
||||
attacker = self.combatant
|
||||
weapon = self.combatant.equipment.weapon
|
||||
|
||||
# figure out advantage (gained by previous stunts)
|
||||
advantage = bool(self.combathandler.advantage_matrix[attacker].pop(defender, False))
|
||||
|
||||
# figure out disadvantage (gained by enemy stunts/actions)
|
||||
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,
|
||||
defender,
|
||||
attack_type=attacker.weapon.attack_type,
|
||||
defense_type=attacker.weapon.defense_type,
|
||||
attack_type=weapon.attack_type,
|
||||
defense_type=attacker.equipment.weapon.defense_type,
|
||||
advantage=advantage,
|
||||
disadvantage=disadvantage,
|
||||
)
|
||||
self.msg(f"$You() $conj(attack) $You({defender.key}) with {weapon.key}: {txt}")
|
||||
if is_hit:
|
||||
self.combathandler.resolve_damage(
|
||||
attacker, defender, critical=quality == "critical success"
|
||||
)
|
||||
# enemy hit, calculate damage
|
||||
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):
|
||||
|
|
@ -299,7 +343,7 @@ class CombatActionStunt(CombatAction):
|
|||
attacker = self.combatant
|
||||
advantage, disadvantage = False, False
|
||||
|
||||
is_success, _ = rules.dice.opposed_saving_throw(
|
||||
is_success, _, txt = rules.dice.opposed_saving_throw(
|
||||
attacker,
|
||||
defender,
|
||||
attack_type=self.attack_type,
|
||||
|
|
@ -307,13 +351,13 @@ class CombatActionStunt(CombatAction):
|
|||
advantage=advantage,
|
||||
disadvantage=disadvantage,
|
||||
)
|
||||
self.msg(f"$You() $conj(attempt) stunt on $You(defender.key). {txt}")
|
||||
if is_success:
|
||||
if advantage:
|
||||
self.combathandler.gain_advantage(attacker, defender)
|
||||
else:
|
||||
self.combathandler.gain_disadvantage(defender, attacker)
|
||||
|
||||
self.msg
|
||||
# only spend a use after being successful
|
||||
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 "
|
||||
"end of the second round. If someone Blocks you successfully, this counter is reset."
|
||||
)
|
||||
|
||||
priority = -5 # checked last
|
||||
|
||||
def use(self, *args, **kwargs):
|
||||
# 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)
|
||||
|
||||
|
||||
|
|
@ -409,7 +456,7 @@ class CombatActionBlock(CombatAction):
|
|||
advantage = bool(self.advantage_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,
|
||||
fleeing_target,
|
||||
attack_type=self.attack_type,
|
||||
|
|
@ -417,12 +464,14 @@ class CombatActionBlock(CombatAction):
|
|||
advantage=advantage,
|
||||
disadvantage=disadvantage,
|
||||
)
|
||||
self.msg(f"$You() tries to block the retreat of $You({fleeing_target.key}). {txt}")
|
||||
|
||||
if is_success:
|
||||
# managed to stop the target from fleeing/disengaging
|
||||
self.combatant.unflee(fleeing_target)
|
||||
self.msg("$You() blocks the retreat of $You({fleeing_target.key})")
|
||||
else:
|
||||
pass # they are getting away!
|
||||
self.msg("$You({fleeing_target.key}) dodges away from you $You()!")
|
||||
|
||||
|
||||
class CombatActionSwapWieldedWeaponOrSpell(CombatAction):
|
||||
|
|
@ -450,8 +499,6 @@ class CombatActionSwapWieldedWeaponOrSpell(CombatAction):
|
|||
|
||||
next_menu_node = "node_select_wield_from_inventory"
|
||||
|
||||
post_action_text = "{combatant} switches weapons."
|
||||
|
||||
def use(self, combatant, item, *args, **kwargs):
|
||||
# this will make use of the item
|
||||
combatant.inventory.use(item)
|
||||
|
|
@ -470,10 +517,9 @@ class CombatActionUseItem(CombatAction):
|
|||
|
||||
next_menu_node = "node_select_use_item_from_inventory"
|
||||
|
||||
post_action_text = "{combatant} used an item."
|
||||
|
||||
def use(self, combatant, item, *args, **kwargs):
|
||||
item.use(combatant, *args, **kwargs)
|
||||
self.msg("$You() $conj(use) an item.")
|
||||
|
||||
|
||||
class CombatActionDoNothing(CombatAction):
|
||||
|
|
@ -492,6 +538,14 @@ class CombatActionDoNothing(CombatAction):
|
|||
|
||||
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):
|
||||
"""
|
||||
|
|
@ -618,6 +672,8 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
self.interval - warning_time, self._warn_time, warning_time
|
||||
)
|
||||
|
||||
self.msg(f"|y_______________________ start turn {self.turn} ___________________________|n")
|
||||
|
||||
for combatant in self.combatants:
|
||||
# cycle combat menu
|
||||
self._init_menu(combatant)
|
||||
|
|
@ -628,10 +684,15 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
End of turn operations.
|
||||
|
||||
1. Do all regular actions
|
||||
2. Roll for any death events
|
||||
2. Remove combatants that disengaged successfully
|
||||
3. Timeout advantages/disadvantages
|
||||
|
||||
"""
|
||||
self.msg(
|
||||
f"|y__________________ turn resolution (turn {self.turn}) ____________________|n\n"
|
||||
)
|
||||
|
||||
# do all actions
|
||||
for combatant in self.combatants:
|
||||
# read the current action type selected by the player
|
||||
|
|
@ -639,7 +700,16 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
combatant, (CombatActionDoNothing(self, combatant), (), {})
|
||||
)
|
||||
# perform the action on the CombatAction instance
|
||||
action.use(*args, **kwargs)
|
||||
try:
|
||||
action.pre_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
|
||||
|
||||
|
|
@ -647,14 +717,37 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
|
||||
for combatant in self.combatants:
|
||||
# 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:
|
||||
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 clarity, we remove here rather than modifying the combatant list
|
||||
# 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)
|
||||
|
||||
# refresh stunt timeouts (note - self.stunt_duration is the same for
|
||||
|
|
@ -788,7 +881,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
if comb is combatant:
|
||||
continue
|
||||
|
||||
name = combatant.key
|
||||
name = comb.key
|
||||
health = f"{comb.hurt_level}"
|
||||
fleeing = ""
|
||||
if comb in self.fleeing_combatants:
|
||||
|
|
@ -798,24 +891,37 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
|
||||
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
|
||||
for adding any combat-specific text-decoration in one place.
|
||||
|
||||
Args:
|
||||
message (str): The message to send.
|
||||
targets (Object or list, optional): Sends message only to
|
||||
one or more particular combatants. If unset, send to
|
||||
everyone in the combat.
|
||||
combatant (Object): The 'You' in the message, if any.
|
||||
broadcast (bool): If `False`, `combatant` must be included and
|
||||
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:
|
||||
for target in make_iter(targets):
|
||||
target.msg(message)
|
||||
else:
|
||||
for target in self.combatants:
|
||||
target.msg(message)
|
||||
location = self.obj
|
||||
location_objs = location.contents
|
||||
|
||||
exclude = []
|
||||
if not broadcast and combatant:
|
||||
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):
|
||||
"""
|
||||
|
|
@ -839,48 +945,6 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
if combatant in self.fleeing_combatants:
|
||||
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):
|
||||
"""
|
||||
Register an action based on its `.key`.
|
||||
|
|
@ -927,7 +991,9 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
return list(self.combatant_actions[combatant].values())
|
||||
|
||||
|
||||
# ------------ start combat menu definitions
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Combat Menu definitions
|
||||
# -----------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _register_action(caller, raw_string, **kwargs):
|
||||
|
|
@ -1165,7 +1231,9 @@ def node_wait_start(caller, raw_string, **kwargs):
|
|||
return text, options
|
||||
|
||||
|
||||
# -------------- end of combat menu definitions
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Access function
|
||||
# -----------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def join_combat(caller, *targets, session=None):
|
||||
|
|
|
|||
|
|
@ -69,6 +69,14 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
|||
"""
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -85,6 +85,20 @@ class EvAdventureWeapon(EvAdventureObject):
|
|||
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):
|
||||
"""
|
||||
Base class for magic runestones. In _Knave_, every spell is represented by a rune stone
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class EvAdventureRollEngine:
|
|||
if 0 < diesize > max_diesize:
|
||||
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))
|
||||
|
||||
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):
|
||||
# normal roll
|
||||
# normal roll, or advantage cancels disadvantage
|
||||
return self.roll("1d20")
|
||||
elif advantage:
|
||||
return max(self.roll("1d20"), self.roll("1d20"))
|
||||
|
|
@ -129,9 +129,10 @@ class EvAdventureRollEngine:
|
|||
modifier (int, optional): An additional +/- modifier to the roll.
|
||||
|
||||
Returns:
|
||||
tuple: (bool, str): If the save was passed or not. The second element is the
|
||||
quality of the roll - None (normal), "critical fail" and "critical success".
|
||||
|
||||
tuple: A tuple `(bool, str, str)`. The bool indicates if the save was passed or not.
|
||||
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:
|
||||
Advantage and disadvantage cancel each other out.
|
||||
|
||||
|
|
@ -147,7 +148,25 @@ class EvAdventureRollEngine:
|
|||
quality = Ability.CRITICAL_SUCCESS
|
||||
else:
|
||||
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(
|
||||
self,
|
||||
|
|
@ -174,14 +193,16 @@ class EvAdventureRollEngine:
|
|||
modifier (int): An additional +/- modifier to the roll.
|
||||
|
||||
Returns:
|
||||
tuple: (bool, str): If the attack succeed or not. The second element is the
|
||||
quality of the roll - None (normal), "critical fail" and "critical success".
|
||||
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". Last
|
||||
element is a text that summarizes the details of the roll.
|
||||
Notes:
|
||||
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,
|
||||
bonus_type=attack_type,
|
||||
target=defender_defense,
|
||||
|
|
@ -189,6 +210,9 @@ class EvAdventureRollEngine:
|
|||
disadvantage=disadvantage,
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -21,9 +21,15 @@ from evennia.scripts.scripthandler import ScriptHandler
|
|||
from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler
|
||||
from evennia.typeclasses.models import TypeclassBase
|
||||
from evennia.utils import ansi, create, funcparser, logger, search
|
||||
from evennia.utils.utils import (class_from_module, is_iter, lazy_property,
|
||||
list_to_string, make_iter, to_str,
|
||||
variable_from_module)
|
||||
from evennia.utils.utils import (
|
||||
class_from_module,
|
||||
is_iter,
|
||||
lazy_property,
|
||||
list_to_string,
|
||||
make_iter,
|
||||
to_str,
|
||||
variable_from_module,
|
||||
)
|
||||
|
||||
_INFLECT = inflect.engine()
|
||||
_MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||
|
|
@ -714,7 +720,15 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
for obj in contents:
|
||||
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.
|
||||
|
||||
|
|
@ -738,6 +752,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
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
|
||||
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
|
||||
messaged objects.
|
||||
|
||||
|
|
@ -802,7 +820,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
# actor-stance replacements
|
||||
inmessage = _MSG_CONTENTS_PARSER.parse(
|
||||
inmessage,
|
||||
raise_errors=True,
|
||||
raise_errors=raise_funcparse_errors,
|
||||
return_string=True,
|
||||
caller=you,
|
||||
receiver=receiver,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue