refactor: convert to ECMAScript where necessary

Also drop @league-of-foundry-developers/foundry-vtt-types.
This commit is contained in:
Johannes Loher 2022-11-17 00:12:29 +01:00
parent df4538f6ed
commit 6277e27056
69 changed files with 1077 additions and 1679 deletions

View file

@ -6,3 +6,5 @@
/.pnp.cjs /.pnp.cjs
/.pnp.loader.mjs /.pnp.loader.mjs
/.yarn/ /.yarn/
client
common

4
.gitignore vendored
View file

@ -32,3 +32,7 @@ junit.xml
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.pnp.* .pnp.*
# foundry
client
common

View file

@ -8,3 +8,5 @@
/.pnp.loader.mjs /.pnp.loader.mjs
/.yarn/ /.yarn/
/.vscode/ /.vscode/
client
common

8
jsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "es2022",
"target": "ES2022"
},
"exclude": ["node_modules", "dist"],
"include": ["src", "client", "common"]
}

3
jsconfig.json.license Normal file
View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 Johannes Loher
SPDX-License-Identifier: MIT

View file

@ -64,18 +64,9 @@
"@commitlint/cli": "17.3.0", "@commitlint/cli": "17.3.0",
"@commitlint/config-conventional": "17.3.0", "@commitlint/config-conventional": "17.3.0",
"@guanghechen/rollup-plugin-copy": "2.1.4", "@guanghechen/rollup-plugin-copy": "2.1.4",
"@league-of-foundry-developers/foundry-vtt-types": "9.280.0",
"@pixi/constants": "6.2.1",
"@pixi/core": "6.2.1",
"@pixi/display": "6.2.1",
"@pixi/graphics": "6.2.1",
"@pixi/math": "6.2.1",
"@pixi/runner": "6.2.1",
"@pixi/settings": "6.2.1",
"@pixi/utils": "6.2.1",
"@rollup/plugin-typescript": "10.0.0",
"@swc/core": "1.3.20", "@swc/core": "1.3.20",
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/jquery": "3.5.14",
"@types/node": "18.11.9", "@types/node": "18.11.9",
"@typescript-eslint/eslint-plugin": "5.44.0", "@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0", "@typescript-eslint/parser": "5.44.0",
@ -85,6 +76,7 @@
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"fs-extra": "10.1.0", "fs-extra": "10.1.0",
"handlebars": "4.7.7",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"prettier": "2.8.0", "prettier": "2.8.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",

View file

@ -1,7 +1,4 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": {
"types": ["@league-of-foundry-developers/foundry-vtt-types"]
},
"include": ["../src", "./"] "include": ["../src", "./"]
} }

View file

