From 5c95fec201147a62ce69de9d4141ec7e99c4cfb7 Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Sun, 5 Oct 2025 23:26:45 -0600 Subject: [PATCH 01/11] Add the socket enabling to the manifest --- system.json | 1 + 1 file changed, 1 insertion(+) diff --git a/system.json b/system.json index 75b3120..0c7b878 100644 --- a/system.json +++ b/system.json @@ -32,6 +32,7 @@ "download": "#{DOWNLOAD}#", "readme": "README.md", "bugs": "", + "socket": true, "flags": { "hotReload": { "extensions": ["css", "hbs", "json", "mjs", "svg"], From 92e844341da1077f6e71a17cb2c7336fbc6a6a59 Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Sun, 5 Oct 2025 23:46:39 -0600 Subject: [PATCH 02/11] Extract the Sands changing into a function of the public API for the HUD --- module/Apps/DelveDiceHUD.mjs | 38 ++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index dfa14a5..2be2f59 100644 --- a/module/Apps/DelveDiceHUD.mjs +++ b/module/Apps/DelveDiceHUD.mjs @@ -189,18 +189,7 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { /** @this {DelveDiceHUD} */ static async #tourDelta(_event, element) { const delta = parseInt(element.dataset.delta); - const initial = game.settings.get(`ripcrypt`, `sandsOfFateInitial`); - let newSands = this._sandsOfFate + delta; - - if (newSands > initial) { - Logger.info(`Cannot go to a previous Delve Tour above the initial value`); - return; - }; - - if (newSands === 0) { - newSands = initial; - await this.alertCrypticEvent(); - }; + await this.sandsOfFateDelta(delta); switch (Math.sign(delta)) { case -1: { @@ -212,9 +201,6 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { break; } }; - - this.#animateSandsTo(newSands); - game.settings.set(`ripcrypt`, `sandsOfFate`, newSands); }; /** @this {DelveDiceHUD} */ @@ -247,5 +233,27 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { game.togglePause(true, { broadcast: true }); }; }; + + /** + * Changes the current Sands of Fate by an amount provided, animating the + * @param {number} delta The amount of change + */ + async sandsOfFateDelta(delta) { + const initial = game.settings.get(`ripcrypt`, `sandsOfFateInitial`); + let newSands = this._sandsOfFate + delta; + + if (newSands > initial) { + Logger.info(`Cannot increase the Sands of Fate to a value about the initial`); + return; + }; + + if (newSands === 0) { + newSands = initial; + await this.alertCrypticEvent(); + }; + + this.#animateSandsTo(newSands); + game.settings.set(`ripcrypt`, `sandsOfFate`, newSands); + }; // #endregion }; From 7c0fb75e0f6c9bc84fb273ef089fe224451db3ff Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Sun, 5 Oct 2025 23:47:52 -0600 Subject: [PATCH 03/11] Get the base functions set up that are required for the roll shortcut --- module/Apps/DelveDiceHUD.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index 2be2f59..a486e4b 100644 --- a/module/Apps/DelveDiceHUD.mjs +++ b/module/Apps/DelveDiceHUD.mjs @@ -6,6 +6,7 @@ import { Logger } from "../utils/Logger.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { ContextMenu } = foundry.applications.ux; +const { Roll } = foundry.dice; const { FatePath } = gameTerms; const CompassRotations = { @@ -39,6 +40,7 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { actions: { tourDelta: this.#tourDelta, setFate: this.#setFate, + hasteRoll: this.#hasteRoll, }, }; @@ -215,6 +217,12 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { this._difficulty = value; game.settings.set(`ripcrypt`, `dc`, value); }; + + /** @this {DelveDiceHUD} */ + static async #hasteRoll() { + // TODO: if not GM, send socket request to GM + await this.rollForHaste(); + }; // #endregion // #region Public API @@ -255,5 +263,10 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { this.#animateSandsTo(newSands); game.settings.set(`ripcrypt`, `sandsOfFate`, newSands); }; + + static async rollForHaste() { + const roll = new Roll(`1d8xo=1`); + await roll.evaluate(); + }; // #endregion }; From c0a9731b02981360932102bc508555e7c6590c1a Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Mon, 6 Oct 2025 23:25:27 -0600 Subject: [PATCH 04/11] Add socket event handling foundations and an updateSands event in anticipation of hasty rolls --- langs/en-ca.json | 6 +++++- module/hooks/init.mjs | 2 ++ module/sockets/_index.mjs | 27 +++++++++++++++++++++++++ module/sockets/updateSands.mjs | 36 ++++++++++++++++++++++++++++++++++ module/utils/clamp.mjs | 3 +++ 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 module/sockets/_index.mjs create mode 100644 module/sockets/updateSands.mjs create mode 100644 module/utils/clamp.mjs diff --git a/langs/en-ca.json b/langs/en-ca.json index cad8c6a..963b2c2 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -193,7 +193,11 @@ "error": { "cannot-equip": "Cannot equip the {itemType}, see console for more details.", "invalid-delta": "The delta for \"{name}\" is not a number, cannot finish processing the action.", - "at-favourite-limit": "Cannot favourite more than three items, unfavourite one to make space." + "at-favourite-limit": "Cannot favourite more than three items, unfavourite one to make space.", + "invalid-socket": "Invalid socket data received, this means a module or system bug is present.", + "unknown-socket-event": "An unknown socket event was received: {event}", + "no-active-gm": "No active @USER.GM is logged in, you must wait for a @USER.GM to be active before you can do that.", + "malformed-socket-payload": "Socket event \"{event}\" received with malformed payload. Details: {details}" }, "warn": { "cannot-go-negative": "\"{name}\" is unable to be a negative number." diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 548e8e2..28c4fd9 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -34,6 +34,7 @@ import { Logger } from "../utils/Logger.mjs"; import { registerCustomComponents } from "../Apps/components/_index.mjs"; import { registerDevSettings } from "../settings/devSettings.mjs"; import { registerMetaSettings } from "../settings/metaSettings.mjs"; +import { registerSockets } from "../sockets/_index.mjs"; import { registerUserSettings } from "../settings/userSettings.mjs"; import { registerWorldSettings } from "../settings/worldSettings.mjs"; @@ -127,6 +128,7 @@ Hooks.once(`init`, () => { CONFIG.Actor.trackableAttributes.hero = HeroData.trackableAttributes; // #endregion + registerSockets(); registerCustomComponents(); Handlebars.registerHelper(helpers); }); diff --git a/module/sockets/_index.mjs b/module/sockets/_index.mjs new file mode 100644 index 0000000..d0b632d --- /dev/null +++ b/module/sockets/_index.mjs @@ -0,0 +1,27 @@ +import { localizer } from "../utils/Localizer.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { updateSands } from "./updateSands.mjs"; + +const events = { + updateSands, +}; + +export function registerSockets() { + Logger.info(`Setting up socket listener`); + + game.socket.on(`system.${game.system.id}`, (data, userID) => { + const { event, payload } = data ?? {}; + if (event == null || payload === undefined) { + ui.notifications.error(localizer(`RipCrypt.notifs.error.invalid-socket`)); + return; + }; + + if (events[event] == null) { + ui.notifications.error(localizer(`RipCrypt.notifs.error.unknown-socket-event`, { event })); + return; + }; + + const user = game.users.get(userID); + events[event](payload, user); + }); +}; diff --git a/module/sockets/updateSands.mjs b/module/sockets/updateSands.mjs new file mode 100644 index 0000000..f851ce1 --- /dev/null +++ b/module/sockets/updateSands.mjs @@ -0,0 +1,36 @@ +import { clamp } from "../utils/clamp.mjs"; +import { localizer } from "../utils/Localizer.mjs"; + +export function updateSands(payload) { + if (!game.user.isActiveGM) { return }; + + // Assert payload validity + const { value, delta } = payload; + if (value == null && delta == null) { + ui.notifications.error(localizer( + `RipCrypt.notifs.error.malformed-socket-payload`, + { + event: `updateSands`, + details: `Either value or delta must be provided`, + }, + )); + return; + }; + + // Take action + if (value != null) { + const initial = game.settings.get(game.system.id, `sandsOfFateInitial`); + let sands = clamp(0, value, initial); + if (sands === 0) { + ui.delveDice.alertCrypticEvent(); + sands = initial; + }; + game.settings.set( + game.system.id, + `sandsOfFate`, + sands, + ); + } else if (delta != null) { + ui.delveDice.sandsOfFateDelta(delta); + }; +}; diff --git a/module/utils/clamp.mjs b/module/utils/clamp.mjs new file mode 100644 index 0000000..94cac4e --- /dev/null +++ b/module/utils/clamp.mjs @@ -0,0 +1,3 @@ +export function clamp(min, ideal, max) { + return Math.max(min, Math.min(ideal, max)); +}; From 77d43f28b4bfb241e68106e25711a39c4dc726bb Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Tue, 7 Oct 2025 22:16:23 -0600 Subject: [PATCH 05/11] Add a setting that makes it so player rolls don't auto-update the sands of fate --- langs/en-ca.json | 4 ++++ module/settings/worldSettings.mjs | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/langs/en-ca.json b/langs/en-ca.json index 963b2c2..f42b159 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -155,6 +155,10 @@ "both": "Notification and Pause Game", "nothing": "Do Nothing" } + }, + "allowUpdateSandsSocket": { + "name": "Player Haste Updates the Sands of Fate", + "hint": "This setting determines if when a player makes a haste check that the result will automatically be applied to the global Sands of Fate. Disabling this is good if you want to let players roll without needing to worry about automation messing anything up while they spam rolls." } }, "Apps": { diff --git a/module/settings/worldSettings.mjs b/module/settings/worldSettings.mjs index ca095ae..ac1643c 100644 --- a/module/settings/worldSettings.mjs +++ b/module/settings/worldSettings.mjs @@ -39,4 +39,14 @@ export function registerWorldSettings() { }, }), }); + + game.settings.register(`ripcrypt`, `allowUpdateSandsSocket`, { + name: `RipCrypt.setting.allowUpdateSandsSocket.name`, + hint: `RipCrypt.setting.allowUpdateSandsSocket.hint`, + scope: `world`, + config: true, + requiresReload: false, + type: Boolean, + default: true, + }); }; From 78d02400d063a90b4ca15aeb869f7f06d9a88040 Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Tue, 7 Oct 2025 22:21:33 -0600 Subject: [PATCH 06/11] Add short circuit to ensure that socket events through the API can't trigger the sands update while disabled --- module/sockets/updateSands.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/module/sockets/updateSands.mjs b/module/sockets/updateSands.mjs index f851ce1..f74d007 100644 --- a/module/sockets/updateSands.mjs +++ b/module/sockets/updateSands.mjs @@ -3,6 +3,7 @@ import { localizer } from "../utils/Localizer.mjs"; export function updateSands(payload) { if (!game.user.isActiveGM) { return }; + if (!game.settings.get(game.system.id, `allowUpdateSandsSocket`)) { return }; // Assert payload validity const { value, delta } = payload; From fa0b1078a1f0b30179e672b6dccd0a8d3ff89165 Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Tue, 7 Oct 2025 22:22:09 -0600 Subject: [PATCH 07/11] Finish the helper in the public API and broadcast the socket event --- module/Apps/DelveDiceHUD.mjs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index a486e4b..2bf4b32 100644 --- a/module/Apps/DelveDiceHUD.mjs +++ b/module/Apps/DelveDiceHUD.mjs @@ -264,9 +264,40 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { game.settings.set(`ripcrypt`, `sandsOfFate`, newSands); }; + /** + * A helper method that rolls the dice required for hasty turns while delving + * and adjusts the Sands of Fate accordingly + */ static async rollForHaste() { + const shouldUpdateSands = game.settings.get(`ripcrypt`, `allowUpdateSandsSocket`); + if (shouldUpdateSands && game.users.activeGM == null) { + ui.notifications.error(localizer(`RipCrypt.notifs.error.no-active-gm`)); + return; + }; + const roll = new Roll(`1d8xo=1`); await roll.evaluate(); + + let delta = 0; + if (roll.dice[0].results[0].exploded) { + delta = -1; + if (roll.dice[0].results[1].result === 1) { + delta = -2; + }; + }; + + roll.toMessage({ flavor: `Haste Check` }); + + // Change the Sands of Fate setting if required + if (delta === 0 || !shouldUpdateSands) { return }; + if (game.user.isActiveGM) { + ui.delveDice.sandsOfFateDelta(delta); + } else { + game.socket.emit(`system.ripcrypt`, { + event: `updateSands`, + payload: { delta }, + }); + }; }; // #endregion }; From 59c66c20a15b9932a3a74f52f8b200e3298da941 Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Tue, 7 Oct 2025 22:22:32 -0600 Subject: [PATCH 08/11] Add an action for rolling a Haste Check --- module/Apps/ActorSheets/StatsCardV1.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/module/Apps/ActorSheets/StatsCardV1.mjs b/module/Apps/ActorSheets/StatsCardV1.mjs index 155168e..cd44ead 100644 --- a/module/Apps/ActorSheets/StatsCardV1.mjs +++ b/module/Apps/ActorSheets/StatsCardV1.mjs @@ -1,4 +1,5 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; +import { DelveDiceHUD } from "../DelveDiceHUD.mjs"; import { filePath } from "../../consts.mjs"; import { gameTerms } from "../../gameTerms.mjs"; import { GenericAppMixin } from "../GenericApp.mjs"; @@ -25,6 +26,7 @@ export class StatsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(Acto resizable: false, }, actions: { + rollForHaste: DelveDiceHUD.rollForHaste, }, form: { submitOnChange: true, From 6e77bdd949021705a45b4ca610fc471bd09ce8d2 Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Wed, 8 Oct 2025 00:05:09 -0600 Subject: [PATCH 09/11] Add an action button for Haste checks in the Stats card UI --- templates/Apps/StatsCardV1/content.hbs | 8 ++++++++ templates/Apps/StatsCardV1/style.css | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/templates/Apps/StatsCardV1/content.hbs b/templates/Apps/StatsCardV1/content.hbs index 99e3c74..b35ebee 100644 --- a/templates/Apps/StatsCardV1/content.hbs +++ b/templates/Apps/StatsCardV1/content.hbs @@ -12,7 +12,15 @@ class="hero_name row-alt" value="{{actor.name}}" name="name" + autocomplete="off" > +
+ +
{{!-- * Armour --}}
diff --git a/templates/Apps/StatsCardV1/style.css b/templates/Apps/StatsCardV1/style.css index e33246e..47764e9 100644 --- a/templates/Apps/StatsCardV1/style.css +++ b/templates/Apps/StatsCardV1/style.css @@ -49,6 +49,13 @@ margin-left: calc(var(--col-gap) * -1); padding-left: var(--col-gap); } + .action-row { + grid-column: span 3; + + button { + border-bottom: 2px dashed var(--accent-3); + } + } .glory-label { grid-column: 2 / span 1; From 4eecd15acf37923a8436afe07ca341799c85ddee Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Wed, 8 Oct 2025 00:06:19 -0600 Subject: [PATCH 10/11] Add the required actions from the component parts into the combined sheet --- module/Apps/ActorSheets/CombinedHeroSheet.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/module/Apps/ActorSheets/CombinedHeroSheet.mjs b/module/Apps/ActorSheets/CombinedHeroSheet.mjs index 3c18491..b81923a 100644 --- a/module/Apps/ActorSheets/CombinedHeroSheet.mjs +++ b/module/Apps/ActorSheets/CombinedHeroSheet.mjs @@ -1,7 +1,6 @@ import { CraftCardV1 } from "./CraftCardV1.mjs"; import { filePath } from "../../consts.mjs"; import { GenericAppMixin } from "../GenericApp.mjs"; -import { Logger } from "../../utils/Logger.mjs"; import { SkillsCardV1 } from "./SkillsCardV1.mjs"; import { StatsCardV1 } from "./StatsCardV1.mjs"; @@ -23,7 +22,9 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi window: { resizable: false, }, - actions: {}, + actions: { + ...StatsCardV1.DEFAULT_OPTIONS.actions, + }, form: { submitOnChange: true, closeOnSubmit: false, From e18b01e42597ee50118c736bd9af0c6a961a5f95 Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Wed, 8 Oct 2025 17:53:36 -0600 Subject: [PATCH 11/11] Apply PR feedback --- module/Apps/DelveDiceHUD.mjs | 7 ------- module/sockets/_index.mjs | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index 2bf4b32..6dd69fe 100644 --- a/module/Apps/DelveDiceHUD.mjs +++ b/module/Apps/DelveDiceHUD.mjs @@ -40,7 +40,6 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { actions: { tourDelta: this.#tourDelta, setFate: this.#setFate, - hasteRoll: this.#hasteRoll, }, }; @@ -217,12 +216,6 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { this._difficulty = value; game.settings.set(`ripcrypt`, `dc`, value); }; - - /** @this {DelveDiceHUD} */ - static async #hasteRoll() { - // TODO: if not GM, send socket request to GM - await this.rollForHaste(); - }; // #endregion // #region Public API diff --git a/module/sockets/_index.mjs b/module/sockets/_index.mjs index d0b632d..e866530 100644 --- a/module/sockets/_index.mjs +++ b/module/sockets/_index.mjs @@ -9,7 +9,7 @@ const events = { export function registerSockets() { Logger.info(`Setting up socket listener`); - game.socket.on(`system.${game.system.id}`, (data, userID) => { + game.socket.on(`system.ripcrypt`, (data, userID) => { const { event, payload } = data ?? {}; if (event == null || payload === undefined) { ui.notifications.error(localizer(`RipCrypt.notifs.error.invalid-socket`));