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 . 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
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue