diff --git a/css/cde-theme.css b/css/cde-theme.css index 0b9a214..2c810a2 100644 --- a/css/cde-theme.css +++ b/css/cde-theme.css @@ -2179,6 +2179,25 @@ section.npc .cde-neon-tabs .item.active { .cde-magic-toggle:hover i { color: #e2e8f4; } +.cde-magic-order-btn { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #7d94b8; + border-radius: 3px; + flex-shrink: 0; + transition: color 0.12s, background 0.12s; +} +.cde-magic-order-btn i { + font-size: 9px; +} +.cde-magic-order-btn:hover { + color: #e2e8f4; + background: rgba(38, 56, 83, 0.25); +} .cde-magic-specialities { border-top: 1px solid #1a2436; padding: 4px 0; @@ -3523,6 +3542,12 @@ ol.item-list li.item .item-controls a.item-control:hover { color: var(--rr-accent, #e2e8f4); text-shadow: 0 0 12px var(--rr-accent, transparent); } +.cde-roll-result .cde-rr-hero .cde-rr-spell-power .cde-rr-spell-power-formula { + font-size: 10px; + color: #7d94b8; + margin: 2px 0; + white-space: nowrap; +} .cde-roll-result .cde-rr-hero .cde-rr-spell-power .cde-rr-spell-power-label { font-size: 9px; font-weight: 700; diff --git a/css/cde-theme.less b/css/cde-theme.less index b2a1e9f..df6cb48 100644 --- a/css/cde-theme.less +++ b/css/cde-theme.less @@ -2253,6 +2253,23 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde &:hover i { color: @cde-text; } } +.cde-magic-order-btn { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: @cde-muted; + border-radius: 3px; + flex-shrink: 0; + transition: color 0.12s, background 0.12s; + + i { font-size: 9px; } + + &:hover { color: @cde-text; background: fade(@cde-border-hi, 25%); } +} + // Specialities list .cde-magic-specialities { border-top: 1px solid @cde-border; @@ -3582,6 +3599,13 @@ ol.item-list { text-shadow: 0 0 12px var(--rr-accent, transparent); } + .cde-rr-spell-power-formula { + font-size: 10px; + color: @cde-muted; + margin: 2px 0; + white-space: nowrap; + } + .cde-rr-spell-power-label { font-size: 9px; font-weight: 700; diff --git a/dist/system.js b/dist/system.js index 495f54d..f1cf426 100644 --- a/dist/system.js +++ b/dist/system.js @@ -774,9 +774,8 @@ var CharacterDataModel = class extends foundry.abstract.TypeDataModel { typeofthrow: numberField(0), aspectskill: numberField(0), bonusmalusskill: numberField(0), - aspectspeciality: numberField(0), rolldifficulty: numberField(0), - bonusmalusspeciality: numberField(0) + freepowerlevels: numberField(0) }) }), aspect: new fields.SchemaField({ @@ -815,6 +814,10 @@ var CharacterDataModel = class extends foundry.abstract.TypeDataModel { nine: componentField(), zero: componentField() }), + magicOrder: new fields.ArrayField( + new fields.StringField({ required: true, nullable: false, initial: "" }), + { required: true, initial: [] } + ), magics: new fields.SchemaField({ internalcinnabar: magicField(), alchemy: magicField(), @@ -931,7 +934,7 @@ var KungfuDataModel = class extends foundry.abstract.TypeDataModel { orientation: stringField("yin"), // yin | yang | yinyang aspect: stringField("metal"), - // metal | eau | terre | feu | bois + // metal | water | earth | fire | wood skill: stringField("kungfu"), // kungfu | rangedcombat speciality: stringField(""), @@ -957,7 +960,7 @@ var SpellDataModel = class extends foundry.abstract.TypeDataModel { description: htmlField(""), specialityname: stringField(""), associatedelement: stringField("metal"), - // metal | eau | terre | feu | bois + // metal | water | earth | fire | wood hei: stringField(""), realizationtimeritual: stringField(""), realizationtimeaccelerated: stringField(""), @@ -1012,7 +1015,7 @@ var WeaponDataModel = class extends foundry.abstract.TypeDataModel { // contact | courte | mediane | longue | extreme obtainLevel: intField(0, { min: 0, max: 5 }), obtainDifficulty: intField(0, { min: 0, max: 3 }), - quantity: intField(1), + quantity: intField(1, { min: 0 }), notes: htmlField("") }; } @@ -1032,7 +1035,7 @@ var ArmorDataModel = class extends foundry.abstract.TypeDataModel { domain: stringField(""), obtainLevel: intField(0, { min: 0, max: 5 }), obtainDifficulty: intField(0, { min: 0, max: 3 }), - quantity: intField(1), + quantity: intField(1, { min: 0 }), notes: htmlField("") }; } @@ -1078,7 +1081,7 @@ var IngredientDataModel = class extends foundry.abstract.TypeDataModel { school: stringField("all"), obtainLevel: intField(0, { min: 0, max: 5 }), obtainDifficulty: intField(0, { min: 0, max: 3 }), - quantity: intField(1), + quantity: intField(1, { min: 0 }), notes: htmlField("") }; } @@ -1568,20 +1571,16 @@ async function showMagicPrompt(params) { 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, + freepowerlevels: params.freepowerlevels ?? 0, typeofthrow: Number(params.typeofthrow ?? 0) }, fields: [ "aspectskill", "bonusmalusskill", "bonusauspiciousdice", - "aspectspeciality", "rolldifficulty", - "bonusmalusspeciality", - "heispend", + "freepowerlevels", "typeofthrow" ] }); @@ -1711,7 +1710,9 @@ 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; } @@ -1813,7 +1814,10 @@ async function rollForActor(actor, rollKey) { d0: wpFaces[0] }, 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) { + } } if ((wpResults.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wpAspectName, wpFaces); if ((wpResults.tinjidice ?? 0) > 0) await updateTinjiFromRoll(wpResults.tinjidice); @@ -1846,14 +1850,6 @@ async function rollForActor(actor, rollKey) { if (kfDefaultAspect >= 0) { defaultAspect = kfDefaultAspect; } - let defaultSpecialAspect = 0; - if (isMagicSpecial && specialLibel) { - const specialCfg = MAGICS?.[skillLibel]?.speciality?.[specialLibel]; - const aspectName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelement]; - if (aspectName) { - defaultSpecialAspect = ASPECT_NAMES.indexOf(aspectName); - } - } let params; if (isMagic) { params = await showMagicPrompt({ @@ -1862,10 +1858,8 @@ async function rollForActor(actor, rollKey) { aspectskill: defaultAspect, bonusmalusskill: 0, bonusauspiciousdice: 0, - aspectspeciality: defaultSpecialAspect, rolldifficulty: 1, - bonusmalusspeciality: 0, - heispend: 0, + freepowerlevels: 0, typeofthrow: typeOfThrow }); } else { @@ -1882,20 +1876,16 @@ async function rollForActor(actor, rollKey) { } if (!params) return; let aspectIndex, bonusMalus, bonusAuspicious, throwMode; - let spellAspectIndex = null; let rollDifficulty = 1; if (isMagic) { const skillAspectIndex = Number(params.aspectskill ?? 0); - spellAspectIndex = Number(params.aspectspeciality ?? skillAspectIndex); aspectIndex = skillAspectIndex; bonusMalus = Number(params.bonusmalusskill ?? 0); bonusAuspicious = Number(params.bonusauspiciousdice ?? 0); rollDifficulty = Math.max(1, Number(params.rolldifficulty ?? 1)); throwMode = Number(params.typeofthrow ?? 0); 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); @@ -1913,22 +1903,33 @@ async function rollForActor(actor, rollKey) { const roll = new Roll(`${numberofdice}d10`); await roll.evaluate(); const rollModeKey = ROLL_MODES[throwMode] ?? "roll"; - const wuXingAspectName = spellAspectIndex !== null ? ASPECT_NAMES[spellAspectIndex] : ASPECT_NAMES[aspectIndex]; + 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); + } + const wuXingAspectName = ASPECT_NAMES[aspectIndex]; const allResults = roll.dice[0]?.results ?? []; const faces = countFaces(allResults); const results = computeWuXingResults(faces, wuXingAspectName, bonusAuspicious); if (!results) return; - const spellPower = isMagic ? results.successesdice * rollDifficulty : null; 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(`\xD7${rollDifficulty} ${game.i18n.localize("CDE.RollDifficulty")}`); } else { const bm = Number(params.bonusmalus ?? 0); @@ -1948,6 +1949,9 @@ async function rollForActor(actor, rollKey) { modifiersText: modParts.length ? modParts.join(" \xB7 ") : "", // 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 ?? "", @@ -1968,7 +1972,10 @@ async function rollForActor(actor, rollKey) { d0: faces[0] }, roll, rollModeKey); 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) { + } } if ((results.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wuXingAspectName, faces); if ((results.tinjidice ?? 0) > 0) await updateTinjiFromRoll(results.tinjidice); @@ -2064,9 +2071,13 @@ var CDEBaseActorSheet = class _CDEBaseActorSheet extends HandlebarsApplicationMi }; // src/ui/sheets/actors/character.js -var CDECharacterSheet = class extends CDEBaseActorSheet { +var CDECharacterSheet = class _CDECharacterSheet extends CDEBaseActorSheet { static DEFAULT_OPTIONS = { - classes: ["character"] + classes: ["character"], + actions: { + moveMagicUp: _CDECharacterSheet.#onMoveMagicUp, + moveMagicDown: _CDECharacterSheet.#onMoveMagicDown + } }; static PARTS = { main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-character-sheet.html" } @@ -2089,25 +2100,35 @@ var CDECharacterSheet = class extends CDEBaseActorSheet { spellsByDiscipline[disc].push(spell); } 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; } _onRender(context, options) { @@ -2191,7 +2212,7 @@ var CDECharacterSheet = class 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)); }); }); } @@ -2205,6 +2226,26 @@ var CDECharacterSheet = class extends CDEBaseActorSheet { }).render(true); }); } + 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; @@ -2274,7 +2315,7 @@ var CDENpcSheet = class 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)); }); }); } @@ -2461,11 +2502,11 @@ var CDESanheiSheet = class 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; } }; @@ -2673,7 +2714,7 @@ var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.Handlebars return; } await setTinjiValue(current - 1); - ChatMessage.create({ + await ChatMessage.create({ user: game.user.id, content: `