Vault sync 2026-10-20: rules, counters, migration 1.4.0 #3
@@ -162,8 +162,8 @@
|
|||||||
"Magia Ognia": {
|
"Magia Ognia": {
|
||||||
"name": "Fire Magic"
|
"name": "Fire Magic"
|
||||||
},
|
},
|
||||||
"Alchemia - Transmutacja": {
|
"Transmutacja": {
|
||||||
"name": "Alchemy - Transmutation"
|
"name": "Transmutation"
|
||||||
},
|
},
|
||||||
"Magia Żywiołów": {
|
"Magia Żywiołów": {
|
||||||
"name": "Elemental Magic"
|
"name": "Elemental Magic"
|
||||||
|
|||||||
+16
-2
@@ -54,6 +54,20 @@ allDocs.push(...(await parseGear(ctx)));
|
|||||||
|
|
||||||
console.log(`[build-packs] parsed ${allDocs.length} documents total`);
|
console.log(`[build-packs] parsed ${allDocs.length} documents total`);
|
||||||
|
|
||||||
|
// 2b. Assign signature spells: every discipline lists the spells of its own school.
|
||||||
|
// Sacred Magic lists only the common prayers (the rest are deity-specific).
|
||||||
|
for (const disc of allDocs) {
|
||||||
|
if (disc.subType !== 'discipline') continue;
|
||||||
|
const sys = disc.system as Record<string, any>;
|
||||||
|
const spells = allDocs.filter((d) => {
|
||||||
|
if (d.subType !== 'spell') return false;
|
||||||
|
const sp = d.system as Record<string, any>;
|
||||||
|
if (sp.school !== disc.id || sp.isSuperspell) return false;
|
||||||
|
return disc.id !== 'sacred' || sp.deity === 'common';
|
||||||
|
});
|
||||||
|
sys.signatureSpells = spells.map((d) => d.name);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Group by pack and emit one JSON per doc into packs-src/<pack>/.
|
// 3. Group by pack and emit one JSON per doc into packs-src/<pack>/.
|
||||||
if (existsSync(packsSrcDir)) rmSync(packsSrcDir, { recursive: true });
|
if (existsSync(packsSrcDir)) rmSync(packsSrcDir, { recursive: true });
|
||||||
const byPack = new Map<string, ParsedDoc[]>();
|
const byPack = new Map<string, ParsedDoc[]>();
|
||||||
@@ -87,10 +101,10 @@ function getFolderHierarchy(doc: ParsedDoc): { name: string; parentName?: string
|
|||||||
return [{ name: 'Magia Żywiołów' }, { name: 'Magia Ziemi', parentName: 'Magia Żywiołów' }];
|
return [{ name: 'Magia Żywiołów' }, { name: 'Magia Ziemi', parentName: 'Magia Żywiołów' }];
|
||||||
}
|
}
|
||||||
if (school === 'alchemyTransmutation') {
|
if (school === 'alchemyTransmutation') {
|
||||||
return [{ name: 'Alchemia' }, { name: 'Alchemia - Transmutacja', parentName: 'Alchemia' }];
|
return [{ name: 'Alchemia' }, { name: 'Transmutacja', parentName: 'Alchemia' }];
|
||||||
}
|
}
|
||||||
if (school === 'alchemyBrewing') {
|
if (school === 'alchemyBrewing') {
|
||||||
return [{ name: 'Alchemia' }, { name: 'Alchemia - Warzenie Eliksirów', parentName: 'Alchemia' }];
|
return [{ name: 'Alchemia' }, { name: 'Warzenie Eliksirów', parentName: 'Alchemia' }];
|
||||||
}
|
}
|
||||||
if (school === 'artifice') {
|
if (school === 'artifice') {
|
||||||
return [{ name: 'Rzemiosło Artefaktów' }];
|
return [{ name: 'Rzemiosło Artefaktów' }];
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ const folderNameTranslations: Record<string, string> = {
|
|||||||
"Magia Ziemi": "Earth Magic",
|
"Magia Ziemi": "Earth Magic",
|
||||||
"Alchemia": "Alchemy",
|
"Alchemia": "Alchemy",
|
||||||
"Alchemia - Transmutacja": "Alchemy - Transmutation",
|
"Alchemia - Transmutacja": "Alchemy - Transmutation",
|
||||||
|
"Transmutacja": "Transmutation",
|
||||||
|
"Warzenie Eliksirów": "Brewing of Elixirs",
|
||||||
"Alchemia - Warzenie Eliksirów": "Alchemy - Brewing of Elixirs",
|
"Alchemia - Warzenie Eliksirów": "Alchemy - Brewing of Elixirs",
|
||||||
"Rzemiosło Artefaktów": "Crafting of Artifacts",
|
"Rzemiosło Artefaktów": "Crafting of Artifacts",
|
||||||
"Golemancja": "Golemancy",
|
"Golemancja": "Golemancy",
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export const parseDisciplines: ParserFn = async (ctx: ParserContext): Promise<Pa
|
|||||||
suggestedSkills: SUGGESTED_SKILLS[seed.id] ?? ['magicalAbilities'],
|
suggestedSkills: SUGGESTED_SKILLS[seed.id] ?? ['magicalAbilities'],
|
||||||
passiveAbility: '',
|
passiveAbility: '',
|
||||||
availableSpells: [],
|
availableSpells: [],
|
||||||
|
signatureSpells: [],
|
||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export class DisciplineData extends foundry.abstract.TypeDataModel {
|
|||||||
}),
|
}),
|
||||||
suggestedSkills: makeArrayField(new f.StringField({ blank: false })),
|
suggestedSkills: makeArrayField(new f.StringField({ blank: false })),
|
||||||
passiveAbility: makeHtmlField(''),
|
passiveAbility: makeHtmlField(''),
|
||||||
|
signatureSpells: makeArrayField(new f.StringField({ blank: false })),
|
||||||
availableSpells: makeArrayField(new f.StringField({ blank: false })),
|
availableSpells: makeArrayField(new f.StringField({ blank: false })),
|
||||||
description: makeHtmlField(''),
|
description: makeHtmlField(''),
|
||||||
};
|
};
|
||||||
|
|||||||
+44
-15
@@ -80,6 +80,12 @@ export interface CastOptions {
|
|||||||
speaker?: ChatMessage.SpeakerData;
|
speaker?: ChatMessage.SpeakerData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A roll made during a cast; rendered inline in the cast card. */
|
||||||
|
interface CastRollEntry {
|
||||||
|
label: string;
|
||||||
|
roll: HbmTSRoll;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CastResult {
|
export interface CastResult {
|
||||||
validation: ValidationResult;
|
validation: ValidationResult;
|
||||||
baseRoll: HbmTSRoll | null;
|
baseRoll: HbmTSRoll | null;
|
||||||
@@ -92,6 +98,16 @@ function notify(text: string, severity: 'warn' | 'error' | 'info' = 'info'): voi
|
|||||||
u.notifications?.[severity](text);
|
u.notifications?.[severity](text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Queue a roll for the cast card and play the 3D dice animation if available. */
|
||||||
|
async function recordRoll(rolls: CastRollEntry[], roll: HbmTSRoll, label: string): Promise<void> {
|
||||||
|
rolls.push({ label, roll });
|
||||||
|
try {
|
||||||
|
await (game as any).dice3d?.showForRoll?.(roll, game.user, true);
|
||||||
|
} catch {
|
||||||
|
// Dice So Nice is optional; the card still shows the faces.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function castSpell(actor: CastableActor, spell: SpellLike, opts: CastOptions = {}): Promise<CastResult> {
|
export async function castSpell(actor: CastableActor, spell: SpellLike, opts: CastOptions = {}): Promise<CastResult> {
|
||||||
// 1. Validate
|
// 1. Validate
|
||||||
const validation = validateCast(actor, spell, opts);
|
const validation = validateCast(actor, spell, opts);
|
||||||
@@ -108,16 +124,17 @@ export async function castSpell(actor: CastableActor, spell: SpellLike, opts: Ca
|
|||||||
// 2-3. Mode dispatch
|
// 2-3. Mode dispatch
|
||||||
const mode = spell.system.castingMode ?? 'standard';
|
const mode = spell.system.castingMode ?? 'standard';
|
||||||
let baseRoll: HbmTSRoll | null = null;
|
let baseRoll: HbmTSRoll | null = null;
|
||||||
|
const rolls: CastRollEntry[] = [];
|
||||||
if (mode === 'sacred') {
|
if (mode === 'sacred') {
|
||||||
baseRoll = opts.hekateMode === 'witch'
|
baseRoll = opts.hekateMode === 'witch'
|
||||||
? await castWitch(actor, spell, opts)
|
? await castWitch(actor, spell, opts, rolls)
|
||||||
: await castSacred(actor, spell, opts);
|
: await castSacred(actor, spell, opts, rolls);
|
||||||
} else if (mode === 'witch') {
|
} else if (mode === 'witch') {
|
||||||
baseRoll = await castWitch(actor, spell, opts);
|
baseRoll = await castWitch(actor, spell, opts, rolls);
|
||||||
} else if (mode === 'blood') {
|
} else if (mode === 'blood') {
|
||||||
baseRoll = await castBlood(actor, spell, opts);
|
baseRoll = await castBlood(actor, spell, opts, rolls);
|
||||||
} else {
|
} else {
|
||||||
baseRoll = await castStandard(actor, spell, opts);
|
baseRoll = await castStandard(actor, spell, opts, rolls);
|
||||||
}
|
}
|
||||||
|
|
||||||
let damageRoll: Roll | null = null;
|
let damageRoll: Roll | null = null;
|
||||||
@@ -134,7 +151,7 @@ export async function castSpell(actor: CastableActor, spell: SpellLike, opts: Ca
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render unified rich chat card (Phase 1.6)
|
// Render unified rich chat card (Phase 1.6)
|
||||||
await renderCastCard(actor, spell, opts, baseRoll, damageRoll, triggersRegistered);
|
await renderCastCard(actor, spell, opts, baseRoll, damageRoll, triggersRegistered, rolls);
|
||||||
|
|
||||||
return { validation, baseRoll, damageRoll, triggersRegistered };
|
return { validation, baseRoll, damageRoll, triggersRegistered };
|
||||||
}
|
}
|
||||||
@@ -146,6 +163,7 @@ async function renderCastCard(
|
|||||||
baseRoll: HbmTSRoll | null,
|
baseRoll: HbmTSRoll | null,
|
||||||
damageRoll: Roll | null,
|
damageRoll: Roll | null,
|
||||||
triggersRegistered: number,
|
triggersRegistered: number,
|
||||||
|
rolls: CastRollEntry[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const success = baseRoll?.ts?.isSuccess ?? false;
|
const success = baseRoll?.ts?.isSuccess ?? false;
|
||||||
const mode = opts.castAsPrayer ? 'prayer' : (spell.system.castingMode ?? 'standard');
|
const mode = opts.castAsPrayer ? 'prayer' : (spell.system.castingMode ?? 'standard');
|
||||||
@@ -173,6 +191,10 @@ async function renderCastCard(
|
|||||||
damage = {
|
damage = {
|
||||||
total: damageRoll.total,
|
total: damageRoll.total,
|
||||||
formula: damageRoll.formula,
|
formula: damageRoll.formula,
|
||||||
|
dice: ((damageRoll as any).dice ?? []).map((d: any) => ({
|
||||||
|
expression: d.expression,
|
||||||
|
results: (d.results ?? []).filter((r: any) => !r.discarded).map((r: any) => r.result),
|
||||||
|
})),
|
||||||
type: spell.system.damageType ?? 'magical',
|
type: spell.system.damageType ?? 'magical',
|
||||||
ignoresArmor: !!spell.system.ignoresArmor,
|
ignoresArmor: !!spell.system.ignoresArmor,
|
||||||
targets: targets.length > 0 ? targets : null,
|
targets: targets.length > 0 ? targets : null,
|
||||||
@@ -198,8 +220,14 @@ async function renderCastCard(
|
|||||||
? spell.system.triggers
|
? spell.system.triggers
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const rollsHtml = await Promise.all(rolls.map(async (r) => ({
|
||||||
|
label: r.label,
|
||||||
|
html: await r.roll.render(),
|
||||||
|
})));
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
spell: { name: spell.name },
|
spell: { name: spell.name },
|
||||||
|
rolls: rollsHtml,
|
||||||
mode,
|
mode,
|
||||||
school: spell.system.school ?? '',
|
school: spell.system.school ?? '',
|
||||||
circle: (spell.system as any).circle ?? 0,
|
circle: (spell.system as any).circle ?? 0,
|
||||||
@@ -216,11 +244,12 @@ async function renderCastCard(
|
|||||||
await ChatMessage.create({
|
await ChatMessage.create({
|
||||||
content: html,
|
content: html,
|
||||||
speaker: opts.speaker ?? ChatMessage.getSpeaker({ actor: actor as any }),
|
speaker: opts.speaker ?? ChatMessage.getSpeaker({ actor: actor as any }),
|
||||||
|
rolls: [...rolls.map((r) => r.roll), ...(damageRoll ? [damageRoll] : [])].map((r) => r.toJSON()),
|
||||||
flags: { 'hbm-rpg-v3': { spellCast: { spellName: spell.name, success } } },
|
flags: { 'hbm-rpg-v3': { spellCast: { spellName: spell.name, success } } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOptions): Promise<HbmTSRoll | null> {
|
async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise<HbmTSRoll | null> {
|
||||||
const baseCost = Math.max(0, spell.system.manaCost ?? 0);
|
const baseCost = Math.max(0, spell.system.manaCost ?? 0);
|
||||||
const manaSpent = Math.max(baseCost, Math.floor(opts.manaSpent ?? baseCost));
|
const manaSpent = Math.max(baseCost, Math.floor(opts.manaSpent ?? baseCost));
|
||||||
const a = actor.system.attributes;
|
const a = actor.system.attributes;
|
||||||
@@ -248,7 +277,7 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
|||||||
flavor,
|
flavor,
|
||||||
});
|
});
|
||||||
await baseRoll.evaluate();
|
await baseRoll.evaluate();
|
||||||
await baseRoll.toMessage({ flavor, speaker: opts.speaker });
|
await recordRoll(rolls, baseRoll, flavor);
|
||||||
|
|
||||||
// Overcast beyond Maksymalny Koszt Zaklęcia - extra TS test.
|
// Overcast beyond Maksymalny Koszt Zaklęcia - extra TS test.
|
||||||
// T depends on the spell's BASE cost; S grows triangularly with the excess
|
// T depends on the spell's BASE cost; S grows triangularly with the excess
|
||||||
@@ -265,13 +294,13 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
|||||||
flavor: overcastFlavor,
|
flavor: overcastFlavor,
|
||||||
});
|
});
|
||||||
await overcastRoll.evaluate();
|
await overcastRoll.evaluate();
|
||||||
await overcastRoll.toMessage({ flavor: overcastFlavor, speaker: opts.speaker });
|
await recordRoll(rolls, overcastRoll, overcastFlavor);
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseRoll;
|
return baseRoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOptions): Promise<HbmTSRoll | null> {
|
async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise<HbmTSRoll | null> {
|
||||||
const a = actor.system.attributes;
|
const a = actor.system.attributes;
|
||||||
const zealCost = Math.max(1, opts.zealSpent ?? 1);
|
const zealCost = Math.max(1, opts.zealSpent ?? 1);
|
||||||
await actor.update({ 'system.attributes.zeal.value': a.zeal.value - zealCost });
|
await actor.update({ 'system.attributes.zeal.value': a.zeal.value - zealCost });
|
||||||
@@ -286,11 +315,11 @@ async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOpti
|
|||||||
flavor,
|
flavor,
|
||||||
});
|
});
|
||||||
await roll.evaluate();
|
await roll.evaluate();
|
||||||
await roll.toMessage({ flavor, speaker: opts.speaker });
|
await recordRoll(rolls, roll, flavor);
|
||||||
return roll;
|
return roll;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptions): Promise<HbmTSRoll | null> {
|
async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise<HbmTSRoll | null> {
|
||||||
const a = actor.system.attributes;
|
const a = actor.system.attributes;
|
||||||
const required = Math.max(0, spell.system.complexityLevel ?? spell.system.components?.symbols?.length ?? 0);
|
const required = Math.max(0, spell.system.complexityLevel ?? spell.system.components?.symbols?.length ?? 0);
|
||||||
const maxSymbols = Math.ceil(a.magic.actual / 2);
|
const maxSymbols = Math.ceil(a.magic.actual / 2);
|
||||||
@@ -320,11 +349,11 @@ async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptio
|
|||||||
flavor,
|
flavor,
|
||||||
});
|
});
|
||||||
await roll.evaluate();
|
await roll.evaluate();
|
||||||
await roll.toMessage({ flavor, speaker: opts.speaker });
|
await recordRoll(rolls, roll, flavor);
|
||||||
return roll;
|
return roll;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptions): Promise<HbmTSRoll | null> {
|
async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise<HbmTSRoll | null> {
|
||||||
const a = actor.system.attributes;
|
const a = actor.system.attributes;
|
||||||
const baseCost = Math.max(0, spell.system.bloodCost ?? 0);
|
const baseCost = Math.max(0, spell.system.bloodCost ?? 0);
|
||||||
const bloodSpent = Math.max(baseCost, Math.floor(opts.bloodSpent ?? baseCost));
|
const bloodSpent = Math.max(baseCost, Math.floor(opts.bloodSpent ?? baseCost));
|
||||||
@@ -362,6 +391,6 @@ async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptio
|
|||||||
flavor,
|
flavor,
|
||||||
});
|
});
|
||||||
await roll.evaluate();
|
await roll.evaluate();
|
||||||
await roll.toMessage({ flavor, speaker: opts.speaker });
|
await recordRoll(rolls, roll, flavor);
|
||||||
return roll;
|
return roll;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -600,6 +600,10 @@
|
|||||||
.hbm.spell-cast-card section { margin: .35rem 0; }
|
.hbm.spell-cast-card section { margin: .35rem 0; }
|
||||||
.hbm.spell-cast-card .damage-line { display: flex; gap: .35rem; align-items: baseline; font-size: 1.2em; }
|
.hbm.spell-cast-card .damage-line { display: flex; gap: .35rem; align-items: baseline; font-size: 1.2em; }
|
||||||
.hbm.spell-cast-card .damage-formula code { font-size: .8em; color: #555; }
|
.hbm.spell-cast-card .damage-formula code { font-size: .8em; color: #555; }
|
||||||
|
.hbm.spell-cast-card .hbm-card-roll { margin: .35rem 0; padding: .25rem; border: 1px solid rgba(107,63,160,.25); border-radius: 4px; }
|
||||||
|
.hbm.spell-cast-card .damage-dice { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .15rem; }
|
||||||
|
.hbm.spell-cast-card .damage-die-group { display: inline-flex; gap: .2rem; align-items: center; }
|
||||||
|
.hbm.spell-cast-card .damage-die-group .die { display: inline-flex; align-items: center; justify-content: center; min-width: 1.6rem; height: 1.6rem; border-radius: 4px; font-weight: bold; background: #ddd; color: #333; }
|
||||||
.hbm.spell-cast-card .hbm-card-targets, .hbm.spell-cast-card .status-buttons { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .25rem; }
|
.hbm.spell-cast-card .hbm-card-targets, .hbm.spell-cast-card .status-buttons { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .25rem; }
|
||||||
.hbm.spell-cast-card button.hbm-action { font-size: .85em; padding: 2px 8px; border-radius: 4px; cursor: pointer; }
|
.hbm.spell-cast-card button.hbm-action { font-size: .85em; padding: 2px 8px; border-radius: 4px; cursor: pointer; }
|
||||||
.hbm.spell-cast-card .hbm-card-hint { font-size: .8em; color: #777; margin: .15rem 0; font-style: italic; }
|
.hbm.spell-cast-card .hbm-card-hint { font-size: .8em; color: #777; margin: .15rem 0; font-style: italic; }
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#each rolls as |r|}}
|
||||||
|
<section class="hbm-card-roll">
|
||||||
|
{{{r.html}}}
|
||||||
|
</section>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
{{#if outcome.success}}
|
{{#if outcome.success}}
|
||||||
{{#if damage}}
|
{{#if damage}}
|
||||||
<section class="hbm-card-damage">
|
<section class="hbm-card-damage">
|
||||||
@@ -34,6 +40,15 @@
|
|||||||
{{#if damage.ignoresArmor}}<span class="badge ignores-armor">{{localize 'HBM.spell.ignoresArmor'}}</span>{{/if}}
|
{{#if damage.ignoresArmor}}<span class="badge ignores-armor">{{localize 'HBM.spell.ignoresArmor'}}</span>{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="damage-formula"><code>{{damage.formula}}</code></div>
|
<div class="damage-formula"><code>{{damage.formula}}</code></div>
|
||||||
|
{{#if damage.dice.length}}
|
||||||
|
<div class="damage-dice">
|
||||||
|
{{#each damage.dice as |d|}}
|
||||||
|
<span class="damage-die-group"><small>{{d.expression}}:</small>
|
||||||
|
{{#each d.results as |face|}}<span class="die">{{face}}</span>{{/each}}
|
||||||
|
</span>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
{{#if damage.targets}}
|
{{#if damage.targets}}
|
||||||
<div class="hbm-card-targets">
|
<div class="hbm-card-targets">
|
||||||
{{#each damage.targets as |t|}}
|
{{#each damage.targets as |t|}}
|
||||||
|
|||||||
Reference in New Issue
Block a user