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:
@@ -105,7 +105,7 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
|
||||
return
|
||||
}
|
||||
await setTinjiValue(current - 1)
|
||||
ChatMessage.create({
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
content: `<div class="cde-tinji-spend-msg">
|
||||
<i class="fas fa-star"></i>
|
||||
|
||||
@@ -177,7 +177,7 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
|
||||
? game.i18n.localize("CDE.Successes")
|
||||
: game.i18n.localize("CDE.AuspiciousDie")
|
||||
|
||||
ChatMessage.create({
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
content: `<div class="cde-loksyu-draw-msg">
|
||||
<div class="cde-loksyu-draw-header">
|
||||
@@ -207,7 +207,7 @@ async function _spendTinjiPostRoll() {
|
||||
return
|
||||
}
|
||||
await setTinjiValue(current - 1)
|
||||
ChatMessage.create({
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
content: `<div class="cde-tinji-spend-msg">
|
||||
<span class="cde-tinji-icon">天</span>
|
||||
|
||||
+44
-47
@@ -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 ----
|
||||
|
||||
@@ -19,6 +19,10 @@ import { CDEBaseActorSheet } from "./base.js"
|
||||
export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["character"],
|
||||
actions: {
|
||||
moveMagicUp: CDECharacterSheet.#onMoveMagicUp,
|
||||
moveMagicDown: CDECharacterSheet.#onMoveMagicDown,
|
||||
},
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
@@ -48,25 +52,35 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
|
||||
// Build magicsDisplay: only include the 5 relevant specialities per magic type + grimoire
|
||||
const systemMagics = context.systemData.magics ?? {}
|
||||
context.magicsDisplay = Object.fromEntries(
|
||||
Object.entries(MAGICS).map(([magicKey, magicDef]) => {
|
||||
const magicData = systemMagics[magicKey] ?? {}
|
||||
return [
|
||||
magicKey,
|
||||
{
|
||||
value: magicData.value ?? 0,
|
||||
visible: magicData.visible ?? false,
|
||||
speciality: Object.fromEntries(
|
||||
Object.keys(magicDef.speciality).map((specKey) => [
|
||||
specKey,
|
||||
{ check: magicData.speciality?.[specKey]?.check ?? false },
|
||||
])
|
||||
),
|
||||
grimoire: spellsByDiscipline[magicKey] ?? [],
|
||||
},
|
||||
]
|
||||
const magicEntries = Object.entries(MAGICS).map(([magicKey, magicDef]) => {
|
||||
const magicData = systemMagics[magicKey] ?? {}
|
||||
return [
|
||||
magicKey,
|
||||
{
|
||||
value: magicData.value ?? 0,
|
||||
visible: magicData.visible ?? false,
|
||||
speciality: Object.fromEntries(
|
||||
Object.keys(magicDef.speciality).map((specKey) => [
|
||||
specKey,
|
||||
{ check: magicData.speciality?.[specKey]?.check ?? false },
|
||||
])
|
||||
),
|
||||
grimoire: spellsByDiscipline[magicKey] ?? [],
|
||||
},
|
||||
]
|
||||
})
|
||||
const order = context.systemData.magicOrder ?? []
|
||||
if (order.length > 0) {
|
||||
magicEntries.sort((a, b) => {
|
||||
const ia = order.indexOf(a[0])
|
||||
const ib = order.indexOf(b[0])
|
||||
if (ia === -1 && ib === -1) return 0
|
||||
if (ia === -1) return 1
|
||||
if (ib === -1) return -1
|
||||
return ia - ib
|
||||
})
|
||||
)
|
||||
}
|
||||
context.magicsDisplay = Object.fromEntries(magicEntries)
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -155,7 +169,7 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
cell.addEventListener("click", (event) => {
|
||||
event.preventDefault()
|
||||
const rollKey = cell.dataset.libelId
|
||||
if (rollKey) rollForActor(this.document, rollKey)
|
||||
if (rollKey) rollForActor(this.document, rollKey)?.catch(err => console.error("Roll failed:", err))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -171,6 +185,28 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
})
|
||||
}
|
||||
|
||||
static async #onMoveMagicUp(event, target) {
|
||||
const key = target.dataset.magicKey
|
||||
let order = this.document.system.magicOrder ?? []
|
||||
if (!order.length) order = [...Object.keys(MAGICS)]
|
||||
else order = [...order]
|
||||
const idx = order.indexOf(key)
|
||||
if (idx <= 0) return
|
||||
[order[idx - 1], order[idx]] = [order[idx], order[idx - 1]]
|
||||
await this.document.update({ "system.magicOrder": order })
|
||||
}
|
||||
|
||||
static async #onMoveMagicDown(event, target) {
|
||||
const key = target.dataset.magicKey
|
||||
let order = this.document.system.magicOrder ?? []
|
||||
if (!order.length) order = [...Object.keys(MAGICS)]
|
||||
else order = [...order]
|
||||
const idx = order.indexOf(key)
|
||||
if (idx === -1 || idx >= order.length - 1) return
|
||||
[order[idx], order[idx + 1]] = [order[idx + 1], order[idx]]
|
||||
await this.document.update({ "system.magicOrder": order })
|
||||
}
|
||||
|
||||
#bindComponentRandomize() {
|
||||
const btn = this.element?.querySelector("[data-action='randomize-component']")
|
||||
if (!btn) return
|
||||
|
||||
@@ -50,7 +50,7 @@ export class CDENpcSheet extends CDEBaseActorSheet {
|
||||
cell.addEventListener("click", (event) => {
|
||||
event.preventDefault()
|
||||
const rollKey = cell.dataset.libelId
|
||||
if (rollKey) rollForActor(this.document, rollKey)
|
||||
if (rollKey) rollForActor(this.document, rollKey)?.catch(err => console.error("Roll failed:", err))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ export class CDESanheiSheet extends CDEBaseItemSheet {
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true })
|
||||
const props = this.document.system.properties
|
||||
context.prop1DescriptionHTML = await enrich(props.prop1.description)
|
||||
context.prop2DescriptionHTML = await enrich(props.prop2.description)
|
||||
context.prop3DescriptionHTML = await enrich(props.prop3.description)
|
||||
context.propFields = this.document.system.schema.fields.properties.fields
|
||||
const props = this.document.system.properties ?? {}
|
||||
context.prop1DescriptionHTML = await enrich(props.prop1?.description)
|
||||
context.prop2DescriptionHTML = await enrich(props.prop2?.description)
|
||||
context.prop3DescriptionHTML = await enrich(props.prop3?.description)
|
||||
context.propFields = this.document.system.schema.fields.properties?.fields
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user