From 58993b8a01f13bc8137fef7bfedeb275ba654745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= <110994627+MangoFVTT@users.noreply.github.com> Date: Sat, 2 Nov 2024 10:14:49 +0100 Subject: [PATCH] feat(rolls): add modifier keys and fastforwarding (#121) * Centralise settings retrieval and setup * Localise settings * Remove redundant imports * Fix documentation * Swap to export functions to match rest of code * Rename function for clarity * General cleanup of export names * Minor documentation change * Add keybindings setup * Setup utils folder * Fix minor errors * Add key modifier processing and skip behaviour for all d20 rolls * Clean up * Add key modifiers to item rolls * Fix incorrect default parameter * Make config values override any modifier * Review suggestions * Add as const to keybindings constant * Fix double dialog * Separate utils --- src/index.ts | 8 +- src/lang/en.json | 6 + .../actor/components/adversary/header.ts | 4 +- .../actor/dialogs/edit-creature-type.ts | 4 +- src/system/dice/index.ts | 54 ++++++--- src/system/documents/item.ts | 27 ++++- src/system/settings.ts | 55 +++++++++ src/system/{util => utils}/actor.ts | 6 - src/system/utils/generic.ts | 110 ++++++++++++++++++ .../{util => utils}/handlebars/application.ts | 0 .../{util => utils}/handlebars/index.ts | 0 .../{util => utils}/handlebars/types.ts | 0 12 files changed, 241 insertions(+), 33 deletions(-) rename src/system/{util => utils}/actor.ts (90%) create mode 100644 src/system/utils/generic.ts rename src/system/{util => utils}/handlebars/application.ts (100%) rename src/system/{util => utils}/handlebars/index.ts (100%) rename src/system/{util => utils}/handlebars/types.ts (100%) diff --git a/src/index.ts b/src/index.ts index 2058a801..d0e25954 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,11 @@ import COSMERE from './system/config'; import './style.scss'; import './system/hooks'; -import { preloadHandlebarsTemplates } from './system/util/handlebars'; -import { registerSystemSettings } from './system/settings'; +import { preloadHandlebarsTemplates } from './system/utils/handlebars'; +import { + registerSystemKeybindings, + registerSystemSettings, +} from './system/settings'; import * as applications from './system/applications'; import * as dataModels from './system/data'; @@ -95,6 +98,7 @@ Hooks.once('init', async () => { // Register settings registerSystemSettings(); + registerSystemKeybindings(); }); /** diff --git a/src/lang/en.json b/src/lang/en.json index 3b0ce656..6a425868 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -943,5 +943,11 @@ "name": "Skip Roll Dialog by Default", "hint": "If enabled, rolls will skip the roll configuration dialog when clicked, using the default roll configuration. The dialog can still be accessed by holding the 'Skip/Show Dialog' key modifier from the system controls." } + }, + "KEYBINDINGS": { + "skipDialogDefault": "Skip/Show Dialog", + "skipDialogAdvantage": "Skip Dialog with Advantage", + "skipDialogDisadvantage": "Skip Dialog with Disadvantage", + "skipDialogRaiseStakes": "Skip Dialog with Raise Stakes" } } diff --git a/src/system/applications/actor/components/adversary/header.ts b/src/system/applications/actor/components/adversary/header.ts index 2e28619b..bb7589d2 100644 --- a/src/system/applications/actor/components/adversary/header.ts +++ b/src/system/applications/actor/components/adversary/header.ts @@ -4,7 +4,7 @@ import { ConstructorOf } from '@system/types/utils'; import { EditCreatureTypeDialog } from '@system/applications/actor/dialogs/edit-creature-type'; // Utils -import ActorUtils from '@system/util/actor'; +import { getTypeLabel } from '@src/system/utils/actor'; // Component imports import { HandlebarsApplicationComponent } from '@system/applications/component-system'; @@ -65,7 +65,7 @@ export class AdversaryHeaderComponent extends HandlebarsApplicationComponent< roleLabel: CONFIG.COSMERE.adversary.roles[context.actor.system.role].label, sizeLabel: CONFIG.COSMERE.sizes[context.actor.system.size].label, - typeLabel: ActorUtils.getTypeLabel(context.actor.system.type), + typeLabel: getTypeLabel(context.actor.system.type), }); } } diff --git a/src/system/applications/actor/dialogs/edit-creature-type.ts b/src/system/applications/actor/dialogs/edit-creature-type.ts index b74bf5c8..ff71c87f 100644 --- a/src/system/applications/actor/dialogs/edit-creature-type.ts +++ b/src/system/applications/actor/dialogs/edit-creature-type.ts @@ -5,7 +5,7 @@ import { AnyObject } from '@system/types/utils'; import { CommonActorData } from '@system/data/actor/common'; // Utils -import ActorUtils from '@system/util/actor'; +import { getTypeLabel } from '@src/system/utils/actor'; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; @@ -128,7 +128,7 @@ export class EditCreatureTypeDialog extends HandlebarsApplicationMixin( actor: this.actor, type: this.type, - typeLabel: ActorUtils.getTypeLabel(this.type), + typeLabel: getTypeLabel(this.type), configuredTypes, }); } diff --git a/src/system/dice/index.ts b/src/system/dice/index.ts index 08868eed..f7f8b47e 100644 --- a/src/system/dice/index.ts +++ b/src/system/dice/index.ts @@ -2,7 +2,8 @@ import { Attribute } from '@system/types/cosmere'; import { D20Roll, D20RollOptions, D20RollData } from './d20-roll'; import { DamageRoll, DamageRollOptions, DamageRollData } from './damage-roll'; -import { RollMode } from './types'; +import { AdvantageMode } from '../types/roll'; +import { determineConfigurationMode } from '../utils/generic'; export * from './d20-roll'; export * from './damage-roll'; @@ -73,28 +74,45 @@ export interface DamageRollConfiguration extends DamageRollOptions { export async function d20Roll( config: D20RollConfigration, ): Promise { + // Handle key modifiers + const { fastForward, advantageMode, plotDie } = determineConfigurationMode( + config.configurable, + config.advantageMode + ? config.advantageMode === AdvantageMode.Advantage + : undefined, + config.advantageMode + ? config.advantageMode === AdvantageMode.Disadvantage + : undefined, + config.plotDie, + ); + + // Replace config values with key modified values + config.advantageMode = advantageMode; + config.plotDie = plotDie; + // Roll parameters const defaultRollMode = config.rollMode ?? game.settings!.get('core', 'rollMode'); // Construct the roll - const roll = new D20Roll(config.parts ?? [], config.data, { - ...config, - }); - - // Prompt dialog to configure the d20 roll - const configured = - config.configurable !== false - ? await roll.configureDialog({ - title: config.title, - plotDie: config.plotDie, - defaultRollMode, - defaultAttribute: - config.defaultAttribute ?? config.data.skill.attribute, - data: config.data, - }) - : roll; - if (configured === null) return null; + const roll = new D20Roll(config.parts ?? [], config.data, { ...config }); + + if (!fastForward) { + // Prompt dialog to configure the d20 roll + const configured = + config.configurable !== false + ? await roll.configureDialog({ + title: config.title, + plotDie: config.plotDie, + defaultRollMode, + defaultAttribute: + config.defaultAttribute ?? + config.data.skill.attribute, + data: config.data, + }) + : roll; + if (configured === null) return null; + } // Evaluate the configure roll await roll.evaluate(); diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index 8f694666..12afe104 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -52,6 +52,7 @@ import { } from '@system/dice'; import { AdvantageMode } from '@system/types/roll'; import { RollMode } from '@system/dice/types'; +import { determineConfigurationMode } from '../utils/generic'; // Constants const CONSUME_CONFIGURATION_DIALOG_TEMPLATE = @@ -446,8 +447,30 @@ export class CosmereItem< this.system.damage.attribute ?? actor.system.skills[damageSkillId].attribute; + options.skillTest ??= {}; + options.damage ??= {}; + + // Handle key modifiers + const { fastForward, advantageMode, plotDie } = + determineConfigurationMode( + options.configurable, + options.skillTest.advantageMode + ? options.skillTest.advantageMode === + AdvantageMode.Advantage + : undefined, + options.skillTest.advantageMode + ? options.skillTest.advantageMode === + AdvantageMode.Disadvantage + : undefined, + options.skillTest.plotDie, + ); + + // Replace config values with key modified values + options.skillTest.advantageMode = advantageMode; + options.skillTest.plotDie = plotDie; + // Perform configuration - if (options.configurable !== false) { + if (!fastForward && options.configurable !== false) { const attackConfig = await AttackConfigurationDialog.show({ title: `${this.name} (${game.i18n!.localize( CONFIG.COSMERE.skills[skillTestSkillId].label, @@ -483,14 +506,12 @@ export class CosmereItem< skillTestAttributeId = attackConfig.attribute; options.rollMode = attackConfig.rollMode; - options.skillTest ??= {}; options.skillTest.plotDie = attackConfig.skillTest.plotDie; options.skillTest.advantageMode = attackConfig.skillTest.advantageMode; options.skillTest.advantageModePlot = attackConfig.skillTest.advantageModePlot; - options.damage ??= {}; options.damage.advantageMode = attackConfig.damageRoll.advantageMode; } diff --git a/src/system/settings.ts b/src/system/settings.ts index ad6084dd..989e5bfb 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -63,6 +63,52 @@ export function registerSystemSettings() { }); } +/** + * Index of identifiers for system keybindings. + */ +export const KEYBINDINGS = { + SKIP_DIALOG_DEFAULT: 'skipDialogDefault', + SKIP_DIALOG_ADVANTAGE: 'skipDialogAdvantage', + SKIP_DIALOG_DISADVANTAGE: 'skipDialogDisadvantage', + SKIP_DIALOG_RAISE_STAKES: 'skipDialogRaiseStakes', +} as const; + +/** + * Register all of the system's keybindings. + */ +export function registerSystemKeybindings() { + const keybindings = [ + { + name: KEYBINDINGS.SKIP_DIALOG_DEFAULT, + editable: [{ key: 'AltLeft' }, { key: 'AltRight' }], + }, + { + name: KEYBINDINGS.SKIP_DIALOG_ADVANTAGE, + editable: [{ key: 'ShiftLeft' }, { key: 'ShiftRight' }], + }, + { + name: KEYBINDINGS.SKIP_DIALOG_DISADVANTAGE, + editable: [ + { key: 'ControlLeft' }, + { key: 'ControlRight' }, + { key: 'OsLeft' }, + { key: 'OsRight' }, + ], + }, + { + name: KEYBINDINGS.SKIP_DIALOG_RAISE_STAKES, + editable: [{ key: 'KeyQ' }], + }, + ]; + + keybindings.forEach((keybind) => { + game.keybindings!.register(SYSTEM_ID, keybind.name, { + name: `KEYBINDINGS.${keybind.name}`, + editable: keybind.editable, + }); + }); +} + /** * Retrieve a specific setting value for the provided key. * @param {string} settingKey The identifier of the setting to retrieve. @@ -71,3 +117,12 @@ export function registerSystemSettings() { export function getSystemSetting(settingKey: string) { return game.settings!.get(SYSTEM_ID, settingKey); } + +/** + * Retrieves an array of keybinding values for the provided key. + * @param {string} keybindingKey The identifier of the keybinding to retrieve. + * @returns {Array} The value of the keybindings associated with the given key. + */ +export function getSystemKeybinding(keybindingKey: string) { + return game.keybindings!.get(SYSTEM_ID, keybindingKey); +} diff --git a/src/system/util/actor.ts b/src/system/utils/actor.ts similarity index 90% rename from src/system/util/actor.ts rename to src/system/utils/actor.ts index 155133bf..7b476797 100644 --- a/src/system/util/actor.ts +++ b/src/system/utils/actor.ts @@ -20,9 +20,3 @@ export function getTypeLabel(type: CommonActorData['type']): string { // Construct type label return `${primaryLabel} ${subtype ? `(${subtype})` : ''}`.trim(); } - -/* --- Default export --- */ - -export default { - getTypeLabel, -}; diff --git a/src/system/utils/generic.ts b/src/system/utils/generic.ts new file mode 100644 index 00000000..638f1548 --- /dev/null +++ b/src/system/utils/generic.ts @@ -0,0 +1,110 @@ +import { + getSystemKeybinding, + getSystemSetting, + KEYBINDINGS, + SETTINGS, +} from '../settings'; +import { AdvantageMode } from '../types/roll'; + +/** + * Determine if the keys of a requested keybinding are pressed. + * @param {string} action Keybinding action within the system namespace. Can have multiple keybindings associated. + * @returns {boolean} True if one of the keybindings for the requested action are triggered, false otherwise. + */ +export function areKeysPressed(action: string): boolean { + const keybinds = getSystemKeybinding(action); + + if (!keybinds || keybinds.length === 0) { + return false; + } + + const activeModifiers = {} as Record; + + const addModifiers = (key: string) => { + if (hasKey(KeyboardManager.MODIFIER_CODES, key)) { + KeyboardManager.MODIFIER_CODES[key].forEach( + (n: string) => + (activeModifiers[n] = game.keyboard!.downKeys.has(n)), + ); + } + }; + addModifiers(KeyboardManager.MODIFIER_KEYS.CONTROL); + addModifiers(KeyboardManager.MODIFIER_KEYS.SHIFT); + addModifiers(KeyboardManager.MODIFIER_KEYS.ALT); + + return getSystemKeybinding(action).some((b) => { + if ( + game.keyboard!.downKeys.has(b.key) && + b.modifiers?.every((m) => activeModifiers[m]) + ) + return true; + if (b.modifiers?.length) return false; + return activeModifiers[b.key]; + }); +} + +/** + * Checks if a given object has the given property key as a key for indexing. + * Adding this check beforehand allows an object to be indexed by that key directly without typescript errors. + * @param {T} obj The object to check for indexing. + * @param {PropertyKey} key The key to check within the object. + * @returns {boolean} True if the given object has requested property key, false otherwise. + */ +export function hasKey( + obj: T, + key: PropertyKey, +): key is keyof T { + return key in obj; +} + +/** + * Processes pressed keys and provided config values to determine final values for a roll, specifically: + * if it should skip the configuration dialog, what advantage mode it is using, and if it has raised stakes. + * @param {boolean} [configure] Should the roll dialog be skipped? + * @param {boolean} [advantage] Is something granting this roll advantage? + * @param {boolean} [disadvantage] Is something granting this roll disadvantage? + * @param {boolean} [raiseStakes] Is something granting this roll raised stakes? + * @returns {{fastForward: boolean, advantageMode: AdvantageMode, plotDie: boolean}} Whether a roll should fast forward, have a plot die, and its advantage mode. + */ +export function determineConfigurationMode( + configure?: boolean, + advantage?: boolean, + disadvantage?: boolean, + raiseStakes?: boolean, +) { + const modifiers = { + advantage: areKeysPressed(KEYBINDINGS.SKIP_DIALOG_ADVANTAGE), + disadvantage: areKeysPressed(KEYBINDINGS.SKIP_DIALOG_DISADVANTAGE), + raiseStakes: areKeysPressed(KEYBINDINGS.SKIP_DIALOG_RAISE_STAKES), + }; + + const fastForward = + configure !== undefined + ? !configure + : isFastForward() || Object.values(modifiers).some((k) => k); + + const hasAdvantage = advantage ?? modifiers.advantage; + const hasDisadvantage = disadvantage ?? modifiers.disadvantage; + const advantageMode = hasAdvantage + ? AdvantageMode.Advantage + : hasDisadvantage + ? AdvantageMode.Disadvantage + : AdvantageMode.None; + const plotDie = raiseStakes ?? modifiers.raiseStakes; + + return { fastForward, advantageMode, plotDie }; +} + +/** + * Processes pressed keys and selected system settings to determine if a roll should fast forward. + * This function allows the swappable behaviour of the Skip/Show Dialog modifier key, making it behave correctly depending on the system setting selected by the user. + * @returns {boolean} Whether a roll should fast forward or not. + */ +export function isFastForward() { + const skipKeyPressed = areKeysPressed(KEYBINDINGS.SKIP_DIALOG_DEFAULT); + const skipByDefault = getSystemSetting(SETTINGS.ROLL_SKIP_DIALOG_DEFAULT); + + return ( + (skipByDefault && !skipKeyPressed) || (!skipByDefault && skipKeyPressed) + ); +} diff --git a/src/system/util/handlebars/application.ts b/src/system/utils/handlebars/application.ts similarity index 100% rename from src/system/util/handlebars/application.ts rename to src/system/utils/handlebars/application.ts diff --git a/src/system/util/handlebars/index.ts b/src/system/utils/handlebars/index.ts similarity index 100% rename from src/system/util/handlebars/index.ts rename to src/system/utils/handlebars/index.ts diff --git a/src/system/util/handlebars/types.ts b/src/system/utils/handlebars/types.ts similarity index 100% rename from src/system/util/handlebars/types.ts rename to src/system/utils/handlebars/types.ts