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:
99
src/ui/apps/singletons.js
Normal file
99
src/ui/apps/singletons.js
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user