feat(loaders): map 8-PC feats

This commit is contained in:
2026-09-04 22:55:45 +02:00
parent 934d443d7d
commit 4d7f1b8233
4 changed files with 391 additions and 4 deletions
+40 -4
View File
@@ -255,7 +255,7 @@ class Action:
]
target_id: str | None = None
spell_name: str | None = None
maneuver: str | None = None # "trip", "disarm", "bull_rush", "sunder"
maneuver: str | None = None # "trip", "disarm", "bull_rush", "sunder", "grapple", "escape"
@dataclass(frozen=True)
@@ -915,7 +915,7 @@ class CombatEngine:
if self.has_condition_flag(state, "cant_attack"):
self._log(f"round {self._current_round} {state.combatant.id}: wait (cannot attack)")
return
if maneuver not in ("trip", "disarm", "bull_rush", "sunder"):
if maneuver not in ("trip", "disarm", "bull_rush", "sunder", "grapple", "escape"):
return
# Calculate CMB and CMD
@@ -974,11 +974,47 @@ class CombatEngine:
self._log(f"round {self._current_round} {state.combatant.id}: sunder vs {target.combatant.id} d20={roll}+{attacker_cmb}={total} CMD {target_cmd} -> SUCCESS ({dmg} damage to weapon)")
else:
self._log(f"round {self._current_round} {state.combatant.id}: sunder vs {target.combatant.id} d20={roll}+{attacker_cmb}={total} CMD {target_cmd} -> FAILED")
elif maneuver == "grapple":
if success:
# Initiate grapple
target.active_conditions.append(
ActiveCondition(
condition=CONDITIONS_BY_NAME["grappled"],
rounds_left=10, # Lasts until broken
source=f"maneuver: grapple by {state.combatant.id}",
)
)
target.conditions.append(CONDITIONS_BY_NAME["grappled"])
target.grappled_with = state.combatant.id
state.grappled_with = target.combatant.id
self._log(f"round {self._current_round} {state.combatant.id}: grapple vs {target.combatant.id} d20={roll}+{attacker_cmb}={total} CMD {target_cmd} -> SUCCESS (grappled)")
else:
self._log(f"round {self._current_round} {state.combatant.id}: grapple vs {target.combatant.id} d20={roll}+{attacker_cmb}={total} CMD {target_cmd} -> FAILED")
elif maneuver == "escape":
# Escape uses CMB or Escape Artist skill
escape_bonus = max(
state.combatant.cmb or (state.combatant.bab + state.combatant.abilities.str_mod + self._size_mod(state)),
self.resolve_skill(state, "escape_artist", 0).total if hasattr(state.combatant, 'skills') and 'escape_artist' in state.combatant.skills else 0
)
target_cmb = target.combatant.cmb or (target.combatant.bab + target.combatant.abilities.str_mod + self._size_mod(target))
target_cmd = 10 + target_cmb + target.combatant.abilities.dex_mod
roll = self._rng.d20()
total = roll + escape_bonus
success = roll != 1 and total >= target_cmd
if success:
# Remove grappled condition from both
target.active_conditions = [ac for ac in target.active_conditions if ac.condition.name != "grappled"]
target.conditions = [c for c in target.conditions if c.name != "grappled"]
state.active_conditions = [ac for ac in state.active_conditions if ac.condition.name != "grappled"]
state.conditions = [c for c in state.conditions if c.name != "grappled"]
target.grappled_with = None
state.grappled_with = None
else:
self._log(f"round {self._current_round} {state.combatant.id}: escape vs {target.combatant.id} d20={roll}+{escape_bonus}={total} CMD {target_cmd} -> FAILED")
def _execute_attack(
self, state: CombatantState, target: CombatantState | None, kind: str
) -> None:
if target is None or not target.active:
return
if self.has_condition_flag(state, "cant_attack"):
self._log(f"round {self._current_round} {state.combatant.id}: wait (cannot attack)")
return