feat: add selectable check modifiers

This commit is contained in:
Johannes Loher 2022-05-13 19:12:28 +02:00
parent d4945cf230
commit 82217dd971
8 changed files with 174 additions and 51 deletions

View file

@ -303,10 +303,19 @@
"DS4.ErrorItemDoesNotHaveEffect": "Das Item '{item}' hat keinen Effekt mit der ID '{id}'.", "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.ErrorActorDoesNotHaveEffect": "Der Aktor '{actor}' hat keinen Effekt mit der ID '{id}'.",
"DS4.DialogRollOptionsCheckTargetNumberLabel": "Probenwert", "DS4.DialogRollOptionsCheckTargetNumberLabel": "Probenwert",
"DS4.DialogRollOptionsGMModifierLabel": "SL-Modifikator", "DS4.DialogRollOptionsCheckModifierLabel": "Modifikator",
"DS4.DialogRollOptionsCheckModifierCustomLabel": "Individueller Modifikator",
"DS4.DialogRollOptionsMaximumCoupResultLabel": "Immersieg bis", "DS4.DialogRollOptionsMaximumCoupResultLabel": "Immersieg bis",
"DS4.DialogRollOptionsMinimumFumbleResultLabel": "Patzer ab", "DS4.DialogRollOptionsMinimumFumbleResultLabel": "Patzer ab",
"DS4.DialogRollOptionsRollModeLabel": "Sichtbarkeit", "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.TooltipBaseValue": "Basiswert",
"DS4.TooltipModifier": "Modifikator", "DS4.TooltipModifier": "Modifikator",
"DS4.TooltipEffects": "Effekte", "DS4.TooltipEffects": "Effekte",

View file

@ -303,10 +303,19 @@
"DS4.ErrorItemDoesNotHaveEffect": "The item '{item}' does not have any effect with the id '{id}'.", "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.ErrorActorDoesNotHaveEffect": "The actor '{actor}' does not have any effect with the id '{id}'.",
"DS4.DialogRollOptionsCheckTargetNumberLabel": "Check Target Number", "DS4.DialogRollOptionsCheckTargetNumberLabel": "Check Target Number",
"DS4.DialogRollOptionsGMModifierLabel": "Game Master Modifier", "DS4.DialogRollOptionsCheckModifierLabel": "Modifier",
"DS4.DialogRollOptionsCheckModifierCustomLabel": "Custom Modifier",
"DS4.DialogRollOptionsMaximumCoupResultLabel": "Coup to", "DS4.DialogRollOptionsMaximumCoupResultLabel": "Coup to",
"DS4.DialogRollOptionsMinimumFumbleResultLabel": "Fumble from", "DS4.DialogRollOptionsMinimumFumbleResultLabel": "Fumble from",
"DS4.DialogRollOptionsRollModeLabel": "Visibility", "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.TooltipBaseValue": "Base Value",
"DS4.TooltipModifier": "Modifier", "DS4.TooltipModifier": "Modifier",
"DS4.TooltipEffects": "Effects", "DS4.TooltipEffects": "Effects",

View file

@ -8,3 +8,8 @@
.ds4-hidden { .ds4-hidden {
display: none; display: none;
} }
// This is needed for higher specifity
form .ds4-hidden {
display: none;
}

View file

@ -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<DialogWithListenersOptions> {
/** @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;
}

View file

@ -331,6 +331,20 @@ const i18nKeys = {
wakeUp: "DS4.ChecksWakeUp", wakeUp: "DS4.ChecksWakeUp",
workMechanism: "DS4.ChecksWorkMechanism", 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 = { 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: { characterProfileDTypes: {
biography: "String", biography: "String",
@ -426,4 +440,17 @@ export const DS4 = {
eyeColor: "String", eyeColor: "String",
specialCharacteristics: "String", specialCharacteristics: "String",
}, },
/**
* Standard check modifiers
*/
checkModifiers: {
routine: 8,
veryEasy: 4,
easy: 2,
normal: 0,
difficult: -2,
veryDifficult: -4,
extremelyDifficult: -8,
},
}; };

View file

