mirror of
https://github.com/evennia/evennia.git
synced 2026-03-17 05:16:31 +01:00
More work on combat
This commit is contained in:
parent
04200f8bfc
commit
a7ced1dbfc
5 changed files with 398 additions and 119 deletions
|
|
@ -8,6 +8,7 @@ from evennia.objects.objects import DefaultCharacter, DefaultObject
|
|||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
from evennia.utils.utils import lazy_property, int2str
|
||||
from .objects import EvAdventureObject
|
||||
from . import rules
|
||||
|
||||
|
||||
class EquipmentError(TypeError):
|
||||
|
|
@ -284,6 +285,7 @@ class EvAdventureCharacter(DefaultCharacter):
|
|||
|
||||
"""
|
||||
|
||||
# these are the ability bonuses. Defense is always 10 higher
|
||||
strength = AttributeProperty(default=1)
|
||||
dexterity = AttributeProperty(default=1)
|
||||
constitution = AttributeProperty(default=1)
|
||||
|
|
@ -308,6 +310,24 @@ class EvAdventureCharacter(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.
|
||||
Will return the "Unarmed" weapon if none other are found.
|
||||
|
||||
"""
|
||||
# TODO
|
||||
|
||||
@property
|
||||
def armor(self):
|
||||
"""
|
||||
Quick access to the character's current armor.
|
||||
Will return the "Unarmored" armor if none other are found.
|
||||
|
||||
"""
|
||||
# TODO
|
||||
|
||||
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.
|
||||
|
|
@ -358,6 +378,37 @@ class EvAdventureCharacter(DefaultCharacter):
|
|||
self.equipment.remove(moved_object)
|
||||
|
||||
|
||||
def at_pre_damage(self, dmg, attacker=None):
|
||||
"""
|
||||
Called when receiving damage for whatever reason. This
|
||||
is called *before* hp is evaluated for defeat/death.
|
||||
|
||||
"""
|
||||
def at_post_damage(self, dmg, attacker=None):
|
||||
"""
|
||||
Called when receiving damage for whatever reason. This
|
||||
is called *before* hp is evaluated for defeat/death.
|
||||
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
def handle_death(self):
|
||||
"""
|
||||
Called when character dies.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class EvAdventureNPC(DefaultCharacter):
|
||||
"""
|
||||
This is the base class for all non-player entities, including monsters. These
|
||||
|
|
|
|||
|
|
@ -19,41 +19,25 @@ from dataclasses import dataclass
|
|||
from collections import defaultdict
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
from evennia.utils.utils import make_iter
|
||||
from . import rules
|
||||
|
||||
MIN_RANGE = 0
|
||||
MAX_RANGE = 4
|
||||
|
||||
@dataclass
|
||||
class CombatantStats:
|
||||
RANGE_NAMES = {
|
||||
0: "close", # melee, short weapons, fists. long weapons with disadvantage
|
||||
1: "near", # melee, long weapons, short weapons with disadvantage
|
||||
2: "medium", # thrown, ranged with disadvantage
|
||||
3: "far", # ranged, thrown with disadvantage
|
||||
4: "disengaging" # no weapons
|
||||
}
|
||||
|
||||
|
||||
class AttackFailure(RuntimeError):
|
||||
"""
|
||||
Represents temporary combat-only data we need to track
|
||||
during combat for a single Character.
|
||||
|
||||
Cannot attack for some reason.
|
||||
"""
|
||||
weapon = None
|
||||
armor = None
|
||||
# abstract distance relationship to other combatants
|
||||
distance_matrix = {}
|
||||
# actions may affect what works better/worse next round
|
||||
advantage_actions_next_turn = []
|
||||
disadvantage_actions_next_turn = []
|
||||
|
||||
def get_distance(self, target):
|
||||
return self.distance_matrix.get(target)
|
||||
|
||||
def change_distance(self, target, change):
|
||||
"""
|
||||
Change the distance to an opponent. This is symmetric.
|
||||
|
||||
Args:
|
||||
target (Object): The target to change the distance to.
|
||||
change (int): How to change the distance. Negative values to
|
||||
approach, positive to move away from target.
|
||||
"""
|
||||
current_dist = self.distance_matrix.get(target) # will raise error if None, as it should
|
||||
new_dist = max(MIN_RANGE, min(MAX_RANGE, current_dist + change))
|
||||
self.distance_matrix[target] = target.distance_matrix[self] = new_dist
|
||||
|
||||
|
||||
class EvAdventureCombat(DefaultScript):
|
||||
|
|
@ -62,12 +46,18 @@ class EvAdventureCombat(DefaultScript):
|
|||
of all active participants. It's also possible to join (or leave) the fray later.
|
||||
|
||||
"""
|
||||
combatants = AttributeProperty(default=dict())
|
||||
queue = AttributeProperty(default=list())
|
||||
combatants = AttributeProperty(default=list())
|
||||
action_queue = AttributeProperty(default=dict())
|
||||
|
||||
# turn counter - abstract time
|
||||
turn = AttributeProperty(default=1)
|
||||
# symmetric distance matrix
|
||||
distance_matrix = {}
|
||||
turn = AttributeProperty(default=0)
|
||||
# symmetric distance matrix (handled dynamically). Mapping {combatant1: {combatant2: dist}, ...}
|
||||
distance_matrix = defaultdict(dict)
|
||||
# advantages or disadvantages gained against different targets
|
||||
advantage_matrix = AttributeProperty(defaultdict(dict))
|
||||
disadvantage_matrix = AttributeProperty(defaultdict(dict))
|
||||
|
||||
stunt_duration = 2
|
||||
|
||||
def _refresh_distance_matrix(self):
|
||||
"""
|
||||
|
|
@ -85,55 +75,255 @@ class EvAdventureCombat(DefaultScript):
|
|||
4. Disengaging/fleeing (no weapons can be used)
|
||||
|
||||
Distance is tracked to each opponent individually. One can move 1 step and attack
|
||||
or up to 3 steps without attacking.
|
||||
or up to 2 steps (closer or further away) without attacking.
|
||||
|
||||
New combatants will start at a distance averaged between the optimal ranges
|
||||
of them and their opponents.
|
||||
|
||||
"""
|
||||
handled = []
|
||||
for combatant1, combatant_stats1 in self.combatants.items():
|
||||
for combatant2, combatant_stats2 in self.combatants.items():
|
||||
combatants = self.combatants
|
||||
distance_matrix = self.distance_matrix
|
||||
|
||||
for combatant1 in combatants:
|
||||
for combatant2 in combatants:
|
||||
|
||||
if combatant1 == combatant2:
|
||||
continue
|
||||
|
||||
# only update if data was not available already (or drifted
|
||||
# out of sync, which should not happen)
|
||||
dist1 = combatant_stats1.get_distance(combatant2)
|
||||
dist2 = combatant_stats2.get_distance(combatant1)
|
||||
if None in (dist1, dist2) or dist1 != dist2:
|
||||
# a new distance-relation - start out at average distance
|
||||
avg_range = round(0.5 * (combatant1.weapon.range_optimal
|
||||
+ combatant2.weapon.range_optimal))
|
||||
combatant_stats1.distance_matrix[combatant2] = avg_range
|
||||
combatant_stats2.distance_matrix[combatant1] = avg_range
|
||||
combatant1_distances = distance_matrix[combatant1]
|
||||
combatant2_distances = distance_matrix[combatant2]
|
||||
|
||||
handled.append(combatant1)
|
||||
handled.append(combatant2)
|
||||
if combatant2 not in combatant1_distances or combatant1 not in combatant2_distances:
|
||||
# this happens on initialization or when a new combatant is added.
|
||||
# we make sure to update both sides to the distance of the longest
|
||||
# optimal weapon range. So ranged weapons have advantage going in.
|
||||
start_optimal = max(combatant1.weapon.distance_optimal,
|
||||
combatant2.weapon.distance_optimal)
|
||||
|
||||
self.combatants = handled
|
||||
combatant1_distances[combatant2] = start_optimal
|
||||
combatant2_distances[combatant1] = start_optimal
|
||||
|
||||
def _move_relative_to(self, combatant, target_combatant, change):
|
||||
def _start_turn(self):
|
||||
"""
|
||||
New turn events
|
||||
|
||||
"""
|
||||
self.turn += 1
|
||||
self.action_queue = {}
|
||||
|
||||
def _end_turn(self):
|
||||
"""
|
||||
End of turn cleanup.
|
||||
|
||||
"""
|
||||
# refresh stunt timeouts
|
||||
oldest_stunt_age = self.turn - self.stunt_duration
|
||||
|
||||
advantage_matrix = self.advantage_matrix
|
||||
disadvantage_matrix = self.disadvantage_matrix
|
||||
|
||||
# to avoid modifying the dict while we iterate over it, we
|
||||
# put the results in new dicts. This also avoids us having to
|
||||
# delete from the old dicts.
|
||||
new_advantage_matrix = {}
|
||||
new_disadvantage_matrix = {}
|
||||
|
||||
for combatant in self.combatants:
|
||||
new_advantage_matrix[combatant] = {
|
||||
target: set_at_turn for target, turn in advantage_matrix.items()
|
||||
if set_at_turn > oldest_stunt_age
|
||||
}
|
||||
new_disadvantage_matrix[combatant] = {
|
||||
target: set_at_turn for target, turn in disadvantage_matrix.items()
|
||||
if set_at_turn > oldest_stunt_age
|
||||
}
|
||||
self.advantage_matrix = new_advantage_matrix
|
||||
self.disadvantage_matrix = new_disadvantage_matrix
|
||||
|
||||
def msg(self, message, targets=None):
|
||||
"""
|
||||
Central place for sending messages to combatants. This allows
|
||||
for decorating the output in one place if needed.
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
if targets:
|
||||
for target in make_iter(targets):
|
||||
target.msg(message)
|
||||
else:
|
||||
for target in self.combatants:
|
||||
target.msg(message)
|
||||
|
||||
def add_combatant(self, combatant):
|
||||
if combatant not in self.combatants:
|
||||
self.combatants.append(combatant)
|
||||
self._refresh_distance_matrix()
|
||||
|
||||
def remove_combatant(self, combatant):
|
||||
if combatant in self.combatants:
|
||||
self.combatants.remove(combatant)
|
||||
self._refresh_distance_matrix()
|
||||
|
||||
def move_relative_to(self, combatant, target_combatant, change):
|
||||
"""
|
||||
Change the distance to a target.
|
||||
|
||||
Args:
|
||||
combatant (Character): The one doing the change.
|
||||
target_combatant (Character): The one changing towards.
|
||||
target_combatant (Character): The one distance is changed to.
|
||||
change (int): A +/- change value. Result is always in range 0..4.
|
||||
|
||||
"""
|
||||
self.combatants[combatant].change_distance(target_combatant, change)
|
||||
self.combatants[target_combatant].change_distance(combatant, change)
|
||||
current_dist = self.distance_matrix[combatant][target_combatant]
|
||||
|
||||
def add_combatant(self, combatant):
|
||||
self.combatants[combatant] = CombatantStats(
|
||||
weapon=combatant.equipment.get("weapon"),
|
||||
armor=combatant.equipment.armor,
|
||||
new_dist = max(MIN_RANGE, min(MAX_RANGE, current_dist + change))
|
||||
|
||||
self.distance_matrix[combatant][target_combatant] = new_dist
|
||||
self.distance_matrix[target_combatant][combatant] = new_dist
|
||||
|
||||
def gain_advantage(self, combatant, target):
|
||||
"""
|
||||
Gain advantage against target. Spent by actions.
|
||||
|
||||
"""
|
||||
self.advantage_matrix[combatant][target] = self.turn
|
||||
|
||||
def gain_disadvantage(self, combatant, target):
|
||||
"""
|
||||
Gain disadvantage against target. Spent by actions.
|
||||
|
||||
"""
|
||||
self.disadvantage_matrix[combatant][target] = self.turn
|
||||
|
||||
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.EvAdventureRollEngine.roll(weapon_dmg_roll)
|
||||
if critical:
|
||||
dmg += rules.EvAdventureRollEngine.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.EvAdventureRollEngine.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 stunt(self, attacker, defender, attack_type="agility",
|
||||
defense_type="agility", optimal_distance=0, suboptimal_distance=1,
|
||||
advantage=True, beneficiary=None):
|
||||
"""
|
||||
Stunts does not hurt anyone, but are used to give advantage/disadvantage to combatants
|
||||
for later turns. The 'attacker' here is the one attemting the stunt against the 'defender'.
|
||||
If successful, advantage is given to attacker against defender and disadvantage to
|
||||
defender againt attacker. It's also possible to replace the attacker with another combatant
|
||||
against the defender - allowing to aid/hinder others on the battlefield.
|
||||
|
||||
Stunt-modifers last a maximum of two turns and are not additive. Advantages and
|
||||
disadvantages against the same target cancel each other out.
|
||||
|
||||
Args:
|
||||
attacker (Object): The one attempting the stunt.
|
||||
defender (Object): The one affected by the stunt.
|
||||
attack_type (str): The ability tested to do the stunt.
|
||||
defense_type (str): The ability used to defend against the stunt.
|
||||
optimal_distance (int): At which distance the stunt works normally.
|
||||
suboptimal_distance (int): At this distance, the stunt is performed at disadvantage.
|
||||
advantage (bool): If False, try to apply disadvantage to defender
|
||||
rather than advantage to attacker.
|
||||
beneficiary (bool): If stunt succeeds, it may benefit another
|
||||
combatant than the `attacker` doing the stunt. This allows for helping
|
||||
allies.
|
||||
|
||||
"""
|
||||
# check if stunt-attacker is at optimal distance
|
||||
distance = self.distance_matrix[attacker][defender]
|
||||
disadvantage = False
|
||||
if suboptimal_distance == distance:
|
||||
# fighting at the wrong range is not good
|
||||
disadvantage = True
|
||||
elif self._get_optimal_distance(attacker) != distance:
|
||||
# if we are neither at optimal nor suboptimal distance, we can't do the stunt
|
||||
# from here.
|
||||
raise AttackFailure(f"You can't perform this stunt "
|
||||
f"from {RANGE_NAMES[distance]} distance (must be "
|
||||
f"{RANGE_NAMES[suboptimal_distance]} or, even better, "
|
||||
f"{RANGE_NAMES[optimal_distance]}).")
|
||||
# quality doesn't matter for stunts, they are either successful or not
|
||||
is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw(
|
||||
attacker, defender,
|
||||
attack_type=attack_type,
|
||||
defense_type=defense_type,
|
||||
advantage=False, disadvantage=disadvantage,
|
||||
)
|
||||
self._refresh_distance_matrix()
|
||||
if is_success:
|
||||
beneficiary = beneficiary if beneficiary else attacker
|
||||
if advantage:
|
||||
self.gain_advantage(beneficiary, defender)
|
||||
else:
|
||||
self.gain_disadvantage(defender, beneficiary)
|
||||
|
||||
def remove_combatant(self, combatant):
|
||||
self.combatants.pop(combatant, None)
|
||||
self._refresh_distance_matrix()
|
||||
return is_success
|
||||
|
||||
def attack(self, attacker, defender):
|
||||
"""
|
||||
Make an attack against a defender. This takes into account distance.
|
||||
|
||||
"""
|
||||
# check if attacker is at optimal distance
|
||||
distance = self.distance_matrix[attacker][defender]
|
||||
|
||||
# figure out advantage (gained by previous stunts)
|
||||
advantage = bool(self.advantage_matrix[attacker].pop(defender, False))
|
||||
|
||||
# figure out disadvantage (by distance or by previous action)
|
||||
disadvantage = bool(self.disadvantage_matrix[attacker].pop(defender, False))
|
||||
if self._get_suboptimal_distance(attacker) == distance:
|
||||
# fighting at the wrong range is not good
|
||||
disadvantage = True
|
||||
elif self._get_optimal_distance(attacker) != distance:
|
||||
# if we are neither at optimal nor suboptimal distance, we can't
|
||||
# attack from this range
|
||||
raise AttackFailure(f"You can't attack with {attacker.weapon.key} "
|
||||
f"from {RANGE_NAMES[distance]} distance.")
|
||||
|
||||
is_hit, quality = rules.EvAdventureRollEngine.opposed_saving_throw(
|
||||
attacker, defender,
|
||||
attack_type=attacker.weapon.attack_type,
|
||||
defense_type=attacker.weapon.defense_type,
|
||||
advantage=advantage, disadvantage=disadvantage
|
||||
)
|
||||
if is_hit:
|
||||
self.resolve_damage(attacker, defender, critical=quality == "critical success")
|
||||
|
||||
return is_hit
|
||||
|
|
|
|||
|
|
@ -44,10 +44,14 @@ class EvAdventureWeapon(EvAdventureObject):
|
|||
|
||||
"""
|
||||
wield_slot = AttributeProperty(default="weapon")
|
||||
|
||||
attack_type = AttributeProperty(default="strength")
|
||||
defense_type = AttributeProperty(default="armor")
|
||||
damage_roll = AttributeProperty(default="1d6")
|
||||
|
||||
# at which ranges this weapon can be used. If not listed, unable to use
|
||||
range_optimal = AttributeProperty(default=0) # normal usage
|
||||
range_suboptimal = AttributeProperty(default=1) # usage with disadvantage
|
||||
distance_optimal = AttributeProperty(default=0) # normal usage (fists)
|
||||
distance_suboptimal = AttributeProperty(default=None) # disadvantage (fists)
|
||||
|
||||
|
||||
class EvAdventureRunestone(EvAdventureWeapon):
|
||||
|
|
|
|||
|
|
@ -366,3 +366,15 @@ initiative = [
|
|||
('4-6', "PC acts first"),
|
||||
]
|
||||
|
||||
|
||||
death_and_dismemberment = [
|
||||
"dead",
|
||||
"dead", # original says 'dismemberment' here, we don't simulate this
|
||||
"weakened", # -1d4 STR
|
||||
"unsteady", # -1d4 DEX
|
||||
"sickly", # -1d4 CON
|
||||
"addled", # -1d4 INT
|
||||
"rattled", # -1d4 WIS
|
||||
"disfigured", # -1d4 CHA
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -175,61 +175,6 @@ class EvAdventureRollEngine:
|
|||
quality = None
|
||||
return (dice_roll + attack_bonus + modifier) > defender_defense, quality
|
||||
|
||||
# specific rolls / actions
|
||||
|
||||
@staticmethod
|
||||
def melee_attack(attacker, defender, advantage=False, disadvantage=False):
|
||||
"""Close attack (strength vs armor)"""
|
||||
return opposed_saving_throw(
|
||||
attacker, defender, attack_type="strength", defense_type="armor",
|
||||
advantage=advantage, disadvantage=disadvantage)
|
||||
|
||||
@staticmethod
|
||||
def ranged_attack(attacker, defender, advantage=False, disadvantage=False):
|
||||
"""Ranged attack (wisdom vs armor)"""
|
||||
return opposed_saving_throw(
|
||||
attacker, defender, attack_type="wisdom", defense_type="armor",
|
||||
advantage=advantage, disadvantage=disadvantage)
|
||||
|
||||
@staticmethod
|
||||
def magic_attack(attacker, defender, advantage=False, disadvantage=False):
|
||||
"""Magic attack (int vs dexterity)"""
|
||||
return opposed_saving_throw(
|
||||
attacker, defender, attack_type="intelligence", defense_type="dexterity",
|
||||
advantage=advantage, disadvantage=disadvantage)
|
||||
|
||||
@staticmethod
|
||||
def morale_check(defender):
|
||||
"""
|
||||
A morale check is done for NPCs/monsters. It's done with a 2d6 against
|
||||
their morale.
|
||||
|
||||
Args:
|
||||
defender (NPC): The entity trying to defend its morale.
|
||||
|
||||
Returns:
|
||||
bool: False if morale roll failed, True otherwise.
|
||||
|
||||
"""
|
||||
return roll('2d6') <= defender.morale
|
||||
|
||||
@staticmethod
|
||||
def healing_from_rest(character):
|
||||
"""
|
||||
A meal and a full night's rest allow for regaining 1d8 + Const bonus HP.
|
||||
|
||||
Args:
|
||||
character (Character): The one resting.
|
||||
|
||||
Returns:
|
||||
int: How much HP was healed. This is never more than how damaged we are.
|
||||
|
||||
"""
|
||||
# we can't heal more than our damage
|
||||
damage = character.hp_max - character.hp
|
||||
healed = roll('1d8') + character.constitution
|
||||
return min(damage, healed)
|
||||
|
||||
@staticmethod
|
||||
def roll_random_table(dieroll, table, table_choices):
|
||||
"""
|
||||
|
|
@ -283,6 +228,83 @@ class EvAdventureRollEngine:
|
|||
roll_result = max(1, min(len(table_choices), roll_result))
|
||||
return table_choices[roll_result - 1]
|
||||
|
||||
# specific rolls / actions
|
||||
|
||||
@staticmethod
|
||||
def morale_check(defender):
|
||||
"""
|
||||
A morale check is done for NPCs/monsters. It's done with a 2d6 against
|
||||
their morale.
|
||||
|
||||
Args:
|
||||
defender (NPC): The entity trying to defend its morale.
|
||||
|
||||
Returns:
|
||||
bool: False if morale roll failed, True otherwise.
|
||||
|
||||
"""
|
||||
return roll('2d6') <= defender.morale
|
||||
|
||||
@staticmethod
|
||||
def healing_from_rest(character):
|
||||
"""
|
||||
A meal and a full night's rest allow for regaining 1d8 + Const bonus HP.
|
||||
|
||||
Args:
|
||||
character (Character): The one resting.
|
||||
|
||||
Returns:
|
||||
int: How much HP was healed. This is never more than how damaged we are.
|
||||
|
||||
"""
|
||||
# we can't heal more than our damage
|
||||
damage = character.hp_max - character.hp
|
||||
healed = roll('1d8') + character.constitution
|
||||
return min(damage, healed)
|
||||
|
||||
death_map = {
|
||||
"weakened": "strength",
|
||||
"unsteady": "dexterity",
|
||||
"sickly": "constitution",
|
||||
"addled": "intelligence",
|
||||
"rattled": "wisdom",
|
||||
"disfigured": "charisma",
|
||||
}
|
||||
|
||||
def roll_death(character):
|
||||
"""
|
||||
Happens when hitting <= 0 hp. unless dead,
|
||||
|
||||
"""
|
||||
|
||||
result = self.roll_random_table('1d8', 'death_and_dismemberment')
|
||||
if result == "dead":
|
||||
character.handle_death()
|
||||
else:
|
||||
# survives with degraded abilities (1d4 roll)
|
||||
abi = death_map[result]
|
||||
|
||||
current_abi = getattr(character, abi)
|
||||
loss = self.roll("1d4")
|
||||
|
||||
current_abi =- loss
|
||||
|
||||
if current_abi < -10:
|
||||
# can't lose more - die
|
||||
character.handle_death()
|
||||
else:
|
||||
new_hp = max(character.hp_max, self.roll("1d4"))
|
||||
setattr(character, abi, current_abi)
|
||||
character.hp = new_hp
|
||||
|
||||
character.msg(
|
||||
"~" * 78 +
|
||||
"\n|yYou survive your brush with death, "
|
||||
f"but are |r{result.upper()}|y and permenently |rlose {loss} {abi}|y.|n\n"
|
||||
f"|GYou recover |g{new_hp}|G health|.\n"
|
||||
+ "~" * 78
|
||||
)
|
||||
|
||||
|
||||
# character generation
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue