Merge branch '068-enable-strict-mode' into 'master'

Enable strict mode

Closes #68

See merge request dungeonslayers/ds4!72
This commit is contained in:
Johannes Loher 2021-02-11 12:07:35 +01:00
commit 33dcbab6d5
14 changed files with 1752 additions and 1466 deletions

View file

@ -1,4 +1,4 @@
image: node:latest image: node:lts
stages: stages:
- prepare - prepare

2994
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -198,7 +198,11 @@
"DS4.RollDialogDefaultTitle": "Proben-Optionen", "DS4.RollDialogDefaultTitle": "Proben-Optionen",
"DS4.RollDialogOkButton": "OK", "DS4.RollDialogOkButton": "OK",
"DS4.RollDialogCancelButton": "Abbrechen", "DS4.RollDialogCancelButton": "Abbrechen",
"DS4.ErrorUnexpectedHtmlType": "Typfehler: Erwartet wurde {exType}, tatsächlich erhalten wurde {realType}", "DS4.ErrorUnexpectedHtmlType": "Typfehler: Erwartet wurde '{exType}', tatsächlich erhalten wurde '{realType}'.",
"DS4.ErrorCouldNotFindForm": "Konnte HTML Element '{htmlElement}' nicht finden.",
"DS4.ErrorActorDoesNotHaveItem": "Der Aktor '{actor}' hat kein Item mit der ID '{id}'.",
"DS4.ErrorUnexpectedError": "Es gab einen unerwarteten Fehler im Dungeonslayers 4 System. Für mehr Details schauen Sie bitte in die Konsole (F12).",
"DS4.ErrorItemDoesNotHaveEffect": "Das Item '{item}' hat keinen Effekt mit der ID '{id}'.",
"DS4.RollDialogTargetLabel": "Probenwert", "DS4.RollDialogTargetLabel": "Probenwert",
"DS4.RollDialogModifierLabel": "SL-Modifikator", "DS4.RollDialogModifierLabel": "SL-Modifikator",
"DS4.RollDialogCoupLabel": "Immersieg bis", "DS4.RollDialogCoupLabel": "Immersieg bis",

View file

@ -198,7 +198,11 @@
"DS4.RollDialogDefaultTitle": "Roll Options", "DS4.RollDialogDefaultTitle": "Roll Options",
"DS4.RollDialogOkButton": "Ok", "DS4.RollDialogOkButton": "Ok",
"DS4.RollDialogCancelButton": "Cancel", "DS4.RollDialogCancelButton": "Cancel",
"DS4.ErrorUnexpectedHtmlType": "Type Error: Expected {exType}, got {realType}", "DS4.ErrorUnexpectedHtmlType": "Type Error: Expected '{exType}' but got '{realType}'.",
"DS4.ErrorCouldNotFindForm": "Could not find HTML element '{htmlElement}'.",
"DS4.ErrorActorDoesNotHaveItem": "The actor '{actor}' does not have any item with the id '{id}'.",
"DS4.ErrorUnexpectedError": "There was an unexpected error in the Dungeonslayers 4 system. For more details, please take a look at the console (F12).",
"DS4.ErrorItemDoesNotHaveEffect": "The item '{item}' does not have any effect with the id '{id}'.",
"DS4.RollDialogTargetLabel": "Check Target Number", "DS4.RollDialogTargetLabel": "Check Target Number",
"DS4.RollDialogModifierLabel": "Game Master Modifier", "DS4.RollDialogModifierLabel": "Game Master Modifier",
"DS4.RollDialogCoupLabel": "Coup to", "DS4.RollDialogCoupLabel": "Coup to",

View file

