fix(rules): ray of enfeeblement applies a rolled Strength penalty (Fort half) instead of fatigued
SRD ray-of-enfeeblement.md: 1d6 + 1 per 2 caster levels (max 1d6+5) Strength penalty, Fortitude half, 1 round/level. Adds a PenaltyEffect type (rolled formula, scaling, half on save, level-scaled duration) applied as negative untyped modifiers on the rolls Strength feeds (attack, damage), since the engine has no ability-score track. Formerly modeled as a fatigued condition with Fort negates.
This commit is contained in:
@@ -135,6 +135,7 @@ from pf1e_simulator.spells import (
|
||||
ConditionEffect,
|
||||
DamageEffect,
|
||||
HealEffect,
|
||||
PenaltyEffect,
|
||||
RecurringDamageSpec,
|
||||
SaveType,
|
||||
SpellEffect,
|
||||
@@ -434,8 +435,9 @@ def _burst_center(
|
||||
|
||||
def _spell_effect_summary(effect: SpellEffect) -> str:
|
||||
"""Short serializable summary of one spell effect (never the description)."""
|
||||
if isinstance(effect, DamageEffect):
|
||||
parts = [dice_to_str(effect.formula), *effect.types]
|
||||
if isinstance(effect, (DamageEffect, PenaltyEffect)):
|
||||
tags = effect.types if isinstance(effect, DamageEffect) else effect.targets
|
||||
parts = [dice_to_str(effect.formula), *tags]
|
||||
if effect.half_on_save:
|
||||
parts.append("half on save")
|
||||
return " ".join(parts)
|
||||
@@ -494,6 +496,8 @@ def _spell_half_on_save(spell: SpellSpec) -> bool:
|
||||
for effect in spell.effects:
|
||||
if isinstance(effect, DamageEffect) and effect.half_on_save:
|
||||
return True
|
||||
if isinstance(effect, PenaltyEffect) and effect.half_on_save:
|
||||
return True
|
||||
if isinstance(effect, AreaEffect) and effect.save_for_half:
|
||||
return True
|
||||
return False
|
||||
@@ -6424,7 +6428,7 @@ class CombatEngine:
|
||||
|
||||
def _apply_targeted_spell_effect(
|
||||
self,
|
||||
effect: DamageEffect | ConditionEffect | HealEffect | BuffEffect,
|
||||
effect: DamageEffect | ConditionEffect | HealEffect | BuffEffect | PenaltyEffect,
|
||||
target: CombatantState,
|
||||
caster_live: _LiveStats,
|
||||
target_live: _LiveStats,
|
||||
@@ -6452,6 +6456,11 @@ class CombatEngine:
|
||||
effect, target, caster_level=caster.combatant.level,
|
||||
empower=empower, maximize=maximize, saved=saved,
|
||||
)
|
||||
if isinstance(effect, PenaltyEffect):
|
||||
return self._apply_penalty_effect(
|
||||
effect, target, caster=caster, saved=saved,
|
||||
empower=empower, maximize=maximize, spell_name=spell_name,
|
||||
)
|
||||
return self._apply_buff_effect(effect, target, spell_name=spell_name)
|
||||
|
||||
def _apply_damage_effect(
|
||||
@@ -6680,6 +6689,48 @@ class CombatEngine:
|
||||
)
|
||||
return f"buff {len(effect.modifiers)} mods ({effect.duration_rounds}r)"
|
||||
|
||||
def _apply_penalty_effect(
|
||||
self,
|
||||
effect: PenaltyEffect,
|
||||
target: CombatantState,
|
||||
*,
|
||||
caster: CombatantState,
|
||||
saved: bool,
|
||||
empower: bool,
|
||||
maximize: bool,
|
||||
spell_name: str,
|
||||
) -> str:
|
||||
"""Apply a rolled ability penalty (SRD Ray of Enfeeblement).
|
||||
|
||||
The penalty is rolled (maximized/empowered like spell damage), halved
|
||||
on a successful save, and applied as negative untyped StatModifiers on
|
||||
the listed targets for ``duration_rounds_per_level`` rounds per caster
|
||||
level. A penalty of 0 or less has no effect.
|
||||
"""
|
||||
formula = effect.effective_formula(caster.combatant.level)
|
||||
if maximize:
|
||||
penalty = formula.max_value()
|
||||
else:
|
||||
penalty = formula.roll(self._rng)
|
||||
if empower:
|
||||
penalty = int(penalty * 1.5)
|
||||
if saved and effect.half_on_save:
|
||||
penalty //= 2
|
||||
if penalty <= 0:
|
||||
return "no penalty"
|
||||
duration = max(1, caster.combatant.level * effect.duration_rounds_per_level)
|
||||
target.active_buffs.append(
|
||||
ActiveBuff(
|
||||
modifiers=tuple(
|
||||
StatModifier(target=t, value=-penalty, bonus_type="")
|
||||
for t in effect.targets
|
||||
),
|
||||
rounds_left=duration,
|
||||
source=f"spell: {spell_name}",
|
||||
)
|
||||
)
|
||||
return f"-{penalty} {'/'.join(effect.targets)} ({duration}r)"
|
||||
|
||||
def _apply_area_effect(
|
||||
self,
|
||||
effect: AreaEffect,
|
||||
@@ -6862,6 +6913,19 @@ class CombatEngine:
|
||||
if isinstance(effect, BuffEffect):
|
||||
duration = effect.duration_rounds * 2 if has_extend else effect.duration_rounds
|
||||
return BuffEffect(modifiers=effect.modifiers, duration_rounds=duration)
|
||||
if isinstance(effect, PenaltyEffect):
|
||||
duration = (
|
||||
effect.duration_rounds_per_level * 2
|
||||
if has_extend
|
||||
else effect.duration_rounds_per_level
|
||||
)
|
||||
return PenaltyEffect(
|
||||
formula=effect.formula,
|
||||
scaling_flat=effect.scaling_flat,
|
||||
targets=effect.targets,
|
||||
duration_rounds_per_level=duration,
|
||||
half_on_save=effect.half_on_save,
|
||||
)
|
||||
if isinstance(effect, AreaEffect):
|
||||
return AreaEffect(
|
||||
shape=effect.shape,
|
||||
|
||||
Reference in New Issue
Block a user