Initial commit

This commit is contained in:
Octoturge
2026-06-09 22:06:56 +02:00
commit fb42c6e8cc
121 changed files with 14976 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
/**
* Magia Otchłani — Abyss Magic logic (Klątwa Otchłani Ch. III–VIII).
*
* The Abyss splits into two disciplines:
* - Magia Aspektów ("aspects") — controlled, low-risk, predictable
* - Pierwotna Magia ("primal") — chaotic, high-power, requires `Dary Otchłani` rolls
*
* This module exposes:
* - dispatchAbyssCast(spell) — returns 'aspects' | 'primal' from spell.discipline.
* - rollAbyssGift(actor) — d100 against a roll table (resolved at runtime
* from the `roll-tables-abyss.dary-otchlani` pack
* when present; otherwise falls back to a chat
* prompt for the GM).
* - rollMistrzLosuPenalty(actor) — d100 penalty roll (Klątwa Otchłani VII).
* - addInsanity(actor, n) — increments actor.system.attributes.insanity;
* when crossing a threshold, fires a hook for
* the GM to apply a mental condition.
*/
import type { CastableActor } from './spell-cast';
export type AbyssDiscipline = 'aspects' | 'primal';
const PRIMAL_KEYS = new Set(['primal', 'pierwotna-magia', 'pierwotna_magia', 'magia-otchlani-pierwotna']);
interface SpellLikeForAbyss {
system?: {
school?: string;
discipline?: string | string[];
requirements?: { discipline?: string[] };
};
}
export function dispatchAbyssCast(spell: SpellLikeForAbyss): AbyssDiscipline {
const disc = spell.system?.discipline ?? spell.system?.requirements?.discipline ?? [];
const list = (Array.isArray(disc) ? disc : [disc]).map((s) => String(s).toLowerCase());
for (const d of list) {
if (PRIMAL_KEYS.has(d)) return 'primal';
}
return 'aspects';
}
/** Insanity thresholds (Klątwa Otchłani VIII). Crossing any triggers a mental-condition roll. */
const INSANITY_THRESHOLDS = [3, 6, 10, 15] as const;
export async function addInsanity(actor: CastableActor, n: number): Promise<{ before: number; after: number; thresholdsCrossed: number[] }> {
if (n <= 0) return { before: 0, after: 0, thresholdsCrossed: [] };
const before = ((actor.system.attributes as any).insanity ?? 0) as number;
const after = before + n;
await actor.update({ 'system.attributes.insanity': after });
const crossed = INSANITY_THRESHOLDS.filter((t) => before < t && after >= t);
if (crossed.length > 0) {
Hooks.callAll('hbm.insanityThreshold', actor, crossed, { before, after });
}
return { before, after, thresholdsCrossed: [...crossed] };
}
/** Roll d100 against a named table inside the abyss roll-tables pack. */
export async function rollAbyssGift(actor: CastableActor, tableName = 'dary-otchlani'): Promise<Roll> {
return rollAgainstAbyssTable(actor, tableName, 'Dary Otchłani');
}
export async function rollMistrzLosuPenalty(actor: CastableActor): Promise<Roll> {
return rollAgainstAbyssTable(actor, 'mistrz-losu', 'Mistrz Losu');
}
async function rollAgainstAbyssTable(actor: CastableActor, slug: string, flavor: string): Promise<Roll> {
const roll = new Roll('1d100');
await roll.evaluate();
const speaker = ChatMessage.getSpeaker({ actor: actor as unknown as Actor });
await roll.toMessage({
flavor: `${flavor} — ${actor.name} (${slug})`,
speaker,
});
Hooks.callAll('hbm.abyssTableRoll', actor, slug, roll.total);
return roll;
}