Files
hbm-foundry-system/src/logic/spell-triggers.ts
T
octoturgeandClaude Sonnet 5.5 c533ad8040 feat: sync rules with vault 2026-10-20 (borrowed mana, Odetchnięcie limit, Oszołomiony, Akcje Natychmiastowe)
Adds src/logic/rules.ts, counters, migration 1.4.0, III/IV mana swap, thaumaturgy school, tests, recompiled translations.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:22:31 +02:00

121 lines
4.1 KiB
TypeScript

/**
* Reactive spell trigger registry.
*
* Spells with `triggers[]` register flag-based listeners on the caster:
* actor.flags.hbm.triggers[] = [{ event, effect, spellId, expiresAt }]
*
* Hooks check the matching event and prompt/resolve the trigger.
*
* Supported events:
* - killWithWeapon (e.g. Szkarłatny Sztylet - free spell after kill)
* - targetCastsSpell (e.g. Klątwa Szkarłatu - opposed save → unconscious)
* - damageTaken (reactive shields)
* - turnStart (per-turn drains)
*
* Triggers are stored as world-flag data (serialisable); the runtime hook
* dispatches based on `event`. Effect strings are advisory text (GM-resolved)
* unless they map to a known machine effect ID.
*/
import { SYSTEM_ID } from '../constants';
export interface SpellTrigger {
event: string;
effect: string;
spellId: string;
spellName: string;
/** Optional: combat round at which this trigger expires. */
expiresAtRound?: number;
/** Single-use after fire. */
oneShot?: boolean;
}
interface ActorWithFlags {
id?: string;
name?: string;
getFlag?: (scope: string, key: string) => unknown;
setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown>;
unsetFlag?: (scope: string, key: string) => Promise<unknown>;
}
function readTriggers(actor: ActorWithFlags): SpellTrigger[] {
const raw = actor.getFlag?.(SYSTEM_ID, 'triggers') as SpellTrigger[] | undefined;
return Array.isArray(raw) ? raw.slice() : [];
}
async function writeTriggers(actor: ActorWithFlags, triggers: SpellTrigger[]): Promise<void> {
if (triggers.length === 0) {
await actor.unsetFlag?.(SYSTEM_ID, 'triggers');
} else {
await actor.setFlag?.(SYSTEM_ID, 'triggers', triggers);
}
}
export async function registerSpellTriggers(
actor: ActorWithFlags,
spell: { id?: string; name?: string; system?: { triggers?: Array<{ event: string; effect: string }> } },
): Promise<number> {
const list = spell.system?.triggers ?? [];
if (!Array.isArray(list) || list.length === 0) return 0;
const existing = readTriggers(actor);
const now = (game as any).combat?.round ?? 0;
for (const t of list) {
existing.push({
event: t.event,
effect: t.effect,
spellId: spell.id ?? '',
spellName: spell.name ?? '',
expiresAtRound: now + 1, // default: end of next round; spell may override later
oneShot: true,
});
}
await writeTriggers(actor, existing);
return list.length;
}
export async function fireTriggers(
actor: ActorWithFlags,
event: string,
payload?: Record<string, unknown>,
): Promise<SpellTrigger[]> {
const triggers = readTriggers(actor);
const fired: SpellTrigger[] = [];
const remaining: SpellTrigger[] = [];
for (const t of triggers) {
if (t.event === event) {
fired.push(t);
// Post chat card so GM can resolve the effect manually if no auto-handler.
await ChatMessage.create({
content: `<strong>${actor.name ?? ''}</strong> - Wyzwalacz: <em>${t.spellName}</em><br/>Efekt: ${t.effect}`,
whisper: ChatMessage.getWhisperRecipients?.('GM') ?? [],
});
if (!t.oneShot) remaining.push(t);
} else {
remaining.push(t);
}
}
if (fired.length > 0) await writeTriggers(actor, remaining);
return fired;
}
export function registerTriggerHooks(): void {
// Combat-end / actor death events feed triggers.
// Foundry hooks: 'updateActor' (HP delta), 'createChatMessage' (attack rolls), 'updateCombat'.
Hooks.on('updateActor', async (actor: any, change: any) => {
const newHp = change?.system?.attributes?.health?.value;
if (typeof newHp === 'number' && newHp <= 0) {
// Find any actor in combat with a killWithWeapon trigger awaiting fire
const combat = (game as any).combat;
if (!combat?.started) return;
for (const c of combat.combatants ?? []) {
const a = c.actor as ActorWithFlags;
if (!a) continue;
const triggers = readTriggers(a);
if (triggers.some((t) => t.event === 'killWithWeapon')) {
await fireTriggers(a, 'killWithWeapon', { victim: actor.id });
}
}
}
});
}