Rework des fiches creature/PJ et Tinji/Loksyu
This commit is contained in:
+26
-35
@@ -1,5 +1,4 @@
|
||||
import { getSingletonActor } from "./singletons.js"
|
||||
import { ACTOR_TYPES } from "../../config/constants.js"
|
||||
import { getLoksyuData, setLoksyuData } from "./singletons.js"
|
||||
|
||||
const SYSTEM_ID = "fvtt-chroniques-de-l-etrange"
|
||||
|
||||
@@ -15,7 +14,7 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
resizable: false,
|
||||
},
|
||||
classes: ["cde-app", "cde-loksyu-standalone"],
|
||||
position: { width: 540, height: "auto" },
|
||||
position: { width: 520, height: "auto" },
|
||||
actions: {
|
||||
resetElement: CDELoksyuApp.#onResetElement,
|
||||
resetAll: CDELoksyuApp.#onResetAll,
|
||||
@@ -28,14 +27,12 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {Actor|null} */
|
||||
#actor = null
|
||||
/** @type {Function|null} bound hook handler */
|
||||
#updateHook = null
|
||||
_updateHook = null
|
||||
|
||||
/** Singleton accessor — open or bring to front */
|
||||
static open() {
|
||||
const existing = Object.values(foundry.applications.instances ?? {}).find(
|
||||
const existing = Array.from(foundry.applications.instances.values()).find(
|
||||
(app) => app instanceof CDELoksyuApp
|
||||
)
|
||||
if (existing) { existing.bringToFront(); return existing }
|
||||
@@ -45,10 +42,7 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
this.#actor = await getSingletonActor(ACTOR_TYPES.loksyu)
|
||||
if (!this.#actor) return { hasActor: false }
|
||||
|
||||
const sys = this.#actor.system
|
||||
const sys = getLoksyuData()
|
||||
const ELEMENTS = [
|
||||
{ key: "wood", nameKey: "CDE.Wood", qualKey: "CDE.WoodQualities", img: `systems/${SYSTEM_ID}/images/cde_bois.webp` },
|
||||
{ key: "fire", nameKey: "CDE.Fire", qualKey: "CDE.FireQualities", img: `systems/${SYSTEM_ID}/images/cde_feu.webp` },
|
||||
@@ -58,12 +52,11 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
]
|
||||
|
||||
return {
|
||||
hasActor: true,
|
||||
canEdit: this.#actor.isOwner,
|
||||
canEdit: game.user.isGM,
|
||||
elements: ELEMENTS.map((el) => ({
|
||||
...el,
|
||||
yang: sys[el.key]?.yang?.value ?? 0,
|
||||
yin: sys[el.key]?.yin?.value ?? 0,
|
||||
yang: sys[el.key]?.yang ?? 0,
|
||||
yin: sys[el.key]?.yin ?? 0,
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -72,16 +65,13 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
super._onRender(context, options)
|
||||
this.#bindInputs()
|
||||
|
||||
// Subscribe to actor updates to keep the app live
|
||||
this.#updateHook = Hooks.on("updateActor", (actor) => {
|
||||
if (actor.id === this.#actor?.id) this.render()
|
||||
})
|
||||
this._updateHook = Hooks.on("cde:loksyuUpdated", () => this.render())
|
||||
}
|
||||
|
||||
_onClose(options) {
|
||||
if (this.#updateHook !== null) {
|
||||
Hooks.off("updateActor", this.#updateHook)
|
||||
this.#updateHook = null
|
||||
if (this._updateHook !== null) {
|
||||
Hooks.off("cde:loksyuUpdated", this._updateHook)
|
||||
this._updateHook = null
|
||||
}
|
||||
super._onClose(options)
|
||||
}
|
||||
@@ -94,28 +84,29 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
const field = ev.currentTarget.dataset.field
|
||||
const val = parseInt(ev.currentTarget.value, 10)
|
||||
if (!field || isNaN(val)) return
|
||||
await this.#actor?.update({ [field]: Math.max(0, val) })
|
||||
// field is like "wood.yin" or "fire.yang"
|
||||
const [aspect, dim] = field.split(".")
|
||||
if (!aspect || !dim) return
|
||||
const data = getLoksyuData()
|
||||
if (!data[aspect]) data[aspect] = { yin: 0, yang: 0 }
|
||||
data[aspect][dim] = Math.max(0, val)
|
||||
await setLoksyuData(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
static async #onResetElement(event, target) {
|
||||
const key = target.dataset.element
|
||||
if (!key || !this.#actor) return
|
||||
await this.#actor.update({
|
||||
[`system.${key}.yin.value`]: 0,
|
||||
[`system.${key}.yang.value`]: 0,
|
||||
})
|
||||
if (!key) return
|
||||
const data = getLoksyuData()
|
||||
data[key] = { yin: 0, yang: 0 }
|
||||
await setLoksyuData(data)
|
||||
}
|
||||
|
||||
static async #onResetAll(_event, _target) {
|
||||
if (!this.#actor) return
|
||||
const KEYS = ["wood", "fire", "earth", "metal", "water"]
|
||||
const update = {}
|
||||
for (const k of KEYS) {
|
||||
update[`system.${k}.yin.value`] = 0
|
||||
update[`system.${k}.yang.value`] = 0
|
||||
}
|
||||
await this.#actor.update(update)
|
||||
const data = getLoksyuData()
|
||||
for (const k of KEYS) data[k] = { yin: 0, yang: 0 }
|
||||
await setLoksyuData(data)
|
||||
}
|
||||
}
|
||||
|
||||
+35
-47
@@ -1,12 +1,10 @@
|
||||
/**
|
||||
* Singleton actor utilities for Loksyu and Tin Ji.
|
||||
* Loksyu / TinJi settings-based helpers.
|
||||
*
|
||||
* Both are world-level shared trackers backed by a singleton Actor document
|
||||
* of type "loksyu" / "tinji". GMs can create them via the Actors sidebar;
|
||||
* the apps find the first one or offer to create it.
|
||||
* Data is stored as world settings instead of singleton Actor documents.
|
||||
*/
|
||||
|
||||
import { ACTOR_TYPES } from "../../config/constants.js"
|
||||
const SYSTEM_ID = "fvtt-chroniques-de-l-etrange"
|
||||
|
||||
/** Wu Xing generating cycle — [successes, auspicious, noxious, loksyu, tinji] */
|
||||
const WU_XING_CYCLE = {
|
||||
@@ -17,7 +15,6 @@ const WU_XING_CYCLE = {
|
||||
water: ["water", "wood", "metal", "fire", "earth"],
|
||||
}
|
||||
|
||||
/** Die face pairs [yin, yang] per aspect (0 = face "10") */
|
||||
const ASPECT_FACES = {
|
||||
metal: [3, 8],
|
||||
water: [1, 6],
|
||||
@@ -26,39 +23,35 @@ const ASPECT_FACES = {
|
||||
wood: [4, 9],
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first actor of the given type in the world, or create one if the
|
||||
* current user is a GM and none exists.
|
||||
*
|
||||
* @param {"loksyu"|"tinji"} type
|
||||
* @returns {Promise<Actor|null>}
|
||||
*/
|
||||
export async function getSingletonActor(type) {
|
||||
const existing = game.actors.find((a) => a.type === type)
|
||||
if (existing) return existing
|
||||
|
||||
if (!game.user.isGM) {
|
||||
ui.notifications.warn(game.i18n.localize(type === ACTOR_TYPES.loksyu ? "CDE.LoksyuNotFound" : "CDE.TinjiNotFound"))
|
||||
return null
|
||||
/** Read the current loksyu data object from world settings */
|
||||
export function getLoksyuData() {
|
||||
return game.settings.get(SYSTEM_ID, "loksyuData") ?? {
|
||||
wood: {yin:0,yang:0}, fire: {yin:0,yang:0}, earth: {yin:0,yang:0}, metal: {yin:0,yang:0}, water: {yin:0,yang:0},
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-create the singleton when the GM opens the app for the first time.
|
||||
const nameKey = type === ACTOR_TYPES.loksyu ? "CDE.UpperCaseLoksyu" : "CDE.UpperCaseTinJi"
|
||||
const actor = await Actor.create({
|
||||
name: game.i18n.localize(nameKey),
|
||||
type,
|
||||
img: type === ACTOR_TYPES.loksyu
|
||||
? "systems/fvtt-chroniques-de-l-etrange/images/loksyu_long.webp"
|
||||
: "systems/fvtt-chroniques-de-l-etrange/images/tinji.webp",
|
||||
})
|
||||
return actor ?? null
|
||||
/** Write the loksyu data object to world settings */
|
||||
export async function setLoksyuData(data) {
|
||||
await game.settings.set(SYSTEM_ID, "loksyuData", data)
|
||||
Hooks.callAll("cde:loksyuUpdated", data)
|
||||
}
|
||||
|
||||
/** Read current TinJi value from world settings */
|
||||
export function getTinjiValue() {
|
||||
return game.settings.get(SYSTEM_ID, "tinjiData") ?? 0
|
||||
}
|
||||
|
||||
/** Write TinJi value to world settings */
|
||||
export async function setTinjiValue(value) {
|
||||
await game.settings.set(SYSTEM_ID, "tinjiData", Math.max(0, value))
|
||||
Hooks.callAll("cde:tinjiUpdated", Math.max(0, value))
|
||||
}
|
||||
|
||||
/**
|
||||
* After a WuXing roll, add the loksyu faces (yin + yang) of the relevant
|
||||
* aspect to the singleton Loksyu actor.
|
||||
* aspect to the loksyu settings data.
|
||||
*
|
||||
* @param {string} activeAspect - The aspect used for the roll (e.g. "fire")
|
||||
* @param {string} activeAspect - e.g. "fire"
|
||||
* @param {Object} faces - Die face counts { 0: n, 1: n, …, 9: n }
|
||||
*/
|
||||
export async function updateLoksyuFromRoll(activeAspect, faces) {
|
||||
@@ -73,27 +66,22 @@ export async function updateLoksyuFromRoll(activeAspect, faces) {
|
||||
const yangCount = faces[yangFace] ?? 0
|
||||
if (yinCount === 0 && yangCount === 0) return
|
||||
|
||||
const actor = await getSingletonActor(ACTOR_TYPES.loksyu)
|
||||
if (!actor) return
|
||||
|
||||
const current = actor.system[lokAspect] ?? { yin: { value: 0 }, yang: { value: 0 } }
|
||||
await actor.update({
|
||||
[`system.${lokAspect}.yin.value`]: (current.yin.value ?? 0) + yinCount,
|
||||
[`system.${lokAspect}.yang.value`]: (current.yang.value ?? 0) + yangCount,
|
||||
})
|
||||
const data = getLoksyuData()
|
||||
const current = data[lokAspect] ?? { yin: 0, yang: 0 }
|
||||
data[lokAspect] = {
|
||||
yin: (current.yin ?? 0) + yinCount,
|
||||
yang: (current.yang ?? 0) + yangCount,
|
||||
}
|
||||
await setLoksyuData(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* After a WuXing roll, add tinji faces to the singleton TinJi actor.
|
||||
* After a WuXing roll, add tinji faces to the TinJi settings.
|
||||
*
|
||||
* @param {number} count - Number of tinji faces rolled
|
||||
*/
|
||||
export async function updateTinjiFromRoll(count) {
|
||||
if (!count || count <= 0) return
|
||||
|
||||
const actor = await getSingletonActor(ACTOR_TYPES.tinji)
|
||||
if (!actor) return
|
||||
|
||||
const current = actor.system.value ?? 0
|
||||
await actor.update({ "system.value": current + count })
|
||||
const current = getTinjiValue()
|
||||
await setTinjiValue(current + count)
|
||||
}
|
||||
|
||||
+16
-34
@@ -1,5 +1,4 @@
|
||||
import { getSingletonActor } from "./singletons.js"
|
||||
import { ACTOR_TYPES } from "../../config/constants.js"
|
||||
import { getTinjiValue, setTinjiValue } from "./singletons.js"
|
||||
|
||||
const SYSTEM_ID = "fvtt-chroniques-de-l-etrange"
|
||||
|
||||
@@ -30,13 +29,11 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {Actor|null} */
|
||||
#actor = null
|
||||
/** @type {Function|null} */
|
||||
#updateHook = null
|
||||
_updateHook = null
|
||||
|
||||
static open() {
|
||||
const existing = Object.values(foundry.applications.instances ?? {}).find(
|
||||
const existing = Array.from(foundry.applications.instances.values()).find(
|
||||
(app) => app instanceof CDETinjiApp
|
||||
)
|
||||
if (existing) { existing.bringToFront(); return existing }
|
||||
@@ -46,29 +43,22 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
this.#actor = await getSingletonActor(ACTOR_TYPES.tinji)
|
||||
if (!this.#actor) return { hasActor: false, value: 0 }
|
||||
|
||||
return {
|
||||
hasActor: true,
|
||||
canEdit: this.#actor.isOwner,
|
||||
value: this.#actor.system.value ?? 0,
|
||||
canEdit: game.user.isGM,
|
||||
value: getTinjiValue(),
|
||||
}
|
||||
}
|
||||
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options)
|
||||
this.#bindDirectInput()
|
||||
|
||||
this.#updateHook = Hooks.on("updateActor", (actor) => {
|
||||
if (actor.id === this.#actor?.id) this.render()
|
||||
})
|
||||
this._updateHook = Hooks.on("cde:tinjiUpdated", () => this.render())
|
||||
}
|
||||
|
||||
_onClose(options) {
|
||||
if (this.#updateHook !== null) {
|
||||
Hooks.off("updateActor", this.#updateHook)
|
||||
this.#updateHook = null
|
||||
if (this._updateHook !== null) {
|
||||
Hooks.off("cde:tinjiUpdated", this._updateHook)
|
||||
this._updateHook = null
|
||||
}
|
||||
super._onClose(options)
|
||||
}
|
||||
@@ -78,39 +68,31 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
|
||||
if (!input) return
|
||||
input.addEventListener("change", async (ev) => {
|
||||
const val = parseInt(ev.currentTarget.value, 10)
|
||||
if (!isNaN(val) && this.#actor) {
|
||||
await this.#actor.update({ "system.value": Math.max(0, val) })
|
||||
}
|
||||
if (!isNaN(val)) await setTinjiValue(val)
|
||||
})
|
||||
}
|
||||
|
||||
static async #onIncrement() {
|
||||
if (!this.#actor) return
|
||||
const current = this.#actor.system.value ?? 0
|
||||
await this.#actor.update({ "system.value": current + 1 })
|
||||
await setTinjiValue(getTinjiValue() + 1)
|
||||
}
|
||||
|
||||
static async #onDecrement() {
|
||||
if (!this.#actor) return
|
||||
const current = this.#actor.system.value ?? 0
|
||||
const current = getTinjiValue()
|
||||
if (current <= 0) return
|
||||
await this.#actor.update({ "system.value": current - 1 })
|
||||
await setTinjiValue(current - 1)
|
||||
}
|
||||
|
||||
static async #onReset() {
|
||||
if (!this.#actor) return
|
||||
await this.#actor.update({ "system.value": 0 })
|
||||
await setTinjiValue(0)
|
||||
}
|
||||
|
||||
/** Spend 1 Tin Ji die and announce it in chat */
|
||||
static async #onSpend() {
|
||||
if (!this.#actor) return
|
||||
const current = this.#actor.system.value ?? 0
|
||||
const current = getTinjiValue()
|
||||
if (current <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty"))
|
||||
return
|
||||
}
|
||||
await this.#actor.update({ "system.value": current - 1 })
|
||||
await setTinjiValue(current - 1)
|
||||
ChatMessage.create({
|
||||
user: game.user.id,
|
||||
content: `<div class="cde-tinji-spend-msg">
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* Post-roll interactive action buttons injected into dice result chat messages.
|
||||
* Allows players to pull dice from the Loksyu (as Successes or dés-fastes)
|
||||
* and allows the GM to spend Tin Ji to intervene.
|
||||
*
|
||||
* After a draw, the originating roll result message is updated in-place
|
||||
* with the new counts, without creating noise.
|
||||
*/
|
||||
|
||||
import { getLoksyuData, setLoksyuData, getTinjiValue, setTinjiValue } from "./apps/singletons.js"
|
||||
|
||||
const SYSTEM_ID = "fvtt-chroniques-de-l-etrange"
|
||||
const RESULT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html"
|
||||
|
||||
const WU_XING_CYCLE = {
|
||||
wood: ["wood", "fire", "water", "earth", "metal"],
|
||||
fire: ["fire", "earth", "wood", "metal", "water"],
|
||||
earth: ["earth", "metal", "fire", "water", "wood"],
|
||||
metal: ["metal", "water", "earth", "wood", "fire"],
|
||||
water: ["water", "wood", "metal", "fire", "earth"],
|
||||
}
|
||||
|
||||
const ASPECT_LABELS = {
|
||||
metal: "CDE.Metal",
|
||||
water: "CDE.Water",
|
||||
earth: "CDE.Earth",
|
||||
fire: "CDE.Fire",
|
||||
wood: "CDE.Wood",
|
||||
}
|
||||
|
||||
const ASPECT_ICONS = {
|
||||
metal: "systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp",
|
||||
water: "systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp",
|
||||
earth: "systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp",
|
||||
fire: "systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp",
|
||||
wood: "systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp",
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject or refresh post-roll action buttons in the given chat message HTML element.
|
||||
* Called from renderChatMessageHTML hook.
|
||||
* @param {ChatMessage} message
|
||||
* @param {HTMLElement} html - the chat message HTML element (li.chat-message)
|
||||
*/
|
||||
export function injectRollActions(message, html) {
|
||||
const rollCard = html.querySelector(".cde-roll-result")
|
||||
if (!rollCard) return
|
||||
const aspect = rollCard.dataset.aspect
|
||||
if (!aspect || !WU_XING_CYCLE[aspect]) return
|
||||
refreshRollActions(rollCard, aspect, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-render the action buttons section based on current Loksyu / TinJi state.
|
||||
*/
|
||||
function refreshRollActions(rollCard, aspect, message) {
|
||||
rollCard.querySelector(".cde-roll-actions")?.remove()
|
||||
|
||||
const cycle = WU_XING_CYCLE[aspect]
|
||||
const fasteAspect = cycle[1]
|
||||
|
||||
const loksyu = getLoksyuData()
|
||||
const tinji = getTinjiValue()
|
||||
|
||||
const successAvail = (loksyu[aspect]?.yin ?? 0) + (loksyu[aspect]?.yang ?? 0)
|
||||
const fasteAvail = (loksyu[fasteAspect]?.yin ?? 0) + (loksyu[fasteAspect]?.yang ?? 0)
|
||||
const isGM = game.user.isGM
|
||||
|
||||
const hasSomething = successAvail > 0 || fasteAvail > 0 || (isGM && tinji > 0)
|
||||
if (!hasSomething) return
|
||||
|
||||
const aspLabel = game.i18n.localize(ASPECT_LABELS[aspect])
|
||||
const fasteLabel = game.i18n.localize(ASPECT_LABELS[fasteAspect])
|
||||
|
||||
let btns = ""
|
||||
|
||||
if (successAvail > 0) {
|
||||
btns += `<button class="cde-roll-action-btn cde-roll-action--success" data-action="loksyu-success">
|
||||
<img src="${ASPECT_ICONS[aspect]}" class="cde-roll-action-icon" alt="${aspLabel}"/>
|
||||
<span class="cde-roll-action-label">+1 ${game.i18n.localize("CDE.Successes")}</span>
|
||||
<span class="cde-roll-action-count">${successAvail}</span>
|
||||
</button>`
|
||||
}
|
||||
|
||||
if (fasteAvail > 0) {
|
||||
btns += `<button class="cde-roll-action-btn cde-roll-action--faste" data-action="loksyu-faste">
|
||||
<img src="${ASPECT_ICONS[fasteAspect]}" class="cde-roll-action-icon" alt="${fasteLabel}"/>
|
||||
<span class="cde-roll-action-label">+1 ${game.i18n.localize("CDE.AuspiciousDie")}</span>
|
||||
<span class="cde-roll-action-count">${fasteAvail}</span>
|
||||
</button>`
|
||||
}
|
||||
|
||||
if (isGM && tinji > 0) {
|
||||
btns += `<button class="cde-roll-action-btn cde-roll-action--tinji" data-action="tinji">
|
||||
<span class="cde-roll-action-tinji-char">天</span>
|
||||
<span class="cde-roll-action-label">${game.i18n.localize("CDE.TinJi2")}</span>
|
||||
<span class="cde-roll-action-count">${tinji}</span>
|
||||
</button>`
|
||||
}
|
||||
|
||||
const wrapper = document.createElement("div")
|
||||
wrapper.className = "cde-roll-actions"
|
||||
wrapper.innerHTML = `
|
||||
<div class="cde-roll-actions-title">
|
||||
<i class="fas fa-yin-yang"></i>
|
||||
${game.i18n.localize("CDE.PostRollActions")}
|
||||
</div>
|
||||
<div class="cde-roll-actions-btns">${btns}</div>
|
||||
`
|
||||
rollCard.appendChild(wrapper)
|
||||
|
||||
wrapper.addEventListener("click", async ev => {
|
||||
const btn = ev.target.closest("[data-action]")
|
||||
if (!btn || btn.disabled) return
|
||||
const action = btn.dataset.action
|
||||
if (action === "loksyu-success") {
|
||||
await _drawFromLoksyu(message, aspect, "success", aspLabel)
|
||||
} else if (action === "loksyu-faste") {
|
||||
await _drawFromLoksyu(message, fasteAspect, "faste", fasteLabel)
|
||||
} else if (action === "tinji") {
|
||||
await _spendTinjiPostRoll()
|
||||
}
|
||||
// Buttons will be re-injected automatically via renderChatMessageHTML
|
||||
// after message.update(). For tinji (no message update), refresh manually.
|
||||
if (action === "tinji") refreshRollActions(rollCard, aspect, message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull one die from a given Loksyu aspect slot, update Loksyu settings,
|
||||
* and update the originating roll-result message in-place.
|
||||
*
|
||||
* @param {ChatMessage} message - the roll result chat message to update
|
||||
* @param {string} aspect - which Loksyu aspect slot to draw from
|
||||
* @param {"success"|"faste"} type
|
||||
* @param {string} aspectLabel - localised aspect name for the notification
|
||||
*/
|
||||
async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
|
||||
const data = getLoksyuData()
|
||||
const entry = data[aspect] ?? { yin: 0, yang: 0 }
|
||||
const total = entry.yin + entry.yang
|
||||
if (total <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.LoksyuEmpty"))
|
||||
return
|
||||
}
|
||||
|
||||
// Remove 1 die (prefer yang first)
|
||||
if (entry.yang > 0) entry.yang--
|
||||
else entry.yin--
|
||||
data[aspect] = entry
|
||||
await setLoksyuData(data)
|
||||
|
||||
// Update the roll-result message in-place if it has stored flags
|
||||
const flags = message?.flags?.[SYSTEM_ID]
|
||||
if (flags?.rollResult && message.isOwner) {
|
||||
const updated = foundry.utils.deepClone(flags.rollResult)
|
||||
if (type === "success") {
|
||||
updated.successesdice = (updated.successesdice ?? 0) + 1
|
||||
updated.loksyuBonusSuc = (updated.loksyuBonusSuc ?? 0) + 1
|
||||
// Recalculate weapon damage if applicable
|
||||
if (updated.damageBase) updated.totalDamage = updated.successesdice * updated.damageBase
|
||||
} else {
|
||||
updated.auspiciousdice = (updated.auspiciousdice ?? 0) + 1
|
||||
updated.loksyuBonusFaste = (updated.loksyuBonusFaste ?? 0) + 1
|
||||
}
|
||||
const newHtml = await foundry.applications.handlebars.renderTemplate(RESULT_TEMPLATE, updated)
|
||||
await message.update({
|
||||
content: newHtml,
|
||||
[`flags.${SYSTEM_ID}.rollResult`]: updated,
|
||||
})
|
||||
// renderChatMessageHTML hook fires automatically → buttons re-injected
|
||||
}
|
||||
|
||||
const remain = entry.yin + entry.yang
|
||||
const typeLabel = type === "success"
|
||||
? game.i18n.localize("CDE.Successes")
|
||||
: game.i18n.localize("CDE.AuspiciousDie")
|
||||
|
||||
ChatMessage.create({
|
||||
user: game.user.id,
|
||||
content: `<div class="cde-loksyu-draw-msg">
|
||||
<div class="cde-loksyu-draw-header">
|
||||
<img src="${ASPECT_ICONS[aspect]}" class="cde-loksyu-draw-aspect-icon" alt="${aspectLabel}"/>
|
||||
<span class="cde-loksyu-draw-user">${game.user.name}</span>
|
||||
<span class="cde-loksyu-draw-action">${game.i18n.localize("CDE.LoksyuDrawsA")}</span>
|
||||
<span class="cde-loksyu-draw-type">${typeLabel}</span>
|
||||
<span class="cde-loksyu-draw-from">${game.i18n.localize("CDE.LoksyuFromAspect")} <em>${aspectLabel}</em></span>
|
||||
</div>
|
||||
<div class="cde-loksyu-draw-footer">
|
||||
<i class="fas fa-yin-yang"></i>
|
||||
<span>${game.i18n.localize("CDE.Loksyu")} ${aspectLabel} : </span>
|
||||
<strong class="cde-loksyu-remain">${remain} ${game.i18n.localize("CDE.LoksyuRemaining")}</strong>
|
||||
</div>
|
||||
</div>`,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Spend 1 Tin Ji point (GM only) and post a notification.
|
||||
*/
|
||||
async function _spendTinjiPostRoll() {
|
||||
if (!game.user.isGM) return
|
||||
const current = getTinjiValue()
|
||||
if (current <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty"))
|
||||
return
|
||||
}
|
||||
await setTinjiValue(current - 1)
|
||||
ChatMessage.create({
|
||||
user: game.user.id,
|
||||
content: `<div class="cde-tinji-spend-msg">
|
||||
<span class="cde-tinji-icon">天</span>
|
||||
<span class="cde-tinji-text">
|
||||
<strong>${game.user.name}</strong> ${game.i18n.localize("CDE.TinjiSpent").replace("{name}", game.user.name)}
|
||||
</span>
|
||||
<span class="cde-tinji-remain">(${current - 1} ${game.i18n.localize("CDE.TinjiRemaining")})</span>
|
||||
</div>`,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all visible roll-result buttons when Loksyu or TinJi settings change.
|
||||
* Wired up via Hooks.on("updateSetting", ...) in system.js.
|
||||
*/
|
||||
export function refreshAllRollActions() {
|
||||
document.querySelectorAll(".chat-message .cde-roll-result[data-aspect]").forEach(card => {
|
||||
const aspect = card.dataset.aspect
|
||||
if (!aspect || !WU_XING_CYCLE[aspect]) return
|
||||
// Find the ChatMessage document from the ancestor element's data-message-id
|
||||
const msgEl = card.closest("[data-message-id]")
|
||||
const msgId = msgEl?.dataset?.messageId
|
||||
const message = msgId ? game.messages.get(msgId) : null
|
||||
refreshRollActions(card, aspect, message)
|
||||
})
|
||||
}
|
||||
|
||||
+23
-20
@@ -250,6 +250,9 @@ async function sendResultMessage(actor, resultData, roll, rollMode) {
|
||||
content: html,
|
||||
rolls: [roll],
|
||||
rollMode,
|
||||
flags: {
|
||||
"fvtt-chroniques-de-l-etrange": { rollResult: { ...resultData } },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -297,43 +300,43 @@ export async function rollForActor(actor, rollKey) {
|
||||
title = game.i18n.localize(`CDE.${skillLibel.charAt(0).toUpperCase() + skillLibel.slice(1)}`)
|
||||
break
|
||||
case "skill":
|
||||
numberofdice = sys.skills[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.skills[skillLibel]?.label ?? "CDE.Roll")
|
||||
numberofdice = sys.skills?.[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.skills?.[skillLibel]?.label ?? "CDE.Roll")
|
||||
break
|
||||
case "special":
|
||||
numberofdice = sys.skills[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.skills[skillLibel]?.label ?? "CDE.Roll")
|
||||
numberofdice = sys.skills?.[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.skills?.[skillLibel]?.label ?? "CDE.Roll")
|
||||
title += ` [${game.i18n.localize("CDE.Speciality")}]`
|
||||
isSpecial = true
|
||||
if (!sys.skills[skillLibel]?.specialities) {
|
||||
if (!sys.skills?.[skillLibel]?.specialities) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error2"))
|
||||
return
|
||||
}
|
||||
break
|
||||
case "resource":
|
||||
numberofdice = sys.resources[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.resources[skillLibel]?.label ?? "CDE.Roll")
|
||||
numberofdice = sys.resources?.[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.resources?.[skillLibel]?.label ?? "CDE.Roll")
|
||||
break
|
||||
case "field":
|
||||
numberofdice = sys.resources[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.resources[skillLibel]?.label ?? "CDE.Roll")
|
||||
numberofdice = sys.resources?.[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.resources?.[skillLibel]?.label ?? "CDE.Roll")
|
||||
title += ` [${game.i18n.localize("CDE.Field")}]`
|
||||
isSpecial = true
|
||||
if (!sys.resources[skillLibel]?.specialities) {
|
||||
if (!sys.resources?.[skillLibel]?.specialities) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error4"))
|
||||
return
|
||||
}
|
||||
break
|
||||
case "magic":
|
||||
numberofdice = sys.magics[skillLibel]?.value ?? 0
|
||||
numberofdice = sys.magics?.[skillLibel]?.value ?? 0
|
||||
isMagic = true
|
||||
title = game.i18n.localize(MAGIC_I18N_KEYS[skillLibel] ?? "CDE.Magics")
|
||||
break
|
||||
case "magicspecial":
|
||||
numberofdice = sys.magics[skillLibel]?.value ?? 0
|
||||
numberofdice = sys.magics?.[skillLibel]?.value ?? 0
|
||||
isMagicSpecial = true
|
||||
isMagic = true
|
||||
if (!sys.magics[skillLibel]?.speciality?.[specialLibel]?.check) {
|
||||
if (!sys.magics?.[skillLibel]?.speciality?.[specialLibel]?.check) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error6"))
|
||||
return
|
||||
}
|
||||
@@ -344,8 +347,8 @@ export async function rollForActor(actor, rollKey) {
|
||||
const kfItem = actor.items.get(skillLibel)
|
||||
if (!kfItem) { ui.notifications.warn(game.i18n.localize("CDE.Error0")); return }
|
||||
const kfSkill = kfItem.system.skill ?? "kungfu"
|
||||
numberofdice = sys.skills[kfSkill]?.value ?? 0
|
||||
title = `${kfItem.name} [${game.i18n.localize(sys.skills[kfSkill]?.label ?? "CDE.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")
|
||||
if (kfDefaultAspect < 0) kfDefaultAspect = 0
|
||||
break
|
||||
@@ -357,7 +360,7 @@ export async function rollForActor(actor, rollKey) {
|
||||
|
||||
const wpType = wpItem.system.weaponType ?? "melee"
|
||||
const wpSkill = WEAPON_TYPE_SKILL[wpType] ?? "kungfu"
|
||||
numberofdice = sys.skills[wpSkill]?.value ?? 0
|
||||
numberofdice = sys.skills?.[wpSkill]?.value ?? 0
|
||||
|
||||
const wpAspectRaw = wpItem.system.damageAspect ?? "metal"
|
||||
const wpAspectIdx = WEAPON_ASPECT_INDEX[wpAspectRaw] ?? 0
|
||||
@@ -372,7 +375,7 @@ export async function rollForActor(actor, rollKey) {
|
||||
|
||||
// Show weapon-specific prompt
|
||||
const wParams = await showWeaponPrompt({
|
||||
title: `${wpItem.name} [${game.i18n.localize(sys.skills[wpSkill]?.label ?? "CDE.WeaponRoll")}]`,
|
||||
title: `${wpItem.name} [${game.i18n.localize(sys.skills?.[wpSkill]?.label ?? "CDE.WeaponRoll")}]`,
|
||||
numberofdice,
|
||||
weaponName: wpItem.name,
|
||||
weaponTypeLabel: WEAPON_TYPE_LABELS[wpType] ?? "CDE.Weapon",
|
||||
@@ -392,7 +395,7 @@ export async function rollForActor(actor, rollKey) {
|
||||
|
||||
// Resolve final pool from weapon prompt values
|
||||
const wpChosenSkill = wParams.weaponskill ?? wpSkill
|
||||
const wpSkillDice = sys.skills[wpChosenSkill]?.value ?? 0
|
||||
const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0
|
||||
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx)
|
||||
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0
|
||||
const wpRangeMalus = RANGE_MALUS[wParams.effectiverange ?? "contact"] ?? 0
|
||||
@@ -537,7 +540,7 @@ export async function rollForActor(actor, rollKey) {
|
||||
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 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
|
||||
@@ -549,7 +552,7 @@ export async function rollForActor(actor, rollKey) {
|
||||
throwMode = Number(params.typeofthrow ?? 0)
|
||||
|
||||
const aspectDice = (typeLibel !== "aspect")
|
||||
? (sys.aspect[ASPECT_NAMES[aspectIndex]]?.value ?? 0)
|
||||
? (sys.aspect?.[ASPECT_NAMES[aspectIndex]]?.value ?? 0)
|
||||
: 0
|
||||
|
||||
numberofdice = numberofdice + aspectDice + bonusMalus - woundMalus
|
||||
|
||||
@@ -22,7 +22,7 @@ export class CDEBaseActorSheet extends HandlebarsApplicationMixin(foundry.applic
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const descriptionHTML = await TextEditor.enrichHTML(this.document.system.description ?? "", { async: true })
|
||||
const descriptionHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description ?? "", { async: true })
|
||||
const cssClass = this.options.classes?.join(" ") ?? ""
|
||||
return {
|
||||
actor: this.document,
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export { CDECharacterSheet } from "./character.js"
|
||||
export { CDENpcSheet } from "./npc.js"
|
||||
export { CDETinjiSheet } from "./tinji.js"
|
||||
export { CDELoksyuSheet } from "./loksyu.js"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { CDEBaseActorSheet } from "./base.js"
|
||||
import { CDELoksyuApp } from "../../apps/loksyu-app.js"
|
||||
|
||||
export class CDELoksyuSheet extends CDEBaseActorSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["loksyu"],
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-loksyu-sheet.html" },
|
||||
}
|
||||
|
||||
tabGroups = { primary: "loksyu" }
|
||||
|
||||
/** Redirect any direct actor-sheet open to the standalone app instead */
|
||||
async _onFirstRender(context, options) {
|
||||
// Close this actor sheet immediately and open the standalone app
|
||||
await this.close({ animate: false })
|
||||
CDELoksyuApp.open()
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { CDEBaseActorSheet } from "./base.js"
|
||||
import { CDETinjiApp } from "../../apps/tinji-app.js"
|
||||
|
||||
export class CDETinjiSheet extends CDEBaseActorSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["tinji"],
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-tinji-sheet.html" },
|
||||
}
|
||||
|
||||
tabGroups = { primary: "tinji" }
|
||||
|
||||
/** Redirect any direct actor-sheet open to the standalone app instead */
|
||||
async _onFirstRender(context, options) {
|
||||
await this.close({ animate: false })
|
||||
CDETinjiApp.open()
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ export class CDEBaseItemSheet extends HandlebarsApplicationMixin(foundry.applica
|
||||
|
||||
async _prepareContext() {
|
||||
const cssClass = this.options.classes?.join(" ") ?? ""
|
||||
const enrichedDescription = await TextEditor.enrichHTML(this.document.system.description ?? "", { async: true })
|
||||
const enrichedNotes = await TextEditor.enrichHTML(this.document.system.notes ?? "", { async: true })
|
||||
const enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description ?? "", { async: true })
|
||||
const enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.notes ?? "", { async: true })
|
||||
return {
|
||||
item: this.document,
|
||||
system: this.document.system,
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { TEMPLATE_PARTIALS } from "../config/constants.js"
|
||||
|
||||
export async function preloadPartials() {
|
||||
return loadTemplates(TEMPLATE_PARTIALS)
|
||||
return foundry.applications.handlebars.loadTemplates(TEMPLATE_PARTIALS)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user