feat: Loksyu & TinJi standalone AppV2 apps with chat buttons and dice automation

- CDELoksyuApp: standalone HandlebarsApplicationMixin(ApplicationV2) app
  - 5-element Wu Xing grid with yin/yang inputs per element
  - Per-element reset buttons + global reset-all
  - Auto-refresh via updateActor hook

- CDETinjiApp: standalone AppV2 for the collective Tin Ji dice pool
  - Large neon counter with +/- buttons and direct input
  - Spend button sends a chat message with remaining count

- singletons.js: shared utilities
  - getSingletonActor: find or auto-create singleton actor
  - updateLoksyuFromRoll: compute lokAspect from Wu Xing cycle, update yin/yang
  - updateTinjiFromRoll: add tinji face count to value

- rolling.js: auto-update both singletons after every dice roll
  (weapon path + main roll path)

- system.js: renderChatLog hook injects Loksyu/TinJi footer buttons
  in the chat sidebar

- loksyu.js / tinji.js: actor sheets redirect to standalone apps
  when opened via the sidebar

- CSS: .cde-loksyu-standalone, .cde-tinji-standalone, .cde-chat-app-buttons,
  .cde-tinji-spend-msg styles added

- i18n: new keys in fr-cde.json and en-cde.json for all new UI strings
  (LoksyuNotFound, TinjiNotFound, Reset, ResetAll, SpendTinji, etc.)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-30 09:51:39 +02:00
parent 6fda4b9246
commit 0689fae792
41 changed files with 1558 additions and 13 deletions
+124
View File
@@ -0,0 +1,124 @@
import { getSingletonActor } from "./singletons.js"
import { ACTOR_TYPES } from "../../config/constants.js"
const SYSTEM_ID = "fvtt-chroniques-de-l-etrange"
export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationMixin(
foundry.applications.api.ApplicationV2
) {
static DEFAULT_OPTIONS = {
id: "cde-tinji-app",
tag: "div",
window: {
title: "CDE.TinJi2",
icon: "fas fa-star",
resizable: false,
},
classes: ["cde-app", "cde-tinji-standalone"],
position: { width: 320, height: "auto" },
actions: {
increment: CDETinjiApp.#onIncrement,
decrement: CDETinjiApp.#onDecrement,
reset: CDETinjiApp.#onReset,
spend: CDETinjiApp.#onSpend,
},
}
static PARTS = {
main: {
template: `systems/${SYSTEM_ID}/templates/apps/cde-tinji-app.html`,
},
}
/** @type {Actor|null} */
#actor = null
/** @type {Function|null} */
#updateHook = null
static open() {
const existing = Object.values(foundry.applications.instances ?? {}).find(
(app) => app instanceof CDETinjiApp
)
if (existing) { existing.bringToFront(); return existing }
const app = new CDETinjiApp()
app.render(true)
return app
}
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,
}
}
_onRender(context, options) {
super._onRender(context, options)
this.#bindDirectInput()
this.#updateHook = Hooks.on("updateActor", (actor) => {
if (actor.id === this.#actor?.id) this.render()
})
}
_onClose(options) {
if (this.#updateHook !== null) {
Hooks.off("updateActor", this.#updateHook)
this.#updateHook = null
}
super._onClose(options)
}
#bindDirectInput() {
const input = this.element?.querySelector("input.cde-tinji-direct")
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) })
}
})
}
static async #onIncrement() {
if (!this.#actor) return
const current = this.#actor.system.value ?? 0
await this.#actor.update({ "system.value": current + 1 })
}
static async #onDecrement() {
if (!this.#actor) return
const current = this.#actor.system.value ?? 0
if (current <= 0) return
await this.#actor.update({ "system.value": current - 1 })
}
static async #onReset() {
if (!this.#actor) return
await this.#actor.update({ "system.value": 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
if (current <= 0) {
ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty"))
return
}
await this.#actor.update({ "system.value": current - 1 })
ChatMessage.create({
user: game.user.id,
content: `<div class="cde-tinji-spend-msg">
<i class="fas fa-star"></i>
<strong>${game.i18n.localize("CDE.TinJi2")}</strong>
${game.i18n.format("CDE.TinjiSpent", { name: game.user.name })}
<span class="cde-tinji-remain">(${current - 1} ${game.i18n.localize("CDE.TinjiRemaining")})</span>
</div>`,
})
}
}