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

99
src/ui/apps/singletons.js Normal file
View File

@@ -0,0 +1,99 @@
/**
* Singleton actor utilities for Loksyu and Tin Ji.
*
* 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.
*/
import { ACTOR_TYPES } from "../../config/constants.js"
/** Wu Xing generating cycle — [successes, auspicious, noxious, loksyu, tinji] */
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"],
}
/** Die face pairs [yin, yang] per aspect (0 = face "10") */
const ASPECT_FACES = {
metal: [3, 8],
water: [1, 6],
earth: [0, 5],
fire: [2, 7],
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
}
// 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
}
/**
* After a WuXing roll, add the loksyu faces (yin + yang) of the relevant
* aspect to the singleton Loksyu actor.
*
* @param {string} activeAspect - The aspect used for the roll (e.g. "fire")
* @param {Object} faces - Die face counts { 0: n, 1: n, …, 9: n }
*/
export async function updateLoksyuFromRoll(activeAspect, faces) {
const cycle = WU_XING_CYCLE[activeAspect]
if (!cycle) return
const lokAspect = cycle[3]
const [yinFace, yangFace] = ASPECT_FACES[lokAspect] ?? []
if (yinFace === undefined) return
const yinCount = faces[yinFace] ?? 0
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,
})
}
/**
* After a WuXing roll, add tinji faces to the singleton TinJi actor.
*
* @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 })
}