Skip to content

Commit

Permalink
feat(rolls): add modifier keys and fastforwarding (#121)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
MangoFVTT authored Nov 2, 2024
1 parent 84a7c16 commit 58993b8
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 33 deletions.
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -95,6 +98,7 @@ Hooks.once('init', async () => {

// Register settings
registerSystemSettings();
registerSystemKeybindings();
});

/**
Expand Down
6 changes: 6 additions & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
4 changes: 2 additions & 2 deletions src/system/applications/actor/components/adversary/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
});
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/system/applications/actor/dialogs/edit-creature-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
});
}
Expand Down
54 changes: 36 additions & 18 deletions src/system/dice/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -73,28 +74,45 @@ export interface DamageRollConfiguration extends DamageRollOptions {
export async function d20Roll(
config: D20RollConfigration,
): Promise<D20Roll | null> {
// 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();
Expand Down
27 changes: 24 additions & 3 deletions src/system/documents/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down
55 changes: 55 additions & 0 deletions src/system/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<object>} The value of the keybindings associated with the given key.
*/
export function getSystemKeybinding(keybindingKey: string) {
return game.keybindings!.get(SYSTEM_ID, keybindingKey);
}
6 changes: 0 additions & 6 deletions src/system/util/actor.ts → src/system/utils/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,3 @@ export function getTypeLabel(type: CommonActorData['type']): string {
// Construct type label
return `${primaryLabel} ${subtype ? `(${subtype})` : ''}`.trim();
}

/* --- Default export --- */

export default {
getTypeLabel,
};
110 changes: 110 additions & 0 deletions src/system/utils/generic.ts
Original file line number Diff line number Diff line change
@@ -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<string, boolean>;

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<T extends object>(
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)
);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit 58993b8

Please sign in to comment.