diff --git a/langs/en-ca.json b/langs/en-ca.json index 24b1475..a5d28cc 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": { @@ -193,7 +197,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/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, 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, diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index dfa14a5..6dd69fe 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 = { @@ -189,18 +190,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 +202,6 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { break; } }; - - this.#animateSandsTo(newSands); - game.settings.set(`ripcrypt`, `sandsOfFate`, newSands); }; /** @this {DelveDiceHUD} */ @@ -247,5 +234,63 @@ 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); + }; + + /** + * 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 }; 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/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, + }); }; diff --git a/module/sockets/_index.mjs b/module/sockets/_index.mjs new file mode 100644 index 0000000..e866530 --- /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.ripcrypt`, (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..f74d007 --- /dev/null +++ b/module/sockets/updateSands.mjs @@ -0,0 +1,37 @@ +import { clamp } from "../utils/clamp.mjs"; +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; + 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)); +}; diff --git a/system.json b/system.json index 615ee6b..8383c4c 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"], diff --git a/templates/Apps/StatsCardV1/content.hbs b/templates/Apps/StatsCardV1/content.hbs index 9c87e00..d4c75e2 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 598d138..13092a8 100644 --- a/templates/Apps/StatsCardV1/style.css +++ b/templates/Apps/StatsCardV1/style.css @@ -53,6 +53,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;