feat(magic): reorder schools, fix Wu Xing aspect & power formula

- `magicOrder` ArrayField + ▲/▼ buttons for manual reordering
- Magic rolls use school's aspect for Wu Xing, not speciality's element
- Spell power: `difficulty × (aspectValue + freePowerLevels)` (not `successes × diff`)
- Prompt replaces `aspectspeciality`/`bonusmalusspeciality`/`heispend` with `freepowerlevels`

fix: code review issues
- combat.js: guard undefined `ids` in rollInitiative
- rolling.js: catch Dice So Nice promise, normalize French→English kungfu aspects
- weapon/armor/ingredient: `{ min: 0 }` on quantity
- character.js/npc.js: catch rollForActor fire-and-forget promises
- roll-actions.js/tinji-app.js: await ChatMessage.create
- sanhei.js: null guard on properties
- spell.js/kungfu.js: fix aspect name comments (French→English)
This commit is contained in:
2026-06-10 15:54:31 +02:00
parent 188717c925
commit 75f79c1c08
21 changed files with 304 additions and 189 deletions
+44 -47
View File
@@ -164,19 +164,16 @@ async function showMagicPrompt(params) {
title: params.title,
template: MAGIC_PROMPT_TEMPLATE,
data: {
numberofdice: params.numberofdice ?? 0,
aspectskill: Number(params.aspectskill ?? 0),
bonusmalusskill: params.bonusmalusskill ?? 0,
numberofdice: params.numberofdice ?? 0,
aspectskill: Number(params.aspectskill ?? 0),
bonusmalusskill: params.bonusmalusskill ?? 0,
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
aspectspeciality: Number(params.aspectspeciality ?? 0),
rolldifficulty: params.rolldifficulty ?? 1,
bonusmalusspeciality: params.bonusmalusspeciality ?? 0,
heispend: params.heispend ?? 0,
typeofthrow: Number(params.typeofthrow ?? 0),
rolldifficulty: params.rolldifficulty ?? 1,
freepowerlevels: params.freepowerlevels ?? 0,
typeofthrow: Number(params.typeofthrow ?? 0),
},
fields: ["aspectskill", "bonusmalusskill", "bonusauspiciousdice",
"aspectspeciality", "rolldifficulty", "bonusmalusspeciality",
"heispend", "typeofthrow"],
"rolldifficulty", "freepowerlevels", "typeofthrow"],
})
}
@@ -318,7 +315,9 @@ export async function rollForActor(actor, rollKey) {
const kfSkill = kfItem.system.skill ?? "kungfu"
numberofdice = sys.skills?.[kfSkill]?.value ?? 0
title = `${kfItem.name} [${game.i18n.localize(sys.skills?.[kfSkill]?.label ?? "CDE.KungFu")}]`
kfDefaultAspect = ASPECT_NAMES.indexOf(kfItem.system.aspect ?? "metal")
const kfAspect = kfItem.system.aspect?.toLowerCase() ?? "metal"
const ASPECT_NORMALIZE = { eau: "water", terre: "earth", feu: "fire", bois: "wood" }
kfDefaultAspect = ASPECT_NAMES.indexOf(ASPECT_NORMALIZE[kfAspect] ?? kfAspect)
if (kfDefaultAspect < 0) kfDefaultAspect = 0
break
}
@@ -427,7 +426,7 @@ export async function rollForActor(actor, rollKey) {
}, wpRoll, ROLL_MODES[wpThrowMode] ?? "roll")
if (game.modules.get("dice-so-nice")?.active && wpMsg?.id) {
await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id)
try { await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id) } catch (_e) { /* DSN not available */ }
}
// Auto-update Loksyu/TinJi singletons from weapon roll faces
if ((wpResults.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wpAspectName, wpFaces)
@@ -439,8 +438,7 @@ export async function rollForActor(actor, rollKey) {
return
}
// For magic rolls the prompt allows adding HEI dice, so don't block early.
// For itemkungfu, allow 0 base dice (user can add bonus dice in the prompt).
// For magic rolls / itemkungfu, allow 0 base dice (user can add bonus dice in the prompt).
if (numberofdice <= 0 && typeLibel !== "aspect" && typeLibel !== "itemkungfu" && !isMagic) {
ui.notifications.warn(game.i18n.localize("CDE.Error0"))
return
@@ -465,16 +463,6 @@ export async function rollForActor(actor, rollKey) {
defaultAspect = kfDefaultAspect
}
let defaultSpecialAspect = 0
if (isMagicSpecial && specialLibel) {
// Look up the speciality's element from the MAGICS config constant
const specialCfg = MAGICS?.[skillLibel]?.speciality?.[specialLibel]
const aspectName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelement]
if (aspectName) {
defaultSpecialAspect = ASPECT_NAMES.indexOf(aspectName)
}
}
// ---- Show roll prompt ----
let params
@@ -485,10 +473,8 @@ export async function rollForActor(actor, rollKey) {
aspectskill: defaultAspect,
bonusmalusskill: 0,
bonusauspiciousdice: 0,
aspectspeciality: defaultSpecialAspect,
rolldifficulty: 1,
bonusmalusspeciality: 0,
heispend: 0,
freepowerlevels: 0,
typeofthrow: typeOfThrow,
})
} else {
@@ -508,22 +494,18 @@ export async function rollForActor(actor, rollKey) {
// ---- Compute total dice and roll ----
let aspectIndex, bonusMalus, bonusAuspicious, throwMode
let spellAspectIndex = null // magic only: aspect of the speciality for Wu Xing
let rollDifficulty = 1 // magic only: multiplier applied to successes
let rollDifficulty = 1 // magic only: multiplier applied to successes
if (isMagic) {
const skillAspectIndex = Number(params.aspectskill ?? 0)
spellAspectIndex = Number(params.aspectspeciality ?? skillAspectIndex)
aspectIndex = skillAspectIndex // used only for skill dice pool
aspectIndex = skillAspectIndex // used for both dice pool and Wu Xing cycle
bonusMalus = Number(params.bonusmalusskill ?? 0)
bonusAuspicious = Number(params.bonusauspiciousdice ?? 0)
rollDifficulty = Math.max(1, Number(params.rolldifficulty ?? 1))
throwMode = Number(params.typeofthrow ?? 0)
// magic: magic skill + aspect + bonuses + 1 (speciality base) + HEI spent
const aspectDice = sys.aspect?.[ASPECT_NAMES[aspectIndex]]?.value ?? 0
const bonusSpec = Number(params.bonusmalusspeciality ?? 0)
const heiDice = Number(params.heispend ?? 0)
numberofdice = numberofdice + aspectDice + bonusMalus + 1 + bonusSpec + heiDice
numberofdice = numberofdice + aspectDice + bonusMalus + 1
} else {
aspectIndex = Number(params.aspect ?? 0)
bonusMalus = Number(params.bonusmalus ?? 0)
@@ -550,31 +532,43 @@ export async function rollForActor(actor, rollKey) {
const rollModeKey = ROLL_MODES[throwMode] ?? "roll"
// ---- Compute spell power (magic only) ----
// Power = rollDifficulty × character aspect value for the speciality's
// associated element (or the school's aspect for base magic rolls).
let spellPower = null
let spellPowerAspectName = null
let spellPowerAspectValue = null
if (isMagic) {
if (isMagicSpecial && specialLibel) {
const specialCfg = MAGICS?.[skillLibel]?.speciality?.[specialLibel]
const elemName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelement]
if (elemName) spellPowerAspectName = elemName
}
if (!spellPowerAspectName) spellPowerAspectName = ASPECT_NAMES[aspectIndex]
spellPowerAspectValue = sys.aspect?.[spellPowerAspectName]?.value ?? 0
const freePowerLevels = Number(params.freepowerlevels ?? 0)
spellPower = rollDifficulty * (spellPowerAspectValue + freePowerLevels)
}
// ---- Compute Wu Xing results ----
// For magic rolls, the spell's aspect (aspectspeciality) governs the Wu Xing
// cycle (which faces count as successes/auspicious/etc.), not the skill aspect.
const wuXingAspectName = spellAspectIndex !== null
? ASPECT_NAMES[spellAspectIndex]
: ASPECT_NAMES[aspectIndex]
// The Wu Xing cycle always uses the roll's aspect (skill aspect for magic,
// skill/resource aspect otherwise) to determine which faces count as
// successes/auspicious/etc.
const wuXingAspectName = ASPECT_NAMES[aspectIndex]
const allResults = roll.dice[0]?.results ?? []
const faces = countFaces(allResults)
const results = computeWuXingResults(faces, wuXingAspectName, bonusAuspicious)
if (!results) return
// For magic, successesdice × rollDifficulty = spell power
const spellPower = isMagic ? results.successesdice * rollDifficulty : null
// ---- Build modifier summary text ----
const modParts = []
if (isMagic) {
const bm = Number(params.bonusmalusskill ?? 0)
const bs = Number(params.bonusmalusspeciality ?? 0)
const hs = Number(params.heispend ?? 0)
const ba = Number(params.bonusauspiciousdice ?? 0)
const fp = Number(params.freepowerlevels ?? 0)
if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`)
if (bs !== 0) modParts.push(`${bs > 0 ? "+" : ""}${bs} ${game.i18n.localize("CDE.SpellBonus")}`)
if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`)
if (hs !== 0) modParts.push(`${hs} ${game.i18n.localize("CDE.HeiSpend")}`)
if (fp !== 0) modParts.push(`+${fp} ${game.i18n.localize("CDE.FreePowerLevels")}`)
if (rollDifficulty !== 1) modParts.push(`×${rollDifficulty} ${game.i18n.localize("CDE.RollDifficulty")}`)
} else {
const bm = Number(params.bonusmalus ?? 0)
@@ -596,6 +590,9 @@ export async function rollForActor(actor, rollKey) {
modifiersText: modParts.length ? modParts.join(" · ") : "",
// Spell power (magic only)
spellPower,
spellPowerAspectLabel: spellPowerAspectName ? game.i18n.localize(ASPECT_LABELS[spellPowerAspectName] ?? "") : "",
spellPowerAspectValue,
spellPowerFreeLevels: isMagic ? Number(params.freepowerlevels ?? 0) : 0,
rollDifficulty: isMagic ? rollDifficulty : null,
// Actor info
actorName: actor.name ?? "",
@@ -610,7 +607,7 @@ export async function rollForActor(actor, rollKey) {
// ---- Wait for Dice So Nice animation ----
if (game.modules.get("dice-so-nice")?.active && msg?.id) {
await game.dice3d.waitFor3DAnimationByMessageID(msg.id)
try { await game.dice3d.waitFor3DAnimationByMessageID(msg.id) } catch (_e) { /* DSN not available */ }
}
// ---- Auto-update Loksyu / TinJi singletons ----