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.loader.mjs
/.yarn/
client
common

4
.gitignore vendored
View File

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

View File

@ -8,3 +8,5 @@
/.pnp.loader.mjs
/.yarn/
/.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/config-conventional": "17.3.0",
"@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",
"@types/fs-extra": "9.0.13",
"@types/jquery": "3.5.14",
"@types/node": "18.11.9",
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
@ -85,6 +76,7 @@
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"fs-extra": "10.1.0",
"handlebars": "4.7.7",
"npm-run-all": "4.1.5",
"prettier": "2.8.0",
"rimraf": "3.0.2",

View File

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

View File

@ -3,21 +3,28 @@
// SPDX-License-Identifier: MIT
export class DS4ActiveEffectConfig extends ActiveEffectConfig {
static override get defaultOptions(): DocumentSheetOptions {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
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);
const checkbox = html[0]?.querySelector<HTMLInputElement>(
'input[name="flags.ds4.itemEffectConfig.applyToItems"]',
);
checkbox?.addEventListener("change", () => this.toggleItemEffectConfig(checkbox.checked));
const checkbox = html[0]?.querySelector('input[name="flags.ds4.itemEffectConfig.applyToItems"]');
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");
elements?.forEach((element) => {
if (active) {

View File

@ -13,15 +13,12 @@ import { notifications } from "../../ui/notifications";
import { enforce, getCanvas, getGame } from "../../utils/utils";
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.
*/
export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetData> {
static override get defaultOptions(): ActorSheet.Options {
export class DS4ActorSheet extends ActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-actor-sheet"],
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";
if (!getGame().user?.isGM && this.actor.limited) return `${basePath}/limited-sheet.hbs`;
return `${basePath}/${this.actor.data.type}-sheet.hbs`;
}
override async getData(): Promise<DS4ActorSheetData> {
/** @override */
async getData(options) {
const itemsByType = Object.fromEntries(
Object.entries(this.actor.itemTypes).map(([itemType, items]) => {
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 data = {
...this.addTooltipsToData(await super.getData()),
...this.addTooltipsToData(await super.getData(options)),
config: DS4,
itemsByType,
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}.
* @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];
valueGroups.forEach((valueGroup) => {
Object.values(valueGroup).forEach((attribute: ModifiableDataBaseTotal<number> & { tooltip?: string }) => {
Object.values(valueGroup).forEach((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.
* @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")}) + ${
value.mod
} (${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);
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.
*
* @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();
const a = event.currentTarget;
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.
*
* @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 name = getGame().i18n.localize(`DS4.New${type.capitalize()}Name`);
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.
*
* @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)
.parents(embeddedDocumentListEntryProperties.Item.selector)
.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.
*
* @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);
this.actor.deleteEmbeddedDocuments("Item", [li.data(embeddedDocumentListEntryProperties.Item.idDataAttribute)]);
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
* {@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");
}
@ -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
* 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();
const a = event.currentTarget;
switch (a.dataset["action"]) {
@ -212,18 +226,20 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/**
* 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);
}
/**
* 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)
.parents(embeddedDocumentListEntryProperties.ActiveEffect.selector)
.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.
*
* @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 id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
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
* {@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");
}
@ -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
* on the `data-property` attribute of the {@link HTMLInputElement} that has been changed and its new value.
*
* @param event - The originating change event
* @param documentName - The name of the embedded document to be changed.
* @param {JQuery.ChangeEvent} event The originating click event
* @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();
const element = $(event.currentTarget).get(0);
enforce(element instanceof HTMLInputElement);
@ -284,17 +303,19 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
* - text input: `string`
* - 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) {
case "checkbox": {
const inverted = Boolean(element.dataset["inverted"]);
const value: boolean = element.checked;
const value = element.checked;
return inverted ? !value : value;
}
case "text": {
const value: string = element.value;
const value = element.value;
return value;
}
case "number": {
@ -311,9 +332,10 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/**
* 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();
const id = $(event.currentTarget)
.parents(embeddedDocumentListEntryProperties.Item.selector)
@ -325,17 +347,22 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
/**
* 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.currentTarget.blur();
const check = event.currentTarget.dataset["check"];
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);
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.
* @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();
const target = event.currentTarget;
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);
items.sort((a, b) => a.data.sort - b.data.sort);
const sortFunction =
(invert: boolean) =>
(a: DS4Item, b: DS4Item): number => {
/**
* @param {boolean} invert Whether or not to inverse the sort order
* @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 propertyB = getProperty(b.data, dataPath);
const comparison =
typeof propertyA === "string" || typeof propertyB === "string"
? compareAsStrings(propertyA, propertyB, invert)
: compareAsNumbers(propertyA, propertyB, invert);
return 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 propertyB = getProperty(b.data, dataPath);
return typeof propertyA === "string" || typeof propertyB === "string"
? compareAsStrings(propertyA, propertyB, invert)
: compareAsNumbers(propertyA, propertyB, invert);
}
return comparison;
};
return comparison;
};
const sortedItems = [...items].sort(sortFunction(false));
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);
}
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);
if (item && !this.actor.canOwnItemType(item.data.type)) {
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.
*/
@ -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());
};
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;
};

View File

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

View File

@ -2,18 +2,20 @@
//
// 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.
*/
export class DialogWithListeners extends Dialog<DialogWithListenersOptions> {
override activateListeners(html: JQuery): void {
export class DialogWithListeners extends Dialog {
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (this.options.activateAdditionalListeners !== undefined) {
this.options.activateAdditionalListeners(html, this);
}
}
}
interface DialogWithListenersOptions extends DialogOptions {
activateAdditionalListeners?: ((html: JQuery, app: DialogWithListeners) => void) | undefined;
}

View File

@ -14,8 +14,9 @@ import { disableOverriddenFields } from "./sheet-helpers";
/**
* The Sheet class for DS4 Items
*/
export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData> {
static override get defaultOptions(): ItemSheet.Options {
export class DS4ItemSheet extends ItemSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-item-sheet"],
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";
return `${basePath}/${this.item.data.type}-sheet.hbs`;
}
override async getData(): Promise<DS4ItemSheetData> {
/** @override */
async getData() {
const data = {
...(await super.getData()),
config: DS4,
@ -41,7 +44,8 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
return data;
}
override _getSubmitData(updateData = {}) {
/** @override */
_getSubmitData(updateData = {}) {
const data = super._getSubmitData(updateData);
// Prevent submitting overridden values
const overrides = foundry.utils.flattenObject(this.item.overrides);
@ -51,9 +55,8 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
return data;
}
override setPosition(
options: Partial<Application.Position> = {},
): (Application.Position & { height: number }) | void {
/** @override */
setPosition(options = {}) {
const position = super.setPosition(options);
if (position) {
const sheetBody = this.element.find(".sheet-body");
@ -64,7 +67,11 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
return position;
}
override activateListeners(html: JQuery): void {
/**
* @override
* @param {JQuery} html
*/
activateListeners(html) {
super.activateListeners(html);
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
* 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();
if (this.item.isOwned) {
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.
* @protected
*/
protected onCreateEffect(): void {
onCreateEffect() {
DS4ActiveEffect.createDefault(this.item);
}
/**
* 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)
.parents(embeddedDocumentListEntryProperties.ActiveEffect.selector)
.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.
*
* @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 id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
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.
*/

View File

@ -4,11 +4,13 @@
import { getGame } from "../utils/utils";
export function disableOverriddenFields(
form: HTMLElement | null,
overrides: Record<string, unknown>,
selector: (key: string) => string,
): void {
/**
* Disable elements in the given form that match the selector returned for overridden properties.
* @param {HTMLElement | null} form The form in which to disable fields
* @param {Record<string, unknown>} overrides The set of overrides of the underlying document
* @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 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(
results: DieWithSubCheck[],

View File

@ -7,70 +7,96 @@ import { DialogWithListeners } from "../apps/dialog-with-listeners";
import { DS4 } from "../config";
import { getGame } from "../utils/utils";
/**
* 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();
/** @typedef {"publicroll" | "gmroll" | "gmroll" | "selfroll"} RollModes */
/**
* Most basic class responsible for generating the chat formula and passing it to the chat as roll.
*/
class CheckFactory {
constructor(
private checkTargetNumber: number,
private checkModifier: number,
options: Partial<DS4CheckFactoryOptions> = {},
) {
this.options = defaultCheckOptions.mergeWith(options);
/**
* @param {number} checkTargetNumber The check target number for this check factory
* @param {number} checkModifier The check modifier for this check factory
* @param {Partial<DS4CheckFactoryOptions>} [options] Options for this check factory
*/
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 formula = this.options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
const formula = this.#options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
const roll = Roll.create(formula);
const speaker = this.options.speaker ?? ChatMessage.getSpeaker();
const speaker = this.#options.speaker ?? ChatMessage.getSpeaker();
return roll.toMessage(
{
speaker,
flavor: this.options.flavor,
flags: this.options.flavorData ? { ds4: { flavorData: this.options.flavorData } } : undefined,
flavor: this.#options.flavor,
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;
return totalCheckTargetNumber >= 0 ? `v${this.checkTargetNumber + this.checkModifier}` : "v0";
/**
* Create the check target number modifier for this roll.
* @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 =
this.options.minimumFumbleResult !== defaultCheckOptions.minimumFumbleResult;
const isMaximumCoupResultRequired = this.options.maximumCoupResult !== defaultCheckOptions.maximumCoupResult;
this.#options.minimumFumbleResult !== this.constructor.defaultOptions.minimumFumbleResult;
const isMaximumCoupResultRequired =
this.#options.maximumCoupResult !== this.constructor.defaultOptions.maximumCoupResult;
if (isMinimumFumbleResultRequired || isMaximumCoupResultRequired) {
return `c${this.options.maximumCoupResult ?? ""}:${this.options.minimumFumbleResult ?? ""}`;
return `c${this.#options.maximumCoupResult ?? ""}:${this.#options.minimumFumbleResult ?? ""}`;
} else {
return null;
}
@ -79,19 +105,18 @@ class CheckFactory {
/**
* Asks the user for all unknown/necessary information and passes them on to perform a roll.
* @param checkTargetNumber - The Check Target Number ("CTN")
* @param options - Options changing the behavior of the roll and message.
* @param {number} checkTargetNumber The Check Target Number ("CTN")
* @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(
checkTargetNumber: number,
options: Partial<DS4CheckFactoryOptions> = {},
): Promise<ChatMessage | unknown> {
export async function createCheckRoll(checkTargetNumber, options = {}) {
// Ask for additional required data;
const interactiveRollData = await askForInteractiveRollData(checkTargetNumber, options);
const newTargetValue = interactiveRollData.checkTargetNumber ?? checkTargetNumber;
const checkModifier = interactiveRollData.checkModifier ?? 0;
const newOptions: Partial<DS4CheckFactoryOptions> = {
/** @type {Partial<DS4CheckFactoryOptions>} */
const newOptions = {
maximumCoupResult: interactiveRollData.maximumCoupResult ?? options.maximumCoupResult,
minimumFumbleResult: interactiveRollData.minimumFumbleResult ?? options.minimumFumbleResult,
useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
@ -117,26 +142,25 @@ export async function createCheckRoll(
* @notes
* 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(
checkTargetNumber: number,
options: Partial<DS4CheckFactoryOptions> = {},
{ template, title }: { template?: string; title?: string } = {},
): Promise<Partial<IntermediateInteractiveRollData>> {
async function askForInteractiveRollData(checkTargetNumber, options = {}, { template, title } = {}) {
const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs";
const usedTitle = title ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle");
const id = foundry.utils.randomID();
const templateData = {
title: usedTitle,
checkTargetNumber: checkTargetNumber,
maximumCoupResult: options.maximumCoupResult ?? defaultCheckOptions.maximumCoupResult,
minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult,
maximumCoupResult: options.maximumCoupResult ?? this.constructor.defaultOptions.maximumCoupResult,
minimumFumbleResult: options.minimumFumbleResult ?? this.constructor.defaultOptions.minimumFumbleResult,
rollMode: options.rollMode ?? getGame().settings.get("core", "rollMode"),
rollModes: CONFIG.Dice.rollModes,
checkModifiers: Object.entries(DS4.i18n.checkModifiers).map(([key, translation]) => {
if (key in DS4.checkModifiers) {
const value = DS4.checkModifiers[key as keyof typeof DS4.checkModifiers];
const value = DS4.checkModifiers[key];
const label = `${translation} (${value >= 0 ? `+${value}` : value})`;
return { value, label };
}
@ -146,7 +170,7 @@ async function askForInteractiveRollData(
};
const renderedHtml = await renderTemplate(usedTemplate, templateData);
const dialogPromise = new Promise<HTMLFormElement>((resolve) => {
const dialogPromise = new Promise((resolve) => {
new DialogWithListeners(
{
title: usedTitle,
@ -190,7 +214,7 @@ async function askForInteractiveRollData(
.parent(".form-group");
html.find(`#check-modifier-${id}`).on("change", (event) => {
if (
(event.currentTarget as HTMLSelectElement).value === "custom" &&
event.currentTarget.value === "custom" &&
checkModifierCustomFormGroup.hasClass("ds4-hidden")
) {
checkModifierCustomFormGroup.removeClass("ds4-hidden");
@ -211,9 +235,10 @@ async function askForInteractiveRollData(
/**
* 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 chosenCheckModifier = formData["check-modifier"]?.value;
@ -237,11 +262,10 @@ function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateInt
/**
* 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.
@ -255,23 +279,29 @@ interface InteractiveRollData {
* 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
* 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.
* @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`
*/
export class DS4Check extends DiceTerm {
constructor({ modifiers = [], results = [], options }: Partial<DiceTerm.TermData> = {}) {
constructor({ modifiers = [], results = [], options } = {}) {
super({
faces: 20,
results,
@ -65,34 +65,44 @@ export class DS4Check extends DiceTerm {
}
}
coup: boolean | null = null;
fumble: boolean | null = null;
/** @type {boolean | null} */
coup = null;
/** @type {boolean | null} */
fumble = null;
canFumble = true;
checkTargetNumber = DS4Check.DEFAULT_CHECK_TARGET_NUMBER;
minimumFumbleResult = DS4Check.DEFAULT_MINIMUM_FUMBLE_RESULT;
maximumCoupResult = DS4Check.DEFAULT_MAXIMUM_COUP_RESULT;
override get expression(): string {
/** @override */
get expression() {
return `ds${this.modifiers.join("")}`;
}
override get total(): string | number | null | undefined {
/** @override */
get total() {
if (this.fumble) return 0;
return super.total;
}
override _evaluateSync({ minimize = false, maximize = false } = {}): this {
/** @override */
_evaluateSync({ minimize = false, maximize = false } = {}) {
super._evaluateSync({ minimize, maximize });
this.evaluateResults();
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
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 results = evaluateCheck(dice, this.checkTargetNumber, {
maximumCoupResult: this.maximumCoupResult,
@ -108,18 +118,19 @@ export class DS4Check extends DiceTerm {
* @remarks "min" and "max" are filtered out because they are irrelevant for
* {@link DS4Check}s and only result in some dice rolls being highlighted
* incorrectly.
* @override
*/
override getResultCSS(result: DiceTerm.Result): (string | null)[] {
getResultCSS(result) {
return super.getResultCSS(result).filter((cssClass) => cssClass !== "min" && cssClass !== "max");
}
static readonly DEFAULT_CHECK_TARGET_NUMBER = 10;
static readonly DEFAULT_MAXIMUM_COUP_RESULT = 1;
static readonly DEFAULT_MINIMUM_FUMBLE_RESULT = 20;
static override DENOMINATION = "s";
static override MODIFIERS = {
c: (): void => undefined, // Modifier is consumed in constructor for maximumCoupResult / minimumFumbleResult
v: (): void => undefined, // Modifier is consumed in constructor for checkTargetNumber
n: (): void => undefined, // Modifier is consumed in constructor for canFumble
static DEFAULT_CHECK_TARGET_NUMBER = 10;
static DEFAULT_MAXIMUM_COUP_RESULT = 1;
static DEFAULT_MINIMUM_FUMBLE_RESULT = 20;
static DENOMINATION = "s";
static MODIFIERS = {
c: () => undefined, // Modifier is consumed in constructor for maximumCoupResult / minimumFumbleResult
v: () => undefined, // Modifier is consumed in constructor for checkTargetNumber
n: () => undefined, // Modifier is consumed in constructor for canFumble
};
}

View File

@ -5,19 +5,17 @@
import { getGame } from "../utils/utils";
import { DS4Check } from "./check";
export class DS4Roll<D extends Record<string, unknown> = Record<string, unknown>> extends Roll<D> {
static override CHAT_TEMPLATE = "systems/ds4/templates/dice/roll.hbs";
export class DS4Roll extends Roll {
/** @override */
static CHAT_TEMPLATE = "systems/ds4/templates/dice/roll.hbs";
/**
* @override
* @remarks
* 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.
*/
override async render({
flavor,
template = (this.constructor as typeof DS4Roll).CHAT_TEMPLATE,
isPrivate = false,
}: Parameters<Roll["render"]>[0] = {}): Promise<string> {
async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false } = {}) {
if (!this._evaluated) await this.evaluate({ async: true });
const firstDiceTerm = this.dice[0];
const isCoup = firstDiceTerm instanceof DS4Check && firstDiceTerm.coup;

View File

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

View File

@ -5,27 +5,22 @@
import { mathEvaluator } from "../expression-evaluation/evaluator";
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 {
ActiveEffect: typeof DS4ActiveEffect;
}
interface FlagConfig {
ActiveEffect: {
ds4?: {
itemEffectConfig?: {
applyToItems?: boolean;
itemName?: string;
condition?: string;
};
};
};
}
}
/**
* @typedef {object} DS4ActiveEffectFlags
* @property {ItemEffectConfig} [itemEffectConfig] Configuration for applying this effect to owned items
*/
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 {
/**
@ -35,13 +30,16 @@ export class DS4ActiveEffect extends ActiveEffect {
/**
* 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.
* @type {boolean}
*/
get isSurpressed(): boolean {
get isSurpressed() {
const originatingItem = this.originatingItem;
if (!originatingItem) {
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.
* @return {import("./item/item").DS4Item | undefined}
*/
get originatingItem(): DS4Item | undefined {
get originatingItem() {
if (!(this.parent instanceof Actor)) {
return;
}
@ -66,27 +65,28 @@ export class DS4ActiveEffect extends ActiveEffect {
/**
* The number of times this effect should be applied.
* @type {number}
*/
get factor(): number {
get factor() {
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);
try {
change.value = DS4ActiveEffect.safeEval(change.value).toString();
} catch (e) {
// 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);
}
/**
* 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 origin = await this.getSource();
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
* set.
* @protected
* @returns {Promise<foundry.abstract.Document | null>}
*/
protected async getSource(): ReturnType<typeof fromUuid> {
async getSource() {
if (this.source === undefined) {
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.
*
* @param parent The parent {@link DS4Actor} or {@link DS4Item} of the effect.
* @returns A promise that resolved to the created effect or udifined of the creation was prevented.
* @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} parent The parent of the effect.
* @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 = {
label: getGame().i18n.localize(`DS4.NewEffectLabel`),
icon: this.FALLBACK_ICON,
@ -119,26 +121,29 @@ export class DS4ActiveEffect extends ActiveEffect {
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);
if (!Number.isNumeric(result)) {
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.
* @param document The Actor or Item to which to apply the effects
* @param effetcs The effects to apply
* @param predicate Apply only changes that fullfill this predicate
* @param {import("./item/item").DS4Item | import("./actor/actor").DS4Actor} document The Actor or Item to which to apply the effects
* @param {DS4ActiveEffect[]} effetcs The effects to apply
* @param {(change: EffectChangeData) => boolean} [predicate=() => true] Apply only changes that fullfill this predicate
*/
static applyEffetcs(
document: DS4Actor | DS4Item,
effetcs: DS4ActiveEffect[],
predicate: (change: EffectChangeData) => boolean = () => true,
): void {
const overrides: Record<string, unknown> = {};
static applyEffetcs(document, effetcs, predicate = () => true) {
/** @type {Record<string, unknown>} */
const overrides = {};
// Organize non-disabled and -surpressed effects by their application priority
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}.
* @param predicate An optional predicate to filter which changes should be considered
* @returns The array of changes from this effect, considering the factor.
* @param {(change: EffectChangeData) => boolean} [predicate=() => true] An optional predicate to filter which changes should be considered
* @returns {EffectChangeDataWithEffect[]} The array of changes from this effect, considering the factor.
* @protected
*/
protected getFactoredChangesWithEffect(
predicate: (change: EffectChangeData) => boolean = () => true,
): EffectChangeDataWithEffect[] {
getFactoredChangesWithEffect(predicate = () => true) {
if (this.data.disabled || this.isSurpressed) {
return [];
}
return this.data.changes.filter(predicate).flatMap((change) => {
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 { 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
*/
export class DS4Actor extends Actor {
override prepareData(): void {
/** @override */
prepareData() {
this.data.reset();
this.prepareBaseData();
this.prepareEmbeddedDocuments();
@ -39,7 +27,8 @@ export class DS4Actor extends Actor {
this.prepareFinalDerivedData();
}
override prepareBaseData(): void {
/** @override */
prepareBaseData() {
const data = this.data;
data.data.rolling = {
@ -48,17 +37,14 @@ export class DS4Actor extends Actor {
};
const attributes = data.data.attributes;
Object.values(attributes).forEach(
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = attribute.base + attribute.mod),
);
Object.values(attributes).forEach((attribute) => (attribute.total = attribute.base + attribute.mod));
const traits = data.data.traits;
Object.values(traits).forEach(
(trait: ModifiableDataBaseTotal<number>) => (trait.total = trait.base + trait.mod),
);
Object.values(traits).forEach((trait) => (trait.total = trait.base + trait.mod));
}
override prepareEmbeddedDocuments() {
/** @override */
prepareEmbeddedDocuments() {
super.prepareEmbeddedDocuments();
this.applyActiveEffectsToItems();
}
@ -71,16 +57,21 @@ export class DS4Actor extends Actor {
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);
}
/**
* 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
*/
itemEffects(item: DS4Item) {
itemEffects(item) {
return this.effects.filter((effect) => {
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);
try {
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
* {@link ActiveEffect}s and {@link Actor#prepareEmbeddedDocuments} calls this.
* @override
*/
override applyActiveEffects(): void {
applyActiveEffects() {
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
* 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
many times effects provided by that talent are applied */
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 = {};
DS4ActiveEffect.applyEffetcs(item, this.itemEffects(item));
}
applyActiveEffectsToBaseData(): void {
/**
* Apply effects to base data
* @protected
*/
applyActiveEffectsToBaseData() {
this.overrides = {};
DS4ActiveEffect.applyEffetcs(
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) =>
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.
* @override
*/
override prepareDerivedData(): void {
prepareDerivedData() {
this.data.data.armorValueSpellMalus = Math.max(this.data.data.armorValueSpellMalus, 0);
this.prepareCombatValues();
this.prepareChecks();
@ -183,8 +196,9 @@ export class DS4Actor extends Actor {
/**
* 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(
(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.
*/
prepareFinalDerivedData(): void {
Object.values(this.data.data.attributes).forEach(
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = Math.ceil(attribute.total)),
);
Object.values(this.data.data.traits).forEach(
(trait: ModifiableDataBaseTotal<number>) => (trait.total = Math.ceil(trait.total)),
);
prepareFinalDerivedData() {
Object.values(this.data.data.attributes).forEach((attribute) => (attribute.total = Math.ceil(attribute.total)));
Object.values(this.data.data.traits).forEach((trait) => (trait.total = Math.ceil(trait.total)));
Object.entries(this.data.data.combatValues)
.filter(([key]) => key !== "movement")
.map(([, value]) => value)
.forEach(
(combatValue: ModifiableDataBaseTotal<number>) => (combatValue.total = Math.ceil(combatValue.total)),
);
(Object.keys(this.data.data.checks) as (keyof typeof this.data.data.checks)[]).forEach((key) => {
.forEach((combatValue) => (combatValue.total = Math.ceil(combatValue.total)));
Object.keys(this.data.data.checks).forEach((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),
* given in dot notation.
* @type {string[]}
*/
get finalDerivedDataProperties(): string[] {
get finalDerivedDataProperties() {
return ["data.combatValues.hitPoints.max", "data.checks.defend"];
}
/**
* 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"];
}
/**
* 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);
}
/**
* Prepares the combat values of the actor.
* Prepares the combat values of the actor.
* @protected
*/
protected prepareCombatValues(): void {
prepareCombatValues() {
const data = this.data.data;
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;
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.
* @type {number}
* @protected
*/
protected get armorValueOfEquippedItems(): number {
get armorValueOfEquippedItems() {
return this.equippedItemsWithArmor.map((item) => item.data.data.armorValue).reduce((a, b) => a + b, 0);
}
/**
* The armor value spell malus from equipped items.
* @type {number}
* @protected
*/
protected get armorValueSpellMalusOfEquippedItems(): number {
get armorValueSpellMalusOfEquippedItems() {
return this.equippedItemsWithArmor
.filter(
(item) =>
@ -284,19 +300,22 @@ export class DS4Actor extends Actor {
.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
.filter(
(item): item is DS4Item & { data: DS4ArmorDataProperties | DS4ShieldDataProperties } =>
item.data.type === "armor" || item.data.type === "shield",
)
.filter((item) => item.data.type === "armor" || item.data.type === "shield")
.filter((item) => item.data.data.equipped);
}
/**
* Prepares the check target numbers of checks for the actor.
* @protected
*/
protected prepareChecks(): void {
prepareChecks() {
const data = this.data.data;
data.checks = {
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.
* This only differs from the base implementation by also allowing negative values.
* @override
*/
override async modifyTokenAttribute(
attribute: string,
value: number,
isDelta = false,
isBar = true,
): Promise<this | undefined> {
async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
const current = foundry.utils.getProperty(this.data.data, attribute);
// Determine the updates to make to the actor data
let updates: Record<string, number>;
/** @type {Record<string, number>} */
let updates;
if (isBar) {
if (isDelta) value = Math.min(Number(current.value) + value, current.max);
updates = { [`data.${attribute}.value`]: value };
@ -360,13 +376,11 @@ export class DS4Actor extends Actor {
/**
* Roll for a given check.
* @param check - The check to perform
* @param options - Additional options to customize the roll
* @param {import("./actor-data-properties-base").Check} check The check to perform
* @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(
check: Check,
options: { speaker?: { token?: TokenDocument; alias?: string } } = {},
): Promise<void> {
async rollCheck(check, options = {}) {
const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker });
await createCheckRoll(this.data.data.checks[check], {
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
* 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();
if (!attributeAndTrait) {
return;
@ -405,10 +420,12 @@ export class DS4Actor extends Actor {
});
}
protected async selectAttributeAndTrait(): Promise<{
attribute: keyof typeof DS4.i18n.attributes;
trait: keyof typeof DS4.i18n.traits;
} | null> {
/**
* Prompt the use to select an attribute and a trait.
* @returns {Promise<AttributeAndTrait | null>}
* @protected
*/
async selectAttributeAndTrait() {
const attributeIdentifier = "attribute-trait-selection-attribute";
const traitIdentifier = "attribute-trait-selection-trait";
return Dialog.prompt({
@ -419,24 +436,20 @@ export class DS4Actor extends Actor {
label: getGame().i18n.localize("DS4.Attribute"),
identifier: attributeIdentifier,
options: Object.fromEntries(
(Object.entries(DS4.i18n.attributes) as [keyof typeof DS4.i18n.attributes, string][]).map(
([attribute, translation]) => [
attribute,
`${translation} (${this.data.data.attributes[attribute].total})`,
],
),
Object.entries(DS4.i18n.attributes).map(([attribute, translation]) => [
attribute,
`${translation} (${this.data.data.attributes[attribute].total})`,
]),
),
},
{
label: getGame().i18n.localize("DS4.Trait"),
identifier: traitIdentifier,
options: Object.fromEntries(
(Object.entries(DS4.i18n.traits) as [keyof typeof DS4.i18n.traits, string][]).map(
([trait, translation]) => [
trait,
`${translation} (${this.data.data.traits[trait].total})`,
],
),
Object.entries(DS4.i18n.traits).map(([trait, translation]) => [
trait,
`${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 type { ItemType } from "../../item/item-data-source";
export class DS4Character extends DS4Actor {
override prepareFinalDerivedData(): void {
/** @override */
prepareFinalDerivedData() {
super.prepareFinalDerivedData();
this.data.data.slayerPoints.max = 3;
}
override get finalDerivedDataProperties(): string[] {
/** @override */
get finalDerivedDataProperties() {
return [...super.finalDerivedDataProperties, "data.slayerPoints.max"];
}
override get ownableItemTypes(): Array<ItemType> {
/** @override */
get ownableItemTypes() {
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"];
}
}
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";
const handler = {
construct(_: typeof DS4Actor, args: ConstructorParameters<typeof DS4Actor>) {
/**
* @param {typeof import("./actor").DS4Actor}
* @param {unknown[]} args
*/
construct(_, args) {
switch (args[0]?.type) {
case "character":
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";
declare global {
interface FlagConfig {
ChatMessage: {
ds4?: {
flavorData?: Record<string, string | number | null>;
};
};
}
}
/**
* @typedef {object} DS4ChatMessageFlags
* @property {Record<string, string | number | null>} [flavorData] Data to use for localizing the flavor of the chat message
*/
/**
* @typedef {Record<string, unknown>} ChatMessageFlags
* @property {DS4ChatMessageFlags} [ds4] Flags for DS4
*/
export class DS4ChatMessage extends ChatMessage {
override prepareData(): void {
prepareData() {
super.prepareData();
if (this.data.flavor) {
const game = getGame();

View File

@ -5,7 +5,3 @@
import { DS4Item } from "../item";
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";
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";
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;
}
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;
}

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";
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";
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";
const handler = {
construct(_: typeof DS4Item, args: ConstructorParameters<typeof DS4Item>) {
/**
* @param {typeof import("./item").DS4Item}
* @param {unknown[]} args
*/
construct(_, args) {
switch (args[0]?.type) {
case "alphabet":
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";
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";
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";
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
import { createCheckRoll, DS4CheckFactoryOptions } from "../../../dice/check-factory";
import { createCheckRoll } from "../../../dice/check-factory";
import { notifications } from "../../../ui/notifications";
import { getGame } from "../../../utils/utils";
import { DS4Item } from "../item";
import { calculateSpellPrice } from "./calculate-spell-price";
export class DS4Spell extends DS4Item {
override prepareDerivedData(): void {
/** @override */
prepareDerivedData() {
this.data.data.rollable = this.data.data.equipped;
this.data.data.price = calculateSpellPrice(this.data.data);
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();
if (!this.data.data.equipped) {
@ -54,7 +56,8 @@ export class DS4Spell extends DS4Item {
opponentDefense !== undefined && opponentDefense !== 0
? "DS4.ItemSpellCheckFlavorWithOpponentDefense"
: "DS4.ItemSpellCheckFlavor";
const flavorData: DS4CheckFactoryOptions["flavorData"] = {
/** @type {import("../../../dice/check-factory").DS4CheckFactoryOptions["flavorData"]} */
const flavorData = {
actor: speaker.alias ?? this.actor.name,
spell: this.name,
};
@ -71,10 +74,12 @@ export class DS4Spell extends DS4Item {
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);
}
}
export interface DS4Spell {
data: foundry.data.ItemData & { type: "spell"; _source: { type: "spell" } };
}

View File

@ -5,17 +5,15 @@
import { DS4Item } from "../item";
export class DS4Talent extends DS4Item {
override prepareDerivedData(): void {
/** @override */
prepareDerivedData() {
super.prepareDerivedData();
const data = this.data.data;
data.rank.total = data.rank.base + data.rank.mod;
}
override get activeEffectFactor(): number | undefined {
/** @override */
get activeEffectFactor() {
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
import { DS4 } from "../../../config";
import { createCheckRoll, DS4CheckFactoryOptions } from "../../../dice/check-factory";
import { createCheckRoll } from "../../../dice/check-factory";
import { notifications } from "../../../ui/notifications";
import { getGame } from "../../../utils/utils";
import { DS4Item } from "../item";
export class DS4Weapon extends DS4Item {
override prepareDerivedData(): void {
/** @override */
prepareDerivedData() {
const data = this.data.data;
data.rollable = data.equipped;
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();
if (!this.data.data.equipped) {
return notifications.warn(
@ -41,14 +43,15 @@ export class DS4Weapon extends DS4Item {
const weaponBonus = this.data.data.weaponBonus;
const attackType = await this.getPerformedAttackType();
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 speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
const flavor =
opponentDefense !== undefined && opponentDefense !== 0
? "DS4.ItemWeaponCheckFlavorWithOpponentDefense"
: "DS4.ItemWeaponCheckFlavor";
const flavorData: DS4CheckFactoryOptions["flavorData"] = {
/** @type {import("../../../dice/check-factory").DS4CheckFactoryOptions["flavorData"]} */
const flavorData = {
actor: speaker.alias ?? this.actor.name,
weapon: this.name,
};
@ -68,7 +71,12 @@ export class DS4Weapon extends DS4Item {
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") {
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 { DS4ActorProxy } from "./actor/proxy";
let fallbackData: foundry.data.ActorData["data"] | undefined = undefined;
/** @type {object | undefined} */
let fallbackData = undefined;
function getFallbackData() {
if (!fallbackData) {
fallbackData = {} as foundry.data.ActorData["data"];
fallbackData = {};
for (const type of getGame().system.template.Actor?.types ?? []) {
foundry.utils.mergeObject(
fallbackData,
new DS4ActorProxy({ type: type as foundry.data.ActorData["type"], name: "temporary" }).data.data,
);
foundry.utils.mergeObject(fallbackData, new DS4ActorProxy({ type, name: "temporary" }).data.data);
}
}
return fallbackData;
}
export class DS4TokenDocument extends TokenDocument {
static override getTrackedAttributes(data?: foundry.data.ActorData["data"], _path: string[] = []) {
static getTrackedAttributes(data, _path = []) {
if (!data) {
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
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 = [
"systems/ds4/templates/sheets/actor/components/actor-header.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 { getGame } from "../utils/utils";
import type { DS4Actor } from "../documents/actor/actor";
import type { DS4Item } from "../documents/item/item";
export function registerForInitHook(): void {
export function registerForInitHook() {
Hooks.once("init", init);
}
@ -81,20 +78,3 @@ async function init() {
await registerHandlebarsPartials();
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";
export function registerForReadyHook(): void {
export function registerForReadyHook() {
Hooks.once("ready", () => {
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
* "renderDialog" hook and the "renderApplication" hook are called (in this order).
*/
export function registerForRenderHooks(): void {
export function registerForRenderHooks() {
["renderApplication", "renderActorSheet", "renderItemSheet"].forEach((hook) => {
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.
*
* @param app - The application in which to activate the listener.
* @param html - The {@link JQuery} representing the HTML of the application.
* @param {Application} app The application in which to activate the listener.
* @param {JQuery} html The {@link JQuery} representing the HTML of the application.
*/
function selectTargetInputOnFocus(app: Application, html: JQuery) {
html.find("input").on("focus", (ev: JQuery.FocusEvent<HTMLInputElement>) => {
function selectTargetInputOnFocus(app, html) {
html.find("input").on("focus", (ev) => {
ev.currentTarget.select();
});
}

View File

@ -8,7 +8,7 @@
import { DS4 } from "../config";
import { getGame } from "../utils/utils";
export function registerForSetupHook(): void {
export function registerForSetupHook() {
Hooks.once("setup", () => {
localizeAndSortConfigObjects();
});
@ -28,17 +28,23 @@ function localizeAndSortConfigObjects() {
"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)];
});
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(
Object.entries(DS4.i18n).map(([key, value]) => {
return [key, localizeObject(value, !noSort.includes(key))];
}),
) as typeof DS4.i18n;
);
}

View File

@ -4,14 +4,12 @@
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}
* 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 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 { getActiveActorAndToken } from "./helpers";
import type { Check } from "../documents/actor/actor-data-properties-base";
/**
* Creates a macro from a check drop.
* Get an existing roll check macro if one exists, otherwise create a new one.
* @param check - The name of the check to perform.
* @param slot - The hotbar slot to use.
* @param {import("../documents/actor/actor-data-properties-base").Check} check The name of the check to perform
* @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);
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 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.
* @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();
if (!actor) {
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.
* Get an existing roll item macro if one exists, otherwise create a new one.
* @param itemData - The item data
* @param slot - The hotbar slot to use
* @param {object} itemData The item data
* @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);
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 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`.
* @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();
if (!actor) {
return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollItemMacro"));

View File

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

View File

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

View File

@ -12,14 +12,16 @@ import {
migrateScenes,
} from "./migrationHelpers";
async function migrate(): Promise<void> {
/** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
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;
return {
data: {
@ -35,6 +37,7 @@ const migrateCompendium = getCompendiumMigrator(
{ migrateToTemplateEarly: false },
);
/** @type {import("./migration").Migration} */
export const migration = {
migrate,
migrateCompendium,

View File

@ -12,19 +12,20 @@ import {
migrateScenes,
} from "./migrationHelpers";
async function migrate(): Promise<void> {
/** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
}
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) {
/** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
function getItemUpdateData(itemData) {
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: string | undefined = itemData.data?.cooldownDuration.unit;
const cooldownDurationUnit = itemData.data?.cooldownDuration.unit;
const updateData: Record<string, unknown> = {
const updateData = {
data: {
"-=scrollPrice": null,
minimumLevels: { healer: null, wizard: null, sorcerer: null },
@ -40,6 +41,7 @@ const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
/** @type {import("./migration").Migration} */
export const migration = {
migrate,
migrateCompendium,

View File

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

View File

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

View File

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

View File

@ -13,7 +13,11 @@ import { migration as migration005 } from "./005";
import { migration as migration006 } from "./006";
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) {
return;
}
@ -30,7 +34,13 @@ async function migrate(): Promise<void> {
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) {
return;
}
@ -76,11 +86,14 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
}
}
async function migrateCompendiumFromTo(
pack: CompendiumCollection<CompendiumCollection.Metadata>,
oldMigrationVersion: number,
targetMigrationVersion: number,
): Promise<void> {
/**
* Migrate a compendium pack from a given version to another version.
* @param {CompendiumCollection} pack The compendium pack to migrate
* @param {number} oldMigrationVersion The old version number
* @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) {
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");
}
function getTargetMigrationVersion(): number {
/**
* Get the target migration version.
* @returns {number} The target migration version
*/
function getTargetMigrationVersion() {
return migrations.length;
}
interface Migration {
migrate: () => Promise<void>;
migrateCompendium: (pack: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>;
}
/**
* @typedef {object} Migration
* @property {() => Promise<void>} migrate
* @property {import("./migrationHelpers").CompendiumMigrator} migrateCompendium
*/
const migrations: Migration[] = [
migration001,
migration002,
migration003,
migration004,
migration005,
migration006,
migration007,
];
/**
* @type {Migration[]}
*/
const migrations = [migration001, migration002, migration003, 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;
}

View File

@ -7,11 +7,14 @@ import { DS4Item } from "../documents/item/item";
import { logger } from "../utils/logger";
import { getGame } from "../utils/utils";
type ItemUpdateDataGetter = (
itemData: Partial<foundry.data.ItemData["_source"]>,
) => DeepPartial<foundry.data.ItemData["_source"]> | Record<string, unknown> | undefined;
/** @typedef {(itemData: Partial<foundry.data.ItemData["_source"]>) => DeepPartial<foundry.data.ItemData["_source"]> | Record<string, unknown> | undefined} ItemUpdateDataGetter */
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 ?? []) {
try {
const updateData = getItemUpdateData(item.toObject());
@ -25,11 +28,14 @@ export async function migrateItems(getItemUpdateData: ItemUpdateDataGetter): Pro
}
}
type ActorUpdateDataGetter = (
itemData: Partial<foundry.data.ActorData["_source"]>,
) => DeepPartial<foundry.data.ActorData["_source"]> | undefined;
/** @typedef {(actorData: Partial<foundry.data.ActorData["_source"]>) => DeepPartial<foundry.data.ActorData["_source"]> | undefined} ActorUpdateDataGetter */
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 ?? []) {
try {
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 ?? []) {
try {
const updateData = getSceneUpdateData(scene.data);
if (updateData) {
logger.info(`Migrating Scene document ${scene.name} (${scene.id})`);
await scene.update(
updateData as DeepPartial<Parameters<foundry.data.SceneData["_initializeSource"]>[0]>,
);
await scene.update(updateData);
}
} catch (err) {
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 ?? []) {
if (compendium.metadata.package !== "world") 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 (
actorData: Partial<foundry.data.ActorData["_source"]>,
): DeepPartial<foundry.data.ActorData["_source"]> | undefined => {
/**
* Get a function to create actor update data that adjusts the owned items of the actor according to the given function.
* @param {ItemUpdateDataGetter} getItemUpdateData The function to generate item update data
* @returns {ActorUpdateDataGetter} A function to get actor update data
*/
export function getActorUpdateDataGetter(getItemUpdateData) {
return (actorData) => {
let hasItemUpdates = false;
const items = actorData.items?.map((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) => {
const tokens = sceneData.tokens.map((token: TokenDocument) => {
/**
* Get a function to create scene update data that adjusts the actors of the tokens of the scene according to the given function.
* @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();
if (!t.actorId || t.actorLink) {
t.actorData = {};
@ -109,7 +131,7 @@ export function getSceneUpdateDataGetter(getActorUpdateData: ActorUpdateDataGett
actorData.type = token.actor?.type;
const update = getActorUpdateData(actorData);
if (update !== undefined) {
["items" as const, "effects" as const].forEach((embeddedName) => {
["items", "effects"].forEach((embeddedName) => {
const embeddedUpdates = update[embeddedName];
if (embeddedUpdates === undefined || !embeddedUpdates.length) return;
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(
{
getItemUpdateData,
getActorUpdateData,
getSceneUpdateData,
}: {
getItemUpdateData?: ItemUpdateDataGetter;
getActorUpdateData?: ActorUpdateDataGetter;
getSceneUpdateData?: SceneUpdateDataGetter;
} = {},
{ getItemUpdateData, getActorUpdateData, getSceneUpdateData } = {},
{ migrateToTemplateEarly = true } = {},
) {
return async (compendium: CompendiumCollection<CompendiumCollection.Metadata>): Promise<void> => {
const type = compendium.metadata.type;
return async (pack) => {
const type = pack.metadata.type;
if (!["Actor", "Item", "Scene"].includes(type)) return;
const wasLocked = compendium.locked;
await compendium.configure({ locked: false });
const wasLocked = pack.locked;
await pack.configure({ locked: false });
if (migrateToTemplateEarly) {
await compendium.migrate();
await pack.migrate();
}
const documents = await compendium.getDocuments();
const documents = await pack.getDocuments();
for (const doc of documents) {
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) {
const updateData = getItemUpdateData(doc.toObject());
updateData && (await doc.update(updateData));
@ -164,23 +191,20 @@ export function getCompendiumMigrator(
const updateData = getActorUpdateData(doc.toObject());
updateData && (await doc.update(updateData));
} else if (doc instanceof Scene && getSceneUpdateData) {
const updateData = getSceneUpdateData(doc.data as foundry.data.SceneData);
updateData &&
(await doc.update(
updateData as DeepPartial<Parameters<foundry.data.SceneData["_initializeSource"]>[0]>,
));
const updateData = getSceneUpdateData(doc.data);
updateData && (await doc.update(updateData));
}
} catch (err) {
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,
);
}
}
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";
export function registerSystemSettings(): void {
export function registerSystemSettings() {
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", {
name: "System Migration Version",
@ -37,13 +37,18 @@ export function registerSystemSettings(): void {
});
}
export interface DS4Settings {
systemMigrationVersion: number;
useSlayingDiceForAutomatedChecks: boolean;
showSlayerPoints: boolean;
}
/**
* @typedef DS4Settings
* @property {number} systemMigrationVersion
* @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();
return {
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",
"minimumCoreVersion": "9.238",
"compatibleCoreVersion": "9",
"esmodules": [
"ds4.js"
],
"styles": [
"css/ds4.css"
],
"esmodules": ["ds4.js"],
"styles": ["css/ds4.css"],
"languages": [
{
"lang": "en",

View File

@ -1,8 +1,9 @@
{
"compilerOptions": {
"outDir": "dist",
"target": "ES2021",
"lib": ["ES2021", "DOM"],
"types": ["@league-of-foundry-developers/foundry-vtt-types"],
"types": ["@types/jquery", "handlebars"],
"esModuleInterop": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
@ -10,7 +11,9 @@
"noUncheckedIndexedAccess": true,
"noImplicitOverride": 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