From 9ca94268c5142cf118cd1d0f5a1a59d66d68e72b Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sat, 17 Oct 2026 09:53:05 +0200 Subject: [PATCH 1/7] fix: show dice in spell cast card, dedupe Alchemy folders, assign signature spells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spell cast card now embeds the TS dice (base + overcast) and damage dice instead of posting separate roll messages; Dice So Nice still animates. - Spell pack Alchemy sub-folders renamed to Transmutacja / Warzenie Eliksirów so the label no longer repeats under the Alchemia folder. - Discipline schema gains signatureSpells (the sheet already bound to it); build-packs fills it with each discipline's own spells. Co-Authored-By: Claude Sonnet 5.5 --- lang/compendium/en/spells-academic.json | 4 +- scripts/build-packs.ts | 18 +++++++- scripts/generate-babele-translations.ts | 2 + scripts/parsers/discipline-parser.ts | 1 + src/data/item-discipline.ts | 1 + src/logic/spell-cast.ts | 59 ++++++++++++++++++------- styles/hbm.css | 4 ++ templates/chat/spell-cast.hbs | 15 +++++++ 8 files changed, 85 insertions(+), 19 deletions(-) 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|}} From 2f3f0057b1923bb431a534e274cc2ccd51d89770 Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sat, 17 Oct 2026 10:19:24 +0200 Subject: [PATCH 2/7] feat: signatureSpells lists all spells of a discipline Co-Authored-By: Claude Sonnet 5.5 --- scripts/build-packs.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/build-packs.ts b/scripts/build-packs.ts index ee073a2..cef495c 100644 --- a/scripts/build-packs.ts +++ b/scripts/build-packs.ts @@ -54,18 +54,18 @@ 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). +// 2b. signatureSpells = every spell belonging to the discipline (own school or +// listed in the spell's requirements.discipline). 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); + sys.signatureSpells = allDocs + .filter((d) => { + if (d.subType !== 'spell') return false; + const sp = d.system as Record; + return sp.school === disc.id || (sp.requirements?.discipline ?? []).includes(disc.id); + }) + .map((d) => d.name); } // 3. Group by pack and emit one JSON per doc into packs-src//. From 522aac86786e144bb00006c18d5ec0579da03796 Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sat, 17 Oct 2026 10:29:10 +0200 Subject: [PATCH 3/7] fix: assign eldritch spells to Abyss - Primal Magic signature list Co-Authored-By: Claude Sonnet 5.5 --- scripts/build-packs.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build-packs.ts b/scripts/build-packs.ts index cef495c..bbb9cf7 100644 --- a/scripts/build-packs.ts +++ b/scripts/build-packs.ts @@ -56,6 +56,8 @@ console.log(`[build-packs] parsed ${allDocs.length} documents total`); // 2b. signatureSpells = every spell belonging to the discipline (own school or // listed in the spell's requirements.discipline). +// Spells with school `eldritch` belong to Abyss - Primal Magic. +const SCHOOL_TO_DISCIPLINE: Record = { eldritch: 'abyssPrimal' }; for (const disc of allDocs) { if (disc.subType !== 'discipline') continue; const sys = disc.system as Record; @@ -63,7 +65,7 @@ for (const disc of allDocs) { .filter((d) => { if (d.subType !== 'spell') return false; const sp = d.system as Record; - return sp.school === disc.id || (sp.requirements?.discipline ?? []).includes(disc.id); + return (SCHOOL_TO_DISCIPLINE[sp.school] ?? sp.school) === disc.id || (sp.requirements?.discipline ?? []).includes(disc.id); }) .map((d) => d.name); } From ee6c1248ea26a3a47919615603d9a62e58a69383 Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sat, 17 Oct 2026 10:31:43 +0200 Subject: [PATCH 4/7] chore: regenerate compendium translations Co-Authored-By: Claude Sonnet 5.5 --- lang/compendium/en/spells-academic.json | 14 +++++++------- lang/compendium/en/spells-eldritch.json | 2 +- lang/compendium/en/spells-general.json | 6 +++--- lang/compendium/en/spells-sacred.json | 6 +++--- lang/compendium/en/talents-npc.json | 4 ++-- lang/compendium/en/talents.json | 4 ++++ 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lang/compendium/en/spells-academic.json b/lang/compendium/en/spells-academic.json index 58d8a57..afbe0f8 100644 --- a/lang/compendium/en/spells-academic.json +++ b/lang/compendium/en/spells-academic.json @@ -63,7 +63,7 @@ }, "Tarcza Domu": { "name": "House Shield", - "description": "Mag otacza wybrany cel gęstą, wielowarstwową osłoną, znacznie mocniejszą niż standardowa Magiczna Tarcza.\r\n\r\nTarcza ma wytrzymałość równą wartości Umiejętności Zdolności Magiczne maga + 3. Gdy cel ma otrzymać obrażenia, są one zmniejszane maksymalnie o obecną wytrzymałość Tarczy, która spada o tyle, ile obrażeń pochłonęła. Gdy wytrzymałość spadnie do 0, zaklęcie przestaje działać.\r\n\r\nDopóki cel jest chroniony tą Tarczą, następne zaklęcie, które rzuci do końca swojej najbliższej Tury, otrzymuje dodatkową kość do puli Testu.\r\n\r\nZa dodatkowe 2 punkty Many, mag może rzucić to zaklęcie na samego siebie zamiast na inny cel." + "description": "Mag otacza wybrany cel gęstą, wielowarstwową osłoną, znacznie mocniejszą niż standardowa Magiczna Tarcza.\n\nTarcza ma wytrzymałość równą wartości Umiejętności Zdolności Magiczne maga + 3. Gdy cel ma otrzymać obrażenia, są one zmniejszane maksymalnie o obecną wytrzymałość Tarczy, która spada o tyle, ile obrażeń pochłonęła. Gdy wytrzymałość spadnie do 0, zaklęcie przestaje działać.\n\nDopóki cel jest chroniony tą Tarczą, następne zaklęcie, które rzuci do końca swojej najbliższej Tury, otrzymuje dodatkową kość do puli Testu.\n\nZa dodatkowe 2 punkty Many, mag może rzucić to zaklęcie na samego siebie zamiast na inny cel." }, "Lodowa Podłoga": { "name": "Ice Floor", @@ -91,7 +91,7 @@ }, "Magiczne Kajdany": { "name": "Magic Shackles", - "description": "Cel zaklęcia czuje, jakby spętały go widmowe, niezniszczalne łańcuchy. Efekt działa wyłącznie na umysł celu - Postać nie jest w rzeczywistości związana.\r\n\r\nPostać musi wykonać Test na Soul (Determinacja) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów.\r\n\r\nW przypadku niepowodzenia Postać jest traktowana tak, jakby była Unieruchomiona, a ponadto nie może rzucać zaklęć." + "description": "Cel zaklęcia czuje, jakby spętały go widmowe, niezniszczalne łańcuchy. Efekt działa wyłącznie na umysł celu - Postać nie jest w rzeczywistości związana.\n\nPostać musi wykonać Test na Soul (Determinacja) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów.\n\nW przypadku niepowodzenia Postać jest traktowana tak, jakby była Unieruchomiona, a ponadto nie może rzucać zaklęć." }, "Magnetyzm": { "name": "Magnetism", @@ -119,7 +119,7 @@ }, "Osłona Świty": { "name": "Retinue Guard", - "description": "Mag rozdziela swoją moc pomiędzy do czterech celów w zasięgu, otaczając każdego z nich osobną, słabszą Tarczą.\r\n\r\nKażda z Tarcz ma niezależną wytrzymałość równą połowie wartości Umiejętności Zdolności Magiczne maga (zaokrąglonej w górę) + 1. Gdy dany cel ma otrzymać obrażenia, są one zmniejszane maksymalnie o wytrzymałość jego własnej Tarczy, która spada o tyle, ile obrażeń pochłonęła. Gdy wytrzymałość danej Tarczy spadnie do 0, znika tylko ta jedna Tarcza - pozostałe działają nadal.\r\n\r\nMag może objąć tym zaklęciem samego siebie jako jeden z celów bez dodatkowego kosztu." + "description": "Mag rozdziela swoją moc pomiędzy do czterech celów w zasięgu, otaczając każdego z nich osobną, słabszą Tarczą.\n\nKażda z Tarcz ma niezależną wytrzymałość równą połowie wartości Umiejętności Zdolności Magiczne maga (zaokrąglonej w górę) + 1. Gdy dany cel ma otrzymać obrażenia, są one zmniejszane maksymalnie o wytrzymałość jego własnej Tarczy, która spada o tyle, ile obrażeń pochłonęła. Gdy wytrzymałość danej Tarczy spadnie do 0, znika tylko ta jedna Tarcza - pozostałe działają nadal.\n\nMag może objąć tym zaklęciem samego siebie jako jeden z celów bez dodatkowego kosztu." }, "Rozsypanie": { "name": "Shatter", @@ -127,7 +127,7 @@ }, "Bańka Ochronna": { "name": "Shield Bubble", - "description": "Mag wyczarowuje kopulastą osłonę o promieniu 4 m (mechanicznie kwadrat o boku 10 m), obejmującą wszystkich sojuszników wewnątrz.\r\n\r\nBańka ma jedną, wspólną wytrzymałość równą wartości Umiejętności Zdolności Magiczne maga + 5. Każdy atak wymierzony w istotę osłoniętą Bańką, który przychodzi z zewnątrz, jest zmniejszany o aktualną wytrzymałość Bańki, która spada o tyle, ile obrażeń pochłonęła - dopóki nie spadnie do 0, po czym zaklęcie przestaje działać. Istoty wewnątrz Bańki mogą swobodnie ją opuszczać i wracać.\r\n\r\nDopóki Bańka trwa, każdy sojusznik wewnątrz niej, który rzuci zaklęcie, otrzymuje dodatkową kość do puli tego Testu.\r\n\r\nZa dodatkowe 3 punkty Many, mag może umieścić środek Bańki na sobie zamiast w innym punkcie w zasięgu." + "description": "Mag wyczarowuje kopulastą osłonę o promieniu 4 m (mechanicznie kwadrat o boku 10 m), obejmującą wszystkich sojuszników wewnątrz.\n\nBańka ma jedną, wspólną wytrzymałość równą wartości Umiejętności Zdolności Magiczne maga + 5. Każdy atak wymierzony w istotę osłoniętą Bańką, który przychodzi z zewnątrz, jest zmniejszany o aktualną wytrzymałość Bańki, która spada o tyle, ile obrażeń pochłonęła - dopóki nie spadnie do 0, po czym zaklęcie przestaje działać. Istoty wewnątrz Bańki mogą swobodnie ją opuszczać i wracać.\n\nDopóki Bańka trwa, każdy sojusznik wewnątrz niej, który rzuci zaklęcie, otrzymuje dodatkową kość do puli tego Testu.\n\nZa dodatkowe 3 punkty Many, mag może umieścić środek Bańki na sobie zamiast w innym punkcie w zasięgu." }, "Ukojenie": { "name": "Solace", @@ -162,12 +162,12 @@ "Magia Ognia": { "name": "Fire Magic" }, - "Transmutacja": { - "name": "Transmutation" - }, "Magia Żywiołów": { "name": "Elemental Magic" }, + "Transmutacja": { + "name": "Transmutation" + }, "Magia Powietrza": { "name": "Air Magic" }, diff --git a/lang/compendium/en/spells-eldritch.json b/lang/compendium/en/spells-eldritch.json index 0a8eb8b..d405767 100644 --- a/lang/compendium/en/spells-eldritch.json +++ b/lang/compendium/en/spells-eldritch.json @@ -3,7 +3,7 @@ "entries": { "Skrót przez Otchłań": { "name": "Abyss Shortcut", - "description": "Mag rozrywa przestrzeń między sobą a znanym sobie miejscem, tworząc na chwilę szczelinę do Otchłani służącą jako skrót, i natychmiast się przez nią przenosi.\r\n\r\nW przeciwieństwie do Teleportacji nie wymaga dziesięciu minut przygotowań - to zaklęcie działa natychmiastowo, w ramach jednej Akcji. Tym, co odróżnia je od akademickiej Teleportacji, jest cena: rozdarcie jest widoczne, głośne i jednoznacznie nienaturalne - nie da się go ukryć ani wykonać po cichu, a wszystko, co jest wyczulone na obecność Otchłani, natychmiast wyczuje miejsce, z którego mag zniknął, i miejsce, w którym się pojawił.\r\n\r\n> [!gm]+ Tylko dla MG\r\n> To zaklęcie jest dobrym hakiem fabularnym - jego regularne używanie zostawia ślad, który potrafią wyśledzić istoty i organizacje wyczulone na magię Otchłani (Inkwizycja, Kult Otchłani, inne istoty z Otchłani). Diana Koniecpolska używa go niechętnie właśnie z tego powodu, woląc twierdzić, że to \"tylko zaawansowany wariant Teleportacji\"." + "description": "Mag rozrywa przestrzeń między sobą a znanym sobie miejscem, tworząc na chwilę szczelinę do Otchłani służącą jako skrót, i natychmiast się przez nią przenosi.\n\nW przeciwieństwie do Teleportacji nie wymaga dziesięciu minut przygotowań - to zaklęcie działa natychmiastowo, w ramach jednej Akcji. Tym, co odróżnia je od akademickiej Teleportacji, jest cena: rozdarcie jest widoczne, głośne i jednoznacznie nienaturalne - nie da się go ukryć ani wykonać po cichu, a wszystko, co jest wyczulone na obecność Otchłani, natychmiast wyczuje miejsce, z którego mag zniknął, i miejsce, w którym się pojawił.\n\n> [!gm]+ Tylko dla MG\n> To zaklęcie jest dobrym hakiem fabularnym - jego regularne używanie zostawia ślad, który potrafią wyśledzić istoty i organizacje wyczulone na magię Otchłani (Inkwizycja, Kult Otchłani, inne istoty z Otchłani). Diana Koniecpolska używa go niechętnie właśnie z tego powodu, woląc twierdzić, że to \"tylko zaawansowany wariant Teleportacji\"." }, "Zakazana Osłona": { "name": "Forbidden Shield", diff --git a/lang/compendium/en/spells-general.json b/lang/compendium/en/spells-general.json index 557d9fa..3f159eb 100644 --- a/lang/compendium/en/spells-general.json +++ b/lang/compendium/en/spells-general.json @@ -19,7 +19,7 @@ }, "Zagłada": { "name": "Doom", - "description": "**Wymagania:** Mistrzostwo (dowolna Dziedzina), co najmniej 3 dodatkowych magów biorących udział w rytuale (wszyscy muszą znać Zagładę) - ponadto żaden mag biorący udział w rytuale nie może być Źródłem Mocy\r\n\r\nZagłada jest ostatecznym superzaklęciem unicestwienia, stworzonym jako rytuał, który nie tylko zabija, ale też wymazuje wszelką nadzieję na odwrócenie skutków. W przeciwieństwie do Destrukcji, która „tylko” eksploduje nagromadzoną manę, Zagłada celowo splata ją z samą strukturą życia istot w zasięgu.\r\n​\r\nZaklęcie wymaga wielogodzinnego rytuału, podczas którego uczestniczący magowie tworzą skomplikowaną sieć połączeń magicznych, zakończoną spięciem w głównym magu. W momencie zakończenia rytuału cała zgromadzona moc zostaje uwolniona w postaci bezgłośnej, szkarłatno‑czarnej fali, która w jednej chwili „wymazuje” życie w obrębie sfery.\r\n\r\nPo udanym rzuceniu zaklęcia emituje ono czerwoną falę światła oraz dźwięk dzwonu, który słyszą w głowie wszystkie istoty w zasięgu zaklęcia, a następnie wszystkie istoty żywe znajdujące się w sferze o średnicy 50 m, wycentrowanej na głównym magu, giną natychmiast, ignorując Pancerz, magiczne osłony, odporności, talenty, Aegis i inne superzaklęcia ochronne. Nie wykonuje się żadnych testów obronnych; śmierć następuje automatycznie. Jedynymi wyjątkami są główny mag prowadzący rytuał oraz istoty, których główny mag świadomie lub podświadomie nie chce zabić.\r\n\r\nW praktyce oznacza to, że MG, na podstawie relacji i emocji głównego maga, ustala listę postaci, które są instynktownie oszczędzane (np. najbliżsi sojusznicy, ukochane osoby). Każda z tych istot jest traktowana jako indywidualny wyjątek: fala Zagłady sprawdza je osobno i nie zadaje im żadnych obrażeń, choć wszystko wokół może zostać unicestwione - nie powstaje żadna „strefa ochronna”, w której inni mogliby się ukryć, ani nie dochodzi do jakiegokolwiek „oczyszczania” z chorób czy pasożytów.\r\n\r\nZagłada ma jednak straszliwą cenę: wszyscy pozostali magowie uczestniczący w rytuale giną, niezależnie od tego, gdzie się znajdują. Ich siły życiowe są świadomie „wypalane” w ramach zaklęcia – śmierć współrzucających nie jest efektem, którego można uniknąć zasięgiem czy osłoną, lecz integralnym kosztem rytuału.\r\n\r\nIstoty zabite przez Zagładę nie mogą zostać wskrzeszone żadną znaną magią - nie działają na nie Reanimacja, nekromancja, superzaklęcia ani boskie modlitwy wskrzeszające. Mechanicznie traktuje się je jak cele Prawdziwej Dezintegracji, z tą różnicą, że choć ciała mogą pozostać (lub nie), dusze są „wymazane” z obiegu – powrót do życia wymagałby wyłącznie wyjątkowej, fabularnej interwencji Mistrza Gry.\r\n​\r\nW miejscu rzucenia Zagłady pozostaje obszar magicznie skażony: choć brak tu „odpadów alchemicznych” jak przy Destrukcji, sama tkanka rzeczywistości jest nadpalona. MG może zdecydować, że każda istota przebywająca dłużej w tym obszarze musi wykonywać okresowe testy Duszy, ryzykując trwałe urazy psychiczne, klątwy lub inne długofalowe konsekwencje - nie jest to jednak integralna część efektu zaklęcia, a raczej fabularne piętno użycia tak nienaturalnej mocy.\r\n\r\n---" + "description": "**Wymagania:** Mistrzostwo (dowolna Dziedzina), co najmniej 3 dodatkowych magów biorących udział w rytuale (wszyscy muszą znać Zagładę) - ponadto żaden mag biorący udział w rytuale nie może być Źródłem Mocy\n\nZagłada jest ostatecznym superzaklęciem unicestwienia, stworzonym jako rytuał, który nie tylko zabija, ale też wymazuje wszelką nadzieję na odwrócenie skutków. W przeciwieństwie do Destrukcji, która „tylko” eksploduje nagromadzoną manę, Zagłada celowo splata ją z samą strukturą życia istot w zasięgu.\n​\nZaklęcie wymaga wielogodzinnego rytuału, podczas którego uczestniczący magowie tworzą skomplikowaną sieć połączeń magicznych, zakończoną spięciem w głównym magu. W momencie zakończenia rytuału cała zgromadzona moc zostaje uwolniona w postaci bezgłośnej, szkarłatno-czarnej fali, która w jednej chwili „wymazuje” życie w obrębie sfery.\n\nPo udanym rzuceniu zaklęcia emituje ono czerwoną falę światła oraz dźwięk dzwonu, który słyszą w głowie wszystkie istoty w zasięgu zaklęcia, a następnie wszystkie istoty żywe znajdujące się w sferze o średnicy 50 m, wycentrowanej na głównym magu, giną natychmiast, ignorując Pancerz, magiczne osłony, odporności, talenty, Aegis i inne superzaklęcia ochronne. Nie wykonuje się żadnych testów obronnych; śmierć następuje automatycznie. Jedynymi wyjątkami są główny mag prowadzący rytuał oraz istoty, których główny mag świadomie lub podświadomie nie chce zabić.\n\nW praktyce oznacza to, że MG, na podstawie relacji i emocji głównego maga, ustala listę postaci, które są instynktownie oszczędzane (np. najbliżsi sojusznicy, ukochane osoby). Każda z tych istot jest traktowana jako indywidualny wyjątek: fala Zagłady sprawdza je osobno i nie zadaje im żadnych obrażeń, choć wszystko wokół może zostać unicestwione - nie powstaje żadna „strefa ochronna”, w której inni mogliby się ukryć, ani nie dochodzi do jakiegokolwiek „oczyszczania” z chorób czy pasożytów.\n\nZagłada ma jednak straszliwą cenę: wszyscy pozostali magowie uczestniczący w rytuale giną, niezależnie od tego, gdzie się znajdują. Ich siły życiowe są świadomie „wypalane” w ramach zaklęcia - śmierć współrzucających nie jest efektem, którego można uniknąć zasięgiem czy osłoną, lecz integralnym kosztem rytuału.\n\nIstoty zabite przez Zagładę nie mogą zostać wskrzeszone żadną znaną magią - nie działają na nie Reanimacja, nekromancja, superzaklęcia ani boskie modlitwy wskrzeszające. Mechanicznie traktuje się je jak cele Prawdziwej Dezintegracji, z tą różnicą, że choć ciała mogą pozostać (lub nie), dusze są „wymazane” z obiegu - powrót do życia wymagałby wyłącznie wyjątkowej, fabularnej interwencji Mistrza Gry.\n​\nW miejscu rzucenia Zagłady pozostaje obszar magicznie skażony: choć brak tu „odpadów alchemicznych” jak przy Destrukcji, sama tkanka rzeczywistości jest nadpalona. MG może zdecydować, że każda istota przebywająca dłużej w tym obszarze musi wykonywać okresowe testy Duszy, ryzykując trwałe urazy psychiczne, klątwy lub inne długofalowe konsekwencje - nie jest to jednak integralna część efektu zaklęcia, a raczej fabularne piętno użycia tak nienaturalnej mocy.\n\n---" }, "Fala Energii": { "name": "Energy Wave", @@ -27,7 +27,7 @@ }, "Przebicie Eteru": { "name": "Ether Pierce", - "description": "Mag skupia swoją siłę, naginając rzeczywistość wokół celu, raniąc go pomimo osłon.\r\n\r\nCel otrzymuje tyle punktów Obrażeń ignorujących Pancerz, magiczne osłony i wszystkie odporności, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga.\r\n\r\nKażde dwa (zamiast pięciu) punkty obrażeń wyłączają jeden punkt Runicznego Pancerza, a przeciwko Magicznej Osłonie (np. Magicznym Tarczom) te same obrażenia liczą się podwójnie." + "description": "Mag skupia swoją siłę, naginając rzeczywistość wokół celu, raniąc go pomimo osłon.\n\nCel otrzymuje tyle punktów Obrażeń ignorujących Pancerz, magiczne osłony i wszystkie odporności, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga.\n\nKażde dwa (zamiast pięciu) punkty obrażeń wyłączają jeden punkt Runicznego Pancerza, a przeciwko Magicznej Osłonie (np. Magicznym Tarczom) te same obrażenia liczą się podwójnie." }, "Poblask": { "name": "Gleam", @@ -47,7 +47,7 @@ }, "Magiczna Tarcza": { "name": "Magic Shield", - "description": "Mag wyczarowuje magiczną osłonę, która chroni wybrany przez niego cel przed atakami.\r\n\r\nMagiczna Tarcza ma wytrzymałość na ataki równą połowie wartości Umiejętności Zdolności Magiczne + 1. Gdy cel ma otrzymać obrażenia, to są one zmniejszane maksymalnie o obecną wytrzymałość Magicznej Tarczy. Każdy atak zmniejsza wytrzymałość Magicznej Tarczy o ilość obrażeń, które zadaje. Jeżeli wytrzymałość Magicznej Tarczy będzie równa 0, zaklęcie przestaje działać." + "description": "Mag wyczarowuje magiczną osłonę, która chroni wybrany przez niego cel przed atakami.\n\nMagiczna Tarcza ma wytrzymałość na ataki równą połowie wartości Umiejętności Zdolności Magiczne + 1. Gdy cel ma otrzymać obrażenia, to są one zmniejszane maksymalnie o obecną wytrzymałość Magicznej Tarczy. Każdy atak zmniejsza wytrzymałość Magicznej Tarczy o ilość obrażeń, które zadaje. Jeżeli wytrzymałość Magicznej Tarczy będzie równa 0, zaklęcie przestaje działać." }, "Szybka Myśl": { "name": "Quick Thought", diff --git a/lang/compendium/en/spells-sacred.json b/lang/compendium/en/spells-sacred.json index 95dc673..d11aff6 100644 --- a/lang/compendium/en/spells-sacred.json +++ b/lang/compendium/en/spells-sacred.json @@ -43,7 +43,7 @@ }, "Urok Niewinności": { "name": "Charm Of Innocence", - "description": "Cel wydaje się wszystkim w otoczeniu urzekający i nieszkodliwy – agresywne działania przeciwników wobec celu wymagają dwóch Sukcesów więcej, a postronne osoby przyjmują wobec niego postawę przyjazną." + "description": "Cel wydaje się wszystkim w otoczeniu urzekający i nieszkodliwy - agresywne działania przeciwników wobec celu wymagają dwóch Sukcesów więcej, a postronne osoby przyjmują wobec niego postawę przyjazną." }, "Strzała Kupidyna": { "name": "Cupids Arrow", @@ -71,7 +71,7 @@ }, "Wyładowanie Elektrostatyczne": { "name": "Electrostatic Discharge", - "description": "Teren dookoła postaci zaczyna iskrzyć od wyładowań elektrostatycznych.\r\n\r\nWszystkie istoty w promieniu 2 m od Postaci tyle punktów Obrażeń ignorujących Pancerz, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne lub Oddanie maga, a następnie muszą wykonać Test 4:S na Wytrzymałość (Body), gdzie Sukcesy (S) wymagane do zdania Testu przez cel są równe ilości sukcesów wyrzuconych przez Postać. W wypadku nieudanego Testu cel zostaje Przewrócony." + "description": "Teren dookoła postaci zaczyna iskrzyć od wyładowań elektrostatycznych.\n\nWszystkie istoty w promieniu 2 m od Postaci tyle punktów Obrażeń ignorujących Pancerz, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne lub Oddanie maga, a następnie muszą wykonać Test 4:S na Wytrzymałość (Body), gdzie Sukcesy (S) wymagane do zdania Testu przez cel są równe ilości sukcesów wyrzuconych przez Postać. W wypadku nieudanego Testu cel zostaje Przewrócony." }, "Fanatyczne Poświęcenie": { "name": "Fanatical Sacrifice", @@ -151,7 +151,7 @@ }, "Grom z Nieba": { "name": "Thunderbolt", - "description": "Na uniżoną prośbę swojego wyznawcy, Zeus-Jowisz ciska swoim piorunem w wybrany punkt.\r\n\r\nWszyscy przeciwnicy w wybranym obszarze muszą wykonać Test 4:S na Wytrzymałość (Body), gdzie Sukcesy (S) oznacza ilość sukcesów wyrzuconą przez Postać. Każdy przeciwnik, który nie zda Testu, otrzymuje tyle punktów Obrażeń, ile wynosi dwukrotność wartości Umiejętności Zdolności Magiczne lub Oddanie Postaci, oraz zostaje Ogłuszony." + "description": "Na uniżoną prośbę swojego wyznawcy, Zeus-Jowisz ciska swoim piorunem w wybrany punkt.\n\nWszyscy przeciwnicy w wybranym obszarze muszą wykonać Test 4:S na Wytrzymałość (Body), gdzie Sukcesy (S) oznacza ilość sukcesów wyrzuconą przez Postać. Każdy przeciwnik, który nie zda Testu, otrzymuje tyle punktów Obrażeń, ile wynosi dwukrotność wartości Umiejętności Zdolności Magiczne lub Oddanie Postaci, oraz zostaje Ogłuszony." }, "Mądrość Salomona": { "name": "Wisdom Of Solomon", diff --git a/lang/compendium/en/talents-npc.json b/lang/compendium/en/talents-npc.json index 7cd157a..305d6e9 100644 --- a/lang/compendium/en/talents-npc.json +++ b/lang/compendium/en/talents-npc.json @@ -71,7 +71,7 @@ }, "Rozkaz Księżniczki": { "name": "Rozkaz Ksiezniczki", - "description": "Narcyzie nie trzeba niczego wymuszać siłą - wystarczy, że każe.\n\nJako Akcję Natychmiastową, kosztem 1 punktu Zapału, ta Postać wybiera jeden cel w zasięgu 15 m, który jest w stanie zrozumieć jej polecenia, i wykonuje Test Soul (Przekonywanie) 4:S. Rozstrzygnięcie następuje zgodnie ze Stanem [Autorytet](../../00.%20Podręcznik%20Gry/Aneks%20A%20-%20Stany%20Postaci/Autorytet.md): jeżeli Autorytet celu jest o dwa stopnie niższy niż Autorytet tej Postaci (zwykle IV), cel nie jest w stanie przeciwstawić się efektowi; w innym wypadku cel wykonuje Test o Trudności wynikającej z różnicy wartości Atrybutu Soul pomiędzy nim a tą Postacią (patrz tabela w Stanie Autorytet), gdzie wymagana liczba Sukcesów (S) jest równa liczbie Sukcesów wyrzuconych przez tę Postać w powyższym Teście.\n\nW wypadku niepowodzenia cel musi wykonywać rozkazy tej Postaci aż do końca jej następnej Tury - łącznie z rozkazami wymierzonymi w jego sojuszników, o ile nie są one wprost samobójcze.\n\nPonieważ jest to Akcja Natychmiastowa, nie koliduje ze zwykłą Akcją tej Postaci (Atakiem, rzuceniem zaklęcia czy ruchem) - w ramach limitu Akcji Natychmiastowych przysługującego jej w danej rundzie zgodnie ze standardowymi zasadami." + "description": "Narcyzie nie trzeba niczego wymuszać siłą - wystarczy, że każe.\n\nJako Akcję Natychmiastową, kosztem 1 punktu Zapału, ta Postać wybiera jeden cel w zasięgu 15 m, który jest w stanie zrozumieć jej polecenia, i wykonuje Test Soul (Przekonywanie) 4:S. Rozstrzygnięcie następuje zgodnie ze Stanem [Autorytet](../../00.%20Podręcznik%20Gry/Aneks%20A%20-%20Stany%20Postaci/Autorytet.md): jeżeli Autorytet celu jest o dwa stopnie niższy niż Autorytet tej Postaci (zwykle IV), cel nie jest w stanie przeciwstawić się efektowi; w innym wypadku cel wykonuje Test o Trudności wynikającej z różnicy wartości Atrybutu Soul pomiędzy nim a tą Postacią (patrz tabela w Stanie Autorytet), gdzie wymagana liczba Sukcesów (S) jest równa liczbie Sukcesów wyrzuconych przez tę Postać w powyższym Teście.\n\nW wypadku niepowodzenia cel musi wykonywać rozkazy tej Postaci aż do końca jej następnej Tury - łącznie z rozkazami wymierzonymi w jego sojuszników, o ile nie są one wprost samobójcze.\n\nPonieważ jest to Akcja Natychmiastowa, nie koliduje ze zwykłą Akcją tej Postaci (Atakiem, rzuceniem zaklęcia czy ruchem). Pierwsze użycie w danej rundzie rozlicza się w ramach standardowego limitu Akcji Natychmiastowych przysługującego jej w tej rundzie. Każde kolejne użycie w tej samej rundzie to wciąż Akcja Natychmiastowa, ale nie zużywa już tego limitu - jej jedynym kosztem, tak jak za pierwszym razem, pozostaje 1 punkt Zapału." }, "Skarbiec Królów": { "name": "Skarbiec Krolow", @@ -123,7 +123,7 @@ }, "Zapora Posłańca": { "name": "Zapora Poslanca", - "description": "Coś w Dianie - to samo coś, co pozwoliło jej niegdyś przeżyć ponad pół godziny w środku rozpadającego się rytuału, który powinien był ją zabić - wciąż czuwa, gdy zbliża się cudza magia.\n\nJako Akcję Natychmiastową, gdy Postać lub sojusznik w promieniu 10 m ma zostać trafiony zaklęciem, Postać może interweniować na dwa sposoby (do wyboru):\n- **Osłona:** cel otrzymuje punkty Magicznej Osłony w liczbie równej wartości Atrybutu Magic Postaci.\n- **Rozproszenie:** Postać wykonuje Test Magic (Zdolności Magiczne) T:5, gdzie liczba wymaganych Sukcesów jest o 1 większa niż liczba Sukcesów rzucającego zaklęcie - w przypadku powodzenia zaklęcie zostaje rozplecione, tak jak przy Talencie Rozplatanie Zaklęć.\n\nTen Talent nie ma ograniczenia użyć na rundę ani na starcie - jego jedynym kosztem jest Akcja Natychmiastowa, których Postać wciąż może wykonać tylko ograniczoną liczbę na rundę zgodnie z normalnymi zasadami." + "description": "Coś w Dianie - to samo coś, co pozwoliło jej niegdyś przeżyć ponad pół godziny w środku rozpadającego się rytuału, który powinien był ją zabić - wciąż czuwa, gdy zbliża się zagrożenie.\n\nJako Akcję Natychmiastową, gdy Postać lub sojusznik w promieniu 10 m ma zostać trafiony zaklęciem, Postać może interweniować na dwa sposoby (do wyboru):\n- **Osłona:** cel otrzymuje punkty Magicznej Osłony w liczbie równej wartości Atrybutu Magic Postaci.\n- **Rozproszenie:** Postać wykonuje Test Magic (Zdolności Magiczne) T:5, gdzie liczba wymaganych Sukcesów jest o 1 większa niż liczba Sukcesów rzucającego zaklęcie - w przypadku powodzenia zaklęcie zostaje rozplecione, tak jak przy Talencie Rozplatanie Zaklęć.\n\nTen Talent nie ma ograniczenia użyć na starcie. Raz na rundę Postać może użyć go za darmo, jako Akcję Natychmiastową w ramach standardowego limitu Akcji Natychmiastowych. Każde kolejne użycie w tej samej rundzie to wciąż Akcja Natychmiastowa, ale nie zużywa już tego limitu - kosztuje wtedy dodatkowo 1 punkt Zapału, tak jak kolejne próby rozplatania w Talencie [Rozplatanie Zaklęć](../../00.%20Podręcznik%20Gry/Rozdział%20IV%20-%20Talenty/Rozplatanie%20Zaklęć.md)." } } } diff --git a/lang/compendium/en/talents.json b/lang/compendium/en/talents.json index 681c754..b8a2287 100644 --- a/lang/compendium/en/talents.json +++ b/lang/compendium/en/talents.json @@ -165,6 +165,10 @@ "name": "Spell Shattering", "description": "Każdy mag posiadający ten Talent może raz na rundę za darmo dokonać próby rozplatania, jednak dane zaklęcie może być rozplatane wyłącznie raz.\n\nMagowie posiadający Zapał mogą próbować rozpleść więcej zaklęć - każda następna taka próba kosztuje jeden punkt Zapału. Magowie sakralni mogą potraktować rozplatanie zaklęcia jako modlitwę, zmieniając Atrybut swojego Testu na Duszę, ale jednocześnie zwiększając koszt wykorzystania tego Talentu o 1 punkt Zapału.\n\nJeżeli Postać widzi istotę rzucającą zaklęcie i jest od niej w zasięgu 20 m, może zadeklarować, że próbuje rozpleść rzucane zaklęcie. Postać wykonuje Test na Zdolności Magiczne (Magic lub Soul, jeśli jest magiem sakralnym), gdzie Trudność (T) jest równa trudności zaklęcia, a liczba wymaganych Sukcesów (S) jest o 1 większa od liczby sukcesów wyrzuconych przez maga rzucającego zaklęcie. Jeżeli wynik Testu Postaci będzie wyższy niż wynik Testu przeciwnika, zaklęcie zostaje rozplecione." }, + "Sztuki Walki": { + "name": "Sztuki Walki", + "description": "Wieloletni trening sprawił, że Twoje ciało samo w sobie jest bronią.\n\nNie stosujesz kar do Ataków bez Broni (patrz: [Atak bez Broni](../Rozdział%20V%20-%20Walka%20i%20Magic/Atak%20bronią.md)). Twoje pięści, kopnięcia, uderzenia głowicą i ogonem liczą się jako broń biała o Obrażeniach równych 1: możesz nimi Odpierać ataki wręcz, a także korzystać z Talentów wymagających broni białej (np. Parowanie, Finta, Riposta).\n\nTen Talent obejmuje również walkę przedmiotami, które nie są bronią (taborety, narzędzia itp.; tarcza nadal nie jest bronią) - nie stosujesz wtedy kar do Ataków Improwizowanych.\n\nWykonując Powalenie lub Rozbrojenie otrzymujesz dwie dodatkowe kości do Testu Przeciwstawnego." + }, "Tajemna Technologia": { "name": "Tajemna Technologia", "description": "PT Testów na rozpoznawanie artefaktów i rozumienie ich działania jest niższy o jeden stopień, jeżeli Postać miała styczność z czymś podobnym (nie musi o tym pamiętać)." From 5f8e62ba023078a397bf675924f4b2d08fbe2965 Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sat, 17 Oct 2026 10:50:12 +0200 Subject: [PATCH 5/7] chore: bump version to 1.3.38 Co-Authored-By: Claude Sonnet 5.5 --- package.json | 2 +- system.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index fa1cac0..6041ed7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hbm-rpg-v3-foundry", - "version": "1.3.37", + "version": "1.3.38", "description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3", "private": true, "type": "module", diff --git a/system.json b/system.json index 3ec8c3b..5caaf5c 100644 --- a/system.json +++ b/system.json @@ -2,9 +2,9 @@ "id": "hbm-rpg-v3", "title": "Homebrew Magic: RPG v3", "description": "System Foundry VTT dla Homebrew Magic: Role Playing Game v3 - autorska gra fabularna z mechaniką puli kości d6 (TS - Trudność:Sukcesy).", - "version": "1.3.37", + "version": "1.3.38", "manifest": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3.json", - "download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.3.37.zip", + "download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.3.38.zip", "compatibility": { "minimum": "13", "verified": "14" From 6b8d93f7727f3879da6902b6395a3f747801ba0c Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sat, 17 Oct 2026 12:35:16 +0200 Subject: [PATCH 6/7] feat: flat spell frontmatter, full source-book titles, drop circle Parser reads human-friendly flat keys (range, targets, castingTime, overcast list, threshold/successes, ...) and expands them to SpellData; fixes [object Object] range, empty targets/casting time and blank overcast rows. SOURCE_BOOKS ids now match vault slugs and show full book titles. Co-Authored-By: Claude Sonnet 5.5 --- lang/en.json | 17 ++-- lang/pl.json | 17 ++-- scripts/extract-spells-to-md.ts | 1 + scripts/parsers/spell-parser.ts | 135 ++++++++++++++++++++--------- src/constants.ts | 6 +- src/data/item-spell.ts | 1 - src/logic/spell-cast.ts | 1 - src/sheets/character-sheet.ts | 2 +- src/sheets/npc-sheet.ts | 2 +- templates/actor/character.hbs | 5 +- templates/actor/npc.hbs | 3 +- templates/chat/spell-cast.hbs | 1 - templates/item/spell.hbs | 3 - tests/parsers/spell-parser.test.ts | 48 ++++++++++ 14 files changed, 165 insertions(+), 77 deletions(-) create mode 100644 tests/parsers/spell-parser.test.ts diff --git a/lang/en.json b/lang/en.json index 53642ae..bfcb748 100644 --- a/lang/en.json +++ b/lang/en.json @@ -105,7 +105,6 @@ "spell": { "label": "Spell", "cast": "Cast spell", - "circle": "Circle", "school": "School", "discipline": "Discipline", "difficulty": "Spell Threshold", @@ -200,14 +199,14 @@ "eros": "Eros" }, "sourceBook": { - "core-rules": "PG", - "magic-book": "KM", - "arcanum-sanguinis": "AS", - "crimson-cult": "CS", - "abyss-curse": "KO", - "humanity-guide": "PLM", - "bestiary": "B", - "gold-steel-magic": "ZSM" + "podrecznik-gry": "Podręcznik Gry", + "ksiega-magii": "Księga Magii", + "arcanum-sanguinis": "Arcanum Sanguinis", + "crimson-cult": "Szkarłatny Kult", + "klatwa-otchlani": "Klątwa Otchłani", + "przewodnik": "Przewodnik", + "bestiariusz": "Bestiariusz", + "zlote-stal-magia": "Złoto i Stal: Magia" }, "aoeShape": { "point": "Point", diff --git a/lang/pl.json b/lang/pl.json index 641ca1d..c8d2f3a 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -105,7 +105,6 @@ "spell": { "label": "Zaklęcie", "cast": "Rzuć zaklęcie", - "circle": "Krąg", "school": "Szkoła", "discipline": "Dziedzina", "difficulty": "Próg zaklęcia", @@ -200,14 +199,14 @@ "eros": "Eros" }, "sourceBook": { - "core-rules": "PG", - "magic-book": "KM", - "arcanum-sanguinis": "AS", - "crimson-cult": "CS", - "abyss-curse": "KO", - "humanity-guide": "PLM", - "bestiary": "B", - "gold-steel-magic": "ZSM" + "podrecznik-gry": "Podręcznik Gry", + "ksiega-magii": "Księga Magii", + "arcanum-sanguinis": "Arcanum Sanguinis", + "crimson-cult": "Szkarłatny Kult", + "klatwa-otchlani": "Klątwa Otchłani", + "przewodnik": "Przewodnik", + "bestiariusz": "Bestiariusz", + "zlote-stal-magia": "Złoto i Stal: Magia" }, "aoeShape": { "point": "Punkt", diff --git a/scripts/extract-spells-to-md.ts b/scripts/extract-spells-to-md.ts index b3ba000..90f3cb5 100644 --- a/scripts/extract-spells-to-md.ts +++ b/scripts/extract-spells-to-md.ts @@ -1,3 +1,4 @@ +// DEPRECATED: one-off extractor that writes the old nested spell frontmatter. The vault now uses the flat format read by scripts/parsers/spell-parser.ts - do not run this over ObsidianNotes/spells. import { resolve, dirname } from 'node:path'; import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/scripts/parsers/spell-parser.ts b/scripts/parsers/spell-parser.ts index 5a22728..8a2f9a6 100644 --- a/scripts/parsers/spell-parser.ts +++ b/scripts/parsers/spell-parser.ts @@ -5,59 +5,109 @@ import type { ParsedDoc, ParserContext, ParserFn, SourceBookId } from './types'; import { slugify } from './helpers'; import yaml from 'js-yaml'; -/** Looser zod schema - mirrors `SpellData` defaults; build pipeline converts to system payload. */ -const SpellSystemSchema = z.object({ +/** + * Flat, human-editable vault frontmatter for a spell. Nothing is nested; + * optional keys can simply be left out. `toSystem()` expands it into the + * (partly nested) Foundry `SpellData` shape. + */ +const SpellNoteSchema = z.object({ castingMode: z.enum(['standard', 'sacred', 'witch', 'blood']), school: z.string(), deity: z.string().default(''), sourceBook: z.string(), manaCost: z.number().int().min(0), bloodCost: z.number().int().min(0).default(0), + threshold: z.number().int().min(1).max(6), + successes: z.number().int().min(1), complexityLevel: z.number().int().min(0).default(0), isSuperspell: z.boolean().default(false), requiresGroupCast: z.boolean().default(false), minCasters: z.number().int().min(1).default(1), nonCombatOnly: z.boolean().default(false), - damageBase: z.string().default(''), - damageType: z.string().default(''), + verbal: z.boolean().default(false), + somatic: z.boolean().default(false), + material: z.coerce.string().default(''), + symbols: z.array(z.string()).default([]), + requiresRace: z.array(z.string()).default([]), + requiresTalent: z.array(z.string()).default([]), + requiresDiscipline: z.array(z.string()).default([]), + castingTime: z.coerce.string().default('Instant'), + range: z.coerce.string().default(''), + targets: z.coerce.string().default(''), + duration: z.coerce.string().default(''), + damageBase: z.coerce.string().default(''), + damageType: z.string().default('magical'), ignoresArmor: z.boolean().default(false), statusEffects: z.array(z.string()).default([]), saveAttribute: z.string().default(''), saveSkill: z.string().default(''), triggers: z.array(z.object({ event: z.string(), effect: z.string() })).default([]), - components: z.object({ - verbal: z.boolean().default(false), - somatic: z.boolean().default(false), - material: z.string().default(''), - symbols: z.array(z.string()).default([]), - }), - requirements: z.object({ - race: z.array(z.string()).default([]), - talent: z.array(z.string()).default([]), - discipline: z.array(z.string()).default([]), - }), - difficulty: z.object({ - threshold: z.number().int().min(1).max(6), - successes: z.number().int().min(1), - }), - range: z.object({ - kind: z.string(), - value: z.number().optional(), - unit: z.string().optional(), - text: z.string().optional(), - }), - duration: z.string().default(''), - castingTime: z.string().default(''), - target: z.string().default(''), - areaOfEffect: z - .object({ shape: z.string(), x: z.number().optional(), y: z.number().optional(), unit: z.string().optional() }) - .nullable() - .default(null), - overcastOptions: z - .array(z.object({ text: z.string(), repeatable: z.boolean().default(true), cost: z.number().int().default(0) })) - .default([]), - description: z.string().default(''), + overcast: z.array(z.string()).default([]), }); +type SpellNote = z.infer; + +type Aoe = { shape: string; x: number; y: number; unit: string }; + +/** Derive the structured area of effect from the free-text `targets` line. */ +export function deriveAreaOfEffect(targets: string): Aoe { + const t = targets.replace(/\s+/g, ' '); + let m = t.match(/kwadrat o boku (\d+)\s*m/i); + if (m) return { shape: 'square', x: Number(m[1]), y: Number(m[1]), unit: 'm' }; + m = t.match(/prostokąt (\d+)\s*m\s*[x×]\s*(\d+)\s*m/i); + if (m) return { shape: 'rectangle', x: Number(m[1]), y: Number(m[2]), unit: 'm' }; + m = t.match(/(?:sfera|kula) o średnicy (\d+)\s*m/i); + if (m) return { shape: 'sphere', x: Number(m[1]) / 2, y: 0, unit: 'm' }; + m = t.match(/(?:sfera|kula|kopuła) o promieniu (\d+)\s*m/i); + if (m) return { shape: 'sphere', x: Number(m[1]), y: 0, unit: 'm' }; + if (/^linia/i.test(t)) return { shape: 'line', x: 0, y: 0, unit: 'm' }; + return { shape: 'point', x: 0, y: 0, unit: 'm' }; +} + +/** Casting time in whole rounds / minutes, for the sheet's numeric fields. */ +export function deriveCastingTime(text: string): { rounds: number; minutes: number } { + let m = text.match(/(\d+)\s*godzin/i); + if (m) return { rounds: 0, minutes: Number(m[1]) * 60 }; + m = text.match(/(\d+)\s*minut/i); + if (m) return { rounds: 0, minutes: Number(m[1]) }; + m = text.match(/(\d+)\s*rund/i); + if (m) return { rounds: Number(m[1]), minutes: 0 }; + return { rounds: 0, minutes: 0 }; +} + +function toSystem(n: SpellNote): Record { + const time = deriveCastingTime(n.castingTime); + return { + school: n.school, + deity: n.deity, + sourceBook: n.sourceBook, + castingMode: n.castingMode, + manaCost: n.manaCost, + bloodCost: n.bloodCost, + difficulty: { threshold: n.threshold, successes: n.successes }, + components: { verbal: n.verbal, somatic: n.somatic, material: n.material, symbols: n.symbols }, + castingTime: n.castingTime, + castingTimeRounds: time.rounds, + castingTimeMinutes: time.minutes, + range: n.range, + targets: n.targets, + duration: n.duration, + areaOfEffect: deriveAreaOfEffect(n.targets), + damageBase: n.damageBase, + damageType: n.damageType || 'magical', + ignoresArmor: n.ignoresArmor, + statusEffects: n.statusEffects, + saveAttribute: n.saveAttribute, + saveSkill: n.saveSkill, + requirements: { race: n.requiresRace, talent: n.requiresTalent, discipline: n.requiresDiscipline }, + isSuperspell: n.isSuperspell, + requiresGroupCast: n.requiresGroupCast, + minCasters: n.minCasters, + nonCombatOnly: n.nonCombatOnly, + overcastOptions: n.overcast.map((description) => ({ description, manaPerStep: 1 })), + triggers: n.triggers, + complexityLevel: n.complexityLevel, + }; +} function getAllFiles(dirPath: string, arrayOfFiles: string[] = []) { const files = readdirSync(dirPath); @@ -100,13 +150,12 @@ export const parseSpells: ParserFn = async (ctx: ParserContext): Promise ({ school, label: game.i18n.localize(`HBM.spellSchool.${school}`), - items: items.sort((a: any, b: any) => (a.system?.circle ?? 0) - (b.system?.circle ?? 0)), + items: items.sort((a: any, b: any) => a.name.localeCompare(b.name, 'pl')), })) .sort((a, b) => a.label.localeCompare(b.label, 'pl')); diff --git a/src/sheets/npc-sheet.ts b/src/sheets/npc-sheet.ts index a3a26f9..1bf7cc2 100644 --- a/src/sheets/npc-sheet.ts +++ b/src/sheets/npc-sheet.ts @@ -49,7 +49,7 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) { const ctx = (await super._prepareContext(options)) as Record; const actor = (this as unknown as { actor: { system: any; items: any[] } }).actor; const spells = actor.items.filter((it: any) => it.type === 'spell') - .sort((a: any, b: any) => (a.system?.circle ?? 0) - (b.system?.circle ?? 0) || a.name.localeCompare(b.name, 'pl')); + .sort((a: any, b: any) => a.name.localeCompare(b.name, 'pl')); const actorEffects = [...((actor as any).effects ?? [])].map((ef: any) => ({ id: ef.id, name: ef.name, diff --git a/templates/actor/character.hbs b/templates/actor/character.hbs index 4dd9577..bb266f8 100644 --- a/templates/actor/character.hbs +++ b/templates/actor/character.hbs @@ -275,14 +275,13 @@ title="Wymagana rasa">🔒{{/if}} - K{{spell.system.circle}} - · {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}} + {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}} · {{localize 'HBM.resources.mana'}}: {{spell.system.manaCost}} {{#if (eq spell.system.castingMode 'sacred')}}{{#if spell.system.deity}} · {{spell.system.deity}}{{/if}}{{/if}} {{#if (eq spell.system.castingMode 'blood')}} · krew: {{spell.system.bloodCost}}{{/if}} {{#if spell.system.sourceBook}} · {{spell.system.sourceBook}}{{/if}} + class="source-pip">{{localize (concat 'HBM.sourceBook.' spell.system.sourceBook)}}{{/if}}