From 82217dd97181874a1f5ba6e4883a2e5b6d5fcd40 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Fri, 13 May 2022 19:12:28 +0200 Subject: [PATCH] feat: add selectable check modifiers --- lang/de.json | 11 ++- lang/en.json | 11 ++- scss/global/_accessibility.scss | 5 ++ src/apps/dialog-with-listeners.ts | 20 +++++ src/config.ts | 29 ++++++- src/hooks/setup.ts | 1 + src/rolls/check-factory.ts | 129 +++++++++++++++++++---------- templates/dialogs/roll-options.hbs | 19 ++++- 8 files changed, 174 insertions(+), 51 deletions(-) create mode 100644 src/apps/dialog-with-listeners.ts diff --git a/lang/de.json b/lang/de.json index 2c0dfd5f..b4beb543 100644 --- a/lang/de.json +++ b/lang/de.json @@ -303,10 +303,19 @@ "DS4.ErrorItemDoesNotHaveEffect": "Das Item '{item}' hat keinen Effekt mit der ID '{id}'.", "DS4.ErrorActorDoesNotHaveEffect": "Der Aktor '{actor}' hat keinen Effekt mit der ID '{id}'.", "DS4.DialogRollOptionsCheckTargetNumberLabel": "Probenwert", - "DS4.DialogRollOptionsGMModifierLabel": "SL-Modifikator", + "DS4.DialogRollOptionsCheckModifierLabel": "Modifikator", + "DS4.DialogRollOptionsCheckModifierCustomLabel": "Individueller Modifikator", "DS4.DialogRollOptionsMaximumCoupResultLabel": "Immersieg bis", "DS4.DialogRollOptionsMinimumFumbleResultLabel": "Patzer ab", "DS4.DialogRollOptionsRollModeLabel": "Sichtbarkeit", + "DS4.CheckModifierRoutine": "Routine", + "DS4.CheckModifierVeryEasy": "Sehr Leicht", + "DS4.CheckModifierEasy": "Leicht", + "DS4.CheckModifierMormal": "Normal", + "DS4.CheckModifierDifficult": "Schwer", + "DS4.CheckModifierVeryDifficult": "Sehr Schwer", + "DS4.CheckModifierExtremelyDifficult": "Äußerst Schwer", + "DS4.CheckModifierCustom": "Individuell", "DS4.TooltipBaseValue": "Basiswert", "DS4.TooltipModifier": "Modifikator", "DS4.TooltipEffects": "Effekte", diff --git a/lang/en.json b/lang/en.json index 19973936..485e9eed 100644 --- a/lang/en.json +++ b/lang/en.json @@ -303,10 +303,19 @@ "DS4.ErrorItemDoesNotHaveEffect": "The item '{item}' does not have any effect with the id '{id}'.", "DS4.ErrorActorDoesNotHaveEffect": "The actor '{actor}' does not have any effect with the id '{id}'.", "DS4.DialogRollOptionsCheckTargetNumberLabel": "Check Target Number", - "DS4.DialogRollOptionsGMModifierLabel": "Game Master Modifier", + "DS4.DialogRollOptionsCheckModifierLabel": "Modifier", + "DS4.DialogRollOptionsCheckModifierCustomLabel": "Custom Modifier", "DS4.DialogRollOptionsMaximumCoupResultLabel": "Coup to", "DS4.DialogRollOptionsMinimumFumbleResultLabel": "Fumble from", "DS4.DialogRollOptionsRollModeLabel": "Visibility", + "DS4.CheckModifierRoutine": "Routine", + "DS4.CheckModifierVeryEasy": "Very Easy", + "DS4.CheckModifierEasy": "Easy", + "DS4.CheckModifierMormal": "Normal", + "DS4.CheckModifierDifficult": "Difficult", + "DS4.CheckModifierVeryDifficult": "Very Difficult", + "DS4.CheckModifierExtremelyDifficult": "Extremely Difficult", + "DS4.CheckModifierCustom": "Custom", "DS4.TooltipBaseValue": "Base Value", "DS4.TooltipModifier": "Modifier", "DS4.TooltipEffects": "Effects", diff --git a/scss/global/_accessibility.scss b/scss/global/_accessibility.scss index cb2172cc..0456cc76 100644 --- a/scss/global/_accessibility.scss +++ b/scss/global/_accessibility.scss @@ -8,3 +8,8 @@ .ds4-hidden { display: none; } + +// This is needed for higher specifity +form .ds4-hidden { + display: none; +} diff --git a/src/apps/dialog-with-listeners.ts b/src/apps/dialog-with-listeners.ts new file mode 100644 index 00000000..574bbe9d --- /dev/null +++ b/src/apps/dialog-with-listeners.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2022 Johannes Loher +// +// SPDX-License-Identifier: MIT + +/** + * A simple extension to the {@link Dialog} class that allows attaching additional listeners. + */ +export class DialogWithListeners extends Dialog { + /** @inheritdoc */ + activateListeners(html: JQuery): void { + super.activateListeners(html); + if (this.options.activateAdditionalListeners !== undefined) { + this.options.activateAdditionalListeners(html, this); + } + } +} + +interface DialogWithListenersOptions extends DialogOptions { + activateAdditionalListeners?: ((html: JQuery, app: DialogWithListeners) => void) | undefined; +} diff --git a/src/config.ts b/src/config.ts index 731744d8..9d7ba5f7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -331,6 +331,20 @@ const i18nKeys = { wakeUp: "DS4.ChecksWakeUp", workMechanism: "DS4.ChecksWorkMechanism", }, + + /** + * Translations for the standard check modifiers + */ + checkModifiers: { + routine: "DS4.CheckModifierRoutine", + veryEasy: "DS4.CheckModifierVeryEasy", + easy: "DS4.CheckModifierEasy", + normal: "DS4.CheckModifierMormal", + difficult: "DS4.CheckModifierDifficult", + veryDifficult: "DS4.CheckModifierVeryDifficult", + extremelyDifficult: "DS4.CheckModifierExtremelyDifficult", + custom: "DS4.CheckModifierCustom", + }, }; export const DS4 = { @@ -412,7 +426,7 @@ export const DS4 = { }, /** - * Define the profile info types for handlebars of a character + * Profile info types for handlebars of a character */ characterProfileDTypes: { biography: "String", @@ -426,4 +440,17 @@ export const DS4 = { eyeColor: "String", specialCharacteristics: "String", }, + + /** + * Standard check modifiers + */ + checkModifiers: { + routine: 8, + veryEasy: 4, + easy: 2, + normal: 0, + difficult: -2, + veryDifficult: -4, + extremelyDifficult: -8, + }, }; diff --git a/src/hooks/setup.ts b/src/hooks/setup.ts index 0a548f2c..84d4d380 100644 --- a/src/hooks/setup.ts +++ b/src/hooks/setup.ts @@ -25,6 +25,7 @@ function localizeAndSortConfigObjects() { "creatureSizeCategories", "spellCategories", "traits", + "checkModifiers", ]; const localizeObject = (obj: T, sort = true): T => { diff --git a/src/rolls/check-factory.ts b/src/rolls/check-factory.ts index dd475d6c..ed714375 100644 --- a/src/rolls/check-factory.ts +++ b/src/rolls/check-factory.ts @@ -3,6 +3,8 @@ // // SPDX-License-Identifier: MIT +import { DialogWithListeners } from "../apps/dialog-with-listeners"; +import { DS4 } from "../config"; import { getGame } from "../helpers"; /** @@ -31,7 +33,7 @@ const defaultCheckOptions = new DefaultCheckOptions(); class CheckFactory { constructor( private checkTargetNumber: number, - private gmModifier: number, + private checkModifier: number, options: Partial = {}, ) { this.options = defaultCheckOptions.mergeWith(options); @@ -58,7 +60,7 @@ class CheckFactory { } createCheckTargetNumberModifier(): string { - const totalCheckTargetNumber = Math.max(this.checkTargetNumber + this.gmModifier, 0); + const totalCheckTargetNumber = Math.max(this.checkTargetNumber + this.checkModifier, 0); return `v${totalCheckTargetNumber}`; } @@ -85,22 +87,22 @@ export async function createCheckRoll( options: Partial = {}, ): Promise { // Ask for additional required data; - const gmModifierData = await askGmModifier(checkTargetNumber, options); + const interactiveRollData = await askForInteractiveRollData(checkTargetNumber, options); - const newTargetValue = gmModifierData.checkTargetNumber ?? checkTargetNumber; - const gmModifier = gmModifierData.gmModifier ?? 0; + const newTargetValue = interactiveRollData.checkTargetNumber ?? checkTargetNumber; + const checkModifier = interactiveRollData.checkModifier ?? 0; const newOptions: Partial = { - maximumCoupResult: gmModifierData.maximumCoupResult ?? options.maximumCoupResult, - minimumFumbleResult: gmModifierData.minimumFumbleResult ?? options.minimumFumbleResult, + maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult, + minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult, useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"), - rollMode: gmModifierData.rollMode ?? options.rollMode, + rollMode: interactiveRollData.rollMode ?? options.rollMode, flavor: options.flavor, flavorData: options.flavorData, speaker: options.speaker, }; // Create Factory - const cf = new CheckFactory(newTargetValue, gmModifier, newOptions); + const cf = new CheckFactory(newTargetValue, checkModifier, newOptions); // Possibly additional processing @@ -109,18 +111,19 @@ export async function createCheckRoll( } /** - * Responsible for rendering the modal interface asking for the modifier specified by GM and (currently) additional data. + * Responsible for rendering the modal interface asking for the check modifier and (currently) additional + * data. * * @notes * At the moment, this asks for more data than it will do after some iterations. * * @returns The data given by the user. */ -async function askGmModifier( +async function askForInteractiveRollData( checkTargetNumber: number, options: Partial = {}, { template, title }: { template?: string; title?: string } = {}, -): Promise> { +): Promise> { const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs"; const usedTitle = title ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle"); const templateData = { @@ -130,43 +133,72 @@ async function askGmModifier( minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult, rollMode: options.rollMode ?? getGame().settings.get("core", "rollMode"), rollModes: CONFIG.Dice.rollModes, + checkModifiers: Object.entries(DS4.i18n.checkModifiers).map(([key, translation]) => { + if (key in DS4.checkModifiers) { + const value = DS4.checkModifiers[key as keyof typeof DS4.checkModifiers]; + const label = `${translation} (${value >= 0 ? `+${value}` : value})`; + return { value, label }; + } + return { value: key, label: translation }; + }), }; const renderedHtml = await renderTemplate(usedTemplate, templateData); const dialogPromise = new Promise((resolve) => { - new Dialog({ - title: usedTitle, - content: renderedHtml, - buttons: { - ok: { - icon: '', - label: getGame().i18n.localize("DS4.GenericOkButton"), - callback: (html) => { - if (!("jquery" in html)) { - throw new Error( - getGame().i18n.format("DS4.ErrorUnexpectedHtmlType", { - exType: "JQuery", - realType: "HTMLElement", - }), - ); - } else { - const innerForm = html[0]?.querySelector("form"); - if (!innerForm) { + new DialogWithListeners( + { + title: usedTitle, + content: renderedHtml, + buttons: { + ok: { + icon: '', + label: getGame().i18n.localize("DS4.GenericOkButton"), + callback: (html) => { + if (!("jquery" in html)) { throw new Error( - getGame().i18n.format("DS4.ErrorCouldNotFindHtmlElement", { htmlElement: "form" }), + getGame().i18n.format("DS4.ErrorUnexpectedHtmlType", { + exType: "JQuery", + realType: "HTMLElement", + }), ); + } else { + const innerForm = html[0]?.querySelector("form"); + if (!innerForm) { + throw new Error( + getGame().i18n.format("DS4.ErrorCouldNotFindHtmlElement", { + htmlElement: "form", + }), + ); + } + resolve(innerForm); } - resolve(innerForm); - } + }, + }, + cancel: { + icon: '', + label: getGame().i18n.localize("DS4.GenericCancelButton"), }, }, - cancel: { - icon: '', - label: getGame().i18n.localize("DS4.GenericCancelButton"), + default: "ok", + }, + { + activateAdditionalListeners: (html, app) => { + const checkModifierCustomFormGroup = html.find("#check-modifier-custom").parent(".form-group"); + html.find("#check-modifier").on("change", (event) => { + if ( + (event.currentTarget as HTMLSelectElement).value === "custom" && + checkModifierCustomFormGroup.hasClass("ds4-hidden") + ) { + checkModifierCustomFormGroup.removeClass("ds4-hidden"); + app.setPosition({ height: "auto" }); + } else if (!checkModifierCustomFormGroup.hasClass("ds4-hidden")) { + checkModifierCustomFormGroup.addClass("ds4-hidden"); + app.setPosition({ height: "auto" }); + } + }); }, }, - default: "ok", - }).render(true); + ).render(true); }); const dialogForm = await dialogPromise; return parseDialogFormData(dialogForm); @@ -176,16 +208,22 @@ async function askGmModifier( * Extracts Dialog data from the returned DOM element. * @param formData - The filed dialog */ -function parseDialogFormData(formData: HTMLFormElement): Partial { +function parseDialogFormData(formData: HTMLFormElement): Partial { const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value); - const chosenGMModifier = parseInt(formData["gm-modifier"]?.value); + const chosenCheckModifier = formData["check-modifier"]?.value; + + const chosenCheckModifierValue = + chosenCheckModifier === "custom" + ? parseInt(formData["check-modifier-custom"]?.value) + : parseInt(chosenCheckModifier); + const chosenMaximumCoupResult = parseInt(formData["maximum-coup-result"]?.value); const chosenMinimumFumbleResult = parseInt(formData["minimum-fumble-result"]?.value); const chosenRollMode = formData["roll-mode"]?.value; return { checkTargetNumber: Number.isSafeInteger(chosenCheckTargetNumber) ? chosenCheckTargetNumber : undefined, - gmModifier: Number.isSafeInteger(chosenGMModifier) ? chosenGMModifier : undefined, + checkModifier: Number.isSafeInteger(chosenCheckModifierValue) ? chosenCheckModifierValue : undefined, maximumCoupResult: Number.isSafeInteger(chosenMaximumCoupResult) ? chosenMaximumCoupResult : undefined, minimumFumbleResult: Number.isSafeInteger(chosenMinimumFumbleResult) ? chosenMinimumFumbleResult : undefined, rollMode: Object.keys(CONFIG.Dice.rollModes).includes(chosenRollMode) ? chosenRollMode : undefined, @@ -195,8 +233,8 @@ function parseDialogFormData(formData: HTMLFormElement): Partial
@@ -20,8 +21,22 @@ SPDX-License-Identifier: MIT value="{{checkTargetNumber}}" />
- - + +
+ {{log checkModifiers}} + +
+
+
+ +