V2 Phase 3: combat rules automation (conditions, damage types, reminders)
Completes the deployable Phase 1-3 chunk: the combat tracker now runs the math.
- condition-effects engine: declarative CONDITION_EFFECTS_5E/PF2E table +
deriveState(system, speed, conditions) → effective speed / advantage state /
incapacitation / pf2e status penalty. Tracker shows effect badges
("Speed 0", "Disadv. attacks", "−2 status"); pure + 10 unit tests.
- damage types: applyDamage(c, amount, type?) consults snapshotted monster
DamageDefenses (immune→0, resist→halve, vulnerable→double), back-compatible.
Monster add snapshots damageDefenses; tracker shows a type picker only when a
creature has typed defenses, and the log notes resisted/vulnerable amounts.
- concentration + death saves are surfaced as REMINDERS, never auto-rolled:
damaging a concentrating PC logs "roll a DC N Con save…"; a downed PC's turn
logs "make a death saving throw." The player rolls and resolves via the Drop
button / death-save pips they already own. (Per user: no auto dice rolls —
it removes the player from their character.)
Build + 271 unit tests green. Verified live: grapple → Speed 0 badge;
fire-immune creature shows the immune-labelled damage picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import type { SystemId } from '@/lib/rules';
|
||||
import type { AbilityKey, AdvState } from './types';
|
||||
|
||||
/**
|
||||
* The mechanical effect a condition imposes — a declarative table, never
|
||||
* hardcoded `if`s in combat code. `deriveState` (creatureState.ts) folds these
|
||||
* into a single query the tracker and dice roller ask.
|
||||
*/
|
||||
export interface ConditionEffect {
|
||||
/** sets effective speed to 0 */
|
||||
speedZero?: boolean;
|
||||
/** this creature's attack rolls */
|
||||
attackRolls?: AdvState;
|
||||
/** attack rolls made AGAINST this creature */
|
||||
attacksAgainst?: AdvState;
|
||||
/** disadvantage on ability checks (5e) */
|
||||
abilityChecks?: boolean;
|
||||
/** per-ability save penalty (auto-fail dominates disadvantage) */
|
||||
saves?: Partial<Record<AbilityKey, 'disadvantage' | 'auto-fail'>>;
|
||||
/** cannot take actions or reactions */
|
||||
incapacitated?: boolean;
|
||||
/** pf2e: a numeric status penalty scaling with the condition's value */
|
||||
statusPenalty?: boolean;
|
||||
}
|
||||
|
||||
const ALL_PHYS: Partial<Record<AbilityKey, 'auto-fail'>> = { str: 'auto-fail', dex: 'auto-fail' };
|
||||
|
||||
/** D&D 5e condition mechanics (keys lowercased to match stored names). */
|
||||
export const CONDITION_EFFECTS_5E: Record<string, ConditionEffect> = {
|
||||
blinded: { attackRolls: 'disadvantage', attacksAgainst: 'advantage' },
|
||||
frightened: { attackRolls: 'disadvantage', abilityChecks: true },
|
||||
grappled: { speedZero: true },
|
||||
incapacitated: { incapacitated: true },
|
||||
invisible: { attackRolls: 'advantage', attacksAgainst: 'disadvantage' },
|
||||
paralyzed: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage', saves: ALL_PHYS },
|
||||
petrified: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage', saves: ALL_PHYS },
|
||||
poisoned: { attackRolls: 'disadvantage', abilityChecks: true },
|
||||
prone: { attackRolls: 'disadvantage', attacksAgainst: 'advantage' },
|
||||
restrained: { speedZero: true, attackRolls: 'disadvantage', attacksAgainst: 'advantage', saves: { dex: 'disadvantage' } },
|
||||
stunned: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage', saves: ALL_PHYS },
|
||||
unconscious: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage', saves: ALL_PHYS },
|
||||
};
|
||||
|
||||
/**
|
||||
* Pathfinder 2e condition mechanics. PF2e uses flat numeric penalties rather
|
||||
* than advantage/disadvantage; valued conditions (frightened N, clumsy N, …)
|
||||
* contribute a status penalty aggregated by `deriveState`.
|
||||
*/
|
||||
export const CONDITION_EFFECTS_PF2E: Record<string, ConditionEffect> = {
|
||||
blinded: { attacksAgainst: 'advantage' },
|
||||
clumsy: { statusPenalty: true },
|
||||
enfeebled: { statusPenalty: true },
|
||||
frightened: { statusPenalty: true },
|
||||
grabbed: { speedZero: true, attacksAgainst: 'advantage' },
|
||||
immobilized: { speedZero: true },
|
||||
paralyzed: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage' },
|
||||
petrified: { incapacitated: true, speedZero: true },
|
||||
prone: { attacksAgainst: 'advantage' },
|
||||
restrained: { speedZero: true, incapacitated: false, attacksAgainst: 'advantage' },
|
||||
sickened: { statusPenalty: true },
|
||||
slowed: { statusPenalty: true },
|
||||
stunned: { incapacitated: true },
|
||||
stupefied: { statusPenalty: true },
|
||||
unconscious: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage' },
|
||||
'off-guard': { attacksAgainst: 'advantage' },
|
||||
};
|
||||
|
||||
export function conditionEffects(system: SystemId): Record<string, ConditionEffect> {
|
||||
return system === 'pf2e' ? CONDITION_EFFECTS_PF2E : CONDITION_EFFECTS_5E;
|
||||
}
|
||||
Reference in New Issue
Block a user