Merge pull request #64 from Eldritch-Oliver/feature/haste-roll-shortcut

Added a button to roll for hasty turns in the Actor Stats sheet
This commit is contained in:
Oliver 2025-10-08 17:55:56 -06:00 committed by GitHub
commit a6047ff009
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 169 additions and 18 deletions

View file

@ -155,6 +155,10 @@
"both": "Notification and Pause Game", "both": "Notification and Pause Game",
"nothing": "Do Nothing" "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": { "Apps": {
@ -193,7 +197,11 @@
"error": { "error": {
"cannot-equip": "Cannot equip the {itemType}, see console for more details.", "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.", "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": { "warn": {
"cannot-go-negative": "\"{name}\" is unable to be a negative number." "cannot-go-negative": "\"{name}\" is unable to be a negative number."

View file

@ -1,7 +1,6 @@
import { CraftCardV1 } from "./CraftCardV1.mjs"; import { CraftCardV1 } from "./CraftCardV1.mjs";
import { filePath } from "../../consts.mjs"; import { filePath } from "../../consts.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../GenericApp.mjs";
import { Logger } from "../../utils/Logger.mjs";
import { SkillsCardV1 } from "./SkillsCardV1.mjs"; import { SkillsCardV1 } from "./SkillsCardV1.mjs";
import { StatsCardV1 } from "./StatsCardV1.mjs"; import { StatsCardV1 } from "./StatsCardV1.mjs";
@ -23,7 +22,9 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi
window: { window: {
resizable: false, resizable: false,
}, },
actions: {}, actions: {
...StatsCardV1.DEFAULT_OPTIONS.actions,
},
form: { form: {
submitOnChange: true, submitOnChange: true,
closeOnSubmit: false, closeOnSubmit: false,

View file

@ -1,4 +1,5 @@
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
import { DelveDiceHUD } from "../DelveDiceHUD.mjs";
import { filePath } from "../../consts.mjs"; import { filePath } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs"; import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../GenericApp.mjs";
@ -25,6 +26,7 @@ export class StatsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(Acto
resizable: false, resizable: false,
}, },
actions: { actions: {
rollForHaste: DelveDiceHUD.rollForHaste,
}, },
form: { form: {
submitOnChange: true, submitOnChange: true,

View file

@ -6,6 +6,7 @@ import { Logger } from "../utils/Logger.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
const { ContextMenu } = foundry.applications.ux; const { ContextMenu } = foundry.applications.ux;
const { Roll } = foundry.dice;
const { FatePath } = gameTerms; const { FatePath } = gameTerms;
const CompassRotations = { const CompassRotations = {
@ -189,18 +190,7 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) {
/** @this {DelveDiceHUD} */ /** @this {DelveDiceHUD} */
static async #tourDelta(_event, element) { static async #tourDelta(_event, element) {
const delta = parseInt(element.dataset.delta); const delta = parseInt(element.dataset.delta);
const initial = game.settings.get(`ripcrypt`, `sandsOfFateInitial`); await this.sandsOfFateDelta(delta);
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();
};
switch (Math.sign(delta)) { switch (Math.sign(delta)) {
case -1: { case -1: {
@ -212,9 +202,6 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) {
break; break;
} }
}; };
this.#animateSandsTo(newSands);
game.settings.set(`ripcrypt`, `sandsOfFate`, newSands);
}; };
/** @this {DelveDiceHUD} */ /** @this {DelveDiceHUD} */
@ -247,5 +234,63 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) {
game.togglePause(true, { broadcast: true }); 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 // #endregion
}; };

View file

@ -34,6 +34,7 @@ import { Logger } from "../utils/Logger.mjs";
import { registerCustomComponents } from "../Apps/components/_index.mjs"; import { registerCustomComponents } from "../Apps/components/_index.mjs";
import { registerDevSettings } from "../settings/devSettings.mjs"; import { registerDevSettings } from "../settings/devSettings.mjs";
import { registerMetaSettings } from "../settings/metaSettings.mjs"; import { registerMetaSettings } from "../settings/metaSettings.mjs";
import { registerSockets } from "../sockets/_index.mjs";
import { registerUserSettings } from "../settings/userSettings.mjs"; import { registerUserSettings } from "../settings/userSettings.mjs";
import { registerWorldSettings } from "../settings/worldSettings.mjs"; import { registerWorldSettings } from "../settings/worldSettings.mjs";
@ -127,6 +128,7 @@ Hooks.once(`init`, () => {
CONFIG.Actor.trackableAttributes.hero = HeroData.trackableAttributes; CONFIG.Actor.trackableAttributes.hero = HeroData.trackableAttributes;
// #endregion // #endregion
registerSockets();
registerCustomComponents(); registerCustomComponents();
Handlebars.registerHelper(helpers); Handlebars.registerHelper(helpers);
}); });

View file

@ -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,
});
}; };

27
module/sockets/_index.mjs Normal file
View file

@ -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);
});
};

View file

@ -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);
};
};

3
module/utils/clamp.mjs Normal file
View file

@ -0,0 +1,3 @@
export function clamp(min, ideal, max) {
return Math.max(min, Math.min(ideal, max));
};

View file

@ -32,6 +32,7 @@
"download": "#{DOWNLOAD}#", "download": "#{DOWNLOAD}#",
"readme": "README.md", "readme": "README.md",
"bugs": "", "bugs": "",
"socket": true,
"flags": { "flags": {
"hotReload": { "hotReload": {
"extensions": ["css", "hbs", "json", "mjs", "svg"], "extensions": ["css", "hbs", "json", "mjs", "svg"],

View file

@ -12,7 +12,15 @@
class="hero_name row-alt" class="hero_name row-alt"
value="{{actor.name}}" value="{{actor.name}}"
name="name" name="name"
autocomplete="off"
> >
<div class="action-row">
<button
data-action="rollForHaste"
>
Haste Check
</button>
</div>
{{!-- * Armour --}} {{!-- * Armour --}}
<div class="armour"> <div class="armour">

View file

@ -53,6 +53,13 @@
margin-left: calc(var(--col-gap) * -1); margin-left: calc(var(--col-gap) * -1);
padding-left: var(--col-gap); padding-left: var(--col-gap);
} }
.action-row {
grid-column: span 3;
button {
border-bottom: 2px dashed var(--accent-3);
}
}
.glory-label { .glory-label {
grid-column: 2 / span 1; grid-column: 2 / span 1;