Weak/Normal/Elite toggle in PF2e stat block header applies standard adjustments (level, AC, HP, saves, Perception, attacks, damage) to individual combatants. Adjusted stats are highlighted blue (elite) or red (weak). Persisted via creatureAdjustment field on Combatant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import type {
|
||
Pf2eCreature,
|
||
TraitBlock,
|
||
TraitSegment,
|
||
} from "./creature-types.js";
|
||
|
||
export type CreatureAdjustment = "weak" | "elite";
|
||
|
||
/** HP bracket delta by creature level (standard PF2e table). */
|
||
function hpBracketDelta(level: number): number {
|
||
if (level <= 1) return 10;
|
||
if (level <= 4) return 15;
|
||
if (level <= 19) return 20;
|
||
return 30;
|
||
}
|
||
|
||
/** Level shift: elite +1 (or +2 if level ≤ 0), weak −1 (or −2 if level is 1). */
|
||
export function adjustedLevel(
|
||
baseLevel: number,
|
||
adjustment: CreatureAdjustment,
|
||
): number {
|
||
if (adjustment === "elite") {
|
||
return baseLevel <= 0 ? baseLevel + 2 : baseLevel + 1;
|
||
}
|
||
return baseLevel === 1 ? baseLevel - 2 : baseLevel - 1;
|
||
}
|
||
|
||
/** Signed HP delta for a given base level and adjustment. */
|
||
export function hpDelta(
|
||
baseLevel: number,
|
||
adjustment: CreatureAdjustment,
|
||
): number {
|
||
const delta = hpBracketDelta(baseLevel);
|
||
return adjustment === "elite" ? delta : -delta;
|
||
}
|
||
|
||
/** AC delta: +2 for elite, −2 for weak. */
|
||
export function acDelta(adjustment: CreatureAdjustment): number {
|
||
return adjustment === "elite" ? 2 : -2;
|
||
}
|
||
|
||
/** Generic ±2 modifier delta. Used for saves, Perception, attacks, damage. */
|
||
export function modDelta(adjustment: CreatureAdjustment): number {
|
||
return adjustment === "elite" ? 2 : -2;
|
||
}
|
||
|
||
const ATTACK_BONUS_RE = /^([+-])(\d+)/;
|
||
const MAP_RE = /\[([+-]\d+)\/([+-]\d+)\]/g;
|
||
const DAMAGE_BONUS_RE = /(\d+d\d+)([+-])(\d+)/g;
|
||
|
||
/**
|
||
* Adjust attack bonus in a formatted attack string.
|
||
* "+15 (agile), 2d12+7 piercing plus Grab" → "+17 (agile), 2d12+9 piercing plus Grab"
|
||
*/
|
||
function adjustAttackText(text: string, delta: number): string {
|
||
// Adjust leading attack bonus: "+15" → "+17"
|
||
let result = text.replace(ATTACK_BONUS_RE, (_, sign, num) => {
|
||
const adjusted = (sign === "+" ? 1 : -1) * Number(num) + delta;
|
||
return adjusted >= 0 ? `+${adjusted}` : `${adjusted}`;
|
||
});
|
||
// Adjust MAP values in brackets: "[+10/+5]" → "[+12/+7]"
|
||
result = result.replace(MAP_RE, (_, m1, m2) => {
|
||
const a1 = Number(m1) + delta;
|
||
const a2 = Number(m2) + delta;
|
||
const f = (n: number) => (n >= 0 ? `+${n}` : `${n}`);
|
||
return `[${f(a1)}/${f(a2)}]`;
|
||
});
|
||
// Adjust damage bonus in "NdN+N type" patterns
|
||
result = result.replace(DAMAGE_BONUS_RE, (_, dice, sign, num) => {
|
||
const current = (sign === "+" ? 1 : -1) * Number(num);
|
||
const adjusted = current + delta;
|
||
if (adjusted === 0) return dice as string;
|
||
return adjusted > 0 ? `${dice}+${adjusted}` : `${dice}${adjusted}`;
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function adjustTraitBlock(block: TraitBlock, delta: number): TraitBlock {
|
||
return {
|
||
...block,
|
||
segments: block.segments.map(
|
||
(seg): TraitSegment =>
|
||
seg.type === "text"
|
||
? { type: "text", value: adjustAttackText(seg.value, delta) }
|
||
: seg,
|
||
),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Apply a weak or elite adjustment to a full PF2e creature.
|
||
* Returns a new Pf2eCreature with all numeric stats adjusted.
|
||
*/
|
||
export function applyPf2eAdjustment(
|
||
creature: Pf2eCreature,
|
||
adjustment: CreatureAdjustment,
|
||
): Pf2eCreature {
|
||
const d = modDelta(adjustment);
|
||
return {
|
||
...creature,
|
||
level: adjustedLevel(creature.level, adjustment),
|
||
ac: creature.ac + d,
|
||
hp: creature.hp + hpDelta(creature.level, adjustment),
|
||
perception: creature.perception + d,
|
||
saveFort: creature.saveFort + d,
|
||
saveRef: creature.saveRef + d,
|
||
saveWill: creature.saveWill + d,
|
||
attacks: creature.attacks?.map((a) => adjustTraitBlock(a, d)),
|
||
};
|
||
}
|