Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ca94268c5 | ||
|
|
e5716e276c |
@@ -162,8 +162,8 @@
|
||||
"Magia Ognia": {
|
||||
"name": "Fire Magic"
|
||||
},
|
||||
"Alchemia - Transmutacja": {
|
||||
"name": "Alchemy - Transmutation"
|
||||
"Transmutacja": {
|
||||
"name": "Transmutation"
|
||||
},
|
||||
"Magia Żywiołów": {
|
||||
"name": "Elemental Magic"
|
||||
|
||||
+16
-2
@@ -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<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>/.
|
||||
if (existsSync(packsSrcDir)) rmSync(packsSrcDir, { recursive: true });
|
||||
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' }];
|
||||
}
|
||||
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' }];
|
||||
|
||||
@@ -146,6 +146,8 @@ const folderNameTranslations: Record<string, string> = {
|
||||
"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",
|
||||
|
||||
@@ -148,6 +148,7 @@ export const parseDisciplines: ParserFn = async (ctx: ParserContext): Promise<Pa
|
||||
suggestedSkills: SUGGESTED_SKILLS[seed.id] ?? ['magicalAbilities'],
|
||||
passiveAbility: '',
|
||||
availableSpells: [],
|
||||
signatureSpells: [],
|
||||
description,
|
||||
},
|
||||
description,
|
||||
|
||||
@@ -12,6 +12,7 @@ export class DisciplineData extends foundry.abstract.TypeDataModel {
|
||||
}),
|
||||
suggestedSkills: makeArrayField(new f.StringField({ blank: false })),
|
||||
passiveAbility: makeHtmlField(''),
|
||||
signatureSpells: makeArrayField(new f.StringField({ blank: false })),
|
||||
availableSpells: makeArrayField(new f.StringField({ blank: false })),
|
||||
description: makeHtmlField(''),
|
||||
};
|
||||
|
||||
+44
-15
@@ -80,6 +80,12 @@ export interface CastOptions {
|
||||
speaker?: ChatMessage.SpeakerData;
|
||||
}
|
||||
|
||||
/** A roll made during a cast; rendered inline in the cast card. */
|
||||
interface CastRollEntry {
|
||||
label: string;
|
||||
roll: HbmTSRoll;
|
||||
}
|
||||
|
||||
export interface CastResult {
|
||||
validation: ValidationResult;
|
||||
baseRoll: HbmTSRoll | null;
|
||||
@@ -92,6 +98,16 @@ function notify(text: string, severity: 'warn' | 'error' | 'info' = 'info'): voi
|
||||
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> {
|
||||
// 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<void> {
|
||||
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<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 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<HbmTSRoll | null> {
|
||||
async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise<HbmTSRoll | null> {
|
||||
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<HbmTSRoll | null> {
|
||||
async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise<HbmTSRoll | null> {
|
||||
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<HbmTSRoll | null> {
|
||||
async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise<HbmTSRoll | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#each rolls as |r|}}
|
||||
<section class="hbm-card-roll">
|
||||
{{{r.html}}}
|
||||
</section>
|
||||
{{/each}}
|
||||
|
||||
{{#if outcome.success}}
|
||||
{{#if damage}}
|
||||
<section class="hbm-card-damage">
|
||||
@@ -34,6 +40,15 @@
|
||||
{{#if damage.ignoresArmor}}<span class="badge ignores-armor">{{localize 'HBM.spell.ignoresArmor'}}</span>{{/if}}
|
||||
</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}}
|
||||
<div class="hbm-card-targets">
|
||||
{{#each damage.targets as |t|}}
|
||||
|
||||
Reference in New Issue
Block a user