diff --git a/lang/compendium/en/spells-academic.json b/lang/compendium/en/spells-academic.json index 81b4107..58d8a57 100644 --- a/lang/compendium/en/spells-academic.json +++ b/lang/compendium/en/spells-academic.json @@ -162,8 +162,8 @@ "Magia Ognia": { "name": "Fire Magic" }, - "Alchemia - Transmutacja": { - "name": "Alchemy - Transmutation" + "Transmutacja": { + "name": "Transmutation" }, "Magia Żywiołów": { "name": "Elemental Magic" diff --git a/scripts/build-packs.ts b/scripts/build-packs.ts index 9520042..ee073a2 100644 --- a/scripts/build-packs.ts +++ b/scripts/build-packs.ts @@ -54,6 +54,20 @@ allDocs.push(...(await parseGear(ctx))); 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; + const spells = allDocs.filter((d) => { + if (d.subType !== 'spell') return false; + const sp = d.system as Record; + 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//. if (existsSync(packsSrcDir)) rmSync(packsSrcDir, { recursive: true }); const byPack = new Map(); @@ -87,10 +101,10 @@ function getFolderHierarchy(doc: ParsedDoc): { name: string; parentName?: string return [{ name: 'Magia Żywiołów' }, { name: 'Magia Ziemi', parentName: 'Magia Żywiołów' }]; } if (school === 'alchemyTransmutation') { - return [{ name: 'Alchemia' }, { name: 'Alchemia - Transmutacja', parentName: 'Alchemia' }]; + return [{ name: 'Alchemia' }, { name: 'Transmutacja', parentName: 'Alchemia' }]; } 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') { return [{ name: 'Rzemiosło Artefaktów' }]; diff --git a/scripts/generate-babele-translations.ts b/scripts/generate-babele-translations.ts index bc9a797..6e83298 100644 --- a/scripts/generate-babele-translations.ts +++ b/scripts/generate-babele-translations.ts @@ -146,6 +146,8 @@ const folderNameTranslations: Record = { "Magia Ziemi": "Earth Magic", "Alchemia": "Alchemy", "Alchemia - Transmutacja": "Alchemy - Transmutation", + "Transmutacja": "Transmutation", + "Warzenie Eliksirów": "Brewing of Elixirs", "Alchemia - Warzenie Eliksirów": "Alchemy - Brewing of Elixirs", "Rzemiosło Artefaktów": "Crafting of Artifacts", "Golemancja": "Golemancy", diff --git a/scripts/parsers/discipline-parser.ts b/scripts/parsers/discipline-parser.ts index bdb49b9..185177e 100644 --- a/scripts/parsers/discipline-parser.ts +++ b/scripts/parsers/discipline-parser.ts @@ -148,6 +148,7 @@ export const parseDisciplines: ParserFn = async (ctx: ParserContext): Promise { + 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 { // 1. Validate const validation = validateCast(actor, spell, opts); @@ -108,16 +124,17 @@ export async function castSpell(actor: CastableActor, spell: SpellLike, opts: Ca // 2-3. Mode dispatch const mode = spell.system.castingMode ?? 'standard'; let baseRoll: HbmTSRoll | null = null; + const rolls: CastRollEntry[] = []; if (mode === 'sacred') { baseRoll = opts.hekateMode === 'witch' - ? await castWitch(actor, spell, opts) - : await castSacred(actor, spell, opts); + ? await castWitch(actor, spell, opts, rolls) + : await castSacred(actor, spell, opts, rolls); } else if (mode === 'witch') { - baseRoll = await castWitch(actor, spell, opts); + baseRoll = await castWitch(actor, spell, opts, rolls); } else if (mode === 'blood') { - baseRoll = await castBlood(actor, spell, opts); + baseRoll = await castBlood(actor, spell, opts, rolls); } else { - baseRoll = await castStandard(actor, spell, opts); + baseRoll = await castStandard(actor, spell, opts, rolls); } 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) - await renderCastCard(actor, spell, opts, baseRoll, damageRoll, triggersRegistered); + await renderCastCard(actor, spell, opts, baseRoll, damageRoll, triggersRegistered, rolls); return { validation, baseRoll, damageRoll, triggersRegistered }; } @@ -146,6 +163,7 @@ async function renderCastCard( baseRoll: HbmTSRoll | null, damageRoll: Roll | null, triggersRegistered: number, + rolls: CastRollEntry[], ): Promise { const success = baseRoll?.ts?.isSuccess ?? false; const mode = opts.castAsPrayer ? 'prayer' : (spell.system.castingMode ?? 'standard'); @@ -173,6 +191,10 @@ async function renderCastCard( damage = { total: damageRoll.total, 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', ignoresArmor: !!spell.system.ignoresArmor, targets: targets.length > 0 ? targets : null, @@ -198,8 +220,14 @@ async function renderCastCard( ? spell.system.triggers : []; + const rollsHtml = await Promise.all(rolls.map(async (r) => ({ + label: r.label, + html: await r.roll.render(), + }))); + const data = { spell: { name: spell.name }, + rolls: rollsHtml, mode, school: spell.system.school ?? '', circle: (spell.system as any).circle ?? 0, @@ -216,11 +244,12 @@ async function renderCastCard( await ChatMessage.create({ content: html, 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 } } }, }); } -async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOptions): Promise { +async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise { const baseCost = Math.max(0, spell.system.manaCost ?? 0); const manaSpent = Math.max(baseCost, Math.floor(opts.manaSpent ?? baseCost)); const a = actor.system.attributes; @@ -248,7 +277,7 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp flavor, }); await baseRoll.evaluate(); - await baseRoll.toMessage({ flavor, speaker: opts.speaker }); + await recordRoll(rolls, baseRoll, flavor); // Overcast beyond Maksymalny Koszt Zaklęcia - extra TS test. // 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, }); await overcastRoll.evaluate(); - await overcastRoll.toMessage({ flavor: overcastFlavor, speaker: opts.speaker }); + await recordRoll(rolls, overcastRoll, overcastFlavor); } return baseRoll; } -async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOptions): Promise { +async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise { const a = actor.system.attributes; const zealCost = Math.max(1, opts.zealSpent ?? 1); 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, }); await roll.evaluate(); - await roll.toMessage({ flavor, speaker: opts.speaker }); + await recordRoll(rolls, roll, flavor); return roll; } -async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptions): Promise { +async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise { const a = actor.system.attributes; const required = Math.max(0, spell.system.complexityLevel ?? spell.system.components?.symbols?.length ?? 0); const maxSymbols = Math.ceil(a.magic.actual / 2); @@ -320,11 +349,11 @@ async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptio flavor, }); await roll.evaluate(); - await roll.toMessage({ flavor, speaker: opts.speaker }); + await recordRoll(rolls, roll, flavor); return roll; } -async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptions): Promise { +async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise { const a = actor.system.attributes; const baseCost = Math.max(0, spell.system.bloodCost ?? 0); const bloodSpent = Math.max(baseCost, Math.floor(opts.bloodSpent ?? baseCost)); @@ -362,6 +391,6 @@ async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptio flavor, }); await roll.evaluate(); - await roll.toMessage({ flavor, speaker: opts.speaker }); + await recordRoll(rolls, roll, flavor); return roll; } diff --git a/styles/hbm.css b/styles/hbm.css index 546697e..33de084 100644 --- a/styles/hbm.css +++ b/styles/hbm.css @@ -600,6 +600,10 @@ .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-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 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; } diff --git a/templates/chat/spell-cast.hbs b/templates/chat/spell-cast.hbs index 66bee4f..ecb9f08 100644 --- a/templates/chat/spell-cast.hbs +++ b/templates/chat/spell-cast.hbs @@ -24,6 +24,12 @@ {{/if}} + {{#each rolls as |r|}} +
+ {{{r.html}}} +
+ {{/each}} + {{#if outcome.success}} {{#if damage}}
@@ -34,6 +40,15 @@ {{#if damage.ignoresArmor}}{{localize 'HBM.spell.ignoresArmor'}}{{/if}}
{{damage.formula}}
+ {{#if damage.dice.length}} +
+ {{#each damage.dice as |d|}} + {{d.expression}}: + {{#each d.results as |face|}}{{face}}{{/each}} + + {{/each}} +
+ {{/if}} {{#if damage.targets}}
{{#each damage.targets as |t|}}