Merge remote-tracking branch 'origin/master' into 037-actor-type-creature

This commit is contained in:
Johannes Loher 2021-01-18 19:12:46 +01:00
commit e9adbd3c3b
11 changed files with 333 additions and 35 deletions

View file

@ -65,37 +65,37 @@ describe("DS4 Rolls with one die and slaying dice, followup throw.", () => {
describe("DS4 Rolls with one die and crit roll modifications.", () => {
it("Should do a crit success on `1`.", () => {
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [1])).toEqual(
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [1])).toEqual(
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [1]),
);
});
it("Should do a crit success on `maxCritSucc`.", () => {
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [2])).toEqual(
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [2])).toEqual(
new RollResult(4, RollResultStatus.CRITICAL_SUCCESS, [2]),
);
});
it("Should do a success on lower edge case `3`.", () => {
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [3])).toEqual(
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [3])).toEqual(
new RollResult(3, RollResultStatus.SUCCESS, [3]),
);
});
it("Should do a success on upper edge case `18`.", () => {
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [18])).toEqual(
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [18])).toEqual(
new RollResult(0, RollResultStatus.FAILURE, [18]),
);
});
it("Should do a crit fail on `minCritFail`.", () => {
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [19])).toEqual(
it("Should do a crit fail on `minCritFailure`.", () => {
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [19])).toEqual(
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19]),
);
});
it("Should do a crit fail on `20`", () => {
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFail: 19 }, [20])).toEqual(
expect(rollCheckSingleDie(4, { maxCritSuccess: 2, minCritFailure: 19 }, [20])).toEqual(
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [20]),
);
});
@ -171,37 +171,37 @@ describe("DS4 Rolls with multiple dice and no modifiers.", () => {
describe("DS4 Rolls with multiple dice and min/max modifiers.", () => {
it("Should do a crit fail on `19` for first roll.", () => {
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [19, 15, 6])).toEqual(
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [19, 15, 6])).toEqual(
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19, 15, 6]),
);
});
it("Should succeed with all rolls crit successes (1 and 2).", () => {
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [2, 1, 2])).toEqual(
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [2, 1, 2])).toEqual(
new RollResult(48, RollResultStatus.CRITICAL_SUCCESS, [2, 1, 2]),
);
});
it("Should succeed with the last roll not being sufficient.", () => {
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 15, 15])).toEqual(
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [15, 15, 15])).toEqual(
new RollResult(30, RollResultStatus.SUCCESS, [15, 15, 15]),
);
});
it("Should succeed with the last roll a crit success `2`.", () => {
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 15, 2])).toEqual(
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [15, 15, 2])).toEqual(
new RollResult(38, RollResultStatus.SUCCESS, [15, 15, 2]),
);
});
it("Should succeed with the last roll being `20` and one crit success '2'.", () => {
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 2, 20])).toEqual(
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [15, 2, 20])).toEqual(
new RollResult(43, RollResultStatus.SUCCESS, [15, 2, 20]),
);
});
it("Should succeed with the last roll being `19` and one crit success '2'.", () => {
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFail: 19 }, [15, 2, 19])).toEqual(
expect(rollCheckMultipleDice(48, { maxCritSuccess: 2, minCritFailure: 19 }, [15, 2, 19])).toEqual(
new RollResult(42, RollResultStatus.SUCCESS, [15, 2, 19]),
);
});
@ -209,7 +209,7 @@ describe("DS4 Rolls with multiple dice and min/max modifiers.", () => {
describe("DS4 Rolls with multiple dice and fail modifiers.", () => {
it("Should do a crit fail on `19` for first roll.", () => {
expect(rollCheckMultipleDice(48, { minCritFail: 19 }, [19, 15, 6])).toEqual(
expect(rollCheckMultipleDice(48, { minCritFailure: 19 }, [19, 15, 6])).toEqual(
new RollResult(0, RollResultStatus.CRITICAL_FAILURE, [19, 15, 6]),
);
});

View file

@ -184,5 +184,18 @@
"DS4.UnitKilometers": "Kilometer",
"DS4.UnitKilometersAbbr": "km",
"DS4.UnitCustom": "individuell",
"DS4.UnitCustomAbbr": " "
"DS4.UnitCustomAbbr": " ",
"DS4.RollDialogDefaultTitle": "Proben-Optionen",
"DS4.RollDialogOkButton": "Ok",
"DS4.RollDialogCancelButton": "Abbrechen",
"DS4.ErrorUnexpectedHtmlType": "Typfehler: Erwartet wurde {exType}, tatsächlich erhalten wurde {realType}",
"DS4.RollDialogTargetLabel": "Probenwert",
"DS4.RollDialogModifierLabel": "SL-Modifikator",
"DS4.RollDialogCoupLabel": "Immersieg bis",
"DS4.RollDialogFumbleLabel": "Patzer ab",
"DS4.RollDialogVisibilityLabel": "Sichtbarkeit",
"DS4.ChatVisibilityRoll": "Alle",
"DS4.ChatVisibilityGmRoll": "Selbst & SL",
"DS4.ChatVisibilityBlindRoll": "Nur SL",
"DS4.ChatVisibilitySelfRoll": "Nur selbst"
}

View file

@ -184,5 +184,18 @@
"DS4.UnitKilometers": "Kilometers",
"DS4.UnitKilometersAbbr": "km",
"DS4.UnitCustom": "Custom Unit",
"DS4.UnitCustomAbbr": " "
"DS4.UnitCustomAbbr": " ",
"DS4.RollDialogDefaultTitle": "Roll Options",
"DS4.RollDialogOkButton": "Ok",
"DS4.RollDialogCancelButton": "Cancel",
"DS4.ErrorUnexpectedHtmlType": "Type Error: Expected {exType}, got {realType}",
"DS4.RollDialogTargetLabel": "Check Target Number",
"DS4.RollDialogModifierLabel": "Game Master Modifier",
"DS4.RollDialogCoupLabel": "Coup to",
"DS4.RollDialogFumbleLabel": "Fumble from",
"DS4.RollDialogVisibilityLabel": "Visibility",
"DS4.ChatVisibilityRoll": "All",
"DS4.ChatVisibilityGmRoll": "Self & GM",
"DS4.ChatVisibilityBlindRoll": "GM only",
"DS4.ChatVisibilitySelfRoll": "Self only"
}

View file

@ -211,7 +211,7 @@ export const DS4 = {
},
/**
* Define the profile info types for hanndlebars of a character
* Define the profile info types for handlebars of a character
*/
characterProfileDTypes: {
biography: "String",
@ -300,4 +300,14 @@ export const DS4 = {
days: "DS4.UnitDaysAbbr",
custom: "DS4.UnitCustomAbbr",
},
/**
* Define localization strings for Chat Visibility
*/
chatVisibilities: {
roll: "DS4.ChatVisibilityRoll",
gmroll: "DS4.ChatVisibilityGmRoll",
blindroll: "DS4.ChatVisibilityBlindRoll",
selfroll: "DS4.ChatVisibilitySelfRoll",
},
};

View file

@ -6,6 +6,7 @@ import { DS4 } from "./config";
import { DS4Check } from "./rolls/check";
import { DS4CharacterActorSheet } from "./actor/sheets/character-sheet";
import { DS4CreatureActorSheet } from "./actor/sheets/creature-sheet";
import { createCheckRoll } from "./rolls/check-factory";
Hooks.once("init", async function () {
console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`);
@ -14,6 +15,7 @@ Hooks.once("init", async function () {
DS4Actor,
DS4Item,
DS4,
createCheckRoll,
};
// Record configuration
@ -100,6 +102,7 @@ Hooks.once("setup", function () {
"temporalUnitsAbbr",
"distanceUnits",
"distanceUnitsAbbr",
"chatVisibilities",
];
// Exclude some from sorting where the default order matters

View file

@ -0,0 +1,237 @@
import { DS4 } from "../config";
/**
* Provides default values for all arguments the `CheckFactory` expects.
*/
class DefaultCheckOptions implements DS4CheckFactoryOptions {
maxCritSuccess = 1;
minCritFailure = 20;
useSlayingDice = false;
rollMode: DS4RollMode = "roll";
mergeWith(other: Partial<DS4CheckFactoryOptions>): DS4CheckFactoryOptions {
return { ...this, ...other } as DS4CheckFactoryOptions;
}
}
/**
* Singleton reference for default value extraction.
*/
const defaultCheckOptions = new DefaultCheckOptions();
/**
* Most basic class responsible for generating the chat formula and passing it to the chat as roll.
*/
class CheckFactory {
constructor(
private checkTargetValue: number,
private gmModifier: number,
passedOptions: Partial<DS4CheckFactoryOptions> = {},
) {
this.checkOptions = new DefaultCheckOptions().mergeWith(passedOptions);
}
private checkOptions: DS4CheckFactoryOptions;
async execute(): Promise<ChatMessage | any> {
const rollCls: typeof Roll = CONFIG.Dice.rolls[0];
const formula = [
"ds",
this.createTargetValueTerm(),
this.createCritTerm(),
this.createSlayingDiceTerm(),
].filterJoin("");
const roll = new rollCls(formula);
const rollModeTemplate = this.checkOptions.rollMode;
console.log(rollModeTemplate);
return roll.toMessage({}, { rollMode: rollModeTemplate, create: true });
}
// Term generators
createTargetValueTerm(): string | null {
if (this.checkTargetValue !== null) {
return "v" + (this.checkTargetValue + this.gmModifier);
} else {
return null;
}
}
createCritTerm(): string | null {
const minCritRequired = this.checkOptions.minCritFailure !== defaultCheckOptions.minCritFailure;
const maxCritRequired = this.checkOptions.maxCritSuccess !== defaultCheckOptions.maxCritSuccess;
if (minCritRequired || maxCritRequired) {
return "c" + (this.checkOptions.maxCritSuccess ?? "") + "," + (this.checkOptions.minCritFailure ?? "");
} else {
return null;
}
}
createSlayingDiceTerm(): string | null {
return this.checkOptions.useSlayingDice ? "x" : null;
}
}
/**
* Asks the user for all unknown/necessary information and passes them on to perform a roll.
* @param targetValue {number} The Check Target Number ("CTN")
* @param options {Partial<DS4CheckFactoryOptions>} Options changing the behaviour of the roll and message.
*/
export async function createCheckRoll(
targetValue: number,
options: Partial<DS4CheckFactoryOptions> = {},
): Promise<ChatMessage | any> {
// Ask for additional required data;
const gmModifierData = await askGmModifier(targetValue, options);
const newOptions: Partial<DS4CheckFactoryOptions> = {
maxCritSuccess: gmModifierData.maxCritSuccess ?? options.maxCritSuccess ?? undefined,
minCritFailure: gmModifierData.minCritFailure ?? options.minCritFailure ?? undefined,
useSlayingDice: gmModifierData.useSlayingDice ?? options.useSlayingDice ?? undefined,
rollMode: gmModifierData.rollMode ?? options.rollMode ?? undefined,
};
// Create Factory
const cf = new CheckFactory(gmModifierData.checkTargetValue, gmModifierData.gmModifier, newOptions);
// Possibly additional processing
// Execute roll
await cf.execute();
}
/**
* Responsible for rendering the modal interface asking for the modifier specified by GM and (currently) additional data.
*
* @notes
* At the moment, this asks for more data than it will do after some iterations.
*
* @returns {Promise<IntermediateGmModifierData>} The data given by the user.
*/
async function askGmModifier(
targetValue: number,
options: Partial<DS4CheckFactoryOptions> = {},
{ template, title }: { template?: string; title?: string } = {},
): Promise<IntermediateGmModifierData> {
// Render model interface and return value
const usedTemplate = template ?? "systems/ds4/templates/roll/roll-options.hbs";
const usedTitle = title ?? game.i18n.localize("DS4.RollDialogDefaultTitle");
const templateData = {
cssClass: "roll-option",
title: usedTitle,
checkTargetValue: targetValue,
maxCritSuccess: options.maxCritSuccess ?? defaultCheckOptions.maxCritSuccess,
minCritFailure: options.minCritFailure ?? defaultCheckOptions.minCritFailure,
rollModes: rollModes,
config: DS4,
};
const renderedHtml = await renderTemplate(usedTemplate, templateData);
const dialogPromise = new Promise<HTMLFormElement>((resolve) => {
new Dialog(
{
title: usedTitle,
close: () => {
// Don't do anything
},
content: renderedHtml,
buttons: {
ok: {
label: game.i18n.localize("DS4.RollDialogOkButton"),
callback: (html: HTMLElement | JQuery) => {
if (!("jquery" in html)) {
throw new Error(
game.i18n.format("DS4.ErrorUnexpectedHtmlType", {
exType: "JQuery",
realType: "HTMLElement",
}),
);
} else {
const innerForm = html[0].querySelector("form");
resolve(innerForm);
}
},
},
cancel: {
label: game.i18n.localize("DS4.RollDialogCancelButton"),
callback: () => {
// Don't do anything
},
},
},
default: "ok",
},
{},
).render(true);
});
const dialogForm = await dialogPromise;
return parseDialogFormData(dialogForm, targetValue);
}
/**
* Extracts Dialog data from the returned DOM element.
* @param formData {HTMLFormElement} The filed dialog
* @param targetValue {number} The previously known target value (slated for removal once data automation is available)
*/
function parseDialogFormData(formData: HTMLFormElement, targetValue: number): IntermediateGmModifierData {
return {
checkTargetValue: parseInt(formData["ctv"]?.value) ?? targetValue,
gmModifier: parseInt(formData["gmmod"]?.value) ?? 0,
maxCritSuccess: parseInt(formData["maxcoup"]?.value) ?? defaultCheckOptions.maxCritSuccess,
minCritFailure: parseInt(formData["minfumble"]?.value) ?? defaultCheckOptions.minCritFailure,
useSlayingDice: false,
rollMode: formData["visibility"]?.value ?? defaultCheckOptions.rollMode,
};
}
/**
* Contains data that needs retrieval from an interactive Dialog.
*/
interface GmModifierData {
gmModifier: number;
rollMode: DS4RollMode;
}
/**
* Contains *CURRENTLY* necessary Data for drafting a roll.
*
* @deprecated
* Quite a lot of this information is requested due to a lack of automation:
* - maxCritSuccess
* - minCritFailure
* - useSlayingDice
* - checkTargetValue
*
* They will and should be removed once effects and data retrieval is in place.
* If a "raw" roll dialog is necessary, create another pre-porcessing Dialog
* class asking for the required information.
* This interface should then be replaced with the `GmModifierData`.
*/
interface IntermediateGmModifierData extends GmModifierData {
checkTargetValue: number;
gmModifier: number;
maxCritSuccess: number;
minCritFailure: number;
// TODO: In final version from system settings
useSlayingDice: boolean;
rollMode: DS4RollMode;
}
/**
* The minimum behavioural options that need to be passed to the factory.
*/
export interface DS4CheckFactoryOptions {
maxCritSuccess: number;
minCritFailure: number;
useSlayingDice: boolean;
rollMode: DS4RollMode;
}
/**
* Defines all possible roll modes, both for iterating and typing.
*/
const rollModes = ["roll", "gmroll", "blindroll", "selfroll"] as const;
type DS4RollModeTuple = typeof rollModes;
export type DS4RollMode = DS4RollModeTuple[number];

View file

@ -86,7 +86,7 @@ export class DS4Check extends DiceTerm {
} else {
return ds4roll(targetValueToUse, {
maxCritSuccess: this.maxCritSuccess,
minCritFail: this.minCritFailure,
minCritFailure: this.minCritFailure,
slayingDiceRepetition: slayingDiceRepetition,
useSlayingDice: slayingDiceRepetition,
});
@ -132,7 +132,6 @@ export class DS4Check extends DiceTerm {
static readonly DEFAULT_TARGET_VALUE = 10;
static readonly DEFAULT_MAX_CRIT_SUCCESS = 1;
static readonly DEFAULT_MIN_CRIT_FAILURE = 20;
// TODO: add to Type declarations
static DENOMINATION = "s";
static MODIFIERS = {
x: "explode",

View file

@ -1,13 +1,13 @@
export interface RollOptions {
maxCritSuccess: number;
minCritFail: number;
minCritFailure: number;
useSlayingDice: boolean;
slayingDiceRepetition: boolean;
}
export class DefaultRollOptions implements RollOptions {
public maxCritSuccess = 1;
public minCritFail = 20;
public minCritFailure = 20;
public useSlayingDice = false;
public slayingDiceRepetition = false;

View file

@ -48,7 +48,7 @@ export function rollCheckSingleDie(
if (rolledDie <= usedOptions.maxCritSuccess) {
return new RollResult(checkTargetValue, RollResultStatus.CRITICAL_SUCCESS, usedDice, true);
} else if (rolledDie >= usedOptions.minCritFail && !isSlayingDiceRepetition(usedOptions)) {
} else if (rolledDie >= usedOptions.minCritFailure && !isSlayingDiceRepetition(usedOptions)) {
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
} else {
if (rolledDie <= checkTargetValue) {
@ -90,7 +90,7 @@ export function rollCheckMultipleDice(
const slayingDiceRepetition = isSlayingDiceRepetition(usedOptions);
// Slaying Dice require a different handling.
if (firstResult >= usedOptions.minCritFail && !slayingDiceRepetition) {
if (firstResult >= usedOptions.minCritFailure && !slayingDiceRepetition) {
return new RollResult(0, RollResultStatus.CRITICAL_FAILURE, usedDice, true);
}

View file

@ -2,25 +2,32 @@
{{!-- INLINE PARTIAL DEFINITIONS --}}
{{!-- ======================================================================== --}}
{{!--
!-- Two templates for displaying values with unit.
!-- Base template to display a value with unit.
!-- @param unitDatum: the object to display; must have a value and a unit attribute
!-- @param localizationString
!-- @param config: the config object
!-- @param unitNames: mapping of allowed unitDatum.unit values to localized unit name
!-- @param unitAbbrs: mapping of allowed unitDatum.unit values to unit abbreviation
--}}
{{#*inline "unit"}}
<div class="unit-data-pair item-num-val"
title="{{localize localizationString}} [{{lookup unitNames unitDatum.unit}}]" >
{{#if unitDatum.value }}
{{unitDatum.value}}{{lookup unitAbbrs unitDatum.unit}}
{{else}}-{{/if}}
</div>
{{/inline}}
{{!--
!-- Two templates based on the "unit" template for displaying values with unit.
!-- Both accept a `config` object holding the unitNames and unitAbbr instead of
!-- directly handing over the latter two.
--}}
{{#*inline "temporalUnit"}}
<div class="unit-data-pair item-num-val"
title="{{localize localizationString}} [{{lookup config.temporalUnits unitDatum.unit}}]" >
{{unitDatum.value}}{{lookup config.temporalUnitsAbbr unitDatum.unit}}
</div>
{{> unit unitNames=config.temporalUnits unitAbbrs=config.temporalUnitsAbbr unitDatum=unitDatum localizationString=localizationString}}
{{/inline}}
{{#*inline "distanceUnit"}}
<div class="unit-data-pair item-num-val"
title="{{localize localizationString}} [{{lookup config.distanceUnits unitDatum.unit}}]" >
{{unitDatum.value}}{{lookup config.distanceUnitsAbbr unitDatum.unit}}
</div>
{{> unit unitNames=config.distanceUnits unitAbbrs=config.distanceUnitsAbbr unitDatum=unitDatum localizationString=localizationString}}
{{/inline}}

View file

@ -0,0 +1,16 @@
<form class="{{cssClass}} grid">
<label for="ctv">{{localize "DS4.RollDialogTargetLabel"}}</label>
<input id="ctv" data-type="Number" type="number" name="ctv" value="{{checkTargetValue}}" />
<label for="gmmod">{{localize "DS4.RollDialogModifierLabel"}}</label>
<input id="gmmod" data-type="Number" type="number" name="gmmod" value="0" />
<label for="maxcoup">{{localize "DS4.RollDialogCoupLabel"}}</label>
<input id="maxcoup" data-type="Number" type="number" name="maxcoup" value="{{maxCritSuccess}}" />
<label for="minfumble">{{localize "DS4.RollDialogFumbleLabel"}}</label>
<input id="minfumble" data-type="Number" type="number" name="minfumble" value="{{minCritFailure}}" />
<label for="visibility">{{localize "DS4.RollDialogVisibilityLabel"}}</label>
<select id="visibility" data-type="String">
{{#each rollModes as |rollMode|}}
<option value="{{rollMode}}">{{lookup ../config.chatVisibilities rollMode}}</option>
{{/each}}
</select>
</form>