@ -25,6 +25,7 @@ function localizeAndSortConfigObjects() {
"creatureSizeCategories", "creatureSizeCategories",
"spellCategories", "spellCategories",
"traits", "traits",
"checkModifiers",
]; ];
const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => { const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => {

View file

@ -3,6 +3,8 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { DialogWithListeners } from "../apps/dialog-with-listeners";
import { DS4 } from "../config";
import { getGame } from "../helpers"; import { getGame } from "../helpers";
/** /**
@ -31,7 +33,7 @@ const defaultCheckOptions = new DefaultCheckOptions();
class CheckFactory { class CheckFactory {
constructor( constructor(
private checkTargetNumber: number, private checkTargetNumber: number,
private gmModifier: number, private checkModifier: number,
options: Partial<DS4CheckFactoryOptions> = {}, options: Partial<DS4CheckFactoryOptions> = {},
) { ) {
this.options = defaultCheckOptions.mergeWith(options); this.options = defaultCheckOptions.mergeWith(options);
@ -58,7 +60,7 @@ class CheckFactory {
} }
createCheckTargetNumberModifier(): string { createCheckTargetNumberModifier(): string {
const totalCheckTargetNumber = Math.max(this.checkTargetNumber + this.gmModifier, 0); const totalCheckTargetNumber = Math.max(this.checkTargetNumber + this.checkModifier, 0);
return `v${totalCheckTargetNumber}`; return `v${totalCheckTargetNumber}`;
} }
@ -85,22 +87,22 @@ export async function createCheckRoll(
options: Partial<DS4CheckFactoryOptions> = {}, options: Partial<DS4CheckFactoryOptions> = {},
): Promise<ChatMessage | unknown> { ): Promise<ChatMessage | unknown> {
// Ask for additional required data; // Ask for additional required data;
const gmModifierData = await askGmModifier(checkTargetNumber, options); const interactiveRollData = await askForInteractiveRollData(checkTargetNumber, options);
const newTargetValue = gmModifierData.checkTargetNumber ?? checkTargetNumber; const newTargetValue = interactiveRollData.checkTargetNumber ?? checkTargetNumber;
const gmModifier = gmModifierData.gmModifier ?? 0; const checkModifier = interactiveRollData.checkModifier ?? 0;
const newOptions: Partial<DS4CheckFactoryOptions> = { const newOptions: Partial<DS4CheckFactoryOptions> = {
maximumCoupResult: gmModifierData.maximumCoupResult ?? options.maximumCoupResult, maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult,
minimumFumbleResult: gmModifierData.minimumFumbleResult ?? options.minimumFumbleResult, minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult,
useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"), useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
rollMode: gmModifierData.rollMode ?? options.rollMode, rollMode: interactiveRollData.rollMode ?? options.rollMode,
flavor: options.flavor, flavor: options.flavor,
flavorData: options.flavorData, flavorData: options.flavorData,
speaker: options.speaker, speaker: options.speaker,
}; };
// Create Factory // Create Factory
const cf = new CheckFactory(newTargetValue, gmModifier, newOptions); const cf = new CheckFactory(newTargetValue, checkModifier, newOptions);
// Possibly additional processing // 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 * @notes
* At the moment, this asks for more data than it will do after some iterations. * At the moment, this asks for more data than it will do after some iterations.
* *
* @returns The data given by the user. * @returns The data given by the user.
*/ */
async function askGmModifier( async function askForInteractiveRollData(
checkTargetNumber: number, checkTargetNumber: number,
options: Partial<DS4CheckFactoryOptions> = {}, options: Partial<DS4CheckFactoryOptions> = {},
{ template, title }: { template?: string; title?: string } = {}, { template, title }: { template?: string; title?: string } = {},
): Promise<Partial<IntermediateGmModifierData>> { ): Promise<Partial<IntermediateInteractiveRollData>> {
const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs"; const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs";
const usedTitle = title ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle"); const usedTitle = title ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle");
const templateData = { const templateData = {
@ -130,43 +133,72 @@ async function askGmModifier(
minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult, minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult,
rollMode: options.rollMode ?? getGame().settings.get("core", "rollMode"), rollMode: options.rollMode ?? getGame().settings.get("core", "rollMode"),
rollModes: CONFIG.Dice.rollModes, 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 renderedHtml = await renderTemplate(usedTemplate, templateData);
const dialogPromise = new Promise<HTMLFormElement>((resolve) => { const dialogPromise = new Promise<HTMLFormElement>((resolve) => {
new Dialog({ new DialogWithListeners(
title: usedTitle, {
content: renderedHtml, title: usedTitle,
buttons: { content: renderedHtml,
ok: { buttons: {
icon: '<i class="fas fa-check"></i>', ok: {
label: getGame().i18n.localize("DS4.GenericOkButton"), icon: '<i class="fas fa-check"></i>',
callback: (html) => { label: getGame().i18n.localize("DS4.GenericOkButton"),
if (!("jquery" in html)) { callback: (html) => {
throw new Error( if (!("jquery" in html)) {
getGame().i18n.format("DS4.ErrorUnexpectedHtmlType", {
exType: "JQuery",
realType: "HTMLElement",
}),
);
} else {
const innerForm = html[0]?.querySelector("form");
if (!innerForm) {
throw new Error( 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: '<i class="fas fa-times"></i>',
label: getGame().i18n.localize("DS4.GenericCancelButton"),
}, },
}, },
cancel: { default: "ok",
icon: '<i class="fas fa-times"></i>', },
label: getGame().i18n.localize("DS4.GenericCancelButton"), {
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; const dialogForm = await dialogPromise;
return parseDialogFormData(dialogForm); return parseDialogFormData(dialogForm);
@ -176,16 +208,22 @@ async function askGmModifier(
* Extracts Dialog data from the returned DOM element. * Extracts Dialog data from the returned DOM element.
* @param formData - The filed dialog * @param formData - The filed dialog
*/ */
function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateGmModifierData> { function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateInteractiveRollData> {
const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value); 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 chosenMaximumCoupResult = parseInt(formData["maximum-coup-result"]?.value);
const chosenMinimumFumbleResult = parseInt(formData["minimum-fumble-result"]?.value); const chosenMinimumFumbleResult = parseInt(formData["minimum-fumble-result"]?.value);
const chosenRollMode = formData["roll-mode"]?.value; const chosenRollMode = formData["roll-mode"]?.value;
return { return {
checkTargetNumber: Number.isSafeInteger(chosenCheckTargetNumber) ? chosenCheckTargetNumber : undefined, checkTargetNumber: Number.isSafeInteger(chosenCheckTargetNumber) ? chosenCheckTargetNumber : undefined,
gmModifier: Number.isSafeInteger(chosenGMModifier) ? chosenGMModifier : undefined, checkModifier: Number.isSafeInteger(chosenCheckModifierValue) ? chosenCheckModifierValue : undefined,
maximumCoupResult: Number.isSafeInteger(chosenMaximumCoupResult) ? chosenMaximumCoupResult : undefined, maximumCoupResult: Number.isSafeInteger(chosenMaximumCoupResult) ? chosenMaximumCoupResult : undefined,
minimumFumbleResult: Number.isSafeInteger(chosenMinimumFumbleResult) ? chosenMinimumFumbleResult : undefined, minimumFumbleResult: Number.isSafeInteger(chosenMinimumFumbleResult) ? chosenMinimumFumbleResult : undefined,
rollMode: Object.keys(CONFIG.Dice.rollModes).includes(chosenRollMode) ? chosenRollMode : undefined, rollMode: Object.keys(CONFIG.Dice.rollModes).includes(chosenRollMode) ? chosenRollMode : undefined,
@ -195,8 +233,8 @@ function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateGmM
/** /**
* Contains data that needs retrieval from an interactive Dialog. * Contains data that needs retrieval from an interactive Dialog.
*/ */
interface GmModifierData { interface InteractiveRollData {
gmModifier: number; checkModifier: number;
rollMode: keyof CONFIG.Dice.RollModes; rollMode: keyof CONFIG.Dice.RollModes;
} }
@ -207,15 +245,14 @@ interface GmModifierData {
* Quite a lot of this information is requested due to a lack of automation: * Quite a lot of this information is requested due to a lack of automation:
* - maximumCoupResult * - maximumCoupResult
* - minimumFumbleResult * - minimumFumbleResult
* - useSlayingDice
* - checkTargetNumber * - checkTargetNumber
* *
* They will and should be removed once effects and data retrieval is in place. * They will and should be removed once effects and data retrieval is in place.
* If a "raw" roll dialog is necessary, create another pre-processing Dialog * If a "raw" roll dialog is necessary, create another pre-processing Dialog
* class asking for the required information. * class asking for the required information.
* This interface should then be replaced with the `GmModifierData`. * This interface should then be replaced with the `InteractiveRollData`.
*/ */
interface IntermediateGmModifierData extends GmModifierData { interface IntermediateInteractiveRollData extends InteractiveRollData {
checkTargetNumber: number; checkTargetNumber: number;
maximumCoupResult: number; maximumCoupResult: number;
minimumFumbleResult: number; minimumFumbleResult: number;

View file

@ -12,6 +12,7 @@ SPDX-License-Identifier: MIT
!-- @param minimumFumbleResult: The preselected minimum fumble result. !-- @param minimumFumbleResult: The preselected minimum fumble result.
!-- @param rollMode: The preselected roll mode (= chat roll-mode). !-- @param rollMode: The preselected roll mode (= chat roll-mode).
!-- @param rollModes: A map of all roll modes and their i18n keys. !-- @param rollModes: A map of all roll modes and their i18n keys.
!-- @param checkModifiers: A map of all check difficulty modifiers and their translations.
--}} --}}
<form class="ds4-roll-options"> <form class="ds4-roll-options">
<div class="form-group"> <div class="form-group">
@ -20,8 +21,22 @@ SPDX-License-Identifier: MIT
value="{{checkTargetNumber}}" /> value="{{checkTargetNumber}}" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="gm-modifier">{{localize "DS4.DialogRollOptionsGMModifierLabel"}}</label> <label for="check-modifier">{{localize "DS4.DialogRollOptionsCheckModifierLabel"}}</label>
<input id="gm-modifier" data-dtype="Number" type="number" name="gm-modifier" value="0" /> <div class="form-fields">
{{log checkModifiers}}
<select id="check-modifier" name="check-modifier" data-dtype="String">
{{#select "0"}}
{{#each checkModifiers as |checkModifier|}}
<option value="{{checkModifier.value}}">{{checkModifier.label}}</option>
{{/each}}
{{/select}}
</select>
</div>
</div>
<div class="form-group ds4-hidden">
<label for="check-modifier-custom">{{localize "DS4.DialogRollOptionsCheckModifierCustomLabel"}}</label>
<input id="check-modifier-custom" data-dtype="Number" type="number" name="check-modifier-custom"
value="0" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="maximum-coup-result">{{localize "DS4.DialogRollOptionsMaximumCoupResultLabel"}}</label> <label for="maximum-coup-result">{{localize "DS4.DialogRollOptionsMaximumCoupResultLabel"}}</label>