@ -3,21 +3,28 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
export class DS4ActiveEffectConfig extends ActiveEffectConfig { export class DS4ActiveEffectConfig extends ActiveEffectConfig {
static override get defaultOptions(): DocumentSheetOptions { /** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
template: "systems/ds4/templates/sheets/active-effect/active-effect-config.hbs", template: "systems/ds4/templates/sheets/active-effect/active-effect-config.hbs",
}); });
} }
override activateListeners(html: JQuery<HTMLElement>): void { /**
* @override
* @param {JQuery} html
*/
activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
const checkbox = html[0]?.querySelector<HTMLInputElement>( const checkbox = html[0]?.querySelector('input[name="flags.ds4.itemEffectConfig.applyToItems"]');
'input[name="flags.ds4.itemEffectConfig.applyToItems"]', checkbox?.addEventListener("change", () => this.#toggleItemEffectConfig(checkbox.checked));
);
checkbox?.addEventListener("change", () => this.toggleItemEffectConfig(checkbox.checked));
} }
private toggleItemEffectConfig(active: boolean) { /**
* Toggle the visibility of the item effect config section
* @param {boolean} active The target state
*/
#toggleItemEffectConfig(active) {
const elements = this.element[0]?.querySelectorAll(".ds4-item-effect-config"); const elements = this.element[0]?.querySelectorAll(".ds4-item-effect-config");
elements?.forEach((element) => { elements?.forEach((element) => {
if (active) { if (active) {

View file

@ -13,15 +13,12 @@ import { notifications } from "../../ui/notifications";
import { enforce, getCanvas, getGame } from "../../utils/utils"; import { enforce, getCanvas, getGame } from "../../utils/utils";
import { disableOverriddenFields } from "../sheet-helpers"; import { disableOverriddenFields } from "../sheet-helpers";
import type { ModifiableDataBaseTotal } from "../../documents/common/common-data";
import type { DS4Settings } from "../../settings";
import type { DS4Item } from "../../documents/item/item";
/** /**
* The base sheet class for all {@link DS4Actor}s. * The base sheet class for all {@link DS4Actor}s.
*/ */
export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetData> { export class DS4ActorSheet extends ActorSheet {
static override get defaultOptions(): ActorSheet.Options { /** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-actor-sheet"], classes: ["sheet", "ds4-actor-sheet"],
height: 635, height: 635,
@ -36,13 +33,15 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
}); });
} }
override get template(): string { /** @override */
get template() {
const basePath = "systems/ds4/templates/sheets/actor"; const basePath = "systems/ds4/templates/sheets/actor";
if (!getGame().user?.isGM && this.actor.limited) return `${basePath}/limited-sheet.hbs`; if (!getGame().user?.isGM && this.actor.limited) return `${basePath}/limited-sheet.hbs`;
return `${basePath}/${this.actor.data.type}-sheet.hbs`; return `${basePath}/${this.actor.data.type}-sheet.hbs`;
} }
override async getData(): Promise<DS4ActorSheetData> { /** @override */
async getData(options) {
const itemsByType = Object.fromEntries( const itemsByType = Object.fromEntries(
Object.entries(this.actor.itemTypes).map(([itemType, items]) => { Object.entries(this.actor.itemTypes).map(([itemType, items]) => {
return [itemType, items.map((item) => item.data).sort((a, b) => (a.sort || 0) - (b.sort || 0))]; return [itemType, items.map((item) => item.data).sort((a, b) => (a.sort || 0) - (b.sort || 0))];
@ -60,7 +59,7 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
const enrichedEffects = await Promise.all(enrichedEffectPromises); const enrichedEffects = await Promise.all(enrichedEffectPromises);
const data = { const data = {
...this.addTooltipsToData(await super.getData()), ...this.addTooltipsToData(await super.getData(options)),
config: DS4, config: DS4,
itemsByType, itemsByType,
enrichedEffects, enrichedEffects,
@ -71,12 +70,14 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Adds tooltips to the attributes, traits, and combatValues of the actor data of the given {@link ActorSheet.Data}. * Adds tooltips to the attributes, traits, and combatValues of the actor data of the given {@link ActorSheet.Data}.
* @param {object} data
* @protected
*/ */
protected addTooltipsToData(data: ActorSheet.Data): ActorSheet.Data { addTooltipsToData(data) {
const valueGroups = [data.data.data.attributes, data.data.data.traits, data.data.data.combatValues]; const valueGroups = [data.data.data.attributes, data.data.data.traits, data.data.data.combatValues];
valueGroups.forEach((valueGroup) => { valueGroups.forEach((valueGroup) => {
Object.values(valueGroup).forEach((attribute: ModifiableDataBaseTotal<number> & { tooltip?: string }) => { Object.values(valueGroup).forEach((attribute) => {
attribute.tooltip = this.getTooltipForValue(attribute); attribute.tooltip = this.getTooltipForValue(attribute);
}); });
}); });
@ -85,8 +86,11 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Generates a tooltip for a given attribute, trait, or combatValue. * Generates a tooltip for a given attribute, trait, or combatValue.
* @param {import("../../documents/common/common-data").ModifiableDataBaseTotal<number>} value The value to get a tooltip for
* @returns {string} The tooltip
* @protected
*/ */
protected getTooltipForValue(value: ModifiableDataBaseTotal<number>): string { getTooltipForValue(value) {
return `${value.base} (${getGame().i18n.localize("DS4.TooltipBaseValue")}) + ${ return `${value.base} (${getGame().i18n.localize("DS4.TooltipBaseValue")}) + ${
value.mod value.mod
} (${getGame().i18n.localize("DS4.TooltipModifier")}) ${getGame().i18n.localize("DS4.TooltipEffects")} ${ } (${getGame().i18n.localize("DS4.TooltipModifier")}) ${getGame().i18n.localize("DS4.TooltipEffects")} ${
@ -94,7 +98,11 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
}`; }`;
} }
override activateListeners(html: JQuery): void { /**
* @param {JQuery} html
* @override
*/
activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.options.editable) return; if (!this.options.editable) return;
@ -123,9 +131,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Handles a click on an element of this sheet to control an embedded item of the actor corresponding to this sheet. * Handles a click on an element of this sheet to control an embedded item of the actor corresponding to this sheet.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onControlItem(event: JQuery.ClickEvent): void { onControlItem(event) {
event.preventDefault(); event.preventDefault();
const a = event.currentTarget; const a = event.currentTarget;
switch (a.dataset["action"]) { switch (a.dataset["action"]) {
@ -141,9 +150,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Creates a new embedded item using the initial data defined in the HTML dataset of the clicked element. * Creates a new embedded item using the initial data defined in the HTML dataset of the clicked element.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onCreateItem(event: JQuery.ClickEvent): void { onCreateItem(event) {
const { type, ...data } = foundry.utils.deepClone(event.currentTarget.dataset); const { type, ...data } = foundry.utils.deepClone(event.currentTarget.dataset);
const name = getGame().i18n.localize(`DS4.New${type.capitalize()}Name`); const name = getGame().i18n.localize(`DS4.New${type.capitalize()}Name`);
const itemData = { const itemData = {
@ -157,9 +167,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Opens the sheet of the embedded item corresponding to the clicked element. * Opens the sheet of the embedded item corresponding to the clicked element.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onEditItem(event: JQuery.ClickEvent): void { onEditItem(event) {
const id = $(event.currentTarget) const id = $(event.currentTarget)
.parents(embeddedDocumentListEntryProperties.Item.selector) .parents(embeddedDocumentListEntryProperties.Item.selector)
.data(embeddedDocumentListEntryProperties.Item.idDataAttribute); .data(embeddedDocumentListEntryProperties.Item.idDataAttribute);
@ -172,9 +183,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Deletes the embedded item corresponding to the clicked element. * Deletes the embedded item corresponding to the clicked element.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onDeleteItem(event: JQuery.ClickEvent): void { onDeleteItem(event) {
const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.Item.selector); const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.Item.selector);
this.actor.deleteEmbeddedDocuments("Item", [li.data(embeddedDocumentListEntryProperties.Item.idDataAttribute)]); this.actor.deleteEmbeddedDocuments("Item", [li.data(embeddedDocumentListEntryProperties.Item.idDataAttribute)]);
li.slideUp(200, () => this.render(false)); li.slideUp(200, () => this.render(false));
@ -184,9 +196,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
* Applies a change to a property of an embedded item depending on the `data-property` attribute of the * Applies a change to a property of an embedded item depending on the `data-property` attribute of the
* {@link HTMLInputElement} that has been changed and its new value. * {@link HTMLInputElement} that has been changed and its new value.
* *
* @param event - The originating change event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onChangeItem(event: JQuery.ChangeEvent): void { onChangeItem(event) {
return this.onChangeEmbeddedDocument(event, "Item"); return this.onChangeEmbeddedDocument(event, "Item");
} }
@ -194,9 +207,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
* Handles a click on an element of this sheet to control an embedded effect of the actor corresponding to this * Handles a click on an element of this sheet to control an embedded effect of the actor corresponding to this
* sheet. * sheet.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onControlEffect(event: JQuery.ClickEvent): void { onControlEffect(event) {
event.preventDefault(); event.preventDefault();
const a = event.currentTarget; const a = event.currentTarget;
switch (a.dataset["action"]) { switch (a.dataset["action"]) {
@ -212,18 +226,20 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Creates a new embedded effect. * Creates a new embedded effect.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onCreateEffect(): void { onCreateEffect() {
DS4ActiveEffect.createDefault(this.actor); DS4ActiveEffect.createDefault(this.actor);
} }
/** /**
* Opens the sheet of the embedded effect corresponding to the clicked element. * Opens the sheet of the embedded effect corresponding to the clicked element.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onEditEffect(event: JQuery.ClickEvent): void { onEditEffect(event) {
const id = $(event.currentTarget) const id = $(event.currentTarget)
.parents(embeddedDocumentListEntryProperties.ActiveEffect.selector) .parents(embeddedDocumentListEntryProperties.ActiveEffect.selector)
.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute); .data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
@ -235,9 +251,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Deletes the embedded item corresponding to the clicked element. * Deletes the embedded item corresponding to the clicked element.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onDeleteEffect(event: JQuery.ClickEvent): void { onDeleteEffect(event) {
const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.ActiveEffect.selector); const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.ActiveEffect.selector);
const id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute); const id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
this.actor.deleteEmbeddedDocuments("ActiveEffect", [id]); this.actor.deleteEmbeddedDocuments("ActiveEffect", [id]);
@ -248,9 +265,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
* Applies a change to a property of an embedded effect depending on the `data-property` attribute of the * Applies a change to a property of an embedded effect depending on the `data-property` attribute of the
* {@link HTMLInputElement} that has been changed and its new value. * {@link HTMLInputElement} that has been changed and its new value.
* *
* @param event - The originating change event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onChangeEffect(event: JQuery.ChangeEvent): void { onChangeEffect(event) {
return this.onChangeEmbeddedDocument(event, "ActiveEffect"); return this.onChangeEmbeddedDocument(event, "ActiveEffect");
} }
@ -258,10 +276,11 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
* Applies a change to a property of an embedded document of the actor belonging to this sheet. The change depends * Applies a change to a property of an embedded document of the actor belonging to this sheet. The change depends
* on the `data-property` attribute of the {@link HTMLInputElement} that has been changed and its new value. * on the `data-property` attribute of the {@link HTMLInputElement} that has been changed and its new value.
* *
* @param event - The originating change event * @param {JQuery.ChangeEvent} event The originating click event
* @param documentName - The name of the embedded document to be changed. * @param {"Item" | "ActiveEffect"} documentName The name of the embedded document to be changed.
* @protected
*/ */
protected onChangeEmbeddedDocument(event: JQuery.ChangeEvent, documentName: "Item" | "ActiveEffect"): void { onChangeEmbeddedDocument(event, documentName) {
event.preventDefault(); event.preventDefault();
const element = $(event.currentTarget).get(0); const element = $(event.currentTarget).get(0);
enforce(element instanceof HTMLInputElement); enforce(element instanceof HTMLInputElement);
@ -284,17 +303,19 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
* - text input: `string` * - text input: `string`
* - number: `number` * - number: `number`
* *
* @param element - The input element to parse the value from * @param {HTMLInputElement} element The input element to parse the value from
* @returns {boolean | string | number} The parsed data
* @protected
*/ */
protected parseValue(element: HTMLInputElement): boolean | string | number { parseValue(element) {
switch (element.type) { switch (element.type) {
case "checkbox": { case "checkbox": {
const inverted = Boolean(element.dataset["inverted"]); const inverted = Boolean(element.dataset["inverted"]);
const value: boolean = element.checked; const value = element.checked;
return inverted ? !value : value; return inverted ? !value : value;
} }
case "text": { case "text": {
const value: string = element.value; const value = element.value;
return value; return value;
} }
case "number": { case "number": {
@ -311,9 +332,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Handle clickable item rolls. * Handle clickable item rolls.
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onRollItem(event: JQuery.ClickEvent): void { onRollItem(event) {
event.preventDefault(); event.preventDefault();
const id = $(event.currentTarget) const id = $(event.currentTarget)
.parents(embeddedDocumentListEntryProperties.Item.selector) .parents(embeddedDocumentListEntryProperties.Item.selector)
@ -325,17 +347,22 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Handle clickable check rolls. * Handle clickable check rolls.
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onRollCheck(event: JQuery.ClickEvent): void { onRollCheck(event) {
event.preventDefault(); event.preventDefault();
event.currentTarget.blur(); event.currentTarget.blur();
const check = event.currentTarget.dataset["check"]; const check = event.currentTarget.dataset["check"];
this.actor.rollCheck(check).catch((e) => notifications.error(e, { log: true })); this.actor.rollCheck(check).catch((e) => notifications.error(e, { log: true }));
} }
override _onDragStart(event: DragEvent): void { /**
const target = event.currentTarget as HTMLElement; * @param {DragEvent} event
* @override
*/
_onDragStart(event) {
const target = event.currentTarget;
if (!(target instanceof HTMLElement)) return super._onDragStart(event); if (!(target instanceof HTMLElement)) return super._onDragStart(event);
const check = target.dataset.check; const check = target.dataset.check;
@ -356,9 +383,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/** /**
* Sort items according to the item list header that has been clicked. * Sort items according to the item list header that has been clicked.
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onSortItems(event: JQuery.ClickEvent<unknown, unknown, HTMLElement>): void { onSortItems(event) {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const type = target.parentElement?.dataset["type"]; const type = target.parentElement?.dataset["type"];
@ -369,26 +397,28 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
const items = this.actor.items.filter((item) => item.type === type); const items = this.actor.items.filter((item) => item.type === type);
items.sort((a, b) => a.data.sort - b.data.sort); items.sort((a, b) => a.data.sort - b.data.sort);
const sortFunction = /**
(invert: boolean) => * @param {boolean} invert Whether or not to inverse the sort order
(a: DS4Item, b: DS4Item): number => { * @returns {(a: import("../../documents/item/item").DS4Item, b: import("../../documents/item/item").DS4Item) => number} A function for sorting items
*/
const sortFunction = (invert) => (a, b) => {
const propertyA = getProperty(a.data, dataPath);
const propertyB = getProperty(b.data, dataPath);
const comparison =
typeof propertyA === "string" || typeof propertyB === "string"
? compareAsStrings(propertyA, propertyB, invert)
: compareAsNumbers(propertyA, propertyB, invert);
if (comparison === 0 && dataPath2 !== undefined) {
const propertyA = getProperty(a.data, dataPath); const propertyA = getProperty(a.data, dataPath);
const propertyB = getProperty(b.data, dataPath); const propertyB = getProperty(b.data, dataPath);
const comparison = return typeof propertyA === "string" || typeof propertyB === "string"
typeof propertyA === "string" || typeof propertyB === "string" ? compareAsStrings(propertyA, propertyB, invert)
? compareAsStrings(propertyA, propertyB, invert) : compareAsNumbers(propertyA, propertyB, invert);
: compareAsNumbers(propertyA, propertyB, invert); }
if (comparison === 0 && dataPath2 !== undefined) { return comparison;
const propertyA = getProperty(a.data, dataPath); };
const propertyB = getProperty(b.data, dataPath);
return typeof propertyA === "string" || typeof propertyB === "string"
? compareAsStrings(propertyA, propertyB, invert)
: compareAsNumbers(propertyA, propertyB, invert);
}
return comparison;
};
const sortedItems = [...items].sort(sortFunction(false)); const sortedItems = [...items].sort(sortFunction(false));
const wasSortedAlready = !sortedItems.find((item, index) => item !== items[index]); const wasSortedAlready = !sortedItems.find((item, index) => item !== items[index]);
@ -405,7 +435,12 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
this.actor.updateEmbeddedDocuments("Item", updates); this.actor.updateEmbeddedDocuments("Item", updates);
} }
protected override async _onDropItem(event: DragEvent, data: ActorSheet.DropData.Item): Promise<unknown> { /**
* @param {DragEvent} event
* @param {object} data
* @override
*/
async _onDropItem(event, data) {
const item = await Item.fromDropData(data); const item = await Item.fromDropData(data);
if (item && !this.actor.canOwnItemType(item.data.type)) { if (item && !this.actor.canOwnItemType(item.data.type)) {
notifications.warn( notifications.warn(
@ -422,19 +457,6 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
} }
} }
interface DS4ActorSheetData extends ActorSheet.Data {
config: typeof DS4;
itemsByType: Record<string, foundry.data.ItemData[]>;
enrichedEffects: EnrichedActiveEffectDataSource[];
settings: DS4Settings;
}
type ActiveEffectDataSource = foundry.data.ActiveEffectData["_source"];
interface EnrichedActiveEffectDataSource extends ActiveEffectDataSource {
sourceName: string;
}
/** /**
* This object contains information about specific properties embedded document list entries for each different type. * This object contains information about specific properties embedded document list entries for each different type.
*/ */
@ -449,10 +471,24 @@ const embeddedDocumentListEntryProperties = Object.freeze({
}, },
}); });
const compareAsStrings = (a: { toString(): string }, b: { toString(): string }, invert: boolean): number => { /**
* Compare two stringifiables as strings.
* @param {{ toString(): string }} a The thing to compare with
* @param {{ toString(): string }} b The thing to compare
* @param {boolean} invert Should the comparison be inverted?
* @return {number} A number that indicates the result of the comparison
*/
const compareAsStrings = (a, b, invert) => {
return invert ? b.toString().localeCompare(a.toString()) : a.toString().localeCompare(b.toString()); return invert ? b.toString().localeCompare(a.toString()) : a.toString().localeCompare(b.toString());
}; };
const compareAsNumbers = (a: number, b: number, invert: boolean): number => { /**
* Compare two number.
* @param {number} a The number to compare with
* @param {number} b The number to compare
* @param {boolean} invert Should the comparison be inverted?
* @return {number} A number that indicates the result of the comparison
*/
const compareAsNumbers = (a, b, invert) => {
return invert ? b - a : a - b; return invert ? b - a : a - b;
}; };

View file

@ -8,7 +8,7 @@ import { DS4ActorSheet } from "./base-sheet";
* The Sheet class for DS4 Character Actors * The Sheet class for DS4 Character Actors
*/ */
export class DS4CharacterActorSheet extends DS4ActorSheet { export class DS4CharacterActorSheet extends DS4ActorSheet {
static override get defaultOptions(): ActorSheet.Options { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-actor-sheet", "ds4-character-sheet"], classes: ["sheet", "ds4-actor-sheet", "ds4-character-sheet"],
}); });

View file

@ -8,7 +8,7 @@ import { DS4ActorSheet } from "./base-sheet";
* The Sheet class for DS4 Creature Actors * The Sheet class for DS4 Creature Actors
*/ */
export class DS4CreatureActorSheet extends DS4ActorSheet { export class DS4CreatureActorSheet extends DS4ActorSheet {
static override get defaultOptions(): ActorSheet.Options { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-actor-sheet", "ds4-creature-sheet"], classes: ["sheet", "ds4-actor-sheet", "ds4-creature-sheet"],
}); });

View file

@ -2,18 +2,20 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
/**
* @typedef {DialogOptions} DialogWithListenersOptions
* @property {(html: JQuery, app: DialogWithListeners) => void} [activateAdditionalListeners] An optional function to attach additional listeners to the dialog
*/
/** /**
* A simple extension to the {@link Dialog} class that allows attaching additional listeners. * A simple extension to the {@link Dialog} class that allows attaching additional listeners.
*/ */
export class DialogWithListeners extends Dialog<DialogWithListenersOptions> { export class DialogWithListeners extends Dialog {
override activateListeners(html: JQuery): void { /** @override */
activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (this.options.activateAdditionalListeners !== undefined) { if (this.options.activateAdditionalListeners !== undefined) {
this.options.activateAdditionalListeners(html, this); this.options.activateAdditionalListeners(html, this);
} }
} }
} }
interface DialogWithListenersOptions extends DialogOptions {
activateAdditionalListeners?: ((html: JQuery, app: DialogWithListeners) => void) | undefined;
}

View file

@ -14,8 +14,9 @@ import { disableOverriddenFields } from "./sheet-helpers";
/** /**
* The Sheet class for DS4 Items * The Sheet class for DS4 Items
*/ */
export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData> { export class DS4ItemSheet extends ItemSheet {
static override get defaultOptions(): ItemSheet.Options { /** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-item-sheet"], classes: ["sheet", "ds4-item-sheet"],
height: 400, height: 400,
@ -25,12 +26,14 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
}); });
} }
override get template(): string { /** @override */
get template() {
const basePath = "systems/ds4/templates/sheets/item"; const basePath = "systems/ds4/templates/sheets/item";
return `${basePath}/${this.item.data.type}-sheet.hbs`; return `${basePath}/${this.item.data.type}-sheet.hbs`;
} }
override async getData(): Promise<DS4ItemSheetData> { /** @override */
async getData() {
const data = { const data = {
...(await super.getData()), ...(await super.getData()),
config: DS4, config: DS4,
@ -41,7 +44,8 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
return data; return data;
} }
override _getSubmitData(updateData = {}) { /** @override */
_getSubmitData(updateData = {}) {
const data = super._getSubmitData(updateData); const data = super._getSubmitData(updateData);
// Prevent submitting overridden values // Prevent submitting overridden values
const overrides = foundry.utils.flattenObject(this.item.overrides); const overrides = foundry.utils.flattenObject(this.item.overrides);
@ -51,9 +55,8 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
return data; return data;
} }
override setPosition( /** @override */
options: Partial<Application.Position> = {}, setPosition(options = {}) {
): (Application.Position & { height: number }) | void {
const position = super.setPosition(options); const position = super.setPosition(options);
if (position) { if (position) {
const sheetBody = this.element.find(".sheet-body"); const sheetBody = this.element.find(".sheet-body");
@ -64,7 +67,11 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
return position; return position;
} }
override activateListeners(html: JQuery): void { /**
* @override
* @param {JQuery} html
*/
activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.options.editable) return; if (!this.options.editable) return;
@ -78,9 +85,10 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
* Handles a click on an element of this sheet to control an embedded effect of the item corresponding to this * Handles a click on an element of this sheet to control an embedded effect of the item corresponding to this
* sheet. * sheet.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onControlEffect(event: JQuery.ClickEvent): void { onControlEffect(event) {
event.preventDefault(); event.preventDefault();
if (this.item.isOwned) { if (this.item.isOwned) {
return notifications.warn(getGame().i18n.localize("DS4.WarningManageActiveEffectOnOwnedItem")); return notifications.warn(getGame().i18n.localize("DS4.WarningManageActiveEffectOnOwnedItem"));
@ -98,17 +106,19 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
/** /**
* Creates a new embedded effect. * Creates a new embedded effect.
* @protected
*/ */
protected onCreateEffect(): void { onCreateEffect() {
DS4ActiveEffect.createDefault(this.item); DS4ActiveEffect.createDefault(this.item);
} }
/** /**
* Opens the sheet of the embedded effect corresponding to the clicked element. * Opens the sheet of the embedded effect corresponding to the clicked element.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @porotected
*/ */
protected onEditEffect(event: JQuery.ClickEvent): void { onEditEffect(event) {
const id = $(event.currentTarget) const id = $(event.currentTarget)
.parents(embeddedDocumentListEntryProperties.ActiveEffect.selector) .parents(embeddedDocumentListEntryProperties.ActiveEffect.selector)
.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute); .data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
@ -120,9 +130,10 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
/** /**
* Deletes the embedded item corresponding to the clicked element. * Deletes the embedded item corresponding to the clicked element.
* *
* @param event - The originating click event * @param {JQuery.ClickEvent} event The originating click event
* @protected
*/ */
protected onDeleteEffect(event: JQuery.ClickEvent): void { onDeleteEffect(event) {
const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.ActiveEffect.selector); const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.ActiveEffect.selector);
const id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute); const id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
this.item.deleteEmbeddedDocuments("ActiveEffect", [id]); this.item.deleteEmbeddedDocuments("ActiveEffect", [id]);
@ -130,13 +141,6 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
} }
} }
interface DS4ItemSheetData extends ItemSheet.Data<ItemSheet.Options> {
config: typeof DS4;
isOwned: boolean;
actor: DS4ItemSheet["item"]["actor"];
isPhysical: boolean;
}
/** /**
* This object contains information about specific properties embedded document list entries for each different type. * This object contains information about specific properties embedded document list entries for each different type.
*/ */

View file

@ -4,11 +4,13 @@
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
export function disableOverriddenFields( /**
form: HTMLElement | null, * Disable elements in the given form that match the selector returned for overridden properties.
overrides: Record<string, unknown>, * @param {HTMLElement | null} form The form in which to disable fields
selector: (key: string) => string, * @param {Record<string, unknown>} overrides The set of overrides of the underlying document
): void { * @param {(key: string) => string} selector A function that generates a selector, based on a property key
*/
export function disableOverriddenFields(form, overrides, selector) {
const inputs = ["INPUT", "SELECT", "TEXTAREA", "BUTTON"]; const inputs = ["INPUT", "SELECT", "TEXTAREA", "BUTTON"];
const titleAddition = `(${getGame().i18n.localize("DS4.TooltipNotEditableDueToEffects")})`; const titleAddition = `(${getGame().i18n.localize("DS4.TooltipNotEditableDueToEffects")})`;

View file

@ -99,7 +99,13 @@ function shouldUseCoupForLastSubCheck(
); );
} }
interface SubCheckResult extends DieWithSubCheck, DiceTerm.Result {} interface SubCheckResult extends DieWithSubCheck {
active?: boolean;
discarded?: boolean;
success?: boolean;
failure?: boolean;
count?: number;
}
function evaluateDiceWithSubChecks( function evaluateDiceWithSubChecks(
results: DieWithSubCheck[], results: DieWithSubCheck[],

View file

@ -7,70 +7,96 @@ import { DialogWithListeners } from "../apps/dialog-with-listeners";
import { DS4 } from "../config"; import { DS4 } from "../config";
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
/** /** @typedef {"publicroll" | "gmroll" | "gmroll" | "selfroll"} RollModes */
* Provides default values for all arguments the `CheckFactory` expects.
*/
class DefaultCheckOptions implements DS4CheckFactoryOptions {
readonly maximumCoupResult = 1;
readonly minimumFumbleResult = 20;
readonly useSlayingDice = false;
readonly rollMode: keyof CONFIG.Dice.RollModes = "publicroll";
readonly flavor: undefined;
mergeWith(other: Partial<DS4CheckFactoryOptions>): DS4CheckFactoryOptions {
return { ...this, ...other };
}
}
/**
* 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. * Most basic class responsible for generating the chat formula and passing it to the chat as roll.
*/ */
class CheckFactory { class CheckFactory {
constructor( /**
private checkTargetNumber: number, * @param {number} checkTargetNumber The check target number for this check factory
private checkModifier: number, * @param {number} checkModifier The check modifier for this check factory
options: Partial<DS4CheckFactoryOptions> = {}, * @param {Partial<DS4CheckFactoryOptions>} [options] Options for this check factory
) { */
this.options = defaultCheckOptions.mergeWith(options); constructor(checkTargetNumber, checkModifier, options = {}) {
this.#checkTargetNumber = checkTargetNumber;
this.#checkModifier = checkModifier;
this.#options = foundry.utils.mergeObject(this.constructor.defaultOptions, options);
} }
private options: DS4CheckFactoryOptions; /**
* The check target number for this check factory.
* @type {number}
*/
#checkTargetNumber;
async execute(): Promise<ChatMessage | undefined> { /**
* The check modifier for this check factory.
* @type {number}
*/
#checkModifier;
/**
* The options for this check factory.
* @type {DS4CheckFactoryOptions}
*/
#options;
/**
* The default options of thos CheckFactory class. Upon instantiation, they are merged with the explicitly provided options.
* @type {DS4CheckFactoryOptions}
*/
static get defaultOptions() {
return {
maximumCoupResult: 1,
minimumFumbleResult: 20,
useSlayingDice: false,
rollMode: "publicroll",
};
}
/**
* Execute this check factory.
* @returns {Promise<ChatMessage | undefined>} A promise that resolves to the created chat message for the roll */
async execute() {
const innerFormula = ["ds", this.createCheckTargetNumberModifier(), this.createCoupFumbleModifier()].filterJoin( const innerFormula = ["ds", this.createCheckTargetNumberModifier(), this.createCoupFumbleModifier()].filterJoin(
"", "",
); );
const formula = this.options.useSlayingDice ? `{${innerFormula}}x` : innerFormula; const formula = this.#options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
const roll = Roll.create(formula); const roll = Roll.create(formula);
const speaker = this.options.speaker ?? ChatMessage.getSpeaker(); const speaker = this.#options.speaker ?? ChatMessage.getSpeaker();
return roll.toMessage( return roll.toMessage(
{ {
speaker, speaker,
flavor: this.options.flavor, flavor: this.#options.flavor,
flags: this.options.flavorData ? { ds4: { flavorData: this.options.flavorData } } : undefined, flags: this.#options.flavorData ? { ds4: { flavorData: this.#options.flavorData } } : undefined,
}, },
{ rollMode: this.options.rollMode, create: true }, { rollMode: this.#options.rollMode, create: true },
); );
} }
createCheckTargetNumberModifier(): string { /**
const totalCheckTargetNumber = this.checkTargetNumber + this.checkModifier; * Create the check target number modifier for this roll.
return totalCheckTargetNumber >= 0 ? `v${this.checkTargetNumber + this.checkModifier}` : "v0"; * @returns string
*/
createCheckTargetNumberModifier() {
const totalCheckTargetNumber = this.#checkTargetNumber + this.#checkModifier;
return totalCheckTargetNumber >= 0 ? `v${this.#checkTargetNumber + this.#checkModifier}` : "v0";
} }
createCoupFumbleModifier(): string | null { /**
* Create the coup fumble modifier for this roll.
* @returns {string | null}
*/
createCoupFumbleModifier() {
const isMinimumFumbleResultRequired = const isMinimumFumbleResultRequired =
this.options.minimumFumbleResult !== defaultCheckOptions.minimumFumbleResult; this.#options.minimumFumbleResult !== this.constructor.defaultOptions.minimumFumbleResult;
const isMaximumCoupResultRequired = this.options.maximumCoupResult !== defaultCheckOptions.maximumCoupResult; const isMaximumCoupResultRequired =
this.#options.maximumCoupResult !== this.constructor.defaultOptions.maximumCoupResult;
if (isMinimumFumbleResultRequired || isMaximumCoupResultRequired) { if (isMinimumFumbleResultRequired || isMaximumCoupResultRequired) {
return `c${this.options.maximumCoupResult ?? ""}:${this.options.minimumFumbleResult ?? ""}`; return `c${this.#options.maximumCoupResult ?? ""}:${this.#options.minimumFumbleResult ?? ""}`;
} else { } else {
return null; return null;
} }
@ -79,19 +105,18 @@ class CheckFactory {
/** /**
* Asks the user for all unknown/necessary information and passes them on to perform a roll. * Asks the user for all unknown/necessary information and passes them on to perform a roll.
* @param checkTargetNumber - The Check Target Number ("CTN") * @param {number} checkTargetNumber The Check Target Number ("CTN")
* @param options - Options changing the behavior of the roll and message. * @param {Partial<DS4CheckFactoryOptions>} [options={}] Options changing the behavior of the roll and message.
* @returns {Promise<ChateMessage|undefined>} A promise that resolves to the chat message created by the roll
*/ */
export async function createCheckRoll( export async function createCheckRoll(checkTargetNumber, options = {}) {
checkTargetNumber: number,
options: Partial<DS4CheckFactoryOptions> = {},
): Promise<ChatMessage | unknown> {
// Ask for additional required data; // Ask for additional required data;
const interactiveRollData = await askForInteractiveRollData(checkTargetNumber, options); const interactiveRollData = await askForInteractiveRollData(checkTargetNumber, options);
const newTargetValue = interactiveRollData.checkTargetNumber ?? checkTargetNumber; const newTargetValue = interactiveRollData.checkTargetNumber ?? checkTargetNumber;
const checkModifier = interactiveRollData.checkModifier ?? 0; const checkModifier = interactiveRollData.checkModifier ?? 0;
const newOptions: Partial<DS4CheckFactoryOptions> = { /** @type {Partial<DS4CheckFactoryOptions>} */
const newOptions = {
maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult, maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult,
minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult, minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult,
useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"), useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
@ -117,26 +142,25 @@ export async function createCheckRoll(
* @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. * @param {number} checkTargetNumber The check target number
* @param {Partial<DS4CheckFactoryOptions>} [options={}] Predefined roll options
* @param {template?: string | undefined; title?: string | undefined} [additionalOptions={}] Additional options to use for the dialog
* @returns {Promise<Partial<IntermediateInteractiveRollData>>} A promise that resolves to the data given by the user.
*/ */
async function askForInteractiveRollData( async function askForInteractiveRollData(checkTargetNumber, options = {}, { template, title } = {}) {
checkTargetNumber: number,
options: Partial<DS4CheckFactoryOptions> = {},
{ template, title }: { template?: string; title?: string } = {},
): 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 id = foundry.utils.randomID(); const id = foundry.utils.randomID();
const templateData = { const templateData = {
title: usedTitle, title: usedTitle,
checkTargetNumber: checkTargetNumber, checkTargetNumber: checkTargetNumber,
maximumCoupResult: options.maximumCoupResult ?? defaultCheckOptions.maximumCoupResult, maximumCoupResult: options.maximumCoupResult ?? this.constructor.defaultOptions.maximumCoupResult,
minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult, minimumFumbleResult: options.minimumFumbleResult ?? this.constructor.defaultOptions.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]) => { checkModifiers: Object.entries(DS4.i18n.checkModifiers).map(([key, translation]) => {
if (key in DS4.checkModifiers) { if (key in DS4.checkModifiers) {
const value = DS4.checkModifiers[key as keyof typeof DS4.checkModifiers]; const value = DS4.checkModifiers[key];
const label = `${translation} (${value >= 0 ? `+${value}` : value})`; const label = `${translation} (${value >= 0 ? `+${value}` : value})`;
return { value, label }; return { value, label };
} }
@ -146,7 +170,7 @@ async function askForInteractiveRollData(
}; };
const renderedHtml = await renderTemplate(usedTemplate, templateData); const renderedHtml = await renderTemplate(usedTemplate, templateData);
const dialogPromise = new Promise<HTMLFormElement>((resolve) => { const dialogPromise = new Promise((resolve) => {
new DialogWithListeners( new DialogWithListeners(
{ {
title: usedTitle, title: usedTitle,
@ -190,7 +214,7 @@ async function askForInteractiveRollData(
.parent(".form-group"); .parent(".form-group");
html.find(`#check-modifier-${id}`).on("change", (event) => { html.find(`#check-modifier-${id}`).on("change", (event) => {
if ( if (
(event.currentTarget as HTMLSelectElement).value === "custom" && event.currentTarget.value === "custom" &&
checkModifierCustomFormGroup.hasClass("ds4-hidden") checkModifierCustomFormGroup.hasClass("ds4-hidden")
) { ) {
checkModifierCustomFormGroup.removeClass("ds4-hidden"); checkModifierCustomFormGroup.removeClass("ds4-hidden");
@ -211,9 +235,10 @@ async function askForInteractiveRollData(
/** /**
* Extracts Dialog data from the returned DOM element. * Extracts Dialog data from the returned DOM element.
* @param formData - The filed dialog * @param {HTMLFormElement} formData The filed dialog
* @returns {Partial<IntermediateInteractiveRollData>}
*/ */
function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateInteractiveRollData> { function parseDialogFormData(formData) {
const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value); const chosenCheckTargetNumber = parseInt(formData["check-target-number"]?.value);
const chosenCheckModifier = formData["check-modifier"]?.value; const chosenCheckModifier = formData["check-modifier"]?.value;
@ -237,11 +262,10 @@ function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateInt
/** /**
* Contains data that needs retrieval from an interactive Dialog. * Contains data that needs retrieval from an interactive Dialog.
* @typedef {object} InteractiveRollData
* @property {number} checkModifier
* @property {RollModes} rollMode
*/ */
interface InteractiveRollData {
checkModifier: number;
rollMode: keyof CONFIG.Dice.RollModes;
}
/** /**
* Contains *CURRENTLY* necessary Data for drafting a roll. * Contains *CURRENTLY* necessary Data for drafting a roll.
@ -255,23 +279,29 @@ interface InteractiveRollData {
* 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 `InteractiveRollData`. * This interface should then be replaced with the {@link InteractiveRollData}.
* @typedef {object} IntermediateInteractiveRollData
* @property {number} checkTargetNumber
* @property {number} maximumCoupResult
* @property {number} minimumFumbleResult
*/ */
interface IntermediateInteractiveRollData extends InteractiveRollData {
checkTargetNumber: number;
maximumCoupResult: number;
minimumFumbleResult: number;
}
/** /**
* The minimum behavioral options that need to be passed to the factory. * The minimum behavioral options that need to be passed to the factory.
* @typedef {object} DS4CheckFactoryOptions
* @property {number} maximumCoupResult
* @property {number} minimumFumbleResult
* @property {boolean} useSlayingDice
* @property {RollModes} rollMode
* @property {string} [flavor]
* @property {Record<string, string | number | null>} [flavorData]
* @property {ChatSpeakerData} [speaker]
*/
/**
* @typedef {object} ChatSpeakerData
* @property {string | null} [scene]
* @property {string | null} [actor]
* @property {string | null} [token]
* @property {string | null} [alias]
*/ */
export interface DS4CheckFactoryOptions {
maximumCoupResult: number;
minimumFumbleResult: number;
useSlayingDice: boolean;
rollMode: keyof CONFIG.Dice.RollModes;
flavor?: string;
flavorData?: Record<string, string | number | null>;
speaker?: ReturnType<typeof ChatMessage.getSpeaker>;
}

View file

@ -16,7 +16,7 @@ import { evaluateCheck, getRequiredNumberOfDice } from "./check-evaluation";
* - Roll a check with a racial ability that makes `5` a coup and default fumble: `/r dsv19c5` * - Roll a check with a racial ability that makes `5` a coup and default fumble: `/r dsv19c5`
*/ */
export class DS4Check extends DiceTerm { export class DS4Check extends DiceTerm {
constructor({ modifiers = [], results = [], options }: Partial<DiceTerm.TermData> = {}) { constructor({ modifiers = [], results = [], options } = {}) {
super({ super({
faces: 20, faces: 20,
results, results,
@ -65,34 +65,44 @@ export class DS4Check extends DiceTerm {
} }
} }
coup: boolean | null = null; /** @type {boolean | null} */
fumble: boolean | null = null; coup = null;
/** @type {boolean | null} */
fumble = null;
canFumble = true; canFumble = true;
checkTargetNumber = DS4Check.DEFAULT_CHECK_TARGET_NUMBER; checkTargetNumber = DS4Check.DEFAULT_CHECK_TARGET_NUMBER;
minimumFumbleResult = DS4Check.DEFAULT_MINIMUM_FUMBLE_RESULT; minimumFumbleResult = DS4Check.DEFAULT_MINIMUM_FUMBLE_RESULT;
maximumCoupResult = DS4Check.DEFAULT_MAXIMUM_COUP_RESULT; maximumCoupResult = DS4Check.DEFAULT_MAXIMUM_COUP_RESULT;
override get expression(): string { /** @override */
get expression() {
return `ds${this.modifiers.join("")}`; return `ds${this.modifiers.join("")}`;
} }
override get total(): string | number | null | undefined { /** @override */
get total() {
if (this.fumble) return 0; if (this.fumble) return 0;
return super.total; return super.total;
} }
override _evaluateSync({ minimize = false, maximize = false } = {}): this { /** @override */
_evaluateSync({ minimize = false, maximize = false } = {}) {
super._evaluateSync({ minimize, maximize }); super._evaluateSync({ minimize, maximize });
this.evaluateResults(); this.evaluateResults();
return this; return this;
} }
override roll({ minimize = false, maximize = false } = {}): DiceTerm.Result { /** @override */
roll({ minimize = false, maximize = false } = {}) {
// Swap minimize / maximize because in DS4, the best possible roll is a 1 and the worst possible roll is a 20 // Swap minimize / maximize because in DS4, the best possible roll is a 1 and the worst possible roll is a 20
return super.roll({ minimize: maximize, maximize: minimize }); return super.roll({ minimize: maximize, maximize: minimize });
} }
evaluateResults(): void { /**
* Perform additional evaluation after the individual results have been evaluated.
* @protected
*/
evaluateResults() {
const dice = this.results.map((die) => die.result); const dice = this.results.map((die) => die.result);
const results = evaluateCheck(dice, this.checkTargetNumber, { const results = evaluateCheck(dice, this.checkTargetNumber, {
maximumCoupResult: this.maximumCoupResult, maximumCoupResult: this.maximumCoupResult,
@ -108,18 +118,19 @@ export class DS4Check extends DiceTerm {
* @remarks "min" and "max" are filtered out because they are irrelevant for * @remarks "min" and "max" are filtered out because they are irrelevant for
* {@link DS4Check}s and only result in some dice rolls being highlighted * {@link DS4Check}s and only result in some dice rolls being highlighted
* incorrectly. * incorrectly.
* @override
*/ */
override getResultCSS(result: DiceTerm.Result): (string | null)[] { getResultCSS(result) {
return super.getResultCSS(result).filter((cssClass) => cssClass !== "min" && cssClass !== "max"); return super.getResultCSS(result).filter((cssClass) => cssClass !== "min" && cssClass !== "max");
} }
static readonly DEFAULT_CHECK_TARGET_NUMBER = 10; static DEFAULT_CHECK_TARGET_NUMBER = 10;
static readonly DEFAULT_MAXIMUM_COUP_RESULT = 1; static DEFAULT_MAXIMUM_COUP_RESULT = 1;
static readonly DEFAULT_MINIMUM_FUMBLE_RESULT = 20; static DEFAULT_MINIMUM_FUMBLE_RESULT = 20;
static override DENOMINATION = "s"; static DENOMINATION = "s";
static override MODIFIERS = { static MODIFIERS = {
c: (): void => undefined, // Modifier is consumed in constructor for maximumCoupResult / minimumFumbleResult c: () => undefined, // Modifier is consumed in constructor for maximumCoupResult / minimumFumbleResult
v: (): void => undefined, // Modifier is consumed in constructor for checkTargetNumber v: () => undefined, // Modifier is consumed in constructor for checkTargetNumber
n: (): void => undefined, // Modifier is consumed in constructor for canFumble n: () => undefined, // Modifier is consumed in constructor for canFumble
}; };
} }

View file

@ -5,19 +5,17 @@
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
import { DS4Check } from "./check"; import { DS4Check } from "./check";
export class DS4Roll<D extends Record<string, unknown> = Record<string, unknown>> extends Roll<D> { export class DS4Roll extends Roll {
static override CHAT_TEMPLATE = "systems/ds4/templates/dice/roll.hbs"; /** @override */
static CHAT_TEMPLATE = "systems/ds4/templates/dice/roll.hbs";
/** /**
* @override
* @remarks * @remarks
* This only differs from {@link Roll#render} in that it provides `isCoup` and `isFumble` properties to the roll * This only differs from {@link Roll#render} in that it provides `isCoup` and `isFumble` properties to the roll
* template if the first dice term is a ds4 check. * template if the first dice term is a ds4 check.
*/ */
override async render({ async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false } = {}) {
flavor,
template = (this.constructor as typeof DS4Roll).CHAT_TEMPLATE,
isPrivate = false,
}: Parameters<Roll["render"]>[0] = {}): Promise<string> {
if (!this._evaluated) await this.evaluate({ async: true }); if (!this._evaluated) await this.evaluate({ async: true });
const firstDiceTerm = this.dice[0]; const firstDiceTerm = this.dice[0];
const isCoup = firstDiceTerm instanceof DS4Check && firstDiceTerm.coup; const isCoup = firstDiceTerm instanceof DS4Check && firstDiceTerm.coup;

View file

@ -6,11 +6,15 @@
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
import { DS4Check } from "./check"; import { DS4Check } from "./check";
export function registerSlayingDiceModifier(): void { export function registerSlayingDiceModifier() {
PoolTerm.MODIFIERS.x = slay; PoolTerm.MODIFIERS.x = slay;
} }
function slay(this: PoolTerm, modifier: string): void { /**
* @this {PoolTerm}
* @param {string} modifier
*/
function slay(modifier) {
const rgx = /[xX]/; const rgx = /[xX]/;
const match = modifier.match(rgx); const match = modifier.match(rgx);
if (!match || !this.rolls) return; if (!match || !this.rolls) return;

View file

@ -5,27 +5,22 @@
import { mathEvaluator } from "../expression-evaluation/evaluator"; import { mathEvaluator } from "../expression-evaluation/evaluator";
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
import type { DS4Actor } from "./actor/actor"; /**
import type { DS4Item } from "./item/item"; * @typedef {object} ItemEffectConfig
* @property {boolean} [applyToItems] Whether or not to apply this effect to owned items instead of the actor
* @property {string} [itemName] Only apply this effect to items with this name
* @property {string} [condition] Only apply this effect to items where this condition is fullfilled
*/
declare global { /**
interface DocumentClassConfig { * @typedef {object} DS4ActiveEffectFlags
ActiveEffect: typeof DS4ActiveEffect; * @property {ItemEffectConfig} [itemEffectConfig] Configuration for applying this effect to owned items
} */
interface FlagConfig {
ActiveEffect: {
ds4?: {
itemEffectConfig?: {
applyToItems?: boolean;
itemName?: string;
condition?: string;
};
};
};
}
}
type PromisedType<T> = T extends Promise<infer U> ? U : T; /**
* @typedef {Record<string, unknown>} ActiveEffectFlags
* @property {DS4ActiveEffectFlags} [ds4] Flags for DS4
*/
export class DS4ActiveEffect extends ActiveEffect { export class DS4ActiveEffect extends ActiveEffect {
/** /**
@ -35,13 +30,16 @@ export class DS4ActiveEffect extends ActiveEffect {
/** /**
* A cached reference to the source document to avoid recurring database lookups * A cached reference to the source document to avoid recurring database lookups
* @type {foundry.abstract.Document | undefined | null}
* @protected
*/ */
protected source: PromisedType<ReturnType<typeof fromUuid>> | undefined = undefined; source = undefined;
/** /**
* Whether or not this effect is currently surpressed. * Whether or not this effect is currently surpressed.
* @type {boolean}
*/ */
get isSurpressed(): boolean { get isSurpressed() {
const originatingItem = this.originatingItem; const originatingItem = this.originatingItem;
if (!originatingItem) { if (!originatingItem) {
return false; return false;
@ -51,8 +49,9 @@ export class DS4ActiveEffect extends ActiveEffect {
/** /**
* The item which this effect originates from if it has been transferred from an item to an actor. * The item which this effect originates from if it has been transferred from an item to an actor.
* @return {import("./item/item").DS4Item | undefined}
*/ */
get originatingItem(): DS4Item | undefined { get originatingItem() {
if (!(this.parent instanceof Actor)) { if (!(this.parent instanceof Actor)) {
return; return;
} }
@ -66,27 +65,28 @@ export class DS4ActiveEffect extends ActiveEffect {
/** /**
* The number of times this effect should be applied. * The number of times this effect should be applied.
* @type {number}
*/ */
get factor(): number { get factor() {
return this.originatingItem?.activeEffectFactor ?? 1; return this.originatingItem?.activeEffectFactor ?? 1;
} }
override apply(document: DS4Actor | DS4Item, change: EffectChangeData): unknown { /** @override */
apply(document, change) {
change.value = Roll.replaceFormulaData(change.value, document.data); change.value = Roll.replaceFormulaData(change.value, document.data);
try { try {
change.value = DS4ActiveEffect.safeEval(change.value).toString(); change.value = DS4ActiveEffect.safeEval(change.value).toString();
} catch (e) { } catch (e) {
// this is a valid case, e.g., if the effect change simply is a string // this is a valid case, e.g., if the effect change simply is a string
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error In the types and foundry's documentation, only actors are allowed, but the implementation actually works for all kinds of documents
return super.apply(document, change); return super.apply(document, change);
} }
/** /**
* Gets the current source name based on the cached source object. * Gets the current source name based on the cached source object.
* @returns {Promise<string>} The current source name
*/ */
async getCurrentSourceName(): Promise<string> { async getCurrentSourceName() {
const game = getGame(); const game = getGame();
const origin = await this.getSource(); const origin = await this.getSource();
if (origin === null) return game.i18n.localize("None"); if (origin === null) return game.i18n.localize("None");
@ -96,8 +96,10 @@ export class DS4ActiveEffect extends ActiveEffect {
/** /**
* Gets the source document for this effect. Uses the cached {@link DS4ActiveEffect#source} if it has already been * Gets the source document for this effect. Uses the cached {@link DS4ActiveEffect#source} if it has already been
* set. * set.
* @protected
* @returns {Promise<foundry.abstract.Document | null>}
*/ */
protected async getSource(): ReturnType<typeof fromUuid> { async getSource() {
if (this.source === undefined) { if (this.source === undefined) {
this.source = this.data.origin !== undefined ? await fromUuid(this.data.origin) : null; this.source = this.data.origin !== undefined ? await fromUuid(this.data.origin) : null;
} }
@ -107,10 +109,10 @@ export class DS4ActiveEffect extends ActiveEffect {
/** /**
* Create a new {@link DS4ActiveEffect} using default data. * Create a new {@link DS4ActiveEffect} using default data.
* *
* @param parent The parent {@link DS4Actor} or {@link DS4Item} of the effect. * @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} parent The parent of the effect.
* @returns A promise that resolved to the created effect or udifined of the creation was prevented. * @returns {Promise<DS4ActiveEffect | undefined>}A promise that resolved to the created effect or udifined of the creation was prevented.
*/ */
static async createDefault(parent: DS4Actor | DS4Item): Promise<DS4ActiveEffect | undefined> { static async createDefault(parent) {
const createData = { const createData = {
label: getGame().i18n.localize(`DS4.NewEffectLabel`), label: getGame().i18n.localize(`DS4.NewEffectLabel`),
icon: this.FALLBACK_ICON, icon: this.FALLBACK_ICON,
@ -119,26 +121,29 @@ export class DS4ActiveEffect extends ActiveEffect {
return this.create(createData, { parent, pack: parent.pack ?? undefined }); return this.create(createData, { parent, pack: parent.pack ?? undefined });
} }
static safeEval(expression: string): number | `${number | boolean}` { /**
* Safely evaluate a mathematical expression.
* @param {string} expression The expression to evaluate
* @returns {number | `${number | boolean}`} The numeric result of the expression
* @throws If the expression could not be evaluated or did not produce a numeric resilt
*/
static safeEval(expression) {
const result = mathEvaluator.evaluate(expression); const result = mathEvaluator.evaluate(expression);
if (!Number.isNumeric(result)) { if (!Number.isNumeric(result)) {
throw new Error(`mathEvaluator.evaluate produced a non-numeric result from expression "${expression}"`); throw new Error(`mathEvaluator.evaluate produced a non-numeric result from expression "${expression}"`);
} }
return result as number | `${number | boolean}`; return result;
} }
/** /**
* Apply the given effects to the gicen Actor or item. * Apply the given effects to the gicen Actor or item.
* @param document The Actor or Item to which to apply the effects * @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} document The Actor or Item to which to apply the effects
* @param effetcs The effects to apply * @param {DS4ActiveEffect[]} effetcs The effects to apply
* @param predicate Apply only changes that fullfill this predicate * @param {(change: EffectChangeData) => boolean} [predicate=() => true] Apply only changes that fullfill this predicate
*/ */
static applyEffetcs( static applyEffetcs(document, effetcs, predicate = () => true) {
document: DS4Actor | DS4Item, /** @type {Record<string, unknown>} */
effetcs: DS4ActiveEffect[], const overrides = {};
predicate: (change: EffectChangeData) => boolean = () => true,
): void {
const overrides: Record<string, unknown> = {};
// Organize non-disabled and -surpressed effects by their application priority // Organize non-disabled and -surpressed effects by their application priority
const changesWithEffect = effetcs.flatMap((e) => e.getFactoredChangesWithEffect(predicate)); const changesWithEffect = effetcs.flatMap((e) => e.getFactoredChangesWithEffect(predicate));
@ -159,22 +164,28 @@ export class DS4ActiveEffect extends ActiveEffect {
/** /**
* Get the array of changes for this effect, considering the {@link DS4ActiveEffect#factor}. * Get the array of changes for this effect, considering the {@link DS4ActiveEffect#factor}.
* @param predicate An optional predicate to filter which changes should be considered * @param {(change: EffectChangeData) => boolean} [predicate=() => true] An optional predicate to filter which changes should be considered
* @returns The array of changes from this effect, considering the factor. * @returns {EffectChangeDataWithEffect[]} The array of changes from this effect, considering the factor.
* @protected
*/ */
protected getFactoredChangesWithEffect( getFactoredChangesWithEffect(predicate = () => true) {
predicate: (change: EffectChangeData) => boolean = () => true,
): EffectChangeDataWithEffect[] {
if (this.data.disabled || this.isSurpressed) { if (this.data.disabled || this.isSurpressed) {
return []; return [];
} }
return this.data.changes.filter(predicate).flatMap((change) => { return this.data.changes.filter(predicate).flatMap((change) => {
change.priority = change.priority ?? change.mode * 10; change.priority = change.priority ?? change.mode * 10;
return Array<EffectChangeDataWithEffect>(this.factor).fill({ effect: this, change }); return Array(this.factor).fill({ effect: this, change });
}); });
} }
} }
type EffectChangeData = foundry.data.ActiveEffectData["changes"][number]; /**
type EffectChangeDataWithEffect = { effect: DS4ActiveEffect; change: EffectChangeData }; * @typedef {foundry.data.ActiveEffectData["changes"][number]} EffectChangeData
*/
/**
* @typedef {object} EffectChangeDataWithEffect
* @property {DS4ActiveEffect} effect
* @property {EffectChangeData} change
*/

View file

@ -11,24 +11,12 @@ import { getGame } from "../../utils/utils";
import { DS4ActiveEffect } from "../active-effect"; import { DS4ActiveEffect } from "../active-effect";
import { isAttribute, isTrait } from "./actor-data-source-base"; import { isAttribute, isTrait } from "./actor-data-source-base";
import type { ModifiableDataBaseTotal } from "../common/common-data";
import type { DS4ArmorDataProperties } from "../item/armor/armor-data-properties";
import type { DS4Item } from "../item/item";
import type { ItemType } from "../item/item-data-source";
import type { DS4ShieldDataProperties } from "../item/shield/shield-data-properties";
import type { Check } from "./actor-data-properties-base";
declare global {
interface DocumentClassConfig {
Actor: typeof DS4Actor;
}
}
/** /**
* The Actor class for DS4 * The Actor class for DS4
*/ */
export class DS4Actor extends Actor { export class DS4Actor extends Actor {
override prepareData(): void { /** @override */
prepareData() {
this.data.reset(); this.data.reset();
this.prepareBaseData(); this.prepareBaseData();
this.prepareEmbeddedDocuments(); this.prepareEmbeddedDocuments();
@ -39,7 +27,8 @@ export class DS4Actor extends Actor {
this.prepareFinalDerivedData(); this.prepareFinalDerivedData();
} }
override prepareBaseData(): void { /** @override */
prepareBaseData() {
const data = this.data; const data = this.data;
data.data.rolling = { data.data.rolling = {
@ -48,17 +37,14 @@ export class DS4Actor extends Actor {
}; };
const attributes = data.data.attributes; const attributes = data.data.attributes;
Object.values(attributes).forEach( Object.values(attributes).forEach((attribute) => (attribute.total = attribute.base + attribute.mod));
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = attribute.base + attribute.mod),
);
const traits = data.data.traits; const traits = data.data.traits;
Object.values(traits).forEach( Object.values(traits).forEach((trait) => (trait.total = trait.base + trait.mod));
(trait: ModifiableDataBaseTotal<number>) => (trait.total = trait.base + trait.mod),
);
} }
override prepareEmbeddedDocuments() { /** @override */
prepareEmbeddedDocuments() {
super.prepareEmbeddedDocuments(); super.prepareEmbeddedDocuments();
this.applyActiveEffectsToItems(); this.applyActiveEffectsToItems();
} }
@ -71,16 +57,21 @@ export class DS4Actor extends Actor {
this.data.data.armorValueSpellMalus = this.armorValueSpellMalusOfEquippedItems; this.data.data.armorValueSpellMalus = this.armorValueSpellMalusOfEquippedItems;
} }
protected get actorEffects() { /**
* The effects that should be applioed to this actor.
* @type {this["effects"]}
* @protected
*/
get actorEffects() {
return this.effects.filter((effect) => !effect.data.flags.ds4?.itemEffectConfig?.applyToItems); return this.effects.filter((effect) => !effect.data.flags.ds4?.itemEffectConfig?.applyToItems);
} }
/** /**
* Get the effects of this actor that should be applied to the given item. * Get the effects of this actor that should be applied to the given item.
* @param item The item for which to get effects * @param {import("../item/item").DS4Item} item The item for which to get effects
* @returns The array of effects that are candidates to be applied to the item * @returns The array of effects that are candidates to be applied to the item
*/ */
itemEffects(item: DS4Item) { itemEffects(item) {
return this.effects.filter((effect) => { return this.effects.filter((effect) => {
const { applyToItems, itemName, condition } = effect.data.flags.ds4?.itemEffectConfig ?? {}; const { applyToItems, itemName, condition } = effect.data.flags.ds4?.itemEffectConfig ?? {};
@ -106,7 +97,14 @@ export class DS4Actor extends Actor {
}); });
} }
protected static replaceFormulaData(formula: string, data: object): string | undefined { /**
* Replace placholders in a formula with data.
* @param {string} formula The formular to enricht with data
* @param {object} data The data to use for enriching
* @returns {string | undefined} The Enriched formula or undefined, if it contains placeholders that cannot be resolved
* @protected
*/
static replaceFormulaData(formula, data) {
const dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi); const dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi);
try { try {
return formula.replace(dataRgx, (_, term) => { return formula.replace(dataRgx, (_, term) => {
@ -124,8 +122,9 @@ export class DS4Actor extends Actor {
/** /**
* We override this with an empty implementation because we have our own custom way of applying * We override this with an empty implementation because we have our own custom way of applying
* {@link ActiveEffect}s and {@link Actor#prepareEmbeddedDocuments} calls this. * {@link ActiveEffect}s and {@link Actor#prepareEmbeddedDocuments} calls this.
* @override
*/ */
override applyActiveEffects(): void { applyActiveEffects() {
return; return;
} }
@ -138,7 +137,7 @@ export class DS4Actor extends Actor {
* no special ordering among talents. This means that having a talents that provide effects that adjust the total * no special ordering among talents. This means that having a talents that provide effects that adjust the total
* rank of talents can lead to nondeterministic behavior and thus must be avoided. * rank of talents can lead to nondeterministic behavior and thus must be avoided.
*/ */
applyActiveEffectsToItems(): void { applyActiveEffectsToItems() {
/* Handle talents before all other item types, because their rank might be affected, which in turn affects how /* Handle talents before all other item types, because their rank might be affected, which in turn affects how
many times effects provided by that talent are applied */ many times effects provided by that talent are applied */
for (const item of this.itemTypes.talent) { for (const item of this.itemTypes.talent) {
@ -150,12 +149,21 @@ export class DS4Actor extends Actor {
} }
} }
protected applyActiveEffectsToItem(item: DS4Item) { /**
* Apply effects to the given item.
* @param {import("../item/item").DS4Item} item The item to which to apply effects
* @protected
*/
applyActiveEffectsToItem(item) {
item.overrides = {}; item.overrides = {};
DS4ActiveEffect.applyEffetcs(item, this.itemEffects(item)); DS4ActiveEffect.applyEffetcs(item, this.itemEffects(item));
} }
applyActiveEffectsToBaseData(): void { /**
* Apply effects to base data
* @protected
*/
applyActiveEffectsToBaseData() {
this.overrides = {}; this.overrides = {};
DS4ActiveEffect.applyEffetcs( DS4ActiveEffect.applyEffetcs(
this, this,
@ -166,7 +174,11 @@ export class DS4Actor extends Actor {
); );
} }
applyActiveEffectsToDerivedData(): void { /**
* Apply effects to derived data
* @protected
*/
applyActiveEffectsToDerivedData() {
DS4ActiveEffect.applyEffetcs(this, this.actorEffects, (change) => DS4ActiveEffect.applyEffetcs(this, this.actorEffects, (change) =>
this.derivedDataProperties.includes(change.key), this.derivedDataProperties.includes(change.key),
); );
@ -174,8 +186,9 @@ export class DS4Actor extends Actor {
/** /**
* Apply transformations to the Actor data after effects have been applied to the base data. * Apply transformations to the Actor data after effects have been applied to the base data.
* @override
*/ */
override prepareDerivedData(): void { prepareDerivedData() {
this.data.data.armorValueSpellMalus = Math.max(this.data.data.armorValueSpellMalus, 0); this.data.data.armorValueSpellMalus = Math.max(this.data.data.armorValueSpellMalus, 0);
this.prepareCombatValues(); this.prepareCombatValues();
this.prepareChecks(); this.prepareChecks();
@ -183,8 +196,9 @@ export class DS4Actor extends Actor {
/** /**
* The list of properties that are derived from others, given in dot notation. * The list of properties that are derived from others, given in dot notation.
* @returns {string[]} The list of derived propertie
*/ */
get derivedDataProperties(): Array<string> { get derivedDataProperties() {
const combatValueProperties = Object.keys(DS4.i18n.combatValues).map( const combatValueProperties = Object.keys(DS4.i18n.combatValues).map(
(combatValue) => `data.combatValues.${combatValue}.total`, (combatValue) => `data.combatValues.${combatValue}.total`,
); );
@ -197,20 +211,14 @@ export class DS4Actor extends Actor {
/** /**
* Apply final transformations to the Actor data after all effects have been applied. * Apply final transformations to the Actor data after all effects have been applied.
*/ */
prepareFinalDerivedData(): void { prepareFinalDerivedData() {
Object.values(this.data.data.attributes).forEach( Object.values(this.data.data.attributes).forEach((attribute) => (attribute.total = Math.ceil(attribute.total)));
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = Math.ceil(attribute.total)), Object.values(this.data.data.traits).forEach((trait) => (trait.total = Math.ceil(trait.total)));
);
Object.values(this.data.data.traits).forEach(
(trait: ModifiableDataBaseTotal<number>) => (trait.total = Math.ceil(trait.total)),
);
Object.entries(this.data.data.combatValues) Object.entries(this.data.data.combatValues)
.filter(([key]) => key !== "movement") .filter(([key]) => key !== "movement")
.map(([, value]) => value) .map(([, value]) => value)
.forEach( .forEach((combatValue) => (combatValue.total = Math.ceil(combatValue.total)));
(combatValue: ModifiableDataBaseTotal<number>) => (combatValue.total = Math.ceil(combatValue.total)), Object.keys(this.data.data.checks).forEach((key) => {
);
(Object.keys(this.data.data.checks) as (keyof typeof this.data.data.checks)[]).forEach((key) => {
this.data.data.checks[key] = Math.ceil(this.data.data.checks[key]); this.data.data.checks[key] = Math.ceil(this.data.data.checks[key]);
}); });
@ -221,30 +229,34 @@ export class DS4Actor extends Actor {
/** /**
* The list of properties that are completely derived (i.e. {@link ActiveEffect}s cannot be applied to them), * The list of properties that are completely derived (i.e. {@link ActiveEffect}s cannot be applied to them),
* given in dot notation. * given in dot notation.
* @type {string[]}
*/ */
get finalDerivedDataProperties(): string[] { get finalDerivedDataProperties() {
return ["data.combatValues.hitPoints.max", "data.checks.defend"]; return ["data.combatValues.hitPoints.max", "data.checks.defend"];
} }
/** /**
* The list of item types that can be owned by this actor. * The list of item types that can be owned by this actor.
* @type {import("../item/item-data-source").ItemType[]}
*/ */
get ownableItemTypes(): Array<ItemType> { get ownableItemTypes() {
return ["weapon", "armor", "shield", "equipment", "loot", "spell"]; return ["weapon", "armor", "shield", "equipment", "loot", "spell"];
} }
/** /**
* Checks whether or not the given item type can be owned by the actor. * Checks whether or not the given item type can be owned by the actor.
* @param itemType - The item type to check * @param {import("../item/item-data-source").ItemType} itemType The item type to check
* @returns {boolean} Whether or not this actor can own the given item type
*/ */
canOwnItemType(itemType: ItemType): boolean { canOwnItemType(itemType) {
return this.ownableItemTypes.includes(itemType); return this.ownableItemTypes.includes(itemType);
} }
/** /**
* Prepares the combat values of the actor. * Prepares the combat values of the actor.
* @protected
*/ */
protected prepareCombatValues(): void { prepareCombatValues() {
const data = this.data.data; const data = this.data.data;
data.combatValues.hitPoints.base = data.attributes.body.total + data.traits.constitution.total + 10; data.combatValues.hitPoints.base = data.attributes.body.total + data.traits.constitution.total + 10;
@ -260,21 +272,25 @@ export class DS4Actor extends Actor {
data.attributes.mind.total + data.traits.dexterity.total - data.armorValueSpellMalus; data.attributes.mind.total + data.traits.dexterity.total - data.armorValueSpellMalus;
Object.values(data.combatValues).forEach( Object.values(data.combatValues).forEach(
(combatValue: ModifiableDataBaseTotal<number>) => (combatValue.total = combatValue.base + combatValue.mod), (combatValue) => (combatValue.total = combatValue.base + combatValue.mod),
); );
} }
/** /**
* The total armor value of the equipped items. * The total armor value of the equipped items.
* @type {number}
* @protected
*/ */
protected get armorValueOfEquippedItems(): number { get armorValueOfEquippedItems() {
return this.equippedItemsWithArmor.map((item) => item.data.data.armorValue).reduce((a, b) => a + b, 0); return this.equippedItemsWithArmor.map((item) => item.data.data.armorValue).reduce((a, b) => a + b, 0);
} }
/** /**
* The armor value spell malus from equipped items. * The armor value spell malus from equipped items.
* @type {number}
* @protected
*/ */
protected get armorValueSpellMalusOfEquippedItems(): number { get armorValueSpellMalusOfEquippedItems() {
return this.equippedItemsWithArmor return this.equippedItemsWithArmor
.filter( .filter(
(item) => (item) =>
@ -284,19 +300,22 @@ export class DS4Actor extends Actor {
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
} }
protected get equippedItemsWithArmor() { /**
* The equipped items of this actor that provide armor.
* @type {(import("../item/armor/armor").DS4Armor | import("../item/shield/shield").DS4Shield)[]}
* @protected
*/
get equippedItemsWithArmor() {
return this.items return this.items
.filter( .filter((item) => item.data.type === "armor" || item.data.type === "shield")
(item): item is DS4Item & { data: DS4ArmorDataProperties | DS4ShieldDataProperties } =>
item.data.type === "armor" || item.data.type === "shield",
)
.filter((item) => item.data.data.equipped); .filter((item) => item.data.data.equipped);
} }
/** /**
* Prepares the check target numbers of checks for the actor. * Prepares the check target numbers of checks for the actor.
* @protected
*/ */
protected prepareChecks(): void { prepareChecks() {
const data = this.data.data; const data = this.data.data;
data.checks = { data.checks = {
appraise: data.attributes.mind.total + data.traits.intellect.total, appraise: data.attributes.mind.total + data.traits.intellect.total,
@ -334,17 +353,14 @@ export class DS4Actor extends Actor {
/** /**
* Handle how changes to a Token attribute bar are applied to the Actor. * Handle how changes to a Token attribute bar are applied to the Actor.
* This only differs from the base implementation by also allowing negative values. * This only differs from the base implementation by also allowing negative values.
* @override
*/ */
override async modifyTokenAttribute( async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
attribute: string,
value: number,
isDelta = false,
isBar = true,
): Promise<this | undefined> {
const current = foundry.utils.getProperty(this.data.data, attribute); const current = foundry.utils.getProperty(this.data.data, attribute);
// Determine the updates to make to the actor data // Determine the updates to make to the actor data
let updates: Record<string, number>; /** @type {Record<string, number>} */
let updates;
if (isBar) { if (isBar) {
if (isDelta) value = Math.min(Number(current.value) + value, current.max); if (isDelta) value = Math.min(Number(current.value) + value, current.max);
updates = { [`data.${attribute}.value`]: value }; updates = { [`data.${attribute}.value`]: value };
@ -360,13 +376,11 @@ export class DS4Actor extends Actor {
/** /**
* Roll for a given check. * Roll for a given check.
* @param check - The check to perform * @param {import("./actor-data-properties-base").Check} check The check to perform
* @param options - Additional options to customize the roll * @param {import("../common/roll-options").RollOptions} [options={}] Additional options to customize the roll
* @returns {Promise<void>} A promise that resolves once the roll has been performed
*/ */
async rollCheck( async rollCheck(check, options = {}) {
check: Check,
options: { speaker?: { token?: TokenDocument; alias?: string } } = {},
): Promise<void> {
const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker }); const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker });
await createCheckRoll(this.data.data.checks[check], { await createCheckRoll(this.data.data.checks[check], {
rollMode: getGame().settings.get("core", "rollMode"), rollMode: getGame().settings.get("core", "rollMode"),
@ -381,9 +395,10 @@ export class DS4Actor extends Actor {
/** /**
* Roll a generic check. A dialog is presented to select the combination of * Roll a generic check. A dialog is presented to select the combination of
* Attribute and Trait to perform the check against. * Attribute and Trait to perform the check against.
* @param options - Additional options to customize the roll * @param {import("../common/roll-options").RollOptions} [options={}] Additional options to customize the roll
* @returns {Promise<void>} A promise that resolves once the roll has been performed
*/ */
async rollGenericCheck(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> { async rollGenericCheck(options = {}) {
const attributeAndTrait = await this.selectAttributeAndTrait(); const attributeAndTrait = await this.selectAttributeAndTrait();
if (!attributeAndTrait) { if (!attributeAndTrait) {
return; return;
@ -405,10 +420,12 @@ export class DS4Actor extends Actor {
}); });
} }
protected async selectAttributeAndTrait(): Promise<{ /**
attribute: keyof typeof DS4.i18n.attributes; * Prompt the use to select an attribute and a trait.
trait: keyof typeof DS4.i18n.traits; * @returns {Promise<AttributeAndTrait | null>}
} | null> { * @protected
*/
async selectAttributeAndTrait() {
const attributeIdentifier = "attribute-trait-selection-attribute"; const attributeIdentifier = "attribute-trait-selection-attribute";
const traitIdentifier = "attribute-trait-selection-trait"; const traitIdentifier = "attribute-trait-selection-trait";
return Dialog.prompt({ return Dialog.prompt({
@ -419,24 +436,20 @@ export class DS4Actor extends Actor {
label: getGame().i18n.localize("DS4.Attribute"), label: getGame().i18n.localize("DS4.Attribute"),
identifier: attributeIdentifier, identifier: attributeIdentifier,
options: Object.fromEntries( options: Object.fromEntries(
(Object.entries(DS4.i18n.attributes) as [keyof typeof DS4.i18n.attributes, string][]).map( Object.entries(DS4.i18n.attributes).map(([attribute, translation]) => [
([attribute, translation]) => [ attribute,
attribute, `${translation} (${this.data.data.attributes[attribute].total})`,
`${translation} (${this.data.data.attributes[attribute].total})`, ]),
],
),
), ),
}, },
{ {
label: getGame().i18n.localize("DS4.Trait"), label: getGame().i18n.localize("DS4.Trait"),
identifier: traitIdentifier, identifier: traitIdentifier,
options: Object.fromEntries( options: Object.fromEntries(
(Object.entries(DS4.i18n.traits) as [keyof typeof DS4.i18n.traits, string][]).map( Object.entries(DS4.i18n.traits).map(([trait, translation]) => [
([trait, translation]) => [ trait,
trait, `${translation} (${this.data.data.traits[trait].total})`,
`${translation} (${this.data.data.traits[trait].total})`, ]),
],
),
), ),
}, },
], ],
@ -474,3 +487,9 @@ export class DS4Actor extends Actor {
}); });
} }
} }
/**
* @typedef {object} AttributeAndTrait
* @property {keyof typeof DS4.i18n.attributes} attribute
* @property {keyof typeof DS4.i18n.traits} trait
*/

View file

@ -4,23 +4,20 @@
import { DS4Actor } from "../actor"; import { DS4Actor } from "../actor";
import type { ItemType } from "../../item/item-data-source";
export class DS4Character extends DS4Actor { export class DS4Character extends DS4Actor {
override prepareFinalDerivedData(): void { /** @override */
prepareFinalDerivedData() {
super.prepareFinalDerivedData(); super.prepareFinalDerivedData();
this.data.data.slayerPoints.max = 3; this.data.data.slayerPoints.max = 3;
} }
override get finalDerivedDataProperties(): string[] { /** @override */
get finalDerivedDataProperties() {
return [...super.finalDerivedDataProperties, "data.slayerPoints.max"]; return [...super.finalDerivedDataProperties, "data.slayerPoints.max"];
} }
override get ownableItemTypes(): Array<ItemType> { /** @override */
get ownableItemTypes() {
return [...super.ownableItemTypes, "talent", "racialAbility", "language", "alphabet"]; return [...super.ownableItemTypes, "talent", "racialAbility", "language", "alphabet"];
} }
} }
export interface DS4Character {
data: foundry.data.ActorData & { type: "character"; _source: { type: "character" } };
}

View file

@ -11,7 +11,3 @@ export class DS4Creature extends DS4Actor {
return [...super.ownableItemTypes, "specialCreatureAbility"]; return [...super.ownableItemTypes, "specialCreatureAbility"];
} }
} }
export interface DS4Creature {
data: foundry.data.ActorData & { type: "creature"; _source: { type: "creature" } };
}

View file

@ -8,7 +8,11 @@ import { DS4Character } from "./character/character";
import { DS4Creature } from "./creature/creature"; import { DS4Creature } from "./creature/creature";
const handler = { const handler = {
construct(_: typeof DS4Actor, args: ConstructorParameters<typeof DS4Actor>) { /**
* @param {typeof import("./actor").DS4Actor}
* @param {unknown[]} args
*/
construct(_, args) {
switch (args[0]?.type) { switch (args[0]?.type) {
case "character": case "character":
return new DS4Character(...args); return new DS4Character(...args);
@ -20,4 +24,5 @@ const handler = {
}, },
}; };
export const DS4ActorProxy: typeof DS4Actor = new Proxy(DS4Actor, handler); /** @type {typeof import("./actor").DS4Actor} */
export const DS4ActorProxy = new Proxy(DS4Actor, handler);

View file

@ -4,18 +4,18 @@
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
declare global { /**
interface FlagConfig { * @typedef {object} DS4ChatMessageFlags
ChatMessage: { * @property {Record<string, string | number | null>} [flavorData] Data to use for localizing the flavor of the chat message
ds4?: { */
flavorData?: Record<string, string | number | null>;
}; /**
}; * @typedef {Record<string, unknown>} ChatMessageFlags
} * @property {DS4ChatMessageFlags} [ds4] Flags for DS4
} */
export class DS4ChatMessage extends ChatMessage { export class DS4ChatMessage extends ChatMessage {
override prepareData(): void { prepareData() {
super.prepareData(); super.prepareData();
if (this.data.flavor) { if (this.data.flavor) {
const game = getGame(); const game = getGame();

View file

@ -5,7 +5,3 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4Alphabet extends DS4Item {} export class DS4Alphabet extends DS4Item {}
export interface DS4Alphabet {
data: foundry.data.ItemData & { type: "alphabet"; _source: { type: "alphabet" } };
}

View file

@ -5,7 +5,3 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4Armor extends DS4Item {} export class DS4Armor extends DS4Item {}
export interface DS4Armor {
data: foundry.data.ItemData & { type: "armor"; _source: { type: "armor" } };
}

View file

@ -5,7 +5,3 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4Equipment extends DS4Item {} export class DS4Equipment extends DS4Item {}
export interface DS4Equipment {
data: foundry.data.ItemData & { type: "equipment"; _source: { type: "equipment" } };
}

View file

@ -17,7 +17,7 @@ export interface DS4ItemDataSourceDataPhysical {
storageLocation: string; storageLocation: string;
} }
export function isDS4ItemDataTypePhysical(input: foundry.data.ItemData["data"]): boolean { export function isDS4ItemDataTypePhysical(input: object): boolean {
return "quantity" in input && "price" in input && "availability" in input && "storageLocation" in input; return "quantity" in input && "price" in input && "availability" in input && "storageLocation" in input;
} }

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
//
// SPDX-License-Identifier: MIT
import { getGame } from "../../utils/utils";
/**
* The Item class for DS4
*/
export class DS4Item extends Item {
/**
* An object that tracks the changes to the data model which were applied by active effects
* @type {Record<string, unknown>}
*/
overrides = {};
/** @override */
prepareDerivedData() {
this.data.data.rollable = false;
}
/**
* Is this item a non-equipped equipable?
* @returns {boolean} Whether the item is a non-equpped equibale or not
*/
isNonEquippedEuipable() {
return "equipped" in this.data.data && !this.data.data.equipped;
}
/**
* The number of times that active effect changes originating from this item should be applied.
* @returns {number | undefined} The number of times the effect should be applied
*/
get activeEffectFactor() {
return 1;
}
/**
* The list of item types that are rollable.
* @returns {import("../item/item-data-source").ItemType[]} The rollable item types
*/
static get rollableItemTypes() {
return ["weapon", "spell"];
}
/**
* Roll a check for an action with this item.
* @param {import("../common/roll-options").RollOptions} [options={}] Additional options to customize the roll
* @returns {Promise<void>} A promise that resolves once the roll has been performed
* @abstract
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async roll(options = {}) {
throw new Error(getGame().i18n.format("DS4.ErrorRollingForItemTypeNotPossible", { type: this.data.type }));
}
}

View file

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
//
// SPDX-License-Identifier: MIT
import { getGame } from "../../utils/utils";
import type { ItemType } from "./item-data-source";
declare global {
interface DocumentClassConfig {
Item: typeof DS4Item;
}
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Hooks {
interface StaticCallbacks {
"ds4.rollItem": (item: DS4Item) => void;
}
}
}
/**
* The Item class for DS4
*/
export class DS4Item extends Item {
/** An object that tracks the changes to the data model which were applied by active effects */
overrides: Record<string, unknown> = {};
override prepareDerivedData(): void {
this.data.data.rollable = false;
}
isNonEquippedEuipable(): boolean {
return "equipped" in this.data.data && !this.data.data.equipped;
}
/**
* The number of times that active effect changes originating from this item should be applied.
*/
get activeEffectFactor(): number | undefined {
return 1;
}
/**
* The list of item types that are rollable.
*/
static get rollableItemTypes(): ItemType[] {
return ["weapon", "spell"];
}
/**
* Roll a check for an action with this item.
* @param options - Additional options to customize the roll
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
throw new Error(getGame().i18n.format("DS4.ErrorRollingForItemTypeNotPossible", { type: this.data.type }));
}
}

View file

@ -5,7 +5,3 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4Language extends DS4Item {} export class DS4Language extends DS4Item {}
export interface DS4Language {
data: foundry.data.ItemData & { type: "language"; _source: { type: "language" } };
}

View file

@ -5,7 +5,3 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4Loot extends DS4Item {} export class DS4Loot extends DS4Item {}
export interface DS4Loot {
data: foundry.data.ItemData & { type: "loot"; _source: { type: "loot" } };
}

View file

@ -17,7 +17,11 @@ import { DS4Talent } from "./talent/talent";
import { DS4Weapon } from "./weapon/weapon"; import { DS4Weapon } from "./weapon/weapon";
const handler = { const handler = {
construct(_: typeof DS4Item, args: ConstructorParameters<typeof DS4Item>) { /**
* @param {typeof import("./item").DS4Item}
* @param {unknown[]} args
*/
construct(_, args) {
switch (args[0]?.type) { switch (args[0]?.type) {
case "alphabet": case "alphabet":
return new DS4Alphabet(...args); return new DS4Alphabet(...args);
@ -47,4 +51,5 @@ const handler = {
}, },
}; };
export const DS4ItemProxy: typeof DS4Item = new Proxy(DS4Item, handler); /** @type {typeof import("./item").DS4Item} */
export const DS4ItemProxy = new Proxy(DS4Item, handler);

View file

@ -5,7 +5,3 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4RacialAbility extends DS4Item {} export class DS4RacialAbility extends DS4Item {}
export interface DS4RacialAbility {
data: foundry.data.ItemData & { type: "racialAbility"; _source: { type: "racialAbility" } };
}

View file

@ -5,7 +5,3 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4Shield extends DS4Item {} export class DS4Shield extends DS4Item {}
export interface DS4Shield {
data: foundry.data.ItemData & { type: "shield"; _source: { type: "shield" } };
}

View file

@ -5,7 +5,3 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4SpecialCreatureAbility extends DS4Item {} export class DS4SpecialCreatureAbility extends DS4Item {}
export interface DS4SpecialCreatureAbility {
data: foundry.data.ItemData & { type: "specialCreatureAbility"; _source: { type: "specialCreatureAbility" } };
}

View file

@ -2,14 +2,15 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { createCheckRoll, DS4CheckFactoryOptions } from "../../../dice/check-factory"; import { createCheckRoll } from "../../../dice/check-factory";
import { notifications } from "../../../ui/notifications"; import { notifications } from "../../../ui/notifications";
import { getGame } from "../../../utils/utils"; import { getGame } from "../../../utils/utils";
import { DS4Item } from "../item"; import { DS4Item } from "../item";
import { calculateSpellPrice } from "./calculate-spell-price"; import { calculateSpellPrice } from "./calculate-spell-price";
export class DS4Spell extends DS4Item { export class DS4Spell extends DS4Item {
override prepareDerivedData(): void { /** @override */
prepareDerivedData() {
this.data.data.rollable = this.data.data.equipped; this.data.data.rollable = this.data.data.equipped;
this.data.data.price = calculateSpellPrice(this.data.data); this.data.data.price = calculateSpellPrice(this.data.data);
if (this.data.data.allowsDefense) { if (this.data.data.allowsDefense) {
@ -17,7 +18,8 @@ export class DS4Spell extends DS4Item {
} }
} }
override async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> { /** @override */
async roll(options = {}) {
const game = getGame(); const game = getGame();
if (!this.data.data.equipped) { if (!this.data.data.equipped) {
@ -54,7 +56,8 @@ export class DS4Spell extends DS4Item {
opponentDefense !== undefined && opponentDefense !== 0 opponentDefense !== undefined && opponentDefense !== 0
? "DS4.ItemSpellCheckFlavorWithOpponentDefense" ? "DS4.ItemSpellCheckFlavorWithOpponentDefense"
: "DS4.ItemSpellCheckFlavor"; : "DS4.ItemSpellCheckFlavor";
const flavorData: DS4CheckFactoryOptions["flavorData"] = { /** @type {import("../../../dice/check-factory").DS4CheckFactoryOptions["flavorData"]} */
const flavorData = {
actor: speaker.alias ?? this.actor.name, actor: speaker.alias ?? this.actor.name,
spell: this.name, spell: this.name,
}; };
@ -71,10 +74,12 @@ export class DS4Spell extends DS4Item {
speaker, speaker,
}); });
/**
* A hook event that fires after an item is rolled.
* @function ds4.rollItem
* @memberof hookEvents
* @param {DS4Item} item Item being rolled.
*/
Hooks.callAll("ds4.rollItem", this); Hooks.callAll("ds4.rollItem", this);
} }
} }
export interface DS4Spell {
data: foundry.data.ItemData & { type: "spell"; _source: { type: "spell" } };
}

View file

@ -5,17 +5,15 @@
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4Talent extends DS4Item { export class DS4Talent extends DS4Item {
override prepareDerivedData(): void { /** @override */
prepareDerivedData() {
super.prepareDerivedData(); super.prepareDerivedData();
const data = this.data.data; const data = this.data.data;
data.rank.total = data.rank.base + data.rank.mod; data.rank.total = data.rank.base + data.rank.mod;
} }
override get activeEffectFactor(): number | undefined { /** @override */
get activeEffectFactor() {
return this.data.data.rank.total; return this.data.data.rank.total;
} }
} }
export interface DS4Talent {
data: foundry.data.ItemData & { type: "talent"; _source: { type: "talent" } };
}

View file

@ -3,13 +3,14 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { DS4 } from "../../../config"; import { DS4 } from "../../../config";
import { createCheckRoll, DS4CheckFactoryOptions } from "../../../dice/check-factory"; import { createCheckRoll } from "../../../dice/check-factory";
import { notifications } from "../../../ui/notifications"; import { notifications } from "../../../ui/notifications";
import { getGame } from "../../../utils/utils"; import { getGame } from "../../../utils/utils";
import { DS4Item } from "../item"; import { DS4Item } from "../item";
export class DS4Weapon extends DS4Item { export class DS4Weapon extends DS4Item {
override prepareDerivedData(): void { /** @override */
prepareDerivedData() {
const data = this.data.data; const data = this.data.data;
data.rollable = data.equipped; data.rollable = data.equipped;
data.opponentDefenseForAttackType = {}; data.opponentDefenseForAttackType = {};
@ -21,7 +22,8 @@ export class DS4Weapon extends DS4Item {
} }
} }
override async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> { /** @override */
async roll(options = {}) {
const game = getGame(); const game = getGame();
if (!this.data.data.equipped) { if (!this.data.data.equipped) {
return notifications.warn( return notifications.warn(
@ -41,14 +43,15 @@ export class DS4Weapon extends DS4Item {
const weaponBonus = this.data.data.weaponBonus; const weaponBonus = this.data.data.weaponBonus;
const attackType = await this.getPerformedAttackType(); const attackType = await this.getPerformedAttackType();
const opponentDefense = this.data.data.opponentDefenseForAttackType[attackType]; const opponentDefense = this.data.data.opponentDefenseForAttackType[attackType];
const combatValue = `${attackType}Attack` as const; const combatValue = `${attackType}Attack`;
const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus; const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus;
const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker }); const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
const flavor = const flavor =
opponentDefense !== undefined && opponentDefense !== 0 opponentDefense !== undefined && opponentDefense !== 0
? "DS4.ItemWeaponCheckFlavorWithOpponentDefense" ? "DS4.ItemWeaponCheckFlavorWithOpponentDefense"
: "DS4.ItemWeaponCheckFlavor"; : "DS4.ItemWeaponCheckFlavor";
const flavorData: DS4CheckFactoryOptions["flavorData"] = { /** @type {import("../../../dice/check-factory").DS4CheckFactoryOptions["flavorData"]} */
const flavorData = {
actor: speaker.alias ?? this.actor.name, actor: speaker.alias ?? this.actor.name,
weapon: this.name, weapon: this.name,
}; };
@ -68,7 +71,12 @@ export class DS4Weapon extends DS4Item {
Hooks.callAll("ds4.rollItem", this); Hooks.callAll("ds4.rollItem", this);
} }
private async getPerformedAttackType(): Promise<"melee" | "ranged"> { /**
* Get the attack type to perform with this weapon. If there are multiple options prompt the user for a choice.
* @returns {Promise<"melee" | "ranged">} The attack type to perform
* @protected
*/
async getPerformedAttackType() {
if (this.data.data.attackType !== "meleeRanged") { if (this.data.data.attackType !== "meleeRanged") {
return this.data.data.attackType; return this.data.data.attackType;
} }
@ -102,7 +110,3 @@ export class DS4Weapon extends DS4Item {
}); });
} }
} }
export interface DS4Weapon {
data: foundry.data.ItemData & { type: "weapon"; _source: { type: "weapon" } };
}

View file

@ -5,23 +5,21 @@
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
import { DS4ActorProxy } from "./actor/proxy"; import { DS4ActorProxy } from "./actor/proxy";
let fallbackData: foundry.data.ActorData["data"] | undefined = undefined; /** @type {object | undefined} */
let fallbackData = undefined;
function getFallbackData() { function getFallbackData() {
if (!fallbackData) { if (!fallbackData) {
fallbackData = {} as foundry.data.ActorData["data"]; fallbackData = {};
for (const type of getGame().system.template.Actor?.types ?? []) { for (const type of getGame().system.template.Actor?.types ?? []) {
foundry.utils.mergeObject( foundry.utils.mergeObject(fallbackData, new DS4ActorProxy({ type, name: "temporary" }).data.data);
fallbackData,
new DS4ActorProxy({ type: type as foundry.data.ActorData["type"], name: "temporary" }).data.data,
);
} }
} }
return fallbackData; return fallbackData;
} }
export class DS4TokenDocument extends TokenDocument { export class DS4TokenDocument extends TokenDocument {
static override getTrackedAttributes(data?: foundry.data.ActorData["data"], _path: string[] = []) { static getTrackedAttributes(data, _path = []) {
if (!data) { if (!data) {
data = getFallbackData(); data = getFallbackData();
} }

21
src/global.d.ts vendored
View file

@ -1,21 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
declare global {
namespace ClientSettings {
interface Values {
"ds4.systemMigrationVersion": number;
"ds4.useSlayingDiceForAutomatedChecks": boolean;
"ds4.showSlayerPoints": boolean;
}
}
namespace PoolTerm {
interface Modifiers {
x: (this: PoolTerm, modifier: string) => void;
}
}
}
export {};

View file

@ -4,7 +4,11 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
export async function registerHandlebarsPartials(): Promise<void> { /**
* Register the Handlebars partials for DS4.
* @returns {Promise<void>} A promise that resolves once all partials have been registered
*/
export async function registerHandlebarsPartials() {
const templatePaths = [ const templatePaths = [
"systems/ds4/templates/sheets/actor/components/actor-header.hbs", "systems/ds4/templates/sheets/actor/components/actor-header.hbs",
"systems/ds4/templates/sheets/actor/components/actor-progression.hbs", "systems/ds4/templates/sheets/actor/components/actor-progression.hbs",

54
src/hooks/hotbar-drop.js Normal file
View file

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { isCheck } from "../documents/actor/actor-data-properties-base";
import { DS4Item } from "../documents/item/item";
import { createRollCheckMacro } from "../macros/roll-check";
import { createRollItemMacro } from "../macros/roll-item";
import { notifications } from "../ui/notifications";
import { getGame } from "../utils/utils";
export function registerForHotbarDropHook() {
Hooks.on("hotbarDrop", onHotbarDrop);
}
/**
* @typedef {Record<string, unknown>} DropData
* @property {string} type
*/
/**
* Handle a drop event on the hotbar
* @param {Hotbar} hotbar The hotbar on which something wqas
* @param {DropData} data The drop data associated to the drop event
* @param {string} slot The slot on the hotbar that somethingwas dropped on
* @returns {Promise<void>}
*/
async function onHotbarDrop(hotbar, data, slot) {
switch (data.type) {
case "Item": {
if (!("data" in data)) {
return notifications.warn(getGame().i18n.localize("DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems"));
}
const itemData = data.data;
if (!DS4Item.rollableItemTypes.includes(itemData.type)) {
return notifications.warn(
getGame().i18n.format("DS4.WarningItemIsNotRollable", {
name: itemData.name,
id: itemData._id,
type: itemData.type,
}),
);
}
return createRollItemMacro(itemData, slot);
}
case "Check": {
if (!("data" in data) || typeof data.data !== "string" || !isCheck(data.data)) {
return notifications.warn(getGame().i18n.localize("DS4.WarningInvalidCheckDropped"));
}
return createRollCheckMacro(data.data, slot);
}
}
}

View file

@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { isCheck } from "../documents/actor/actor-data-properties-base";
import { DS4Item } from "../documents/item/item";
import { createRollCheckMacro } from "../macros/roll-check";
import { createRollItemMacro } from "../macros/roll-item";
import { notifications } from "../ui/notifications";
import { getGame } from "../utils/utils";
export function registerForHotbarDropHook(): void {
Hooks.on("hotbarDrop", async (hotbar: Hotbar, data: HotbarDropData, slot: string) => {
switch (data.type) {
case "Item": {
if (!isItemDropData(data) || !("data" in data)) {
return notifications.warn(
getGame().i18n.localize("DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems"),
);
}
const itemData = data.data;
if (!DS4Item.rollableItemTypes.includes(itemData.type)) {
return notifications.warn(
getGame().i18n.format("DS4.WarningItemIsNotRollable", {
name: itemData.name,
id: itemData._id,
type: itemData.type,
}),
);
}
return createRollItemMacro(itemData, slot);
}
case "Check": {
if (!("data" in data) || typeof data.data !== "string" || !isCheck(data.data)) {
return notifications.warn(getGame().i18n.localize("DS4.WarningInvalidCheckDropped"));
}
return createRollCheckMacro(data.data, slot);
}
}
});
}
type HotbarDropData = ActorSheet.DropData.Item | ({ type: string } & Partial<Record<string, unknown>>);
function isItemDropData(dropData: HotbarDropData): dropData is ActorSheet.DropData.Item {
return dropData.type === "Item";
}

View file

@ -27,10 +27,7 @@ import { preloadFonts } from "../ui/fonts";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
import type { DS4Actor } from "../documents/actor/actor"; export function registerForInitHook() {
import type { DS4Item } from "../documents/item/item";
export function registerForInitHook(): void {
Hooks.once("init", init); Hooks.once("init", init);
} }
@ -81,20 +78,3 @@ async function init() {
await registerHandlebarsPartials(); await registerHandlebarsPartials();
registerHandlebarsHelpers(); registerHandlebarsHelpers();
} }
declare global {
interface Game {
ds4: {
DS4Actor: typeof DS4Actor;
DS4Item: typeof DS4Item;
DS4: typeof DS4;
createCheckRoll: typeof createCheckRoll;
migration: typeof migration;
macros: typeof macros;
};
}
interface CONFIG {
DS4: typeof DS4;
}
}

View file

@ -4,7 +4,7 @@
import { migration } from "../migration/migration"; import { migration } from "../migration/migration";
export function registerForReadyHook(): void { export function registerForReadyHook() {
Hooks.once("ready", () => { Hooks.once("ready", () => {
migration.migrate(); migration.migrate();
}); });

View file

@ -7,7 +7,7 @@
* @remarks The render hooks of all classes in the class hierarchy are called, so e.g. for a {@link Dialog}, both the * @remarks The render hooks of all classes in the class hierarchy are called, so e.g. for a {@link Dialog}, both the
* "renderDialog" hook and the "renderApplication" hook are called (in this order). * "renderDialog" hook and the "renderApplication" hook are called (in this order).
*/ */
export function registerForRenderHooks(): void { export function registerForRenderHooks() {
["renderApplication", "renderActorSheet", "renderItemSheet"].forEach((hook) => { ["renderApplication", "renderActorSheet", "renderItemSheet"].forEach((hook) => {
Hooks.on(hook, selectTargetInputOnFocus); Hooks.on(hook, selectTargetInputOnFocus);
}); });
@ -16,11 +16,11 @@ export function registerForRenderHooks(): void {
/** /**
* Select the text of input elements in given application when focused via an on focus listener. * Select the text of input elements in given application when focused via an on focus listener.
* *
* @param app - The application in which to activate the listener. * @param {Application} app The application in which to activate the listener.
* @param html - The {@link JQuery} representing the HTML of the application. * @param {JQuery} html The {@link JQuery} representing the HTML of the application.
*/ */
function selectTargetInputOnFocus(app: Application, html: JQuery) { function selectTargetInputOnFocus(app, html) {
html.find("input").on("focus", (ev: JQuery.FocusEvent<HTMLInputElement>) => { html.find("input").on("focus", (ev) => {
ev.currentTarget.select(); ev.currentTarget.select();
}); });
} }

View file

@ -8,7 +8,7 @@
import { DS4 } from "../config"; import { DS4 } from "../config";
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
export function registerForSetupHook(): void { export function registerForSetupHook() {
Hooks.once("setup", () => { Hooks.once("setup", () => {
localizeAndSortConfigObjects(); localizeAndSortConfigObjects();
}); });
@ -28,17 +28,23 @@ function localizeAndSortConfigObjects() {
"checkModifiers", "checkModifiers",
]; ];
const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => { /**
const localized = Object.entries(obj).map(([key, value]): [string, string] => { * @template {Record<string, string>} T
* @param {T} obj The object to localize
* @param {boolean} [sort=true] whether or not to sort the object
* @returns {T} the localized object
*/
const localizeObject = (obj, sort = true) => {
const localized = Object.entries(obj).map(([key, value]) => {
return [key, getGame().i18n.localize(value)]; return [key, getGame().i18n.localize(value)];
}); });
if (sort) localized.sort((a, b) => a[1].localeCompare(b[1])); if (sort) localized.sort((a, b) => a[1].localeCompare(b[1]));
return Object.fromEntries(localized) as T; return Object.fromEntries(localized);
}; };
DS4.i18n = Object.fromEntries( DS4.i18n = Object.fromEntries(
Object.entries(DS4.i18n).map(([key, value]) => { Object.entries(DS4.i18n).map(([key, value]) => {
return [key, localizeObject(value, !noSort.includes(key))]; return [key, localizeObject(value, !noSort.includes(key))];
}), }),
) as typeof DS4.i18n; );
} }

View file

@ -4,14 +4,12 @@
import { getCanvas, getGame } from "../utils/utils"; import { getCanvas, getGame } from "../utils/utils";
import type { DS4Actor } from "../documents/actor/actor";
/** /**
* Gets the currently active actor and token based on how {@link ChatMessage} * Gets the currently active actor and token based on how {@link ChatMessage}
* determines the current speaker. * determines the current speaker.
* @returns The currently active {@link DS4Actor} and {@link TokenDocument}. * @returns {{actor?: import("../documents/actor/actor").DS4Actor, token?: TokenDocument}} The currently active actor and token.
*/ */
export function getActiveActorAndToken(): { actor?: DS4Actor; token?: TokenDocument } { export function getActiveActorAndToken() {
const speaker = ChatMessage.getSpeaker(); const speaker = ChatMessage.getSpeaker();
const speakerToken = speaker.token ? getCanvas().tokens?.get(speaker.token)?.document : undefined; const speakerToken = speaker.token ? getCanvas().tokens?.get(speaker.token)?.document : undefined;

View file

@ -7,19 +7,23 @@ import { notifications } from "../ui/notifications";
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
import { getActiveActorAndToken } from "./helpers"; import { getActiveActorAndToken } from "./helpers";
import type { Check } from "../documents/actor/actor-data-properties-base";
/** /**
* Creates a macro from a check drop. * Creates a macro from a check drop.
* Get an existing roll check macro if one exists, otherwise create a new one. * Get an existing roll check macro if one exists, otherwise create a new one.
* @param check - The name of the check to perform. * @param {import("../documents/actor/actor-data-properties-base").Check} check The name of the check to perform
* @param slot - The hotbar slot to use. * @param {string} slot The hotbar slot to use
* @returns {Promise<void>} A promise that resoolves when the macro has been created.
*/ */
export async function createRollCheckMacro(check: Check, slot: string): Promise<void> { export async function createRollCheckMacro(check, slot) {
const macro = await getOrCreateRollCheckMacro(check); const macro = await getOrCreateRollCheckMacro(check);
getGame().user?.assignHotbarMacro(macro ?? null, slot); await getGame().user?.assignHotbarMacro(macro ?? null, slot);
} }
async function getOrCreateRollCheckMacro(check: Check): Promise<Macro | undefined> { /**
* @param {import("../documents/actor/actor-data-properties-base").Check} check The name of the check to perform
* @returns {Promise<Macro|undefined>} A promise that resolves to the created macro
*/
async function getOrCreateRollCheckMacro(check) {
const command = `game.ds4.macros.rollCheck("${check}");`; const command = `game.ds4.macros.rollCheck("${check}");`;
const existingMacro = getGame().macros?.find( const existingMacro = getGame().macros?.find(
@ -43,8 +47,10 @@ async function getOrCreateRollCheckMacro(check: Check): Promise<Macro | undefine
/** /**
* Executes the roll check macro for the given check. * Executes the roll check macro for the given check.
* @param {import("../documents/actor/actor-data-properties-base").Check} check The name of the check to perform
* @returns {Promise<void>} A promise that resolves once the check has been performed.
*/ */
export async function rollCheck(check: Check): Promise<void> { export async function rollCheck(check) {
const { actor, token } = getActiveActorAndToken(); const { actor, token } = getActiveActorAndToken();
if (!actor) { if (!actor) {
return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro")); return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro"));

View file

@ -9,15 +9,20 @@ import { getActiveActorAndToken } from "./helpers";
/** /**
* Creates a macro from an item drop. * Creates a macro from an item drop.
* Get an existing roll item macro if one exists, otherwise create a new one. * Get an existing roll item macro if one exists, otherwise create a new one.
* @param itemData - The item data * @param {object} itemData The item data
* @param slot - The hotbar slot to use * @param {string} slot The hotbar slot to use
* @returns {Promise<void>} A promise that resolves once the macro has been created.
*/ */
export async function createRollItemMacro(itemData: foundry.data.ItemData["_source"], slot: string): Promise<void> { export async function createRollItemMacro(itemData, slot) {
const macro = await getOrCreateRollItemMacro(itemData); const macro = await getOrCreateRollItemMacro(itemData);
getGame().user?.assignHotbarMacro(macro ?? null, slot); await getGame().user?.assignHotbarMacro(macro ?? null, slot);
} }
async function getOrCreateRollItemMacro(itemData: foundry.data.ItemData["_source"]): Promise<Macro | undefined> { /**
* @param {object} itemData The item data
* @returns {Promise<Macro | undefined>} A promise that resolves to the created macro
*/
async function getOrCreateRollItemMacro(itemData) {
const command = `game.ds4.macros.rollItem("${itemData._id}");`; const command = `game.ds4.macros.rollItem("${itemData._id}");`;
const existingMacro = getGame().macros?.find((m) => m.name === itemData.name && m.data.command === command); const existingMacro = getGame().macros?.find((m) => m.name === itemData.name && m.data.command === command);
@ -39,8 +44,10 @@ async function getOrCreateRollItemMacro(itemData: foundry.data.ItemData["_source
/** /**
* Executes the roll item macro for the item associated to the given `itemId`. * Executes the roll item macro for the item associated to the given `itemId`.
* @param {string} itemId The id of the item to roll
* @returns {Promise<void>} A promise that resolves once the item has been rolled.
*/ */
export async function rollItem(itemId: string): Promise<void> { export async function rollItem(itemId) {
const { actor, token } = getActiveActorAndToken(); const { actor, token } = getActiveActorAndToken();
if (!actor) { if (!actor) {
return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollItemMacro")); return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollItemMacro"));

View file

@ -10,13 +10,15 @@ import {
migrateScenes, migrateScenes,
} from "./migrationHelpers"; } from "./migrationHelpers";
async function migrate(): Promise<void> { /** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateActors(getActorUpdateData); await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium); await migrateCompendiums(migrateCompendium);
} }
function getActorUpdateData(): Record<string, unknown> { /** @type {import("./migrationHelpers").ActorUpdateDataGetter} */
function getActorUpdateData() {
const updateData = { const updateData = {
data: { data: {
combatValues: [ combatValues: [
@ -28,7 +30,7 @@ function getActorUpdateData(): Record<string, unknown> {
"rangedAttack", "rangedAttack",
"spellcasting", "spellcasting",
"targetedSpellcasting", "targetedSpellcasting",
].reduce((acc: Partial<Record<string, { "-=base": null }>>, curr) => { ].reduce((acc, curr) => {
acc[curr] = { "-=base": null }; acc[curr] = { "-=base": null };
return acc; return acc;
}, {}), }, {}),
@ -40,6 +42,7 @@ function getActorUpdateData(): Record<string, unknown> {
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData); const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getActorUpdateData, getSceneUpdateData }); const migrateCompendium = getCompendiumMigrator({ getActorUpdateData, getSceneUpdateData });
/** @type {import("./migration").Migration} */
export const migration = { export const migration = {
migrate, migrate,
migrateCompendium, migrateCompendium,

View file

@ -12,18 +12,18 @@ import {
migrateScenes, migrateScenes,
} from "./migrationHelpers"; } from "./migrationHelpers";
async function migrate(): Promise<void> { /** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateItems(getItemUpdateData); await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData); await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium); await migrateCompendiums(migrateCompendium);
} }
function getItemUpdateData( /** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
itemData: Partial<foundry.data.ItemData["_source"]>, function getItemUpdateData(itemData) {
): DeepPartial<foundry.data.ItemData["_source"]> | undefined {
if (!["equipment", "trinket"].includes(itemData.type ?? "")) return undefined; if (!["equipment", "trinket"].includes(itemData.type ?? "")) return undefined;
return { type: itemData.type === "equipment" ? ("loot" as const) : ("equipment" as const) }; return { type: itemData.type === "equipment" ? "loot" : "equipment" };
} }
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData); const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
@ -33,6 +33,7 @@ const migrateCompendium = getCompendiumMigrator(
{ migrateToTemplateEarly: false }, { migrateToTemplateEarly: false },
); );
/** @type {import("./migration").Migration} */
export const migration = { export const migration = {
migrate, migrate,
migrateCompendium, migrateCompendium,

View file

@ -12,14 +12,16 @@ import {
migrateScenes, migrateScenes,
} from "./migrationHelpers"; } from "./migrationHelpers";
async function migrate(): Promise<void> { /** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateItems(getItemUpdateData); await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData); await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium); await migrateCompendiums(migrateCompendium);
} }
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) { /** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
function getItemUpdateData(itemData) {
if (!["loot"].includes(itemData.type ?? "")) return undefined; if (!["loot"].includes(itemData.type ?? "")) return undefined;
return { return {
data: { data: {
@ -35,6 +37,7 @@ const migrateCompendium = getCompendiumMigrator(
{ migrateToTemplateEarly: false }, { migrateToTemplateEarly: false },
); );
/** @type {import("./migration").Migration} */
export const migration = { export const migration = {
migrate, migrate,
migrateCompendium, migrateCompendium,

View file

@ -12,19 +12,20 @@ import {
migrateScenes, migrateScenes,
} from "./migrationHelpers"; } from "./migrationHelpers";
async function migrate(): Promise<void> { /** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateItems(getItemUpdateData); await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData); await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium); await migrateCompendiums(migrateCompendium);
} }
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) { /** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
function getItemUpdateData(itemData) {
if (itemData.type !== "spell") return; if (itemData.type !== "spell") return;
// @ts-expect-error the type of cooldownDuration was UnitData<TemporalUnit> at the point for this migration, but it changed later on const cooldownDurationUnit = itemData.data?.cooldownDuration.unit;
const cooldownDurationUnit: string | undefined = itemData.data?.cooldownDuration.unit;
const updateData: Record<string, unknown> = { const updateData = {
data: { data: {
"-=scrollPrice": null, "-=scrollPrice": null,
minimumLevels: { healer: null, wizard: null, sorcerer: null }, minimumLevels: { healer: null, wizard: null, sorcerer: null },
@ -40,6 +41,7 @@ const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData); const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData }); const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
/** @type {import("./migration").Migration} */
export const migration = { export const migration = {
migrate, migrate,
migrateCompendium, migrateCompendium,

View file

@ -21,22 +21,22 @@ const hoursPerDay = 24;
const roundsPerDay = hoursPerDay / roundsPerHour; const roundsPerDay = hoursPerDay / roundsPerHour;
const secondsPerDay = secondsPerMinute * minutesPerHour * hoursPerDay; const secondsPerDay = secondsPerMinute * minutesPerHour * hoursPerDay;
async function migrate(): Promise<void> { /** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateItems(getItemUpdateData); await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData); await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium); await migrateCompendiums(migrateCompendium);
} }
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) { /** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
function getItemUpdateData(itemData) {
if (itemData.type !== "spell") return; if (itemData.type !== "spell") return;
// @ts-expect-error the type of cooldownDuration is changed from UnitData<TemporalUnit> to CooldownDuation with this migration const cooldownDurationUnit = itemData.data?.cooldownDuration.unit;
const cooldownDurationUnit: string | undefined = itemData.data?.cooldownDuration.unit; const cooldownDurationValue = itemData.data?.cooldownDuration.value;
// @ts-expect-error the type of cooldownDuration is changed from UnitData<TemporalUnit> to CooldownDuation with this migration
const cooldownDurationValue: string | undefined = itemData.data?.cooldownDuration.value;
const cooldownDuration = migrateCooldownDuration(cooldownDurationValue, cooldownDurationUnit); const cooldownDuration = migrateCooldownDuration(cooldownDurationValue, cooldownDurationUnit);
const updateData: Record<string, unknown> = { const updateData = {
data: { data: {
cooldownDuration, cooldownDuration,
}, },
@ -88,7 +88,13 @@ function migrateCooldownDuration(cooldownDurationValue = "", cooldownDurationUni
} }
} }
function getRounds(unit: string, value: number): number { /**
* Given a unit and a value, return the correct number of rounds
* @param {string} unit The unit
* @param {number} value The value
* @returns {number} The number of rounds
*/
function getRounds(unit, value) {
switch (unit) { switch (unit) {
case "rounds": { case "rounds": {
return value; return value;
@ -112,6 +118,7 @@ const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData); const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData }); const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
/** @type {import("./migration").Migration} */
export const migration = { export const migration = {
migrate, migrate,
migrateCompendium, migrateCompendium,

View file

@ -12,26 +12,25 @@ import {
migrateScenes, migrateScenes,
} from "./migrationHelpers"; } from "./migrationHelpers";
import type { DS4SpellDataSourceData } from "../documents/item/spell/spell-data-source"; /** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
async function migrate(): Promise<void> {
await migrateItems(getItemUpdateData); await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData); await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium); await migrateCompendiums(migrateCompendium);
} }
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) { /** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
function getItemUpdateData(itemData) {
if (itemData.type !== "spell") return; if (itemData.type !== "spell") return;
// @ts-expect-error spellCategory is removed with this migration const spellCategory = itemData.data?.spellCategory;
const spellCategory: string | undefined = itemData.data?.spellCategory;
const spellGroups = migrateSpellCategory(spellCategory); const spellGroups = migrateSpellCategory(spellCategory);
// @ts-expect-error bonus is removed with this migration // @ts-expect-error bonus is removed with this migration
const bonus: string | undefined = itemData.data?.bonus; const bonus = itemData.data?.bonus;
const spellModifier = migrateBonus(bonus); const spellModifier = migrateBonus(bonus);
const updateData: Record<string, unknown> = { const updateData = {
data: { data: {
spellGroups, spellGroups,
"-=spellCategory": null, "-=spellCategory": null,
@ -42,7 +41,12 @@ function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>)
return updateData; return updateData;
} }
function migrateSpellCategory(spellCategory: string | undefined): DS4SpellDataSourceData["spellGroups"] { /**
* Migrate a spell category to spell groups.
* @param {string | undefined} spellCategory The spell category
* @returns {import("../documents/item/spell/spell-data-source").DS4SpellDataSourceData["spellGroups"]} The spell groups for the given category
*/
function migrateSpellCategory(spellCategory) {
const spellGroups = { const spellGroups = {
lightning: false, lightning: false,
earth: false, earth: false,
@ -95,7 +99,12 @@ function migrateSpellCategory(spellCategory: string | undefined): DS4SpellDataSo
return spellGroups; return spellGroups;
} }
function migrateBonus(bonus: string | undefined): DS4SpellDataSourceData["spellModifier"] { /**
* Migrate a spell bonus to a spell modifier.
* @param {string | undefined} bonus The spell bonus
* @returns {import("../documents/item/spell/spell-data-source").DS4SpellDataSourceData["spellModifier"]} The spell modifier
*/
function migrateBonus(bonus) {
const spellModifier = { numerical: 0, complex: "" }; const spellModifier = { numerical: 0, complex: "" };
if (bonus) { if (bonus) {
if (Number.isNumeric(bonus)) { if (Number.isNumeric(bonus)) {
@ -111,6 +120,7 @@ const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData); const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData }); const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
/** @type {import("./migration").Migration} */
export const migration = { export const migration = {
migrate, migrate,
migrateCompendium, migrateCompendium,

View file

@ -12,14 +12,16 @@ import {
migrateScenes, migrateScenes,
} from "./migrationHelpers"; } from "./migrationHelpers";
async function migrate(): Promise<void> { /** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateItems(getItemUpdateData); await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData); await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium); await migrateCompendiums(migrateCompendium);
} }
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) { /** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
function getItemUpdateData(itemData) {
if (itemData.type !== "spell") return; if (itemData.type !== "spell") return;
return { return {
@ -33,6 +35,7 @@ const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData); const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData }); const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
/** @type {import("./migration").Migration} */
export const migration = { export const migration = {
migrate, migrate,
migrateCompendium, migrateCompendium,

View file

@ -13,7 +13,11 @@ import { migration as migration005 } from "./005";
import { migration as migration006 } from "./006"; import { migration as migration006 } from "./006";
import { migration as migration007 } from "./007"; import { migration as migration007 } from "./007";
async function migrate(): Promise<void> { /**
* Perform migrations.
* @returns {Promise<void>} A promise that resolves once all migrations have completed
*/
async function migrate() {
if (!getGame().user?.isGM) { if (!getGame().user?.isGM) {
return; return;
} }
@ -30,7 +34,13 @@ async function migrate(): Promise<void> {
return migrateFromTo(oldMigrationVersion, targetMigrationVersion); return migrateFromTo(oldMigrationVersion, targetMigrationVersion);
} }
async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion: number): Promise<void> { /**
* Migrate from a given version to another version.
* @param {number} oldMigrationVersion The old migration version
* @param {number} targetMigrationVersion The migration version to migrate to
* @returns {Promise<void>} A promise the resolves once the migration is complete
*/
async function migrateFromTo(oldMigrationVersion, targetMigrationVersion) {
if (!getGame().user?.isGM) { if (!getGame().user?.isGM) {
return; return;
} }
@ -76,11 +86,14 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
} }
} }
async function migrateCompendiumFromTo( /**
pack: CompendiumCollection<CompendiumCollection.Metadata>, * Migrate a compendium pack from a given version to another version.
oldMigrationVersion: number, * @param {CompendiumCollection} pack The compendium pack to migrate
targetMigrationVersion: number, * @param {number} oldMigrationVersion The old version number
): Promise<void> { * @param {number} targetMigrationVersion The target version number
* @returns {Promise<void>} A promise that resolves once the migration is complete
*/
async function migrateCompendiumFromTo(pack, oldMigrationVersion, targetMigrationVersion) {
if (!getGame().user?.isGM) { if (!getGame().user?.isGM) {
return; return;
} }
@ -128,30 +141,39 @@ async function migrateCompendiumFromTo(
} }
} }
function getCurrentMigrationVersion(): number { /**
* Get the current migration version.
* @returns {number} The current migration version
*/
function getCurrentMigrationVersion() {
return getGame().settings.get("ds4", "systemMigrationVersion"); return getGame().settings.get("ds4", "systemMigrationVersion");
} }
function getTargetMigrationVersion(): number { /**
* Get the target migration version.
* @returns {number} The target migration version
*/
function getTargetMigrationVersion() {
return migrations.length; return migrations.length;
} }
interface Migration { /**
migrate: () => Promise<void>; * @typedef {object} Migration
migrateCompendium: (pack: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>; * @property {() => Promise<void>} migrate
} * @property {import("./migrationHelpers").CompendiumMigrator} migrateCompendium
*/
const migrations: Migration[] = [ /**
migration001, * @type {Migration[]}
migration002, */
migration003, const migrations = [migration001, migration002, migration003, migration004, migration005, migration006, migration007];
migration004,
migration005,
migration006,
migration007,
];
function isFirstWorldStart(migrationVersion: number): boolean { /**
* DOes the migration version indicate the world is being started for the first time?
* @param {number} migrationVersion A migration version
* @returns {boolean} Whether the migration version indicates it is the first start of the world
*/
function isFirstWorldStart(migrationVersion) {
return migrationVersion < 0; return migrationVersion < 0;
} }

View file

@ -7,11 +7,14 @@ import { DS4Item } from "../documents/item/item";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { getGame } from "../utils/utils"; import { getGame } from "../utils/utils";
type ItemUpdateDataGetter = ( /** @typedef {(itemData: Partial<foundry.data.ItemData["_source"]>) => DeepPartial<foundry.data.ItemData["_source"]> | Record<string, unknown> | undefined} ItemUpdateDataGetter */
itemData: Partial<foundry.data.ItemData["_source"]>,
) => DeepPartial<foundry.data.ItemData["_source"]> | Record<string, unknown> | undefined;
export async function migrateItems(getItemUpdateData: ItemUpdateDataGetter): Promise<void> { /**
* Migrate world items.
* @param {ItemUpdateDataGetter} getItemUpdateData A function for getting the update data for a given item data object
* @returns {Promise<void>} A promise that resolves once the migration is complete
*/
export async function migrateItems(getItemUpdateData) {
for (const item of getGame().items ?? []) { for (const item of getGame().items ?? []) {
try { try {
const updateData = getItemUpdateData(item.toObject()); const updateData = getItemUpdateData(item.toObject());
@ -25,11 +28,14 @@ export async function migrateItems(getItemUpdateData: ItemUpdateDataGetter): Pro
} }
} }
type ActorUpdateDataGetter = ( /** @typedef {(actorData: Partial<foundry.data.ActorData["_source"]>) => DeepPartial<foundry.data.ActorData["_source"]> | undefined} ActorUpdateDataGetter */
itemData: Partial<foundry.data.ActorData["_source"]>,
) => DeepPartial<foundry.data.ActorData["_source"]> | undefined;
export async function migrateActors(getActorUpdateData: ActorUpdateDataGetter): Promise<void> { /**
* Migrate world actors.
* @param {ActorUpdateDataGetter} getActorUpdateData A function for getting the update data for a given actor data object
* @returns {Promise<void>} A promise that resolves once the migration is complete
*/
export async function migrateActors(getActorUpdateData) {
for (const actor of getGame().actors ?? []) { for (const actor of getGame().actors ?? []) {
try { try {
const updateData = getActorUpdateData(actor.toObject()); const updateData = getActorUpdateData(actor.toObject());
@ -46,17 +52,20 @@ export async function migrateActors(getActorUpdateData: ActorUpdateDataGetter):
} }
} }
type SceneUpdateDataGetter = (sceneData: foundry.data.SceneData) => DeepPartial<foundry.data.SceneData["_source"]>; /** @typedef {(aceneData: foundry.data.SceneData) => DeepPartial<foundry.data.SceneData["_source"]> | undefined} SceneUpdateDataGetter */
export async function migrateScenes(getSceneUpdateData: SceneUpdateDataGetter): Promise<void> { /**
* Migrate world scenes.
* @param {SceneUpdateDataGetter} getSceneUpdateData A function for getting the update data for a given scene data object
* @returns {Promise<void>} A promise that resolves once the migration is complete
*/
export async function migrateScenes(getSceneUpdateData) {
for (const scene of getGame().scenes ?? []) { for (const scene of getGame().scenes ?? []) {
try { try {
const updateData = getSceneUpdateData(scene.data); const updateData = getSceneUpdateData(scene.data);
if (updateData) { if (updateData) {
logger.info(`Migrating Scene document ${scene.name} (${scene.id})`); logger.info(`Migrating Scene document ${scene.name} (${scene.id})`);
await scene.update( await scene.update(updateData);
updateData as DeepPartial<Parameters<foundry.data.SceneData["_initializeSource"]>[0]>,
);
} }
} catch (err) { } catch (err) {
logger.error( logger.error(
@ -67,9 +76,14 @@ export async function migrateScenes(getSceneUpdateData: SceneUpdateDataGetter):
} }
} }
type CompendiumMigrator = (compendium: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>; /** @typedef {(pack: CompendiumCollection) => Promise<void>} CompendiumMigrator*/
export async function migrateCompendiums(migrateCompendium: CompendiumMigrator): Promise<void> { /**
* Migrate world compendium packs.
* @param {CompendiumMigrator} migrateCompendium A function for migrating a single compendium pack
* @returns {Promise<void>} A promise that resolves once the migration is complete
*/
export async function migrateCompendiums(migrateCompendium) {
for (const compendium of getGame().packs ?? []) { for (const compendium of getGame().packs ?? []) {
if (compendium.metadata.package !== "world") continue; if (compendium.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(compendium.metadata.type)) continue; if (!["Actor", "Item", "Scene"].includes(compendium.metadata.type)) continue;
@ -77,10 +91,13 @@ export async function migrateCompendiums(migrateCompendium: CompendiumMigrator):
} }
} }
export function getActorUpdateDataGetter(getItemUpdateData: ItemUpdateDataGetter): ActorUpdateDataGetter { /**
return ( * Get a function to create actor update data that adjusts the owned items of the actor according to the given function.
actorData: Partial<foundry.data.ActorData["_source"]>, * @param {ItemUpdateDataGetter} getItemUpdateData The function to generate item update data
): DeepPartial<foundry.data.ActorData["_source"]> | undefined => { * @returns {ActorUpdateDataGetter} A function to get actor update data
*/
export function getActorUpdateDataGetter(getItemUpdateData) {
return (actorData) => {
let hasItemUpdates = false; let hasItemUpdates = false;
const items = actorData.items?.map((itemData) => { const items = actorData.items?.map((itemData) => {
const update = getItemUpdateData(itemData); const update = getItemUpdateData(itemData);
@ -95,9 +112,14 @@ export function getActorUpdateDataGetter(getItemUpdateData: ItemUpdateDataGetter
}; };
} }
export function getSceneUpdateDataGetter(getActorUpdateData: ActorUpdateDataGetter): SceneUpdateDataGetter { /**
return (sceneData: foundry.data.SceneData) => { * Get a function to create scene update data that adjusts the actors of the tokens of the scene according to the given function.
const tokens = sceneData.tokens.map((token: TokenDocument) => { * @param {ActorUpdateDataGetter} getItemUpdateData The function to generate actor update data
* @returns {SceneUpdateDataGetter} A function to get scene update data
*/
export function getSceneUpdateDataGetter(getActorUpdateData) {
return (sceneData) => {
const tokens = sceneData.tokens.map((token) => {
const t = token.toObject(); const t = token.toObject();
if (!t.actorId || t.actorLink) { if (!t.actorId || t.actorLink) {
t.actorData = {}; t.actorData = {};
@ -109,7 +131,7 @@ export function getSceneUpdateDataGetter(getActorUpdateData: ActorUpdateDataGett
actorData.type = token.actor?.type; actorData.type = token.actor?.type;
const update = getActorUpdateData(actorData); const update = getActorUpdateData(actorData);
if (update !== undefined) { if (update !== undefined) {
["items" as const, "effects" as const].forEach((embeddedName) => { ["items", "effects"].forEach((embeddedName) => {
const embeddedUpdates = update[embeddedName]; const embeddedUpdates = update[embeddedName];
if (embeddedUpdates === undefined || !embeddedUpdates.length) return; if (embeddedUpdates === undefined || !embeddedUpdates.length) return;
const updates = new Map(embeddedUpdates.flatMap((u) => (u && u._id ? [[u._id, u]] : []))); const updates = new Map(embeddedUpdates.flatMap((u) => (u && u._id ? [[u._id, u]] : [])));
@ -131,32 +153,37 @@ export function getSceneUpdateDataGetter(getActorUpdateData: ActorUpdateDataGett
}; };
} }
/**
* @typedef {object} UpdateDataGetters
* @property {ItemUpdateDataGetter} [getItemUpdateData]
* @property {ActorUpdateDataGetter} [getActorUpdateData]
* @property {SceneUpdateDataGetter} [getSceneUpdateData]
*/
/**
* Get a compendium migrator for the given update data getters.
* @param {UpdateDataGetters} [updateDataGetters={}] The functions to use for getting update data
* @param {{migrateToTemplateEarly?: boolean}} [options={}] Additional options for the compendium migrator
* @returns {CompendiumMigrator} The resulting compendium migrator
*/
export function getCompendiumMigrator( export function getCompendiumMigrator(
{ { getItemUpdateData, getActorUpdateData, getSceneUpdateData } = {},
getItemUpdateData,
getActorUpdateData,
getSceneUpdateData,
}: {
getItemUpdateData?: ItemUpdateDataGetter;
getActorUpdateData?: ActorUpdateDataGetter;
getSceneUpdateData?: SceneUpdateDataGetter;
} = {},
{ migrateToTemplateEarly = true } = {}, { migrateToTemplateEarly = true } = {},
) { ) {
return async (compendium: CompendiumCollection<CompendiumCollection.Metadata>): Promise<void> => { return async (pack) => {
const type = compendium.metadata.type; const type = pack.metadata.type;
if (!["Actor", "Item", "Scene"].includes(type)) return; if (!["Actor", "Item", "Scene"].includes(type)) return;
const wasLocked = compendium.locked; const wasLocked = pack.locked;
await compendium.configure({ locked: false }); await pack.configure({ locked: false });
if (migrateToTemplateEarly) { if (migrateToTemplateEarly) {
await compendium.migrate(); await pack.migrate();
} }
const documents = await compendium.getDocuments(); const documents = await pack.getDocuments();
for (const doc of documents) { for (const doc of documents) {
try { try {
logger.info(`Migrating document ${doc.name} (${doc.id}) in compendium ${compendium.collection}`); logger.info(`Migrating document ${doc.name} (${doc.id}) in compendium ${pack.collection}`);
if (doc instanceof DS4Item && getItemUpdateData) { if (doc instanceof DS4Item && getItemUpdateData) {
const updateData = getItemUpdateData(doc.toObject()); const updateData = getItemUpdateData(doc.toObject());
updateData && (await doc.update(updateData)); updateData && (await doc.update(updateData));
@ -164,23 +191,20 @@ export function getCompendiumMigrator(
const updateData = getActorUpdateData(doc.toObject()); const updateData = getActorUpdateData(doc.toObject());
updateData && (await doc.update(updateData)); updateData && (await doc.update(updateData));
} else if (doc instanceof Scene && getSceneUpdateData) { } else if (doc instanceof Scene && getSceneUpdateData) {
const updateData = getSceneUpdateData(doc.data as foundry.data.SceneData); const updateData = getSceneUpdateData(doc.data);
updateData && updateData && (await doc.update(updateData));
(await doc.update(
updateData as DeepPartial<Parameters<foundry.data.SceneData["_initializeSource"]>[0]>,
));
} }
} catch (err) { } catch (err) {
logger.error( logger.error(
`Error during migration of document ${doc.name} (${doc.id}) in compendium ${compendium.collection}, continuing anyways.`, `Error during migration of document ${doc.name} (${doc.id}) in compendium ${pack.collection}, continuing anyways.`,
err, err,
); );
} }
} }
if (!migrateToTemplateEarly) { if (!migrateToTemplateEarly) {
await compendium.migrate(); await pack.migrate();
} }
await compendium.configure({ locked: wasLocked }); await pack.configure({ locked: wasLocked });
}; };
} }

View file

@ -4,11 +4,11 @@
import { getGame } from "./utils/utils"; import { getGame } from "./utils/utils";
export function registerSystemSettings(): void { export function registerSystemSettings() {
const game = getGame(); const game = getGame();
/** /**
* Track the migrations version of the latest migration that has been applied * Track the migration version of the latest migration that has been applied.
*/ */
game.settings.register("ds4", "systemMigrationVersion", { game.settings.register("ds4", "systemMigrationVersion", {
name: "System Migration Version", name: "System Migration Version",
@ -37,13 +37,18 @@ export function registerSystemSettings(): void {
}); });
} }
export interface DS4Settings { /**
systemMigrationVersion: number; * @typedef DS4Settings
useSlayingDiceForAutomatedChecks: boolean; * @property {number} systemMigrationVersion
showSlayerPoints: boolean; * @property {boolean} useSlayingDiceForAutomatedChecks
} * @property {boolean} showSlayerPoints
*/
export function getDS4Settings(): DS4Settings { /**
* Get the current values for DS4 settings.
* @returns {DS4Settings}
*/
export function getDS4Settings() {
const game = getGame(); const game = getGame();
return { return {
systemMigrationVersion: game.settings.get("ds4", "systemMigrationVersion"), systemMigrationVersion: game.settings.get("ds4", "systemMigrationVersion"),

61
src/ui/notifications.js Normal file
View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { logger } from "../utils/logger";
import { getNotificationsSafe } from "../utils/utils";
/**
* @typedef {Object} NotificationOptions
* @property {boolean} [permanent=false]
* @property {boolean} [log=false]
*/
/**
* @typedef {(message: string, options?: NotificationOptions) => void} NotificationFunction
*/
/**
* @typedef {"info" | "warn" | "error"} NotificationType
*/
/**
* @param {NotificationType} type The type of the notification
* @returns {NotificationFunction}
*/
function getNotificationFunction(type) {
return (message, { permanent = false, log = false } = {}) => {
if (ui.notifications) {
ui.notifications[type](message, { permanent });
if (log) {
logger[type](message);
}
} else {
logger[type](message);
}
};
}
/**
* @param {string} message
* @param {NotificationType} type
* @param {NotificationOptions} [options={}]
*/
function notify(message, type, { permanent = false, log = false } = {}) {
const notifications = getNotificationsSafe();
if (notifications) {
notifications.notify(message, type, { permanent });
if (log) {
logger.getLoggingFunction(type)(message);
}
} else {
logger.getLoggingFunction(type)(message);
}
}
export const notifications = Object.freeze({
info: getNotificationFunction("info"),
warn: getNotificationFunction("warn"),
error: getNotificationFunction("error"),
notify,
});

View file

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { logger } from "../utils/logger";
function getNotificationFunction(type: "info" | "warn" | "error") {
return (message: string, { permanent = false, log = false }: { permanent?: boolean; log?: boolean } = {}): void => {
if (ui.notifications) {
ui.notifications[type](message, { permanent });
if (log) {
logger[type](message);
}
} else {
logger[type](message);
}
};
}
export const notifications = Object.freeze({
info: getNotificationFunction("info"),
warn: getNotificationFunction("warn"),
error: getNotificationFunction("error"),
notify: (
message: string,
type: "info" | "warning" | "error" = "info",
{ permanent = false, log = false }: { permanent?: boolean; log?: boolean } = {},
): void => {
if (ui.notifications) {
ui.notifications.notify(message, type, { permanent });
if (log) {
logger.getLoggingFunction(type)(message);
}
} else {
logger.getLoggingFunction(type)(message);
}
},
});

61
src/utils/utils.js Normal file
View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
/**
* Tests if the given `value` is truthy.
*
* If it is not truthy, an {@link Error} is thrown, which depends on the given `message` parameter:
* - If `message` is a string`, it is used to construct a new {@link Error} which then is thrown.
* - If `message` is an instance of {@link Error}, it is thrown.
* - If `message` is `undefined`, an {@link Error} with a default message is thrown.
* @param {unknown} value The value to check for truthyness
* @param {string | Error} [message] An error message to use when the check fails
* @returns {asserts value}
*/
export function enforce(value, message) {
if (!value) {
if (!message) {
message =
getGameSafe()?.i18n.localize("DS4.ErrorUnexpectedError") ??
"There was an unexpected error in the Dungeonslayers 4 system. For more details, please take a look at the console (F12).";
}
throw message instanceof Error ? message : new Error(message);
}
}
/**
* A wrapper that returns the canvas, if it is ready.
* @throws if the canvas is not ready yet
* @returns {Canvas}
*/
export function getCanvas() {
enforce(canvas instanceof Canvas && canvas.ready, getGame().i18n.localize("DS4.ErrorCanvasIsNotInitialized"));
return canvas;
}
/**
* A wrapper that returns the game, if it already exists.
* @throws {Error} if the game is not ready yet
* @returns {Game}
*/
export function getGame() {
enforce(game instanceof Game, "Game is not initialized yet.");
return game;
}
/**
* A wrapper that returns the game, or `undefined` if it doesn't exist yet
* @returns {Game | undefined}
*/
export function getGameSafe() {
return game instanceof Game ? game : undefined;
}
/**
* A wrapper that returns `ui.notifications`, or `undefined` if it doesn't exist yet
* @returns {Notifications | undefined}
*/
export function getNotificationsSafe() {
return ui.notifications instanceof Notifications ? ui.notifications : undefined;
}

View file

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
/**
* Tests if the given `value` is truthy.
*
* If it is not truthy, an {@link Error} is thrown, which depends on the given `message` parameter:
* - If `message` is a string`, it is used to construct a new {@link Error} which then is thrown.
* - If `message` is an instance of {@link Error}, it is thrown.
* - If `message` is `undefined`, an {@link Error} with a default message is thrown.
*/
export function enforce(value: unknown, message?: string | Error): asserts value {
if (!value) {
if (!message) {
message =
getGameSafe()?.i18n.localize("DS4.ErrorUnexpectedError") ??
"There was an unexpected error in the Dungeonslayers 4 system. For more details, please take a look at the console (F12).";
}
throw message instanceof Error ? message : new Error(message);
}
}
export function getCanvas(): Canvas {
if (!(canvas instanceof Canvas) || !canvas.ready) {
throw new Error(getGame().i18n.localize("DS4.ErrorCanvasIsNotInitialized"));
}
return canvas;
}
export function getGame(): Game {
if (!(game instanceof Game)) {
throw new Error("Game is not initialized yet.");
}
return game;
}
export function getGameSafe(): Game | undefined {
return game instanceof Game ? game : undefined;
}

View file

@ -36,12 +36,8 @@
"version": "1.18.2", "version": "1.18.2",
"minimumCoreVersion": "9.238", "minimumCoreVersion": "9.238",
"compatibleCoreVersion": "9", "compatibleCoreVersion": "9",
"esmodules": [ "esmodules": ["ds4.js"],
"ds4.js" "styles": ["css/ds4.css"],
],
"styles": [
"css/ds4.css"
],
"languages": [ "languages": [
{ {
"lang": "en", "lang": "en",

View file

@ -1,8 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"outDir": "dist",
"target": "ES2021", "target": "ES2021",
"lib": ["ES2021", "DOM"], "lib": ["ES2021", "DOM"],
"types": ["@league-of-foundry-developers/foundry-vtt-types"], "types": ["@types/jquery", "handlebars"],
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
@ -10,7 +11,9 @@
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"importsNotUsedAsValues": "error" "importsNotUsedAsValues": "error",
"checkJs": false,
"allowJs": true
}, },
"include": ["src"] "include": ["src", "client", "common"]
} }

832
yarn.lock

File diff suppressed because it is too large Load diff