Use socket to manage loksyu
This commit is contained in:
@@ -43,6 +43,9 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
/** @type {Function|null} bound hook handler */
|
||||
_updateHook = null
|
||||
|
||||
/** @type {Function|null} updateSetting hook handler (for socket-propagated writes) */
|
||||
_settingHook = null
|
||||
|
||||
/** Singleton accessor — open or bring to front */
|
||||
static open() {
|
||||
const existing = Array.from(foundry.applications.instances.values()).find(
|
||||
@@ -78,7 +81,14 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
super._onRender(context, options)
|
||||
this.#bindInputs()
|
||||
|
||||
this._updateHook = Hooks.on("cde:loksyuUpdated", () => this.render())
|
||||
if (!this._updateHook) {
|
||||
this._updateHook = Hooks.on("cde:loksyuUpdated", () => this.render())
|
||||
}
|
||||
if (!this._settingHook) {
|
||||
this._settingHook = Hooks.on("updateSetting", (setting) => {
|
||||
if (setting.key === `${SYSTEM_ID}.loksyuData`) this.render()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_onClose(options) {
|
||||
@@ -86,6 +96,10 @@ export class CDELoksyuApp extends foundry.applications.api.HandlebarsApplication
|
||||
Hooks.off("cde:loksyuUpdated", this._updateHook)
|
||||
this._updateHook = null
|
||||
}
|
||||
if (this._settingHook !== null) {
|
||||
Hooks.off("updateSetting", this._settingHook)
|
||||
this._settingHook = null
|
||||
}
|
||||
super._onClose(options)
|
||||
}
|
||||
|
||||
|
||||
+110
-13
@@ -15,10 +15,14 @@
|
||||
* Loksyu / TinJi settings-based helpers.
|
||||
*
|
||||
* Data is stored as world settings instead of singleton Actor documents.
|
||||
* Socket-based replication allows non-GM players to update Loksyu and
|
||||
* TinJi — the GM processes the actual writes.
|
||||
*/
|
||||
|
||||
import { SYSTEM_ID, WU_XING_CYCLE, ASPECT_FACES } from "../../config/constants.js"
|
||||
|
||||
const SOCKET_CHANNEL = `system.${SYSTEM_ID}`
|
||||
|
||||
/** Read the current loksyu data object from world settings */
|
||||
export function getLoksyuData() {
|
||||
return game.settings.get(SYSTEM_ID, "loksyuData") ?? {
|
||||
@@ -26,27 +30,112 @@ export function getLoksyuData() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Write the loksyu data object to world settings */
|
||||
export async function setLoksyuData(data) {
|
||||
/** Write the loksyu data object to world settings (GM only). */
|
||||
async function writeLoksyuData(data) {
|
||||
await game.settings.set(SYSTEM_ID, "loksyuData", data)
|
||||
Hooks.callAll("cde:loksyuUpdated", data)
|
||||
}
|
||||
|
||||
/** Write TinJi value to world settings (GM only). */
|
||||
async function writeTinjiValue(value) {
|
||||
value = Math.max(0, value)
|
||||
await game.settings.set(SYSTEM_ID, "tinjiData", value)
|
||||
Hooks.callAll("cde:tinjiUpdated", value)
|
||||
}
|
||||
|
||||
/** Write the loksyu data object — non-GM emits via socket. */
|
||||
export async function setLoksyuData(data) {
|
||||
if (game.user.isGM) return writeLoksyuData(data)
|
||||
game.socket.emit(SOCKET_CHANNEL, { action: "setLoksyuData", 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 */
|
||||
/** Write TinJi value — non-GM emits via socket. */
|
||||
export async function setTinjiValue(value) {
|
||||
await game.settings.set(SYSTEM_ID, "tinjiData", Math.max(0, value))
|
||||
Hooks.callAll("cde:tinjiUpdated", Math.max(0, value))
|
||||
if (game.user.isGM) return writeTinjiValue(value)
|
||||
game.socket.emit(SOCKET_CHANNEL, { action: "setTinjiValue", value })
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-GM: request an atomic Loksyu die draw via socket. The GM reads
|
||||
* current state, decrements, and writes — avoiding stale-read races.
|
||||
*/
|
||||
export function requestLoksyuDraw(aspect, order) {
|
||||
game.socket.emit(SOCKET_CHANNEL, { action: "loksyuDraw", aspect, order })
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-GM: request an atomic TinJi spend via socket. The GM reads current
|
||||
* value, decrements if > 0, and writes.
|
||||
*/
|
||||
export function requestTinjiSpend() {
|
||||
game.socket.emit(SOCKET_CHANNEL, { action: "tinjiSpend" })
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the socket listener that processes Loksyu/TinJi write requests
|
||||
* from non-GM clients. Only the GM actually performs the writes; other
|
||||
* clients ignore the message.
|
||||
*
|
||||
* Must be called after the `ready` hook (when game.socket is available).
|
||||
*/
|
||||
export function registerSingletonSocket() {
|
||||
game.socket.on(SOCKET_CHANNEL, async (payload) => {
|
||||
if (!game.user.isGM) return
|
||||
switch (payload.action) {
|
||||
case "setLoksyuData":
|
||||
await writeLoksyuData(payload.data)
|
||||
break
|
||||
case "setTinjiValue":
|
||||
await writeTinjiValue(payload.value)
|
||||
break
|
||||
case "updateLoksyuFromRoll":
|
||||
await updateLoksyuFromRoll(payload.activeAspect, payload.faces)
|
||||
break
|
||||
case "updateTinjiFromRoll":
|
||||
await updateTinjiFromRoll(payload.delta)
|
||||
break
|
||||
case "loksyuDraw": {
|
||||
const data = getLoksyuData()
|
||||
const entry = data[payload.aspect] ?? { yin: 0, yang: 0 }
|
||||
const order = payload.order ?? "yang-first"
|
||||
if (order === "yin-first") {
|
||||
if (entry.yin > 0) entry.yin--
|
||||
else entry.yang--
|
||||
} else if (order === "balanced") {
|
||||
if (entry.yin > entry.yang) entry.yin--
|
||||
else if (entry.yang > entry.yin) entry.yang--
|
||||
else if (entry.yang > 0) entry.yang--
|
||||
else entry.yin--
|
||||
} else {
|
||||
if (entry.yang > 0) entry.yang--
|
||||
else entry.yin--
|
||||
}
|
||||
data[payload.aspect] = entry
|
||||
await writeLoksyuData(data)
|
||||
break
|
||||
}
|
||||
case "tinjiSpend": {
|
||||
const cur = getTinjiValue()
|
||||
if (cur > 0) await writeTinjiValue(cur - 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* After a WuXing roll, add the loksyu faces (yin + yang) of the relevant
|
||||
* aspect to the loksyu settings data.
|
||||
*
|
||||
* Non-GM: emits raw activeAspect+faces via socket so the GM recomputes
|
||||
* from current state — avoids stale-read races when two players roll
|
||||
* simultaneously.
|
||||
*
|
||||
* @param {string} activeAspect - e.g. "fire"
|
||||
* @param {Object} faces - Die face counts { 0: n, 1: n, …, 9: n }
|
||||
*/
|
||||
@@ -62,22 +151,30 @@ export async function updateLoksyuFromRoll(activeAspect, faces) {
|
||||
const yangCount = faces[yangFace] ?? 0
|
||||
if (yinCount === 0 && yangCount === 0) return
|
||||
|
||||
const data = getLoksyuData()
|
||||
const current = data[lokAspect] ?? { yin: 0, yang: 0 }
|
||||
data[lokAspect] = {
|
||||
yin: (current.yin ?? 0) + yinCount,
|
||||
yang: (current.yang ?? 0) + yangCount,
|
||||
if (game.user.isGM) {
|
||||
const data = getLoksyuData()
|
||||
const current = data[lokAspect] ?? { yin: 0, yang: 0 }
|
||||
data[lokAspect] = { yin: (current.yin ?? 0) + yinCount, yang: (current.yang ?? 0) + yangCount }
|
||||
await writeLoksyuData(data)
|
||||
} else {
|
||||
game.socket.emit(SOCKET_CHANNEL, { action: "updateLoksyuFromRoll", activeAspect, faces })
|
||||
}
|
||||
await setLoksyuData(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* After a WuXing roll, add tinji faces to the TinJi settings.
|
||||
*
|
||||
* Non-GM: emits delta via socket so the GM adds to the current value
|
||||
* atomically.
|
||||
*
|
||||
* @param {number} count - Number of tinji faces rolled
|
||||
*/
|
||||
export async function updateTinjiFromRoll(count) {
|
||||
if (!count || count <= 0) return
|
||||
const current = getTinjiValue()
|
||||
await setTinjiValue(current + count)
|
||||
if (game.user.isGM) {
|
||||
const current = getTinjiValue()
|
||||
await writeTinjiValue(current + count)
|
||||
} else {
|
||||
game.socket.emit(SOCKET_CHANNEL, { action: "updateTinjiFromRoll", delta: count })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
|
||||
/** @type {Function|null} */
|
||||
_updateHook = null
|
||||
|
||||
/** @type {Function|null} */
|
||||
_settingHook = null
|
||||
|
||||
static open() {
|
||||
const existing = Array.from(foundry.applications.instances.values()).find(
|
||||
(app) => app instanceof CDETinjiApp
|
||||
@@ -64,7 +67,14 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options)
|
||||
this.#bindDirectInput()
|
||||
this._updateHook = Hooks.on("cde:tinjiUpdated", () => this.render())
|
||||
if (!this._updateHook) {
|
||||
this._updateHook = Hooks.on("cde:tinjiUpdated", () => this.render())
|
||||
}
|
||||
if (!this._settingHook) {
|
||||
this._settingHook = Hooks.on("updateSetting", (setting) => {
|
||||
if (setting.key === `${SYSTEM_ID}.tinjiData`) this.render()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_onClose(options) {
|
||||
@@ -72,6 +82,10 @@ export class CDETinjiApp extends foundry.applications.api.HandlebarsApplicationM
|
||||
Hooks.off("cde:tinjiUpdated", this._updateHook)
|
||||
this._updateHook = null
|
||||
}
|
||||
if (this._settingHook !== null) {
|
||||
Hooks.off("updateSetting", this._settingHook)
|
||||
this._settingHook = null
|
||||
}
|
||||
super._onClose(options)
|
||||
}
|
||||
|
||||
|
||||
+13
-7
@@ -20,7 +20,7 @@
|
||||
* with the new counts, without creating noise.
|
||||
*/
|
||||
|
||||
import { getLoksyuData, setLoksyuData, getTinjiValue, setTinjiValue } from "./apps/singletons.js"
|
||||
import { getLoksyuData, setLoksyuData, getTinjiValue, setTinjiValue, requestLoksyuDraw, requestTinjiSpend } from "./apps/singletons.js"
|
||||
import { SYSTEM_ID, WU_XING_CYCLE, ASPECT_LABELS, ASPECT_ICONS } from "../config/constants.js"
|
||||
|
||||
const RESULT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html"
|
||||
@@ -53,9 +53,8 @@ function refreshRollActions(rollCard, aspect, message) {
|
||||
|
||||
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)
|
||||
const hasSomething = successAvail > 0 || fasteAvail > 0 || tinji > 0
|
||||
if (!hasSomething) return
|
||||
|
||||
const aspLabel = game.i18n.localize(ASPECT_LABELS[aspect])
|
||||
@@ -79,7 +78,7 @@ function refreshRollActions(rollCard, aspect, message) {
|
||||
</button>`
|
||||
}
|
||||
|
||||
if (isGM && tinji > 0) {
|
||||
if (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>
|
||||
@@ -149,7 +148,11 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
|
||||
else entry.yin--
|
||||
}
|
||||
data[aspect] = entry
|
||||
await setLoksyuData(data)
|
||||
if (game.user.isGM) {
|
||||
await setLoksyuData(data)
|
||||
} else {
|
||||
requestLoksyuDraw(aspect, order)
|
||||
}
|
||||
|
||||
// Update the roll-result message in-place if it has stored flags
|
||||
const flags = message?.flags?.[SYSTEM_ID]
|
||||
@@ -200,13 +203,16 @@ async function _drawFromLoksyu(message, aspect, type, aspectLabel) {
|
||||
* 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)
|
||||
if (game.user.isGM) {
|
||||
await setTinjiValue(current - 1)
|
||||
} else {
|
||||
requestTinjiSpend()
|
||||
}
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
content: `<div class="cde-tinji-spend-msg">
|
||||
|
||||
Reference in New Issue
Block a user