@ -6,7 +6,7 @@ import { DS4Actor } from "../actor";
/** /**
* The base Sheet class for all DS4 Actors * The base Sheet class for all DS4 Actors
*/ */
export class DS4ActorSheet extends ActorSheet<DS4Actor> { export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
// TODO(types): Improve mergeObject in upstream so that it isn't necessary to provide all parameters (see https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/272) // TODO(types): Improve mergeObject in upstream so that it isn't necessary to provide all parameters (see https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/272)
/** @override */ /** @override */
static get defaultOptions(): BaseEntitySheet.Options { static get defaultOptions(): BaseEntitySheet.Options {
@ -48,9 +48,9 @@ export class DS4ActorSheet extends ActorSheet<DS4Actor> {
* object itemsByType. * object itemsByType.
* @returns The data fed to the template of the actor sheet * @returns The data fed to the template of the actor sheet
*/ */
getData(): ActorSheet.Data<DS4Actor> | Promise<ActorSheet.Data<DS4Actor>> { async getData(): Promise<ActorSheet.Data<DS4Actor>> {
const data = { const data = {
...super.getData(), ...(await super.getData()),
// Add the localization config to the data: // Add the localization config to the data:
config: DS4, config: DS4,
// Add the items explicitly sorted by type to the data: // Add the items explicitly sorted by type to the data:
@ -72,7 +72,14 @@ export class DS4ActorSheet extends ActorSheet<DS4Actor> {
// Update Inventory Item // Update Inventory Item
html.find(".item-edit").on("click", (ev) => { html.find(".item-edit").on("click", (ev) => {
const li = $(ev.currentTarget).parents(".item"); const li = $(ev.currentTarget).parents(".item");
const item = this.actor.getOwnedItem(li.data("itemId")); const id = li.data("itemId");
const item = this.actor.getOwnedItem(id);
if (!item) {
throw new Error(game.i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
}
if (!item.sheet) {
throw new Error(game.i18n.localize("DS4.ErrorUnexpectedError"));
}
item.sheet.render(true); item.sheet.render(true);
}); });
@ -221,7 +228,7 @@ export class DS4ActorSheet extends ActorSheet<DS4Actor> {
): Promise<boolean | undefined | ActorSheet.OwnedItemData<DS4Actor>> { ): Promise<boolean | undefined | ActorSheet.OwnedItemData<DS4Actor>> {
const item = ((await Item.fromDropData(data)) as unknown) as DS4Item; const item = ((await Item.fromDropData(data)) as unknown) as DS4Item;
if (item && !this.actor.canOwnItemType(item.data.type)) { if (item && !this.actor.canOwnItemType(item.data.type)) {
ui.notifications.warn( ui.notifications?.warn(
game.i18n.format("DS4.WarningActorCannotOwnItem", { game.i18n.format("DS4.WarningActorCannotOwnItem", {
actorName: this.actor.name, actorName: this.actor.name,
actorType: this.actor.data.type, actorType: this.actor.data.type,

View file

@ -9,7 +9,7 @@ import { createCheckRoll } from "./rolls/check-factory";
import { registerSystemSettings } from "./settings"; import { registerSystemSettings } from "./settings";
import { migration } from "./migrations"; import { migration } from "./migrations";
Hooks.once("init", async function () { Hooks.once("init", async () => {
console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`); console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`);
game.ds4 = { game.ds4 = {
@ -68,23 +68,11 @@ async function registerHandlebarsPartials() {
/** /**
* This function runs after game data has been requested and loaded from the servers, so entities exist * This function runs after game data has been requested and loaded from the servers, so entities exist
*/ */
Hooks.once("setup", function () { Hooks.once("setup", () => {
const noSort = ["attributes", "traits", "combatValues", "creatureSizeCategories"]; localizeAndSortConfigObjects();
// Localize and sort CONFIG objects
for (const o of Object.keys(DS4.i18n)) {
const localized = Object.entries(DS4.i18n[o]).map((e) => {
return [e[0], game.i18n.localize(e[1] as string)];
});
if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1]));
DS4.i18n[o] = localized.reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {});
}
}); });
Hooks.once("ready", function () { Hooks.once("ready", () => {
migration.migrate(); migration.migrate();
}); });
@ -105,3 +93,24 @@ Hooks.once("ready", function () {
}); });
}); });
}); });
/**
* Localizes all objects in {@link DS4.i18n} and sorts them unless they are explicitly excluded.
*/
function localizeAndSortConfigObjects() {
const noSort = ["attributes", "traits", "combatValues", "creatureSizeCategories"];
const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => {
const localized = Object.entries(obj).map(([key, value]) => {
return [key, game.i18n.localize(value)];
});
if (sort) localized.sort((a, b) => a[1].localeCompare(b[1]));
return Object.fromEntries(localized);
};
DS4.i18n = Object.fromEntries(
Object.entries(DS4.i18n).map(([key, value]) => {
return [key, localizeObject(value, !noSort.includes(key))];
}),
) as typeof DS4.i18n;
}

View file

@ -5,7 +5,7 @@ import { isDS4ItemDataTypePhysical } from "./item-data";
/** /**
* The Sheet class for DS4 Items * The Sheet class for DS4 Items
*/ */
export class DS4ItemSheet extends ItemSheet<DS4Item> { export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
/** @override */ /** @override */
static get defaultOptions(): BaseEntitySheet.Options { static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions; const superDefaultOptions = super.defaultOptions;
@ -41,9 +41,9 @@ export class DS4ItemSheet extends ItemSheet<DS4Item> {
} }
/** @override */ /** @override */
getData(): ItemSheet.Data<DS4Item> | Promise<ItemSheet.Data<DS4Item>> { async getData(): Promise<ItemSheet.Data<DS4Item>> {
const data = { const data = {
...super.getData(), ...(await super.getData()),
config: DS4, config: DS4,
isOwned: this.item.isOwned, isOwned: this.item.isOwned,
actor: this.item.actor, actor: this.item.actor,
@ -83,7 +83,7 @@ export class DS4ItemSheet extends ItemSheet<DS4Item> {
event.preventDefault(); event.preventDefault();
if (this.item.isOwned) { if (this.item.isOwned) {
return ui.notifications.warn(game.i18n.localize("DS4.WarningManageActiveEffectOnOwnedItem")); return ui.notifications?.warn(game.i18n.localize("DS4.WarningManageActiveEffectOnOwnedItem"));
} }
const a = event.currentTarget; const a = event.currentTarget;
const li = $(a).parents(".effect"); const li = $(a).parents(".effect");
@ -92,7 +92,11 @@ export class DS4ItemSheet extends ItemSheet<DS4Item> {
case "create": case "create":
return this._createActiveEffect(); return this._createActiveEffect();
case "edit": case "edit":
const effect = this.item.effects.get(li.data("effectId")); const id = li.data("effectId");
const effect = this.item.effects.get(id);
if (!effect) {
throw new Error(game.i18n.format("DS4.ErrorItemDoesNotHaveEffect", { id, item: this.item.name }));
}
return effect.sheet.render(true); return effect.sheet.render(true);
case "delete": { case "delete": {
return this.item.deleteEmbeddedEntity("ActiveEffect", li.data("effectId")); return this.item.deleteEmbeddedEntity("ActiveEffect", li.data("effectId"));

View file

@ -1,7 +1,7 @@
import { migrate as migrate001 } from "./migrations/001"; import { migrate as migrate001 } from "./migrations/001";
async function migrate(): Promise<void> { async function migrate(): Promise<void> {
if (!game.user.isGM) { if (!game.user?.isGM) {
return; return;
} }
@ -18,14 +18,14 @@ async function migrate(): Promise<void> {
} }
async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion: number): Promise<void> { async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion: number): Promise<void> {
if (!game.user.isGM) { if (!game.user?.isGM) {
return; return;
} }
const migrationsToExecute = migrations.slice(oldMigrationVersion, targetMigrationVersion); const migrationsToExecute = migrations.slice(oldMigrationVersion, targetMigrationVersion);
if (migrationsToExecute.length > 0) { if (migrationsToExecute.length > 0) {
ui.notifications.info( ui.notifications?.info(
game.i18n.format("DS4.InfoSystemUpdateStart", { game.i18n.format("DS4.InfoSystemUpdateStart", {
currentVersion: oldMigrationVersion, currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion, targetVersion: targetMigrationVersion,
@ -40,7 +40,7 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
await migration(); await migration();
game.settings.set("ds4", "systemMigrationVersion", currentMigrationVersion); game.settings.set("ds4", "systemMigrationVersion", currentMigrationVersion);
} catch (err) { } catch (err) {
ui.notifications.error( ui.notifications?.error(
game.i18n.format("DS4.ErrorDuringMigration", { game.i18n.format("DS4.ErrorDuringMigration", {
currentVersion: oldMigrationVersion, currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion, targetVersion: targetMigrationVersion,
@ -54,7 +54,7 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
} }
} }
ui.notifications.info( ui.notifications?.info(
game.i18n.format("DS4.InfoSystemUpdateCompleted", { game.i18n.format("DS4.InfoSystemUpdateCompleted", {
currentVersion: oldMigrationVersion, currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion, targetVersion: targetMigrationVersion,

View file

@ -1,5 +1,5 @@
export async function migrate(): Promise<void> { export async function migrate(): Promise<void> {
for (const a of game.actors.entities) { for (const a of game.actors?.entities ?? []) {
const updateData = getActorUpdateData(); const updateData = getActorUpdateData();
console.log(`Migrating actor ${a.name}`); console.log(`Migrating actor ${a.name}`);
await a.update(updateData, { enforceTypes: false }); await a.update(updateData, { enforceTypes: false });
@ -18,7 +18,7 @@ function getActorUpdateData(): Record<string, unknown> {
"rangedAttack", "rangedAttack",
"spellcasting", "spellcasting",
"targetedSpellcasting", "targetedSpellcasting",
].reduce((acc, curr) => { ].reduce((acc: Partial<Record<string, { "-=base": null }>>, curr) => {
acc[curr] = { "-=base": null }; acc[curr] = { "-=base": null };
return acc; return acc;
}, {}), }, {}),

View file

@ -131,34 +131,42 @@ async function askGmModifier(
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 Dialog(
title: usedTitle, {
content: renderedHtml, title: usedTitle,
buttons: { content: renderedHtml,
ok: { buttons: {
icon: '<i class="fas fa-check"></i>', ok: {
label: game.i18n.localize("DS4.RollDialogOkButton"), icon: '<i class="fas fa-check"></i>',
callback: (html) => { label: game.i18n.localize("DS4.RollDialogOkButton"),
if (!("jquery" in html)) { callback: (html) => {
throw new Error( if (!("jquery" in html)) {
game.i18n.format("DS4.ErrorUnexpectedHtmlType", { throw new Error(
exType: "JQuery", game.i18n.format("DS4.ErrorUnexpectedHtmlType", {
realType: "HTMLElement", exType: "JQuery",
}), realType: "HTMLElement",
); }),
} else { );
const innerForm = html[0].querySelector("form"); } else {
resolve(innerForm); const innerForm = html[0].querySelector("form");
} if (!innerForm) {
throw new Error(
game.i18n.format("DS4.ErrorCouldNotFindHtmlElement", { htmlElement: "form" }),
);
}
resolve(innerForm);
}
},
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("DS4.RollDialogCancelButton"),
}, },
}, },
cancel: { default: "ok",
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("DS4.RollDialogCancelButton"),
},
}, },
default: "ok", { jQuery: true },
}).render(true); ).render(true);
}); });
const dialogForm = await dialogPromise; const dialogForm = await dialogPromise;
return parseDialogFormData(dialogForm); return parseDialogFormData(dialogForm);

View file

@ -56,8 +56,8 @@ export class DS4Check extends DiceTerm {
} }
} }
success = null; success: boolean | null = null;
failure = null; failure: boolean | null = null;
targetValue = DS4Check.DEFAULT_TARGET_VALUE; targetValue = DS4Check.DEFAULT_TARGET_VALUE;
minCritFailure = DS4Check.DEFAULT_MIN_CRIT_FAILURE; minCritFailure = DS4Check.DEFAULT_MIN_CRIT_FAILURE;
maxCritSuccess = DS4Check.DEFAULT_MAX_CRIT_SUCCESS; maxCritSuccess = DS4Check.DEFAULT_MAX_CRIT_SUCCESS;
@ -93,16 +93,11 @@ export class DS4Check extends DiceTerm {
} }
} }
/** Term Modifiers */
noop(): this {
return this;
}
// DS4 only allows recursive explosions // DS4 only allows recursive explosions
explode(modifier: string): this { explode(modifier: string): void {
const rgx = /[xX]/; const rgx = /[xX]/;
const match = modifier.match(rgx); const match = modifier.match(rgx);
if (!match) return this; if (!match) return;
this.results = (this.results as Array<RollResult>) this.results = (this.results as Array<RollResult>)
.map((r) => { .map((r) => {
@ -135,7 +130,7 @@ export class DS4Check extends DiceTerm {
static DENOMINATION = "s"; static DENOMINATION = "s";
static MODIFIERS = { static MODIFIERS = {
x: "explode", x: "explode",
c: "noop", // Modifier is consumed in constructor for target value c: (): void => undefined, // Modifier is consumed in constructor for crit
v: "noop", // Modifier is consumed in constructor for target value v: (): void => undefined, // Modifier is consumed in constructor for target value
}; };
} }

View file

@ -11,7 +11,7 @@ import { calculateRollResult, isDiceSwapNecessary, isSlayingDiceRepetition, sepa
export function ds4roll( export function ds4roll(
checkTargetValue: number, checkTargetValue: number,
rollOptions: Partial<RollOptions> = {}, rollOptions: Partial<RollOptions> = {},
dice: Array<number> = null, dice: Array<number> = [],
): RollResult { ): RollResult {
if (checkTargetValue <= 20) { if (checkTargetValue <= 20) {
return rollCheckSingleDie(checkTargetValue, rollOptions, dice); return rollCheckSingleDie(checkTargetValue, rollOptions, dice);
@ -36,11 +36,11 @@ export function ds4roll(
export function rollCheckSingleDie( export function rollCheckSingleDie(
checkTargetValue: number, checkTargetValue: number,
rollOptions: Partial<RollOptions>, rollOptions: Partial<RollOptions>,
dice: Array<number> = null, dice: Array<number> = [],
): RollResult { ): RollResult {
const usedOptions = new DefaultRollOptions().mergeWith(rollOptions); const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
if (dice?.length != 1) { if (dice.length != 1) {
dice = [new DS4RollProvider().getNextRoll()]; dice = [new DS4RollProvider().getNextRoll()];
} }
const usedDice = dice; const usedDice = dice;
@ -75,13 +75,13 @@ export function rollCheckSingleDie(
export function rollCheckMultipleDice( export function rollCheckMultipleDice(
targetValue: number, targetValue: number,
rollOptions: Partial<RollOptions>, rollOptions: Partial<RollOptions>,
dice: Array<number> = null, dice: Array<number> = [],
): RollResult { ): RollResult {
const usedOptions = new DefaultRollOptions().mergeWith(rollOptions); const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
const remainderTargetValue = targetValue % 20; const remainderTargetValue = targetValue % 20;
const numberOfDice = Math.ceil(targetValue / 20); const numberOfDice = Math.ceil(targetValue / 20);
if (!dice || dice.length != numberOfDice) { if (dice.length != numberOfDice) {
dice = new DS4RollProvider().getNextRolls(numberOfDice); dice = new DS4RollProvider().getNextRolls(numberOfDice);
} }
const usedDice = dice; const usedDice = dice;

View file

@ -0,0 +1,37 @@
const notifications = {
info: (message: string, { permanent = false }: { permanent?: boolean } = {}): void => {
if (ui.notifications) {
ui.notifications.info(message, { permanent });
} else {
console.info(message);
}
},
warn: (message: string, { permanent = false }: { permanent?: boolean } = {}): void => {
if (ui.notifications) {
ui.notifications.warn(message, { permanent });
} else {
console.log(message);
}
},
error: (message: string, { permanent = false }: { permanent?: boolean } = {}): void => {
if (ui.notifications) {
ui.notifications.error(message, { permanent });
} else {
console.warn(message);
}
},
notify: (
message: string,
type: "info" | "warning" | "error" = "info",
{ permanent = false }: { permanent?: boolean } = {},
): void => {
if (ui.notifications) {
ui.notifications.notify(message, type, { permanent });
} else {
const log = { info: console.info, warning: console.warn, error: console.error }[type];
log(message);
}
},
};
export default notifications;

View file

@ -6,7 +6,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": false "strict": true
}, },
"include": ["src"] "include": ["src"]
} }