Merge branch 'v10' into 'main'

V10 compatibility

See merge request 
This commit is contained in:
Johannes Loher 2022-11-28 01:41:51 +00:00
commit 22e0fa6781
112 changed files with 42988 additions and 26573 deletions
.eslintignore.gitignore.prettierignorejsconfig.jsonjsconfig.json.licensepackage.json
packs
spec
src
system.json
templates/sheets/actor

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",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,175 +4,263 @@
"name": "Gormanische Schrift",
"type": "alphabet",
"img": "icons/svg/book.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342357,
"modifiedTime": 1668995456385,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "GQNpFENXcjJGeYr2",
"name": "Freiwort",
"type": "language",
"img": "systems/ds4/assets/icons/game-icons/lorc/conversation.svg",
"data": {
"description": "<p>Freiwort ist die Gemeinsprache der Freien Lande.</p>"
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": "<p>Freiwort ist die Gemeinsprache der Freien Lande.</p>"
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342358,
"modifiedTime": 1668995456386,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "O1U9jd0yJoydHwT8",
"name": "Zasarische Schrift",
"type": "alphabet",
"img": "icons/svg/book.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342359,
"modifiedTime": 1668995456388,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "PzkVTViII6ungWyp",
"name": "Zwergisch",
"type": "language",
"img": "systems/ds4/assets/icons/game-icons/lorc/conversation.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342360,
"modifiedTime": 1668995456389,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "josgKzD9UmDOvTup",
"name": "Ornamentschrift",
"type": "alphabet",
"img": "icons/svg/book.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342361,
"modifiedTime": 1668995456390,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "k8FSxBda9CoLOJqZ",
"name": "Kaitanische Schrift",
"type": "alphabet",
"img": "icons/svg/book.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342362,
"modifiedTime": 1668995456391,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "n2Nbg0ttFzcMxp7T",
"name": "Keilschrift",
"type": "alphabet",
"img": "icons/svg/book.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342364,
"modifiedTime": 1668995456392,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "n6KU1XIzbPWups0D",
"name": "Kaitanisch",
"type": "language",
"img": "systems/ds4/assets/icons/game-icons/lorc/conversation.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342365,
"modifiedTime": 1668995456393,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "usEWD48iYnMHO3Ty",
"name": "Zasarisch",
"type": "language",
"img": "systems/ds4/assets/icons/game-icons/lorc/conversation.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342366,
"modifiedTime": 1668995456394,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "xpvHuSywc8lJa2UN",
"name": "Elfisch",
"type": "language",
"img": "systems/ds4/assets/icons/game-icons/lorc/conversation.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342367,
"modifiedTime": 1668995456395,
"lastModifiedBy": "DS4BuildSystem00"
}
},
{
"_id": "ylqXcZHRbIBeV20Z",
"name": "Ahnenrunen",
"type": "alphabet",
"img": "icons/svg/book.svg",
"data": {
"description": ""
},
"effects": [],
"folder": null,
"sort": 0,
"permission": {
"flags": {},
"system": {
"description": ""
},
"ownership": {
"default": 0
},
"flags": {}
"_stats": {
"systemId": "ds4",
"systemVersion": "1.18.1",
"coreVersion": "10.290",
"createdTime": 1668995342375,
"modifiedTime": 1668995456396,
"lastModifiedBy": "DS4BuildSystem00"
}
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

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,16 +33,18 @@ 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`;
return `${basePath}/${this.actor.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))];
return [itemType, [...items].sort((a, b) => (a.sort || 0) - (b.sort || 0))];
}),
);
@ -54,39 +53,48 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
...effect.toObject(),
sourceName: await effect.getCurrentSourceName(),
factor: effect.factor,
isEffectivelyEnabled: !effect.data.disabled && !effect.isSurpressed,
isEffectivelyEnabled: !effect.disabled && !effect.isSurpressed,
};
});
const enrichedEffects = await Promise.all(enrichedEffectPromises);
const data = {
...this.addTooltipsToData(await super.getData()),
const context = {
...this.addTooltipsToData(await super.getData(options)),
config: DS4,
itemsByType,
enrichedEffects,
settings: getDS4Settings(),
};
return data;
return context;
}
/**
* Adds tooltips to the attributes, traits, and combatValues of the actor data of the given {@link ActorSheet.Data}.
* Adds tooltips to the attributes, traits, and combatValues of the given context object.
* @param {object} context
* @protected
*/
protected addTooltipsToData(data: ActorSheet.Data): ActorSheet.Data {
const valueGroups = [data.data.data.attributes, data.data.data.traits, data.data.data.combatValues];
addTooltipsToData(context) {
const valueGroups = [
context.data.system.attributes,
context.data.system.traits,
context.data.system.combatValues,
];
valueGroups.forEach((valueGroup) => {
Object.values(valueGroup).forEach((attribute: ModifiableDataBaseTotal<number> & { tooltip?: string }) => {
Object.values(valueGroup).forEach((attribute) => {
attribute.tooltip = this.getTooltipForValue(attribute);
});
});
return data;
return context;
}
/**
* 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 +102,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 +135,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,25 +154,23 @@ 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 {
const { type, ...data } = foundry.utils.deepClone(event.currentTarget.dataset);
onCreateItem(event) {
const { type } = foundry.utils.deepClone(event.currentTarget.dataset);
const name = getGame().i18n.localize(`DS4.New${type.capitalize()}Name`);
const itemData = {
name: name,
type: type,
data: data,
};
const itemData = { name, type };
Item.create(itemData, { parent: this.actor, pack: this.actor.pack ?? undefined });
}
/**
* 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"];
@ -366,29 +394,32 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
const dataPath = target.dataset["dataPath"];
enforce(dataPath !== undefined, `Could not find property 'dataPath' in the dataset of ${target}`);
const dataPath2 = target.dataset["dataPath2"];
/** @type {import("../../documents/item/item").DS4Item[]}*/
const items = this.actor.items.filter((item) => item.type === type);
items.sort((a, b) => a.data.sort - b.data.sort);
items.sort((a, b) => a.sort - b.sort);
const sortFunction =
(invert: boolean) =>
(a: DS4Item, b: DS4Item): number => {
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);
/**
* @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, dataPath);
const propertyB = getProperty(b, 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);
return typeof propertyA === "string" || typeof propertyB === "string"
? compareAsStrings(propertyA, propertyB, invert)
: compareAsNumbers(propertyA, propertyB, invert);
}
if (comparison === 0 && dataPath2 !== undefined) {
const propertyA = getProperty(a, dataPath);
const propertyB = getProperty(b, 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,15 +436,20 @@ 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> {
const item = await Item.fromDropData(data);
if (item && !this.actor.canOwnItemType(item.data.type)) {
/**
* @param {DragEvent} event
* @param {object} data
* @override
*/
async _onDropItem(event, data) {
const item = await Item.implementation.fromDropData(data);
if (item && !this.actor.canOwnItemType(item.type)) {
notifications.warn(
getGame().i18n.format("DS4.WarningActorCannotOwnItem", {
actorName: this.actor.name,
actorType: this.actor.data.type,
actorType: this.actor.type,
itemName: item.name,
itemType: item.data.type,
itemType: item.type,
}),
);
return false;
@ -422,19 +458,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 +472,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,9 +8,18 @@ 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"],
});
}
/** @override */
async getData(options = {}) {
const context = await super.getData(options);
context.data.system.profile.biography = await TextEditor.enrichHTML(context.data.system.profile.biography, {
async: true,
});
return context;
}
}

View file

@ -8,9 +8,19 @@ 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"],
});
}
/** @override */
async getData(options = {}) {
const context = await super.getData(options);
context.data.system.baseInfo.description = await TextEditor.enrichHTML(
context.data.system.baseInfo.description,
{ async: true },
);
return context;
}
}

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

@ -6,7 +6,6 @@
import { DS4 } from "../config";
import { DS4ActiveEffect } from "../documents/active-effect";
import { isDS4ItemDataTypePhysical } from "../documents/item/item-data-source-base";
import { notifications } from "../ui/notifications";
import { enforce, getGame } from "../utils/utils";
import { disableOverriddenFields } from "./sheet-helpers";
@ -14,8 +13,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,23 +25,29 @@ 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`;
return `${basePath}/${this.item.type}-sheet.hbs`;
}
override async getData(): Promise<DS4ItemSheetData> {
const data = {
...(await super.getData()),
/** @override */
async getData(options = {}) {
const superContext = await super.getData(options);
superContext.data.system.description = await TextEditor.enrichHTML(superContext.data.system.description, {
async: true,
});
const context = {
...superContext,
config: DS4,
isOwned: this.item.isOwned,
actor: this.item.actor,
isPhysical: isDS4ItemDataTypePhysical(this.item.data.data),
};
return data;
return context;
}
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 +57,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 +69,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 +87,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 +108,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 +132,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 +143,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

@ -0,0 +1,196 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { mathEvaluator } from "../expression-evaluation/evaluator";
import { getGame } from "../utils/utils";
/**
* @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
*/
/**
* @typedef {object} DS4ActiveEffectFlags
* @property {ItemEffectConfig} [itemEffectConfig] Configuration for applying this effect to owned items
*/
/**
* @typedef {Record<string, unknown>} ActiveEffectFlags
* @property {DS4ActiveEffectFlags} [ds4] Flags for DS4
*/
export class DS4ActiveEffect extends ActiveEffect {
/**
* A fallback icon that can be used if no icon is defined for the effect.
*/
static FALLBACK_ICON = "icons/svg/aura.svg";
/**
* A cached reference to the source document to avoid recurring database lookups
* @type {foundry.abstract.Document | undefined | null}
* @protected
*/
source = undefined;
/**
* Whether or not this effect is currently surpressed.
* @type {boolean}
*/
get isSurpressed() {
const originatingItem = this.originatingItem;
if (!originatingItem) {
return false;
}
return originatingItem.isNonEquippedEuipable();
}
/**
* 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() {
if (!(this.parent instanceof Actor)) {
return;
}
const itemIdRegex = /Item\.([a-zA-Z0-9]+)/;
const itemId = this.origin?.match(itemIdRegex)?.[1];
if (!itemId) {
return;
}
return this.parent.items.get(itemId);
}
/**
* The number of times this effect should be applied.
* @type {number}
*/
get factor() {
return this.originatingItem?.activeEffectFactor ?? 1;
}
/** @override */
apply(document, change) {
change.value = Roll.replaceFormulaData(change.value, document);
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
}
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() {
const game = getGame();
const origin = await this.getSource();
if (origin === null) return game.i18n.localize("None");
return origin.name ?? game.i18n.localize("Unknown");
}
/**
* 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>}
*/
async getSource() {
if (this.source === undefined) {
this.source = this.origin != null ? await fromUuid(this.origin) : null;
}
return this.source;
}
/**
* Create a new {@link DS4ActiveEffect} using default values.
*
* @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) {
const createData = {
label: getGame().i18n.localize(`DS4.NewEffectLabel`),
icon: this.FALLBACK_ICON,
};
return this.create(createData, { parent, pack: parent.pack ?? undefined });
}
/**
* 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;
}
/**
* Apply the given effects to the gicen Actor or item.
* @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, 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));
changesWithEffect.sort((a, b) => (a.change.priority ?? 0) - (b.change.priority ?? 0));
// Apply all changes
for (const changeWithEffect of changesWithEffect) {
const result = changeWithEffect.effect.apply(document, changeWithEffect.change);
if (result !== null) overrides[changeWithEffect.change.key] = result;
}
// Expand the set of final overrides
document.overrides = foundry.utils.expandObject({
...foundry.utils.flattenObject(document.overrides),
...overrides,
});
}
/**
* Get the array of changes for this effect, considering the {@link DS4ActiveEffect#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
*/
getFactoredChangesWithEffect(predicate = () => true) {
if (this.disabled || this.isSurpressed) {
return [];
}
return this.changes.filter(predicate).flatMap((change) => {
change.priority = change.priority ?? change.mode * 10;
return Array(this.factor).fill({ effect: this, change });
});
}
}
/**
* @typedef {object} EffectChangeDataWithEffect
* @property {DS4ActiveEffect} effect
* @property {EffectChangeData} change
*/
/**
* @typedef {object} EffectChangeData
* @property {string} key The attribute path in the Actor or Item data which the change modifies
* @property {string} value The value of the change effect
* @property {number} mode The modification mode with which the change is applied
* @property {number} priority The priority level with which this change is applied
*/

View file

@ -1,180 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { mathEvaluator } from "../expression-evaluation/evaluator";
import { getGame } from "../utils/utils";
import type { DS4Actor } from "./actor/actor";
import type { DS4Item } from "./item/item";
declare global {
interface DocumentClassConfig {
ActiveEffect: typeof DS4ActiveEffect;
}
interface FlagConfig {
ActiveEffect: {
ds4?: {
itemEffectConfig?: {
applyToItems?: boolean;
itemName?: string;
condition?: string;
};
};
};
}
}
type PromisedType<T> = T extends Promise<infer U> ? U : T;
export class DS4ActiveEffect extends ActiveEffect {
/**
* A fallback icon that can be used if no icon is defined for the effect.
*/
static FALLBACK_ICON = "icons/svg/aura.svg";
/**
* A cached reference to the source document to avoid recurring database lookups
*/
protected source: PromisedType<ReturnType<typeof fromUuid>> | undefined = undefined;
/**
* Whether or not this effect is currently surpressed.
*/
get isSurpressed(): boolean {
const originatingItem = this.originatingItem;
if (!originatingItem) {
return false;
}
return originatingItem.isNonEquippedEuipable();
}
/**
* The item which this effect originates from if it has been transferred from an item to an actor.
*/
get originatingItem(): DS4Item | undefined {
if (!(this.parent instanceof Actor)) {
return;
}
const itemIdRegex = /Item\.([a-zA-Z0-9]+)/;
const itemId = this.data.origin?.match(itemIdRegex)?.[1];
if (!itemId) {
return;
}
return this.parent.items.get(itemId);
}
/**
* The number of times this effect should be applied.
*/
get factor(): number {
return this.originatingItem?.activeEffectFactor ?? 1;
}
override apply(document: DS4Actor | DS4Item, change: EffectChangeData): unknown {
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.
*/
async getCurrentSourceName(): Promise<string> {
const game = getGame();
const origin = await this.getSource();
if (origin === null) return game.i18n.localize("None");
return origin.name ?? game.i18n.localize("Unknown");
}
/**
* Gets the source document for this effect. Uses the cached {@link DS4ActiveEffect#source} if it has already been
* set.
*/
protected async getSource(): ReturnType<typeof fromUuid> {
if (this.source === undefined) {
this.source = this.data.origin !== undefined ? await fromUuid(this.data.origin) : null;
}
return this.source;
}
/**
* 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.
*/
static async createDefault(parent: DS4Actor | DS4Item): Promise<DS4ActiveEffect | undefined> {
const createData = {
label: getGame().i18n.localize(`DS4.NewEffectLabel`),
icon: this.FALLBACK_ICON,
};
return this.create(createData, { parent, pack: parent.pack ?? undefined });
}
static safeEval(expression: string): number | `${number | boolean}` {
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}`;
}
/**
* 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
*/
static applyEffetcs(
document: DS4Actor | DS4Item,
effetcs: DS4ActiveEffect[],
predicate: (change: EffectChangeData) => boolean = () => true,
): void {
const overrides: Record<string, unknown> = {};
// Organize non-disabled and -surpressed effects by their application priority
const changesWithEffect = effetcs.flatMap((e) => e.getFactoredChangesWithEffect(predicate));
changesWithEffect.sort((a, b) => (a.change.priority ?? 0) - (b.change.priority ?? 0));
// Apply all changes
for (const changeWithEffect of changesWithEffect) {
const result = changeWithEffect.effect.apply(document, changeWithEffect.change);
if (result !== null) overrides[changeWithEffect.change.key] = result;
}
// Expand the set of final overrides
document.overrides = foundry.utils.expandObject({
...foundry.utils.flattenObject(document.overrides),
...overrides,
});
}
/**
* 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.
*/
protected getFactoredChangesWithEffect(
predicate: (change: EffectChangeData) => boolean = () => true,
): EffectChangeDataWithEffect[] {
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 });
});
}
}
type EffectChangeData = foundry.data.ActiveEffectData["changes"][number];
type EffectChangeDataWithEffect = { effect: DS4ActiveEffect; change: EffectChangeData };

View file

@ -0,0 +1,491 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver RÜmpelein
//
// SPDX-License-Identifier: MIT
import { DS4 } from "../../config";
import { createCheckRoll } from "../../dice/check-factory";
import { mathEvaluator } from "../../expression-evaluation/evaluator";
import { logger } from "../../utils/logger";
import { getGame } from "../../utils/utils";
import { DS4ActiveEffect } from "../active-effect";
import { isAttribute, isTrait } from "./actor-data-source-base";
/**
* The Actor class for DS4
*/
export class DS4Actor extends Actor {
/** @override */
prepareData() {
this.prepareBaseData();
this.prepareEmbeddedDocuments();
this.prepareIntermediateData();
this.applyActiveEffectsToBaseData();
this.prepareDerivedData();
this.applyActiveEffectsToDerivedData();
this.prepareFinalDerivedData();
}
/** @override */
prepareBaseData() {
this.system.rolling = {
minimumFumbleResult: 20,
maximumCoupResult: 1,
};
Object.values(this.system.attributes).forEach(
(attribute) => (attribute.total = attribute.base + attribute.mod),
);
Object.values(this.system.traits).forEach((trait) => (trait.total = trait.base + trait.mod));
}
/** @override */
prepareEmbeddedDocuments() {
super.prepareEmbeddedDocuments();
this.applyActiveEffectsToItems();
}
/**
* Apply transformations to the Actor data after embedded documents have been prepared, but before effects have been
* applied to the Actor.
*/
prepareIntermediateData() {
this.system.armorValueSpellMalus = this.armorValueSpellMalusOfEquippedItems;
}
/**
* The effects that should be applioed to this actor.
* @type {import("../active-effect").DS4ActiveEffect[]}
* @protected
*/
get actorEffects() {
return this.effects.filter((effect) => !effect.flags.ds4?.itemEffectConfig?.applyToItems);
}
/**
* Get the effects of this actor that should be applied to the given item.
* @param {import("../item/item").DS4Item} item The item for which to get effects
* @returns {import("../active-effect").DS4ActiveEffect[]} The array of effects that are candidates to be applied to the item
*/
itemEffects(item) {
return this.effects.filter((effect) => {
const { applyToItems, itemName, condition } = effect.flags.ds4?.itemEffectConfig ?? {};
if (!applyToItems || (itemName !== undefined && itemName !== "" && itemName !== item.name)) {
return false;
}
if (condition !== undefined && condition !== "") {
try {
const replacedCondition = DS4Actor.replaceFormulaData(condition, {
item,
actor: this,
effect,
});
return replacedCondition !== undefined ? Boolean(mathEvaluator.evaluate(replacedCondition)) : false;
} catch (error) {
logger.warn(error);
return false;
}
}
return true;
});
}
/**
* 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) => {
const value = foundry.utils.getProperty(data, term);
if (value == null) {
throw new Error();
}
return String(value).trim();
});
} catch {
return undefined;
}
}
/**
* 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
*/
applyActiveEffects() {
return;
}
/**
* Apply active effects to items.
*
* @remarks
* Talents are handled before all other item types, because if the total rank of a talent is affected by any
* effects, that affects how many times effects provided by this talent need to be applied. At the moment, there is
* 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() {
/* 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) {
this.applyActiveEffectsToItem(item);
}
for (const item of this.items) {
if (item.type === "talent") continue;
this.applyActiveEffectsToItem(item);
}
}
/**
* 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));
}
/**
* Apply effects to base data
* @protected
*/
applyActiveEffectsToBaseData() {
this.overrides = {};
DS4ActiveEffect.applyEffetcs(
this,
this.actorEffects,
(change) =>
!this.derivedDataProperties.includes(change.key) &&
!this.finalDerivedDataProperties.includes(change.key),
);
}
/**
* Apply effects to derived data
* @protected
*/
applyActiveEffectsToDerivedData() {
DS4ActiveEffect.applyEffetcs(this, this.actorEffects, (change) =>
this.derivedDataProperties.includes(change.key),
);
}
/**
* Apply transformations to the Actor data after effects have been applied to the base data.
* @override
*/
prepareDerivedData() {
this.system.armorValueSpellMalus = Math.max(this.system.armorValueSpellMalus, 0);
this.prepareCombatValues();
this.prepareChecks();
}
/**
* The list of properties that are derived from others, given in dot notation.
* @returns {string[]} The list of derived propertie
*/
get derivedDataProperties() {
const combatValueProperties = Object.keys(DS4.i18n.combatValues).map(
(combatValue) => `system.combatValues.${combatValue}.total`,
);
const checkProperties = Object.keys(DS4.i18n.checks)
.filter((check) => check !== "defend")
.map((check) => `system.checks.${check}`);
return combatValueProperties.concat(checkProperties);
}
/**
* Apply final transformations to the Actor data after all effects have been applied.
*/
prepareFinalDerivedData() {
Object.values(this.system.attributes).forEach((attribute) => (attribute.total = Math.ceil(attribute.total)));
Object.values(this.system.traits).forEach((trait) => (trait.total = Math.ceil(trait.total)));
Object.entries(this.system.combatValues)
.filter(([key]) => key !== "movement")
.forEach(([, combatValue]) => (combatValue.total = Math.ceil(combatValue.total)));
Object.keys(this.system.checks).forEach((key) => {
this.system.checks[key] = Math.ceil(this.system.checks[key]);
});
this.system.combatValues.hitPoints.max = this.system.combatValues.hitPoints.total;
this.system.checks.defend = this.system.combatValues.defense.total;
}
/**
* 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() {
return ["system.combatValues.hitPoints.max", "system.checks.defend"];
}
/**
* The list of item types that can be owned by this actor.
* @type {import("../item/item-data-source").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 {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) {
return this.ownableItemTypes.includes(itemType);
}
/**
* Prepares the combat values of the actor.
* @protected
*/
prepareCombatValues() {
const system = this.system;
system.combatValues.hitPoints.base = system.attributes.body.total + system.traits.constitution.total + 10;
system.combatValues.defense.base =
system.attributes.body.total + system.traits.constitution.total + this.armorValueOfEquippedItems;
system.combatValues.initiative.base = system.attributes.mobility.total + system.traits.agility.total;
system.combatValues.movement.base = system.attributes.mobility.total / 2 + 1;
system.combatValues.meleeAttack.base = system.attributes.body.total + system.traits.strength.total;
system.combatValues.rangedAttack.base = system.attributes.mobility.total + system.traits.dexterity.total;
system.combatValues.spellcasting.base =
system.attributes.mind.total + system.traits.aura.total - system.armorValueSpellMalus;
system.combatValues.targetedSpellcasting.base =
system.attributes.mind.total + system.traits.dexterity.total - system.armorValueSpellMalus;
Object.values(system.combatValues).forEach(
(combatValue) => (combatValue.total = combatValue.base + combatValue.mod),
);
}
/**
* The total armor value of the equipped items.
* @type {number}
* @protected
*/
get armorValueOfEquippedItems() {
return this.equippedItemsWithArmor.map((item) => item.system.armorValue).reduce((a, b) => a + b, 0);
}
/**
* The armor value spell malus from equipped items.
* @type {number}
* @protected
*/
get armorValueSpellMalusOfEquippedItems() {
return this.equippedItemsWithArmor
.filter((item) => !(item.type === "armor" && ["cloth", "natural"].includes(item.system.armorMaterialType)))
.reduce((sum, item) => sum + item.system.armorValue, 0);
}
/**
* 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.type === "armor" || item.type === "shield") && item.system.equipped);
}
/**
* Prepares the check target numbers of checks for the actor.
* @protected
*/
prepareChecks() {
const system = this.system;
system.checks = {
appraise: system.attributes.mind.total + system.traits.intellect.total,
changeSpell: system.attributes.mind.total + system.traits.intellect.total,
climb: system.attributes.mobility.total + system.traits.strength.total,
communicate: system.attributes.mind.total + system.traits.dexterity.total + this.itemTypes.language.length,
decipherScript: system.attributes.mind.total + system.traits.intellect.total,
defend: 0, // assigned in prepareFinalDerivedData as it must always match data.combatValues.defense.total and is not changeable by effects
defyPoison: system.attributes.body.total + system.traits.constitution.total,
disableTraps: system.attributes.mind.total + system.traits.dexterity.total,
featOfStrength: system.attributes.body.total + system.traits.strength.total,
flirt: system.attributes.mind.total + system.traits.aura.total,
haggle:
system.attributes.mind.total + Math.max(system.traits.intellect.total, system.traits.intellect.total),
hide: system.attributes.mobility.total + system.traits.agility.total,
identifyMagic: system.attributes.mind.total + system.traits.intellect.total,
jump: system.attributes.mobility.total + system.traits.agility.total,
knowledge: system.attributes.mind.total + system.traits.intellect.total,
openLock: system.attributes.mind.total + system.traits.dexterity.total,
perception: Math.max(system.attributes.mind.total + system.traits.intellect.total, 8),
pickPocket: system.attributes.mobility.total + system.traits.dexterity.total,
readTracks: system.attributes.mind.total + system.traits.intellect.total,
resistDisease: system.attributes.body.total + system.traits.constitution.total,
ride: system.attributes.mobility.total + Math.max(system.traits.agility.total, system.traits.aura.total),
search: Math.max(system.attributes.mind.total + system.traits.intellect.total, 8),
senseMagic: system.attributes.mind.total + system.traits.aura.total,
sneak: system.attributes.mobility.total + system.traits.agility.total,
startFire: system.attributes.mind.total + system.traits.dexterity.total,
swim: system.attributes.mobility.total + system.traits.strength.total,
wakeUp: system.attributes.mind.total + system.traits.intellect.total,
workMechanism:
system.attributes.mind.total + Math.max(system.traits.dexterity.total, system.traits.intellect.total),
};
}
/**
* 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.
* @param {string} attribute The attribute path
* @param {number} value The target attribute value
* @param {boolean} [isDelta=false] Whether the number represents a relative change (true) or an absolute change (false)
* @param {boolean} [isBar=true] Whether the new value is part of an attribute bar, or just a direct value
* @returns {Promise<DS4Actor>} The updated Actor document
* @override
*/
async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
const current = foundry.utils.getProperty(this.system, attribute);
// Determine the updates to make to the actor data
/** @type {Record<string, number>} */
let updates;
if (isBar) {
if (isDelta) value = Math.min(Number(current.value) + value, current.max);
updates = { [`system.${attribute}.value`]: value };
} else {
if (isDelta) value = Number(current) + value;
updates = { [`system.${attribute}`]: value };
}
// Call a hook to handle token resource bar updates
const allowed = Hooks.call("modifyTokenAttribute", { attribute, value, isDelta, isBar }, updates);
return allowed !== false ? this.update(updates) : this;
}
/**
* Roll for a given check.
* @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, options = {}) {
const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker });
await createCheckRoll(this.system.checks[check], {
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: this.system.rolling.maximumCoupResult,
minimumFumbleResult: this.system.rolling.minimumFumbleResult,
flavor: "DS4.ActorCheckFlavor",
flavorData: { actor: speaker.alias ?? this.name, check: DS4.i18nKeys.checks[check] },
speaker,
});
}
/**
* Roll a generic check. A dialog is presented to select the combination of
* Attribute and Trait to perform the check against.
* @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 = {}) {
const attributeAndTrait = await this.selectAttributeAndTrait();
if (!attributeAndTrait) {
return;
}
const { attribute, trait } = attributeAndTrait;
const checkTargetNumber = this.system.attributes[attribute].total + this.system.traits[trait].total;
const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker });
await createCheckRoll(checkTargetNumber, {
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: this.system.rolling.maximumCoupResult,
minimumFumbleResult: this.system.rolling.minimumFumbleResult,
flavor: "DS4.ActorGenericCheckFlavor",
flavorData: {
actor: speaker.alias ?? this.name,
attribute: DS4.i18n.attributes[attribute],
trait: DS4.i18n.traits[trait],
},
speaker,
});
}
/**
* 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({
title: getGame().i18n.localize("DS4.DialogAttributeTraitSelection"),
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [
{
label: getGame().i18n.localize("DS4.Attribute"),
identifier: attributeIdentifier,
options: Object.fromEntries(
Object.entries(DS4.i18n.attributes).map(([attribute, translation]) => [
attribute,
`${translation} (${this.system.attributes[attribute].total})`,
]),
),
},
{
label: getGame().i18n.localize("DS4.Trait"),
identifier: traitIdentifier,
options: Object.fromEntries(
Object.entries(DS4.i18n.traits).map(([trait, translation]) => [
trait,
`${translation} (${this.system.traits[trait].total})`,
]),
),
},
],
}),
label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => {
const selectedAttribute = html.find(`#${attributeIdentifier}`).val();
if (!isAttribute(selectedAttribute)) {
throw new Error(
getGame().i18n.format("DS4.ErrorUnexpectedAttribute", {
actualAttribute: selectedAttribute,
expectedTypes: Object.keys(DS4.i18n.attributes)
.map((attribute) => `'${attribute}'`)
.join(", "),
}),
);
}
const selectedTrait = html.find(`#${traitIdentifier}`).val();
if (!isTrait(selectedTrait)) {
throw new Error(
getGame().i18n.format("DS4.ErrorUnexpectedTrait", {
actualTrait: selectedTrait,
expectedTypes: Object.keys(DS4.i18n.traits)
.map((attribute) => `'${attribute}'`)
.join(", "),
}),
);
}
return {
attribute: selectedAttribute,
trait: selectedTrait,
};
},
rejectClose: false,
});
}
}
/**
* @typedef {object} AttributeAndTrait
* @property {keyof typeof DS4.i18n.attributes} attribute
* @property {keyof typeof DS4.i18n.traits} trait
*/

View file

@ -1,476 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver RÜmpelein
//
// SPDX-License-Identifier: MIT
import { DS4 } from "../../config";
import { createCheckRoll } from "../../dice/check-factory";
import { mathEvaluator } from "../../expression-evaluation/evaluator";
import { logger } from "../../utils/logger";
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 {
this.data.reset();
this.prepareBaseData();
this.prepareEmbeddedDocuments();
this.prepareIntermediateData();
this.applyActiveEffectsToBaseData();
this.prepareDerivedData();
this.applyActiveEffectsToDerivedData();
this.prepareFinalDerivedData();
}
override prepareBaseData(): void {
const data = this.data;
data.data.rolling = {
minimumFumbleResult: 20,
maximumCoupResult: 1,
};
const attributes = data.data.attributes;
Object.values(attributes).forEach(
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = attribute.base + attribute.mod),
);
const traits = data.data.traits;
Object.values(traits).forEach(
(trait: ModifiableDataBaseTotal<number>) => (trait.total = trait.base + trait.mod),
);
}
override prepareEmbeddedDocuments() {
super.prepareEmbeddedDocuments();
this.applyActiveEffectsToItems();
}
/**
* Apply transformations to the Actor data after embedded documents have been prepared, but before effects have been
* applied to the Actor.
*/
prepareIntermediateData() {
this.data.data.armorValueSpellMalus = this.armorValueSpellMalusOfEquippedItems;
}
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
* @returns The array of effects that are candidates to be applied to the item
*/
itemEffects(item: DS4Item) {
return this.effects.filter((effect) => {
const { applyToItems, itemName, condition } = effect.data.flags.ds4?.itemEffectConfig ?? {};
if (!applyToItems || (itemName !== undefined && itemName !== "" && itemName !== item.name)) {
return false;
}
if (condition !== undefined && condition !== "") {
try {
const replacedCondition = DS4Actor.replaceFormulaData(condition, {
item: item.data,
actor: this.data,
effect: effect.data,
});
return replacedCondition !== undefined ? Boolean(mathEvaluator.evaluate(replacedCondition)) : false;
} catch (error) {
logger.warn(error);
return false;
}
}
return true;
});
}
protected static replaceFormulaData(formula: string, data: object): string | undefined {
const dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi);
try {
return formula.replace(dataRgx, (_, term) => {
const value = foundry.utils.getProperty(data, term);
if (value == null) {
throw new Error();
}
return String(value).trim();
});
} catch {
return undefined;
}
}
/**
* 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 applyActiveEffects(): void {
return;
}
/**
* Apply active effects to items.
*
* @remarks
* Talents are handled before all other item types, because if the total rank of a talent is affected by any
* effects, that affects how many times effects provided by this talent need to be applied. At the moment, there is
* 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 {
/* 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) {
this.applyActiveEffectsToItem(item);
}
for (const item of this.items) {
if (item.type === "talent") continue;
this.applyActiveEffectsToItem(item);
}
}
protected applyActiveEffectsToItem(item: DS4Item) {
item.overrides = {};
DS4ActiveEffect.applyEffetcs(item, this.itemEffects(item));
}
applyActiveEffectsToBaseData(): void {
this.overrides = {};
DS4ActiveEffect.applyEffetcs(
this,
this.actorEffects,
(change) =>
!this.derivedDataProperties.includes(change.key) &&
!this.finalDerivedDataProperties.includes(change.key),
);
}
applyActiveEffectsToDerivedData(): void {
DS4ActiveEffect.applyEffetcs(this, this.actorEffects, (change) =>
this.derivedDataProperties.includes(change.key),
);
}
/**
* Apply transformations to the Actor data after effects have been applied to the base data.
*/
override prepareDerivedData(): void {
this.data.data.armorValueSpellMalus = Math.max(this.data.data.armorValueSpellMalus, 0);
this.prepareCombatValues();
this.prepareChecks();
}
/**
* The list of properties that are derived from others, given in dot notation.
*/
get derivedDataProperties(): Array<string> {
const combatValueProperties = Object.keys(DS4.i18n.combatValues).map(
(combatValue) => `data.combatValues.${combatValue}.total`,
);
const checkProperties = Object.keys(DS4.i18n.checks)
.filter((check) => check !== "defend")
.map((check) => `data.checks.${check}`);
return combatValueProperties.concat(checkProperties);
}
/**
* 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)),
);
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) => {
this.data.data.checks[key] = Math.ceil(this.data.data.checks[key]);
});
this.data.data.combatValues.hitPoints.max = this.data.data.combatValues.hitPoints.total;
this.data.data.checks.defend = this.data.data.combatValues.defense.total;
}
/**
* The list of properties that are completely derived (i.e. {@link ActiveEffect}s cannot be applied to them),
* given in dot notation.
*/
get finalDerivedDataProperties(): string[] {
return ["data.combatValues.hitPoints.max", "data.checks.defend"];
}
/**
* The list of item types that can be owned by this actor.
*/
get ownableItemTypes(): Array<ItemType> {
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
*/
canOwnItemType(itemType: ItemType): boolean {
return this.ownableItemTypes.includes(itemType);
}
/**
* Prepares the combat values of the actor.
*/
protected prepareCombatValues(): void {
const data = this.data.data;
data.combatValues.hitPoints.base = data.attributes.body.total + data.traits.constitution.total + 10;
data.combatValues.defense.base =
data.attributes.body.total + data.traits.constitution.total + this.armorValueOfEquippedItems;
data.combatValues.initiative.base = data.attributes.mobility.total + data.traits.agility.total;
data.combatValues.movement.base = data.attributes.mobility.total / 2 + 1;
data.combatValues.meleeAttack.base = data.attributes.body.total + data.traits.strength.total;
data.combatValues.rangedAttack.base = data.attributes.mobility.total + data.traits.dexterity.total;
data.combatValues.spellcasting.base =
data.attributes.mind.total + data.traits.aura.total - data.armorValueSpellMalus;
data.combatValues.targetedSpellcasting.base =
data.attributes.mind.total + data.traits.dexterity.total - data.armorValueSpellMalus;
Object.values(data.combatValues).forEach(
(combatValue: ModifiableDataBaseTotal<number>) => (combatValue.total = combatValue.base + combatValue.mod),
);
}
/**
* The total armor value of the equipped items.
*/
protected get armorValueOfEquippedItems(): number {
return this.equippedItemsWithArmor.map((item) => item.data.data.armorValue).reduce((a, b) => a + b, 0);
}
/**
* The armor value spell malus from equipped items.
*/
protected get armorValueSpellMalusOfEquippedItems(): number {
return this.equippedItemsWithArmor
.filter(
(item) =>
!(item.data.type === "armor" && ["cloth", "natural"].includes(item.data.data.armorMaterialType)),
)
.map((item) => item.data.data.armorValue)
.reduce((a, b) => a + b, 0);
}
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.data.equipped);
}
/**
* Prepares the check target numbers of checks for the actor.
*/
protected prepareChecks(): void {
const data = this.data.data;
data.checks = {
appraise: data.attributes.mind.total + data.traits.intellect.total,
changeSpell: data.attributes.mind.total + data.traits.intellect.total,
climb: data.attributes.mobility.total + data.traits.strength.total,
communicate: data.attributes.mind.total + data.traits.dexterity.total + this.itemTypes.language.length,
decipherScript: data.attributes.mind.total + data.traits.intellect.total,
defend: 0, // assigned in prepareFinalDerivedData as it must always match data.combatValues.defense.total and is not changeable by effects
defyPoison: data.attributes.body.total + data.traits.constitution.total,
disableTraps: data.attributes.mind.total + data.traits.dexterity.total,
featOfStrength: data.attributes.body.total + data.traits.strength.total,
flirt: data.attributes.mind.total + data.traits.aura.total,
haggle: data.attributes.mind.total + Math.max(data.traits.intellect.total, data.traits.intellect.total),
hide: data.attributes.mobility.total + data.traits.agility.total,
identifyMagic: data.attributes.mind.total + data.traits.intellect.total,
jump: data.attributes.mobility.total + data.traits.agility.total,
knowledge: data.attributes.mind.total + data.traits.intellect.total,
openLock: data.attributes.mind.total + data.traits.dexterity.total,
perception: Math.max(data.attributes.mind.total + data.traits.intellect.total, 8),
pickPocket: data.attributes.mobility.total + data.traits.dexterity.total,
readTracks: data.attributes.mind.total + data.traits.intellect.total,
resistDisease: data.attributes.body.total + data.traits.constitution.total,
ride: data.attributes.mobility.total + Math.max(data.traits.agility.total, data.traits.aura.total),
search: Math.max(data.attributes.mind.total + data.traits.intellect.total, 8),
senseMagic: data.attributes.mind.total + data.traits.aura.total,
sneak: data.attributes.mobility.total + data.traits.agility.total,
startFire: data.attributes.mind.total + data.traits.dexterity.total,
swim: data.attributes.mobility.total + data.traits.strength.total,
wakeUp: data.attributes.mind.total + data.traits.intellect.total,
workMechanism:
data.attributes.mind.total + Math.max(data.traits.dexterity.total, data.traits.intellect.total),
};
}
/**
* 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 async modifyTokenAttribute(
attribute: string,
value: number,
isDelta = false,
isBar = true,
): Promise<this | undefined> {
const current = foundry.utils.getProperty(this.data.data, attribute);
// Determine the updates to make to the actor data
let updates: Record<string, number>;
if (isBar) {
if (isDelta) value = Math.min(Number(current.value) + value, current.max);
updates = { [`data.${attribute}.value`]: value };
} else {
if (isDelta) value = Number(current) + value;
updates = { [`data.${attribute}`]: value };
}
// Call a hook to handle token resource bar updates
const allowed = Hooks.call("modifyTokenAttribute", { attribute, value, isDelta, isBar }, updates);
return allowed !== false ? this.update(updates) : this;
}
/**
* Roll for a given check.
* @param check - The check to perform
* @param options - Additional options to customize the roll
*/
async rollCheck(
check: Check,
options: { speaker?: { token?: TokenDocument; alias?: string } } = {},
): Promise<void> {
const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker });
await createCheckRoll(this.data.data.checks[check], {
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: this.data.data.rolling.maximumCoupResult,
minimumFumbleResult: this.data.data.rolling.minimumFumbleResult,
flavor: "DS4.ActorCheckFlavor",
flavorData: { actor: speaker.alias ?? this.name, check: DS4.i18nKeys.checks[check] },
speaker,
});
}
/**
* 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
*/
async rollGenericCheck(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
const attributeAndTrait = await this.selectAttributeAndTrait();
if (!attributeAndTrait) {
return;
}
const { attribute, trait } = attributeAndTrait;
const checkTargetNumber = this.data.data.attributes[attribute].total + this.data.data.traits[trait].total;
const speaker = ChatMessage.getSpeaker({ actor: this, ...options.speaker });
await createCheckRoll(checkTargetNumber, {
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: this.data.data.rolling.maximumCoupResult,
minimumFumbleResult: this.data.data.rolling.minimumFumbleResult,
flavor: "DS4.ActorGenericCheckFlavor",
flavorData: {
actor: speaker.alias ?? this.name,
attribute: DS4.i18n.attributes[attribute],
trait: DS4.i18n.traits[trait],
},
speaker,
});
}
protected async selectAttributeAndTrait(): Promise<{
attribute: keyof typeof DS4.i18n.attributes;
trait: keyof typeof DS4.i18n.traits;
} | null> {
const attributeIdentifier = "attribute-trait-selection-attribute";
const traitIdentifier = "attribute-trait-selection-trait";
return Dialog.prompt({
title: getGame().i18n.localize("DS4.DialogAttributeTraitSelection"),
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [
{
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})`,
],
),
),
},
{
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})`,
],
),
),
},
],
}),
label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => {
const selectedAttribute = html.find(`#${attributeIdentifier}`).val();
if (!isAttribute(selectedAttribute)) {
throw new Error(
getGame().i18n.format("DS4.ErrorUnexpectedAttribute", {
actualAttribute: selectedAttribute,
expectedTypes: Object.keys(DS4.i18n.attributes)
.map((attribute) => `'${attribute}'`)
.join(", "),
}),
);
}
const selectedTrait = html.find(`#${traitIdentifier}`).val();
if (!isTrait(selectedTrait)) {
throw new Error(
getGame().i18n.format("DS4.ErrorUnexpectedTrait", {
actualTrait: selectedTrait,
expectedTypes: Object.keys(DS4.i18n.traits)
.map((attribute) => `'${attribute}'`)
.join(", "),
}),
);
}
return {
attribute: selectedAttribute,
trait: selectedTrait,
};
},
rejectClose: false,
});
}
}

View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4Actor } from "../actor";
export class DS4Character extends DS4Actor {
/** @override */
prepareFinalDerivedData() {
super.prepareFinalDerivedData();
this.system.slayerPoints.max = 3;
}
/** @override */
get finalDerivedDataProperties() {
return [...super.finalDerivedDataProperties, "system.slayerPoints.max"];
}
/** @override */
get ownableItemTypes() {
return [...super.ownableItemTypes, "talent", "racialAbility", "language", "alphabet"];
}
}

View file

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4Actor } from "../actor";
import type { ItemType } from "../../item/item-data-source";
export class DS4Character extends DS4Actor {
override prepareFinalDerivedData(): void {
super.prepareFinalDerivedData();
this.data.data.slayerPoints.max = 3;
}
override get finalDerivedDataProperties(): string[] {
return [...super.finalDerivedDataProperties, "data.slayerPoints.max"];
}
override get ownableItemTypes(): Array<ItemType> {
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

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { getGame } from "../utils/utils";
/**
* @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 {
prepareData() {
super.prepareData();
if (this.flavor) {
const game = getGame();
const flavorData = Object.fromEntries(
Object.entries(this.flags.ds4?.flavorData ?? {}).map(([key, value]) => [
key,
typeof value === "string" ? game.i18n.localize(value) : value,
]),
);
this.flavor = game.i18n.format(this.flavor, flavorData);
}
}
}

View file

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { getGame } from "../utils/utils";
declare global {
interface FlagConfig {
ChatMessage: {
ds4?: {
flavorData?: Record<string, string | number | null>;
};
};
}
}
export class DS4ChatMessage extends ChatMessage {
override prepareData(): void {
super.prepareData();
if (this.data.flavor) {
const game = getGame();
const flavorData = Object.fromEntries(
Object.entries(this.data.flags.ds4?.flavorData ?? {}).map(([key, value]) => [
key,
typeof value === "string" ? game.i18n.localize(value) : value,
]),
);
this.data.flavor = game.i18n.format(this.data.flavor, flavorData);
}
}
}

View file

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
/**
* @typedef {object} RollOptions
* @property {Speaker} speaker
*/
/**
* @typedef {object} Speaker
* @property {TokenDocument} [token]
* @property {string} [alias]
*/
export {}

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,10 +17,6 @@ export interface DS4ItemDataSourceDataPhysical {
storageLocation: string;
}
export function isDS4ItemDataTypePhysical(input: foundry.data.ItemData["data"]): boolean {
return "quantity" in input && "price" in input && "availability" in input && "storageLocation" in input;
}
export interface DS4ItemDataSourceDataEquipable {
equipped: boolean;
}

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.system.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.system && !this.system.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.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,30 +2,32 @@
//
// 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 {
this.data.data.rollable = this.data.data.equipped;
this.data.data.price = calculateSpellPrice(this.data.data);
if (this.data.data.allowsDefense) {
this.data.data.opponentDefense = 0;
/** @override */
prepareDerivedData() {
this.system.rollable = this.system.equipped;
this.system.price = calculateSpellPrice(this.system);
if (this.system.allowsDefense) {
this.system.opponentDefense = 0;
}
}
override async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
/** @override */
async roll(options = {}) {
const game = getGame();
if (!this.data.data.equipped) {
if (!this.system.equipped) {
return notifications.warn(
game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
name: this.name,
id: this.id,
type: this.data.type,
type: this.type,
}),
);
}
@ -34,27 +36,28 @@ export class DS4Spell extends DS4Item {
throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id }));
}
const ownerDataData = this.actor.data.data;
const hasComplexModifier = this.data.data.spellModifier.complex !== "";
const ownerSystemData = this.actor.system;
const hasComplexModifier = this.system.spellModifier.complex !== "";
if (hasComplexModifier === undefined) {
notifications.info(
game.i18n.format("DS4.InfoManuallyEnterSpellModifier", {
name: this.name,
spellModifier: this.data.data.spellModifier.complex,
spellModifier: this.system.spellModifier.complex,
}),
);
}
const spellType = this.data.data.spellType;
const opponentDefense = this.data.data.opponentDefense;
const spellType = this.system.spellType;
const opponentDefense = this.system.opponentDefense;
const checkTargetNumber =
ownerDataData.combatValues[spellType].total +
(hasComplexModifier ? 0 : this.data.data.spellModifier.numerical);
ownerSystemData.combatValues[spellType].total +
(hasComplexModifier ? 0 : this.system.spellModifier.numerical);
const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
const flavor =
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,
};
@ -64,17 +67,19 @@ export class DS4Spell extends DS4Item {
await createCheckRoll(checkTargetNumber, {
rollMode: game.settings.get("core", "rollMode"),
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
maximumCoupResult: ownerSystemData.rolling.maximumCoupResult,
minimumFumbleResult: ownerSystemData.rolling.minimumFumbleResult,
flavor: flavor,
flavorData: flavorData,
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

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4Item } from "../item";
export class DS4Talent extends DS4Item {
/** @override */
prepareDerivedData() {
super.prepareDerivedData();
this.system.rank.total = this.system.rank.base + this.system.rank.mod;
}
/** @override */
get activeEffectFactor() {
return this.system.rank.total;
}
}

View file

@ -1,21 +0,0 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4Item } from "../item";
export class DS4Talent extends DS4Item {
override prepareDerivedData(): void {
super.prepareDerivedData();
const data = this.data.data;
data.rank.total = data.rank.base + data.rank.mod;
}
override get activeEffectFactor(): number | undefined {
return this.data.data.rank.total;
}
}
export interface DS4Talent {
data: foundry.data.ItemData & { type: "talent"; _source: { type: "talent" } };
}

View file

@ -3,32 +3,34 @@
// 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 {
const data = this.data.data;
data.rollable = data.equipped;
data.opponentDefenseForAttackType = {};
if (data.attackType === "melee" || data.attackType === "meleeRanged") {
data.opponentDefenseForAttackType.melee = data.opponentDefense;
/** @override */
prepareDerivedData() {
const system = this.system;
system.rollable = system.equipped;
system.opponentDefenseForAttackType = {};
if (system.attackType === "melee" || system.attackType === "meleeRanged") {
system.opponentDefenseForAttackType.melee = system.opponentDefense;
}
if (data.attackType === "ranged" || data.attackType === "meleeRanged") {
data.opponentDefenseForAttackType.ranged = data.opponentDefense;
if (system.attackType === "ranged" || system.attackType === "meleeRanged") {
system.opponentDefenseForAttackType.ranged = system.opponentDefense;
}
}
override async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
/** @override */
async roll(options = {}) {
const game = getGame();
if (!this.data.data.equipped) {
if (!this.system.equipped) {
return notifications.warn(
game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
name: this.name,
id: this.id,
type: this.data.type,
type: this.type,
}),
);
}
@ -37,18 +39,19 @@ export class DS4Weapon extends DS4Item {
throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id }));
}
const ownerDataData = this.actor.data.data;
const weaponBonus = this.data.data.weaponBonus;
const ownerSystemData = this.actor.system;
const weaponBonus = this.system.weaponBonus;
const attackType = await this.getPerformedAttackType();
const opponentDefense = this.data.data.opponentDefenseForAttackType[attackType];
const combatValue = `${attackType}Attack` as const;
const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus;
const opponentDefense = this.system.opponentDefenseForAttackType[attackType];
const combatValue = `${attackType}Attack`;
const checkTargetNumber = ownerSystemData.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,
};
@ -58,8 +61,8 @@ export class DS4Weapon extends DS4Item {
await createCheckRoll(checkTargetNumber, {
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
maximumCoupResult: ownerSystemData.rolling.maximumCoupResult,
minimumFumbleResult: ownerSystemData.rolling.minimumFumbleResult,
speaker,
flavor,
flavorData,
@ -68,9 +71,14 @@ export class DS4Weapon extends DS4Item {
Hooks.callAll("ds4.rollItem", this);
}
private async getPerformedAttackType(): Promise<"melee" | "ranged"> {
if (this.data.data.attackType !== "meleeRanged") {
return this.data.data.attackType;
/**
* 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.system.attackType !== "meleeRanged") {
return this.system.attackType;
}
const { melee, ranged } = { ...DS4.i18n.attackTypes };
@ -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" }).system);
}
}
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",

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

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { createRollCheckMacro } from "../macros/roll-check";
import { createRollItemMacro } from "../macros/roll-item";
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 {void | false}
*/
function onHotbarDrop(hotbar, data, slot) {
switch (data.type) {
case "Item": {
createRollItemMacro(data, slot);
return false;
}
case "Check": {
createRollCheckMacro(data, slot);
return false;
}
}
}

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;

62
src/macros/roll-check.js Normal file
View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4 } from "../config";
import { isCheck } from "../documents/actor/actor-data-properties-base";
import { notifications } from "../ui/notifications";
import { getGame } from "../utils/utils";
import { getActiveActorAndToken } from "./helpers";
/**
* Creates a macro from a check drop.
* Get an existing roll check macro if one exists, otherwise create a new one.
* @param {object} data The check drop data
* @param {string} slot The hotbar slot to use
* @returns {Promise<void>} A promise that resolves when the macro has been created.
*/
export async function createRollCheckMacro(data, slot) {
if (!("data" in data) || typeof data.data !== "string" || !isCheck(data.data)) {
return notifications.warn(getGame().i18n.localize("DS4.WarningInvalidCheckDropped"));
}
const macro = await getOrCreateRollCheckMacro(data.data);
await getGame().user?.assignHotbarMacro(macro ?? null, slot);
}
/**
* @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((m) => m.name === DS4.i18n.checks[check] && m.command === command);
if (existingMacro) {
return existingMacro;
}
return Macro.create(
{
command,
name: DS4.i18n.checks[check],
type: "script",
img: DS4.icons.checks[check],
flags: { "ds4.checkMacro": true },
},
{ renderSheet: false },
);
}
/**
* 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) {
const { actor, token } = getActiveActorAndToken();
if (!actor) {
return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro"));
}
return actor.rollCheck(check, { speaker: { token } }).catch((e) => notifications.error(e, { log: true }));
}

View file

@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4 } from "../config";
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.
*/
export async function createRollCheckMacro(check: Check, slot: string): Promise<void> {
const macro = await getOrCreateRollCheckMacro(check);
getGame().user?.assignHotbarMacro(macro ?? null, slot);
}
async function getOrCreateRollCheckMacro(check: Check): Promise<Macro | undefined> {
const command = `game.ds4.macros.rollCheck("${check}");`;
const existingMacro = getGame().macros?.find(
(m) => m.name === DS4.i18n.checks[check] && m.data.command === command,
);
if (existingMacro) {
return existingMacro;
}
return Macro.create(
{
command,
name: DS4.i18n.checks[check],
type: "script",
img: DS4.icons.checks[check],
flags: { "ds4.checkMacro": true },
},
{ renderSheet: false },
);
}
/**
* Executes the roll check macro for the given check.
*/
export async function rollCheck(check: Check): Promise<void> {
const { actor, token } = getActiveActorAndToken();
if (!actor) {
return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro"));
}
return actor.rollCheck(check, { speaker: { token } }).catch((e) => notifications.error(e, { log: true }));
}

View file

@ -2,22 +2,42 @@
//
// SPDX-License-Identifier: MIT
import { DS4Item } from "../documents/item/item";
import { notifications } from "../ui/notifications";
import { getGame } from "../utils/utils";
import { getActiveActorAndToken } from "./helpers";
/**
* Creates a macro from an item drop.
* Create 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} data The item drop 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(data, slot) {
const item = await Item.implementation.fromDropData(data);
if (!item.parent) {
return notifications.warn(getGame().i18n.localize("DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems"));
}
if (!DS4Item.rollableItemTypes.includes(item.type)) {
return notifications.warn(
getGame().i18n.format("DS4.WarningItemIsNotRollable", {
name: item.name,
id: item.id,
type: item.type,
}),
);
}
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 +59,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,15 +10,17 @@ 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: {
system: {
combatValues: [
"hitPoints",
"defense",
@ -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,17 +12,19 @@ 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: {
system: {
"-=equipped": null,
},
};
@ -35,6 +37,7 @@ const migrateCompendium = getCompendiumMigrator(
{ migrateToTemplateEarly: false },
);
/** @type {import("./migration").Migration} */
export const migration = {
migrate,
migrateCompendium,

View file

@ -12,20 +12,21 @@ 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.system?.cooldownDuration.unit;
const updateData: Record<string, unknown> = {
data: {
const updateData = {
system: {
"-=scrollPrice": null,
minimumLevels: { healer: null, wizard: null, sorcerer: null },
cooldownDuration: {
@ -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,23 +21,23 @@ 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.system?.cooldownDuration.unit;
const cooldownDurationValue = itemData.system?.cooldownDuration.value;
const cooldownDuration = migrateCooldownDuration(cooldownDurationValue, cooldownDurationUnit);
const updateData: Record<string, unknown> = {
data: {
const updateData = {
system: {
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,27 +12,26 @@ 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.system?.spellCategory;
const spellGroups = migrateSpellCategory(spellCategory);
// @ts-expect-error bonus is removed with this migration
const bonus: string | undefined = itemData.data?.bonus;
const bonus = itemData.system?.bonus;
const spellModifier = migrateBonus(bonus);
const updateData: Record<string, unknown> = {
data: {
const updateData = {
system: {
spellGroups,
"-=spellCategory": null,
spellModifier,
@ -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,18 +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;
return {
data: {
system: {
allowsDefense: false,
},
};
@ -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,

61
src/migration/008.js Normal file
View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getItemUpdateDataGetter,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
/** @type {import("./migration").Migration["migrate"]} */
async function migrate() {
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
}
/** @type {import("./migrationHelpers").EffectUpdateDataGetter} */
function getEffectUpdateData(effectData) {
const data = foundry.utils.deepClone(effectData);
let hasUpdates = false;
if ("changes" in data) {
for (const change of data.changes) {
const newValue = change.value.replaceAll(/@data\./g, "@system.");
if (newValue !== change.value) {
hasUpdates = true;
change.value = newValue;
}
}
}
/** @type {string | undefined} */
const condition = data.flags?.ds4?.itemEffectConfig?.condition;
if (condition !== undefined) {
const newCondition = condition.replaceAll(/(@actor|@item|@effect)\.data/g, "$1.system");
if (newCondition !== condition) {
hasUpdates = true;
data.flags.ds4.itemEffectConfig.condition = newCondition;
}
}
if (hasUpdates) {
return data;
}
}
const getItemUpdateData = getItemUpdateDataGetter(getEffectUpdateData);
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData, getEffectUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
/** @type {import("./migration").Migration} */
export const migration = {
migrate,
migrateCompendium,
};

View file

@ -12,8 +12,13 @@ import { migration as migration004 } from "./004";
import { migration as migration005 } from "./005";
import { migration as migration006 } from "./006";
import { migration as migration007 } from "./007";
import { migration as migration008 } from "./008";
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 +35,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 +87,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,20 +142,32 @@ 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[] = [
/**
* @type {Migration[]}
*/
const migrations = [
migration001,
migration002,
migration003,
@ -149,9 +175,15 @@ const migrations: Migration[] = [
migration005,
migration006,
migration007,
migration008,
];
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

@ -0,0 +1,250 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4Actor } from "../documents/actor/actor";
import { DS4Item } from "../documents/item/item";
import { logger } from "../utils/logger";
import { getGame } from "../utils/utils";
/** @typedef {(effectData: object) => Record<string, unknown> | undefined} EffectUpdateDataGetter */
/** @typedef {(itemData: object) => Record<string, unknown> | undefined} ItemUpdateDataGetter */
/**
* 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());
if (updateData) {
logger.info(`Migrating Item document ${item.name} (${item.id})`);
await item.update(updateData), { enforceTypes: false };
}
} catch (err) {
logger.error(`Error during migration of Item document ${item.name} (${item.id}), continuing anyways.`, err);
}
}
}
/** @typedef {(actorData: object>) => Record<string, unknown> | undefined} ActorUpdateDataGetter */
/**
* 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());
if (updateData) {
logger.info(`Migrating Actor document ${actor.name} (${actor.id})`);
await actor.update(updateData);
}
} catch (err) {
logger.error(
`Error during migration of Actor document ${actor.name} (${actor.id}), continuing anyways.`,
err,
);
}
}
}
/** @typedef {(scene: Scene) => Record<string, unknown> | undefined} SceneUpdateDataGetter */
/**
* 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);
if (updateData) {
logger.info(`Migrating Scene document ${scene.name} (${scene.id})`);
await scene.update(updateData);
// We need to clear the old syntehtic actors from the cache
scene.tokens.forEach((t) => (t._actor = null));
}
} catch (err) {
logger.error(
`Error during migration of Scene document ${scene.name} (${scene.id}), continuing anyways.`,
err,
);
}
}
}
/** @typedef {(pack: CompendiumCollection) => Promise<void>} CompendiumMigrator */
/**
* 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;
await migrateCompendium(compendium);
}
}
/**
* Get a function to create item update data based on the given function to update embedded documents.
* @param {EffectUpdateDataGetter} [getEffectUpdateData] A function to generate effect update data
* @returns {ItemUpdateDataGetter} A function to get item update data
*/
export function getItemUpdateDataGetter(getEffectUpdateData) {
return (itemData) => {
let hasEffectUpdates = false;
const effects = itemData.effects?.map((effectData) => {
const update = getEffectUpdateData(effectData);
if (update) {
hasEffectUpdates = true;
return foundry.utils.mergeObject(effectData, update, { inplace: false, performDeletions: true });
} else {
return effectData;
}
});
return hasEffectUpdates ? { effects } : undefined;
};
}
/**
* Get a function to create actor update data based on the given function to update embedded documents.
* @param {ItemUpdateDataGetter} [getItemUpdateData] A function to generate item update data
* @param {EffectUpdateDataGetter} [getEffectUpdateData] A function to generate effect update data
* @returns {ActorUpdateDataGetter} A function to get actor update data
*/
export function getActorUpdateDataGetter(getItemUpdateData, getEffectUpdateData) {
return (actorData) => {
let hasItemUpdates = false;
const items = actorData.items?.map((itemData) => {
const update = getItemUpdateData?.(itemData);
if (update) {
hasItemUpdates = true;
return foundry.utils.mergeObject(itemData, update, { inplace: false, performDeletions: true });
} else {
return itemData;
}
});
let hasEffectUpdates = false;
const effects = actorData.effects?.map((effectData) => {
const update = getEffectUpdateData?.(effectData);
if (update) {
hasEffectUpdates = true;
return foundry.utils.mergeObject(effectData, update, { inplace: false, performDeletions: true });
} else {
return effectData;
}
});
const result = {
items: hasItemUpdates ? items : undefined,
effects: hasEffectUpdates ? effects : undefined,
};
return hasItemUpdates | hasEffectUpdates ? result : undefined;
};
}
/**
* 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 (scene) => {
const tokens = scene.tokens.map((token) => {
const t = token.toObject();
if (!t.actorId || t.actorLink) {
t.actorData = {};
} else if (!getGame().actors?.has(t.actorId)) {
t.actorId = null;
t.actorData = {};
} else if (!t.actorLink) {
const actorData = foundry.utils.deepClone(t.actorData);
actorData.type = token.actor?.type;
const update = getActorUpdateData?.(actorData);
if (update !== undefined) {
["items", "effects"].forEach((embeddedName) => {
const embeddedUpdates = update[embeddedName];
if (!embeddedUpdates?.length) return;
const updates = new Map(embeddedUpdates.flatMap((u) => (u && u._id ? [[u._id, u]] : [])));
const originals = t.actorData[embeddedName];
if (!originals) return;
originals.forEach((original) => {
if (!original._id) return;
const update = updates.get(original._id);
if (update) foundry.utils.mergeObject(original, update, { performDeletions: true });
});
delete update[embeddedName];
});
foundry.utils.mergeObject(t.actorData, update);
}
}
return t;
});
return { tokens };
};
}
/**
* @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 } = {},
{ migrateToTemplateEarly = true } = {},
) {
return async (pack) => {
const type = pack.metadata.type;
if (!["Actor", "Item", "Scene"].includes(type)) return;
const wasLocked = pack.locked;
await pack.configure({ locked: false });
if (migrateToTemplateEarly) {
await pack.migrate();
}
const documents = await pack.getDocuments();
for (const doc of documents) {
try {
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));
} else if (doc instanceof DS4Actor && getActorUpdateData) {
const updateData = getActorUpdateData(doc.toObject());
updateData && (await doc.update(updateData));
} else if (doc instanceof Scene && getSceneUpdateData) {
const updateData = getSceneUpdateData(doc);
updateData && (await doc.update(updateData));
}
} catch (err) {
logger.error(
`Error during migration of document ${doc.name} (${doc.id}) in compendium ${pack.collection}, continuing anyways.`,
err,
);
}
}
if (!migrateToTemplateEarly) {
await pack.migrate();
}
await pack.configure({ locked: wasLocked });
};
}

View file

@ -1,186 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4Actor } from "../documents/actor/actor";
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;
export async function migrateItems(getItemUpdateData: ItemUpdateDataGetter): Promise<void> {
for (const item of getGame().items ?? []) {
try {
const updateData = getItemUpdateData(item.toObject());
if (updateData) {
logger.info(`Migrating Item document ${item.name} (${item.id})`);
await item.update(updateData), { enforceTypes: false };
}
} catch (err) {
logger.error(`Error during migration of Item document ${item.name} (${item.id}), continuing anyways.`, err);
}
}
}
type ActorUpdateDataGetter = (
itemData: Partial<foundry.data.ActorData["_source"]>,
) => DeepPartial<foundry.data.ActorData["_source"]> | undefined;
export async function migrateActors(getActorUpdateData: ActorUpdateDataGetter): Promise<void> {
for (const actor of getGame().actors ?? []) {
try {
const updateData = getActorUpdateData(actor.toObject());
if (updateData) {
logger.info(`Migrating Actor document ${actor.name} (${actor.id})`);
await actor.update(updateData);
}
} catch (err) {
logger.error(
`Error during migration of Actor document ${actor.name} (${actor.id}), continuing anyways.`,
err,
);
}
}
}
type SceneUpdateDataGetter = (sceneData: foundry.data.SceneData) => DeepPartial<foundry.data.SceneData["_source"]>;
export async function migrateScenes(getSceneUpdateData: SceneUpdateDataGetter): Promise<void> {
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]>,
);
}
} catch (err) {
logger.error(
`Error during migration of Scene document ${scene.name} (${scene.id}), continuing anyways.`,
err,
);
}
}
}
type CompendiumMigrator = (compendium: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>;
export async function migrateCompendiums(migrateCompendium: CompendiumMigrator): Promise<void> {
for (const compendium of getGame().packs ?? []) {
if (compendium.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(compendium.metadata.type)) continue;
await migrateCompendium(compendium);
}
}
export function getActorUpdateDataGetter(getItemUpdateData: ItemUpdateDataGetter): ActorUpdateDataGetter {
return (
actorData: Partial<foundry.data.ActorData["_source"]>,
): DeepPartial<foundry.data.ActorData["_source"]> | undefined => {
let hasItemUpdates = false;
const items = actorData.items?.map((itemData) => {
const update = getItemUpdateData(itemData);
if (update) {
hasItemUpdates = true;
return { ...itemData, ...update };
} else {
return itemData;
}
});
return hasItemUpdates ? { items } : undefined;
};
}
export function getSceneUpdateDataGetter(getActorUpdateData: ActorUpdateDataGetter): SceneUpdateDataGetter {
return (sceneData: foundry.data.SceneData) => {
const tokens = sceneData.tokens.map((token: TokenDocument) => {
const t = token.toObject();
if (!t.actorId || t.actorLink) {
t.actorData = {};
} else if (!getGame().actors?.has(t.actorId)) {
t.actorId = null;
t.actorData = {};
} else if (!t.actorLink) {
const actorData = foundry.utils.deepClone(t.actorData);
actorData.type = token.actor?.type;
const update = getActorUpdateData(actorData);
if (update !== undefined) {
["items" as const, "effects" as const].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]] : [])));
const originals = t.actorData[embeddedName];
if (!originals) return;
originals.forEach((original) => {
if (!original._id) return;
const update = updates.get(original._id);
if (update) foundry.utils.mergeObject(original, update);
});
delete update[embeddedName];
});
foundry.utils.mergeObject(t.actorData, update);
}
}
return t;
});
return { tokens };
};
}
export function getCompendiumMigrator(
{
getItemUpdateData,
getActorUpdateData,
getSceneUpdateData,
}: {
getItemUpdateData?: ItemUpdateDataGetter;
getActorUpdateData?: ActorUpdateDataGetter;
getSceneUpdateData?: SceneUpdateDataGetter;
} = {},
{ migrateToTemplateEarly = true } = {},
) {
return async (compendium: CompendiumCollection<CompendiumCollection.Metadata>): Promise<void> => {
const type = compendium.metadata.type;
if (!["Actor", "Item", "Scene"].includes(type)) return;
const wasLocked = compendium.locked;
await compendium.configure({ locked: false });
if (migrateToTemplateEarly) {
await compendium.migrate();
}
const documents = await compendium.getDocuments();
for (const doc of documents) {
try {
logger.info(`Migrating document ${doc.name} (${doc.id}) in compendium ${compendium.collection}`);
if (doc instanceof DS4Item && getItemUpdateData) {
const updateData = getItemUpdateData(doc.toObject());
updateData && (await doc.update(updateData));
} else if (doc instanceof DS4Actor && getActorUpdateData) {
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]>,
));
}
} catch (err) {
logger.error(
`Error during migration of document ${doc.name} (${doc.id}) in compendium ${compendium.collection}, continuing anyways.`,
err,
);
}
}
if (!migrateToTemplateEarly) {
await compendium.migrate();
}
await compendium.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

@ -1,5 +1,5 @@
{
"name": "ds4",
"id": "ds4",
"title": "Dungeonslayers 4",
"description": "An implementation of the <a href='https://www.dungeonslayers.net'>Dungeonslayers</a> 4 game system for Foundry Virtual Tabletop.",
"authors": [
@ -34,14 +34,12 @@
"bugs": "https://git.f3l.de/dungeonslayers/ds4/-/issues",
"changelog": "https://git.f3l.de/dungeonslayers/ds4/-/releases/v1.18.2",
"version": "1.18.2",
"minimumCoreVersion": "9.238",
"compatibleCoreVersion": "9",
"esmodules": [
"ds4.js"
],
"styles": [
"css/ds4.css"
],
"compatibility": {
"minimum": "10.290",
"verified": "10"
},
"esmodules": ["ds4.js"],
"styles": ["css/ds4.css"],
"languages": [
{
"lang": "en",

View file

@ -8,40 +8,41 @@ SPDX-License-Identifier: MIT
<div class="ds4-actor-progression">
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.combatValues.hitPoints.value-{{data._id}}"
<h2 class="ds4-actor-progression__label"><label for="system.combatValues.hitPoints.value-{{data._id}}"
title="{{localize 'DS4.CombatValuesHitPointsCurrent'}}">{{localize
"DS4.CombatValuesHitPointsCurrentAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input" type="number" name="data.combatValues.hitPoints.value"
id="data.combatValues.hitPoints.value-{{data._id}}" value="{{data.data.combatValues.hitPoints.value}}"
<input class="ds4-actor-progression__input" type="number" name="system.combatValues.hitPoints.value"
id="system.combatValues.hitPoints.value-{{data._id}}" value="{{data.system.combatValues.hitPoints.value}}"
data-dtype="Number" />
</div>
{{#if (eq data.type "character")}}
{{#if settings.showSlayerPoints}}
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.slayersPoints.value-{{data._id}}"
<h2 class="ds4-actor-progression__label"><label for="system.slayersPoints.value-{{data._id}}"
title="{{localize 'DS4.CharacterSlayerPoints'}}">{{localize "DS4.CharacterSlayerPointsAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input ds4-actor-progression__input--slayer-points" type="number"
max="{{data.data.slayerPoints.max}}" min="0" step="1" name="data.slayerPoints.value"
id="data.slayersPoints.value-{{data._id}}" value="{{data.data.slayerPoints.value}}" data-dtype="Number" />
max="{{data.system.slayerPoints.max}}" min="0" step="1" name="system.slayerPoints.value"
id="system.slayersPoints.value-{{data._id}}" value="{{data.system.slayerPoints.value}}"
data-dtype="Number" />
</div>
{{/if}}
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.progression.level-{{data._id}}"
<h2 class="ds4-actor-progression__label"><label for="system.progression.level-{{data._id}}"
title="{{localize 'DS4.CharacterProgressionLevel'}}">{{localize
"DS4.CharacterProgressionLevelAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input" type="number" min="0" name="data.progression.level"
id="data.progression.level-{{data._id}}" value="{{data.data.progression.level}}" data-dtype="Number" />
<input class="ds4-actor-progression__input" type="number" min="0" name="system.progression.level"
id="system.progression.level-{{data._id}}" value="{{data.system.progression.level}}" data-dtype="Number" />
</div>
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.progression.experiencePoints-{{data._id}}"
<h2 class="ds4-actor-progression__label"><label for="system.progression.experiencePoints-{{data._id}}"
title="{{localize 'DS4.CharacterProgressionExperiencePoints'}}">{{localize
"DS4.CharacterProgressionExperiencePointsAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input" type="number" min="0" name="data.progression.experiencePoints"
id="data.progression.experiencePoints-{{data._id}}" value="{{data.data.progression.experiencePoints}}"
<input class="ds4-actor-progression__input" type="number" min="0" name="system.progression.experiencePoints"
id="system.progression.experiencePoints-{{data._id}}" value="{{data.system.progression.experiencePoints}}"
data-dtype="Number" />
</div>
{{/if}}

View file

@ -5,6 +5,6 @@ SPDX-License-Identifier: MIT
--}}
<div class="ds4-biography">
{{editor content=data.data.profile.biography target="data.profile.biography" button=true owner=owner
editable=editable}}
{{editor data.system.profile.biography target="system.profile.biography" button=true owner=owner
editable=editable engine="prosemirror"}}
</div>

View file

@ -9,55 +9,56 @@ SPDX-License-Identifier: MIT
<div class="ds4-actor-properties">
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.race-{{data._id}}">{{config.i18n.characterBaseInfo.race}}</label>
<input type="text" name="data.baseInfo.race" id="data.baseInfo.race-{{data._id}}"
value="{{data.data.baseInfo.race}}" data-dtype="String" />
for="system.baseInfo.race-{{data._id}}">{{config.i18n.characterBaseInfo.race}}</label>
<input type="text" name="system.baseInfo.race" id="system.baseInfo.race-{{data._id}}"
value="{{data.system.baseInfo.race}}" data-dtype="String" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.culture-{{data._id}}">{{config.i18n.characterBaseInfo.culture}}</label>
<input id="data.baseInfo.culture-{{data._id}}" type="text" name="data.baseInfo.culture"
value="{{data.data.baseInfo.culture}}" data-dtype="String" />
for="system.baseInfo.culture-{{data._id}}">{{config.i18n.characterBaseInfo.culture}}</label>
<input id="system.baseInfo.culture-{{data._id}}" type="text" name="system.baseInfo.culture"
value="{{data.system.baseInfo.culture}}" data-dtype="String" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.progression.progressPoints.used-{{data._id}}">{{config.i18n.characterProgression.progressPoints}}</label>
for="system.progression.progressPoints.used-{{data._id}}">{{config.i18n.characterProgression.progressPoints}}</label>
<div class="ds4-actor-properties__property-multi-input">
<input id="data.progression.progressPoints.used-{{data._id}}" type="number"
name="data.progression.progressPoints.used" value="{{data.data.progression.progressPoints.used}}"
<input id="system.progression.progressPoints.used-{{data._id}}" type="number"
name="system.progression.progressPoints.used" value="{{data.system.progression.progressPoints.used}}"
data-dtype="Number" />
<span class="input-divider"> / </span>
<label class="ds4-hidden" for="data.progression.progressPoints.total-{{data._id}}">Total
<label class="ds4-hidden" for="system.progression.progressPoints.total-{{data._id}}">Total
Progression Points</label>
<input type="number" id="data.progression.progressPoints.total-{{data._id}}"
name="data.progression.progressPoints.total" value="{{data.data.progression.progressPoints.total}}"
<input type="number" id="system.progression.progressPoints.total-{{data._id}}"
name="system.progression.progressPoints.total" value="{{data.system.progression.progressPoints.total}}"
data-dtype="Number" />
</div>
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.progression.talentPoints.used-{{data._id}}">{{config.i18n.characterProgression.talentPoints}}</label>
for="system.progression.talentPoints.used-{{data._id}}">{{config.i18n.characterProgression.talentPoints}}</label>
<div class="ds4-actor-properties__property-multi-input">
<input type="number" name="data.progression.talentPoints.used"
id="data.progression.talentPoints.used-{{data._id}}" value="{{data.data.progression.talentPoints.used}}"
data-dtype="Number" />
<input type="number" name="system.progression.talentPoints.used"
id="system.progression.talentPoints.used-{{data._id}}"
value="{{data.system.progression.talentPoints.used}}" data-dtype="Number" />
<span class="input-divider"> / </span>
<label for="data.progression.talentPoints.total-{{data._id}}" class="ds4-hidden">Total Talent Points</label>
<input type="number" name="data.progression.talentPoints.total"
id="data.progression.talentPoints.total-{{data._id}}"
value="{{data.data.progression.talentPoints.total}}" data-dtype="Number" />
<label for="system.progression.talentPoints.total-{{data._id}}" class="ds4-hidden">Total Talent
Points</label>
<input type="number" name="system.progression.talentPoints.total"
id="system.progression.talentPoints.total-{{data._id}}"
value="{{data.system.progression.talentPoints.total}}" data-dtype="Number" />
</div>
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.class-{{data._id}}">{{config.i18n.characterBaseInfo.class}}</label>
<input type="text" id="data.baseInfo.class-{{data._id}}" name="data.baseInfo.class"
value="{{data.data.baseInfo.class}}" data-dtype="String" />
for="system.baseInfo.class-{{data._id}}">{{config.i18n.characterBaseInfo.class}}</label>
<input type="text" id="system.baseInfo.class-{{data._id}}" name="system.baseInfo.class"
value="{{data.system.baseInfo.class}}" data-dtype="String" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.heroClass-{{data._id}}">{{config.i18n.characterBaseInfo.heroClass}}</label>
<input type="text" id="data.baseInfo.heroClass-{{data._id}}" name="data.baseInfo.heroClass"
value="{{data.data.baseInfo.heroClass}}" data-dtype="String" />
for="system.baseInfo.heroClass-{{data._id}}">{{config.i18n.characterBaseInfo.heroClass}}</label>
<input type="text" id="system.baseInfo.heroClass-{{data._id}}" name="system.baseInfo.heroClass"
value="{{data.system.baseInfo.heroClass}}" data-dtype="String" />
</div>
</div>

View file

@ -7,6 +7,6 @@ SPDX-License-Identifier: MIT
<div class="ds4-checks">
{{#each config.i18n.checks as |check-label check-key|}}
{{> systems/ds4/templates/sheets/actor/components/check.hbs check-key=check-key check-target-number=(lookup
../data.data.checks check-key) check-label=check-label}}
../data.system.checks check-key) check-label=check-label}}
{{/each}}
</div>

View file

@ -25,8 +25,8 @@ SPDX-License-Identifier: MIT
title="{{combat-value-title}} {{localize 'DS4.TooltipBaseValue'}}">{{combat-value-data.base}}</span>
<span>+</span>
<input class="ds4-combat-value__formula-modifier" type="number"
id="data.combatValues.{{combat-value-key}}.mod-{{actor-id}}"
name="data.combatValues.{{combat-value-key}}.mod" value='{{combat-value-data.mod}}' data-dtype="Number"
id="system.combatValues.{{combat-value-key}}.mod-{{actor-id}}"
name="system.combatValues.{{combat-value-key}}.mod" value='{{combat-value-data.mod}}' data-dtype="Number"
title="{{combat-value-title}} {{localize 'DS4.TooltipModifier'}}" />
</div>
</div>

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: MIT
<div class="ds4-combat-values">
{{#each config.i18n.combatValues as |combat-value-title combat-value-key|}}
{{> systems/ds4/templates/sheets/actor/components/combat-value.hbs combat-value-key=combat-value-key
combat-value-data=(lookup ../data.data.combatValues combat-value-key) combat-value-label=(lookup
combat-value-data=(lookup ../data.system.combatValues combat-value-key) combat-value-label=(lookup
../config.i18n.combatValuesSheet combat-value-key) combat-value-title=combat-value-title
actor-id=../data._id}}
{{/each}}

View file

@ -15,17 +15,17 @@ SPDX-License-Identifier: MIT
--}}
<div class="ds4-core-value ds4-core-value--{{core-value-variant}}">
<label for="data.{{core-value-variant}}s.{{core-value-key}}.base-{{actor-id}}"
<label for="system.{{core-value-variant}}s.{{core-value-key}}.base-{{actor-id}}"
class="ds4-core-value__label">{{core-value-label}}</label>
<div class="ds4-core-value__value">
<input class="ds4-core-value__value-input" type="number"
name="data.{{core-value-variant}}s.{{core-value-key}}.base"
id="data.{{core-value-variant}}s.{{core-value-key}}.base-{{actor-id}}" value='{{core-value-data.base}}'
name="system.{{core-value-variant}}s.{{core-value-key}}.base"
id="system.{{core-value-variant}}s.{{core-value-key}}.base-{{actor-id}}" value='{{core-value-data.base}}'
data-dtype="Number" title="{{core-value-label}} {{localize 'DS4.TooltipBaseValue'}}" />
<span>+</span>
<input class="ds4-core-value__value-input" type="number"
name="data.{{core-value-variant}}s.{{core-value-key}}.mod"
id="data.{{core-value-variant}}s.{{core-value-key}}.mod-{{actor-id}}" value='{{core-value-data.mod}}'
name="system.{{core-value-variant}}s.{{core-value-key}}.mod"
id="system.{{core-value-variant}}s.{{core-value-key}}.mod-{{actor-id}}" value='{{core-value-data.mod}}'
data-dtype="Number" title="{{core-value-label}} {{localize 'DS4.TooltipModifier'}}" />
<span class="ds4-core-value__value-arrow">➞</span>
<span class="ds4-core-value__value-total"

View file

@ -8,12 +8,12 @@ SPDX-License-Identifier: MIT
<div class="ds4-core-values">
{{#each config.i18n.attributes as |attribute-label attribute-key|}}
{{> systems/ds4/templates/sheets/actor/components/core-value.hbs core-value-label=attribute-label
core-value-key=attribute-key core-value-data=(lookup ../data.data.attributes
core-value-key=attribute-key core-value-data=(lookup ../data.system.attributes
attribute-key) core-value-variant="attribute" actor-id=../data._id}}
{{/each}}
{{#each config.i18n.traits as |trait-label trait-key|}}
{{> systems/ds4/templates/sheets/actor/components/core-value.hbs core-value-label=trait-label
core-value-key=trait-key
core-value-data=(lookup ../data.data.traits trait-key) core-value-variant="trait" actor-id=../data._id}}
core-value-data=(lookup ../data.system.traits trait-key) core-value-variant="trait" actor-id=../data._id}}
{{/each}}
</div>

View file

@ -9,10 +9,10 @@ SPDX-License-Identifier: MIT
<div class="ds4-actor-properties">
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.creatureType-{{data._id}}">{{config.i18n.creatureBaseInfo.creatureType}}</label>
<select class="ds4-actor-properties__property-select" id="data.baseInfo.creatureType-{{data._id}}"
name="data.baseInfo.creatureType" data-dtype="String">
{{#select data.data.baseInfo.creatureType}}
for="system.baseInfo.creatureType-{{data._id}}">{{config.i18n.creatureBaseInfo.creatureType}}</label>
<select class="ds4-actor-properties__property-select" id="system.baseInfo.creatureType-{{data._id}}"
name="system.baseInfo.creatureType" data-dtype="String">
{{#select data.system.baseInfo.creatureType}}
{{#each config.i18n.creatureTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
@ -21,22 +21,22 @@ SPDX-License-Identifier: MIT
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.loot-{{data._id}}">{{config.i18n.creatureBaseInfo.loot}}</label>
<input type="text" id="data.baseInfo.loot-{{data._id}}" name="data.baseInfo.loot"
value="{{data.data.baseInfo.loot}}" data-dtype="String" />
for="system.baseInfo.loot-{{data._id}}">{{config.i18n.creatureBaseInfo.loot}}</label>
<input type="text" id="system.baseInfo.loot-{{data._id}}" name="system.baseInfo.loot"
value="{{data.system.baseInfo.loot}}" data-dtype="String" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.foeFactor-{{data._id}}">{{config.i18n.creatureBaseInfo.foeFactor}}</label>
<input type="text" id="data.baseInfo.foeFactor-{{data._id}}" name="data.baseInfo.foeFactor"
value="{{data.data.baseInfo.foeFactor}}" data-dtype="Number" />
for="system.baseInfo.foeFactor-{{data._id}}">{{config.i18n.creatureBaseInfo.foeFactor}}</label>
<input type="text" id="system.baseInfo.foeFactor-{{data._id}}" name="system.baseInfo.foeFactor"
value="{{data.system.baseInfo.foeFactor}}" data-dtype="Number" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.sizeCategory-{{data._id}}">{{config.i18n.creatureBaseInfo.sizeCategory}}</label>
<select class="ds4-actor-properties__property-select" id="data.baseInfo.sizeCategory-{{data._id}}"
name="data.baseInfo.sizeCategory" data-dtype="String">
{{#select data.data.baseInfo.sizeCategory}}
for="system.baseInfo.sizeCategory-{{data._id}}">{{config.i18n.creatureBaseInfo.sizeCategory}}</label>
<select class="ds4-actor-properties__property-select" id="system.baseInfo.sizeCategory-{{data._id}}"
name="system.baseInfo.sizeCategory" data-dtype="String">
{{#select data.system.baseInfo.sizeCategory}}
{{#each config.i18n.creatureSizeCategories as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
@ -45,8 +45,8 @@ SPDX-License-Identifier: MIT
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.experiencePoints-{{data._id}}">{{config.i18n.creatureBaseInfo.experiencePoints}}</label>
<input type="text" id="data.baseInfo.experiencePoints-{{data._id}}" name="data.baseInfo.experiencePoints"
value="{{data.data.baseInfo.experiencePoints}}" data-dtype="Number" />
for="system.baseInfo.experiencePoints-{{data._id}}">{{config.i18n.creatureBaseInfo.experiencePoints}}</label>
<input type="text" id="system.baseInfo.experiencePoints-{{data._id}}" name="system.baseInfo.experiencePoints"
value="{{data.system.baseInfo.experiencePoints}}" data-dtype="Number" />
</div>
</div>

View file

@ -8,9 +8,9 @@ SPDX-License-Identifier: MIT
<h4 class="ds4-currency-title">{{localize 'DS4.CharacterCurrency'}}</h4>
<div class="ds4-currency">
{{#each data.data.currency as |value key|}}
<label for="data.currency.{{key}}" class="flex05">{{lookup ../config.i18n.characterCurrency key}}</label>
{{#each data.system.currency as |value key|}}
<label for="system.currency.{{key}}" class="flex05">{{lookup ../config.i18n.characterCurrency key}}</label>
<input class="ds4-currency__value ds4-currency__value--{{key}}" type="number" min="0" step="1"
name="data.currency.{{key}}" id="data.currency.{{key}}" value="{{value}}" data-dtype="Number" />
name="system.currency.{{key}}" id="system.currency.{{key}}" value="{{value}}" data-dtype="Number" />
{{/each}}
</div>

View file

@ -5,6 +5,6 @@ SPDX-License-Identifier: MIT
--}}
<div class="ds4-description">
{{editor content=data.data.baseInfo.description target="data.baseInfo.description" button=true owner=owner
editable=editable}}
{{editor data.system.baseInfo.description target="system.baseInfo.description" button=true owner=owner
editable=editable engine="prosemirror"}}
</div>

View file

@ -19,24 +19,24 @@ SPDX-License-Identifier: MIT
{{!-- equipped --}}
{{#if isEquipable}}
<input class="ds4-embedded-document-list__editable ds4-embedded-document-list__editable--checkbox change-item" type="checkbox" {{checked
itemData.data.equipped}} data-dtype="Boolean" data-property="data.equipped"
itemData.system.equipped}} data-dtype="Boolean" data-property="system.equipped"
title="{{localize 'DS4.ItemEquipped'}}">
{{/if}}
{{!-- image --}}
{{> systems/ds4/templates/sheets/shared/components/rollable-image.hbs rollable=(and itemData.data.rollable @root/editable)
{{> systems/ds4/templates/sheets/shared/components/rollable-image.hbs rollable=(and itemData.system.rollable @root/editable)
src=itemData.img alt=(localize "DS4.DocumentImageAltText" name=itemData.name) title=itemData.name
rollableTitle=(localize "DS4.RollableImageRollableTitle" name=itemData.name) rollableClass="rollable-item"}}
{{!-- amount --}}
{{#if hasQuantity}}
<input class="ds4-embedded-document-list__editable change-item" type="number" min="0" step="1" value="{{itemData.data.quantity}}"
data-dtype="Number" data-property="data.quantity" title="{{localize 'DS4.Quantity'}}" />
<input class="ds4-embedded-document-list__editable change-item" type="number" min="0" step="1" value="{{itemData.system.quantity}}"
data-dtype="Number" data-property="system.quantity" title="{{localize 'DS4.Quantity'}}" />
{{/if}}
{{!-- name --}}
<input class="ds4-embedded-document-list__editable change-item" type="text" value="{{itemData.name}}" data-dtype="String"
data-property="name" title="{{htmlToPlainText itemData.data.description}}" />
data-property="name" title="{{htmlToPlainText itemData.system.description}}" />
{{!-- item type specifics --}}
{{#if @partial-block }}
@ -45,8 +45,8 @@ SPDX-License-Identifier: MIT
{{!-- description --}}
{{#unless hideDescription}}
<div class="ds4-embedded-document-list__description" title="{{htmlToPlainText itemData.data.description}}">
{{{itemData.data.description}}}</div>
<div class="ds4-embedded-document-list__description" title="{{htmlToPlainText itemData.system.description}}">
{{{itemData.system.description}}}</div>
{{/unless}}
{{!-- control button group --}}

View file

@ -18,7 +18,7 @@ SPDX-License-Identifier: MIT
<li class="ds4-embedded-document-list__row ds4-embedded-document-list__row--header" data-type={{type}}>
{{!-- equipped --}}
{{#if isEquipable}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.equipped"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.equipped"
title="{{localize 'DS4.SortByItemEquipped'}}">
{{localize 'DS4.ItemEquippedAbbr'}}</div>
{{/if}}
@ -28,7 +28,7 @@ SPDX-License-Identifier: MIT
{{!-- amount --}}
{{#if hasQuantity}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.quantity"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.quantity"
title="{{localize 'DS4.SortByQuantity'}}">#</div>
{{/if}}
@ -44,7 +44,7 @@ SPDX-License-Identifier: MIT
{{!-- description --}}
{{#unless hideDescription}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.description"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.description"
title="{{localize 'DS4.SortByDescription'}}">{{localize
'DS4.Description'}}</div>
{{/unless}}

View file

@ -13,19 +13,19 @@ SPDX-License-Identifier: MIT
{{#> systems/ds4/templates/sheets/actor/components/item-list-header.hbs isEquipable=true hasQuantity=true
type='weapon'}}
{{!-- attack type --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.attackType"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.attackType"
title="{{localize 'DS4.SortByAttackType'}}">
{{localize
'DS4.AttackTypeAbbr'}}</div>
{{!-- weapon bonus --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.weaponBonus"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.weaponBonus"
title="{{localize 'DS4.SortByWeaponBonus'}}">
{{localize 'DS4.WeaponBonusAbbr'}}
</div>
{{!-- opponent defense --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.opponentDefense"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.opponentDefense"
title="{{localize 'DS4.SortByOpponentDefense'}}">
{{localize 'DS4.OpponentDefenseAbbr'}}
</div>
@ -36,24 +36,24 @@ SPDX-License-Identifier: MIT
hasQuantity=true}}
{{!-- attack type --}}
<img class="ds4-embedded-document-list__image"
src="{{lookup @root/config.icons.attackTypes itemData.data.attackType}}"
title="{{lookup @root/config.i18n.attackTypes itemData.data.attackType}}" />
src="{{lookup @root/config.icons.attackTypes itemData.system.attackType}}"
title="{{lookup @root/config.i18n.attackTypes itemData.system.attackType}}" />
{{!-- weapon bonus --}}
<div>{{ itemData.data.weaponBonus}}</div>
<div>{{ itemData.system.weaponBonus}}</div>
{{!-- opponent defense --}}
<div>
{{#if itemData.data.opponentDefenseForAttackType.melee includeZero=true}}
{{#if itemData.data.opponentDefenseForAttackType.ranged includeZero=true}}
{{#if itemData.system.opponentDefenseForAttackType.melee includeZero=true}}
{{#if itemData.system.opponentDefenseForAttackType.ranged includeZero=true}}
<span
title="{{localize 'DS4.OpponentDefenseMelee'}}">{{itemData.data.opponentDefenseForAttackType.melee}}</span>/<span
title="{{localize 'DS4.OpponentDefenseRanged'}}">{{itemData.data.opponentDefenseForAttackType.ranged}}</span>
title="{{localize 'DS4.OpponentDefenseMelee'}}">{{itemData.system.opponentDefenseForAttackType.melee}}</span>/<span
title="{{localize 'DS4.OpponentDefenseRanged'}}">{{itemData.system.opponentDefenseForAttackType.ranged}}</span>
{{else}}
{{itemData.data.opponentDefenseForAttackType.melee}}
{{itemData.system.opponentDefenseForAttackType.melee}}
{{/if}}
{{else}}
{{itemData.data.opponentDefenseForAttackType.ranged}}
{{itemData.system.opponentDefenseForAttackType.ranged}}
{{/if}}
</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
@ -70,15 +70,15 @@ documentType='item' type='weapon'}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-header.hbs isEquipable=true hasQuantity=true
type="armor"}}
{{!-- armor material type --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.armorMaterialType"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.armorMaterialType"
title="{{localize 'DS4.SortByArmorMaterialType'}}">{{localize 'DS4.ArmorMaterialTypeAbbr'}}</div>
{{!-- armor type --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.armorType"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.armorType"
title="{{localize 'DS4.SortByArmorType'}}">{{localize 'DS4.ArmorTypeAbbr'}}</div>
{{!-- armor value --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.armorValue"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.armorValue"
title="{{localize 'DS4.SortByArmorValue'}}">
{{localize 'DS4.ArmorValueAbbr'}}
</div>
@ -88,17 +88,17 @@ documentType='item' type='weapon'}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-entry.hbs itemData=itemData isEquipable=true
hasQuantity=true}}
{{!-- armor material type --}}
<div title="{{lookup @root/config.i18n.armorMaterialTypes itemData.data.armorMaterialType}}">
{{lookup @root/config.i18n.armorMaterialTypesAbbr itemData.data.armorMaterialType}}
<div title="{{lookup @root/config.i18n.armorMaterialTypes itemData.system.armorMaterialType}}">
{{lookup @root/config.i18n.armorMaterialTypesAbbr itemData.system.armorMaterialType}}
</div>
{{!-- armor type --}}
<div title="{{lookup @root/config.i18n.armorTypes itemData.data.armorType}}">
{{lookup @root/config.i18n.armorTypesAbbr itemData.data.armorType}}
<div title="{{lookup @root/config.i18n.armorTypes itemData.system.armorType}}">
{{lookup @root/config.i18n.armorTypesAbbr itemData.system.armorType}}
</div>
{{!-- armor value --}}
<div>{{ itemData.data.armorValue}}</div>
<div>{{ itemData.system.armorValue}}</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
{{/each}}
</ol>
@ -113,7 +113,7 @@ documentType='item' type='armor'}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-header.hbs isEquipable=true hasQuantity=true
type='shield'}}
{{!-- armor value --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.armorValue"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.armorValue"
title="{{localize 'DS4.SortByArmorValue'}}">
{{localize 'DS4.ArmorValueAbbr'}}
</div>
@ -122,7 +122,7 @@ documentType='item' type='armor'}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-entry.hbs itemData=itemData isEquipable=true
hasQuantity=true}}
{{!-- armor value --}}
<div>{{itemData.data.armorValue}}</div>
<div>{{itemData.system.armorValue}}</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
{{/each}}
</ol>
@ -137,7 +137,7 @@ documentType='item' type='shield'}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-header.hbs isEquipable=true hasQuantity=true
type='equipment'}}
{{!-- storage location --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.storageLocation"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.storageLocation"
title="{{localize 'DS4.SortByStorageLocation'}}">{{localize 'DS4.StorageLocation'}}</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-header.hbs}}
{{#each itemsByType.equipment as |itemData id|}}
@ -145,7 +145,7 @@ documentType='item' type='shield'}}
hasQuantity=true}}
{{!-- storage location --}}
<input class="ds4-embedded-document-list__editable change-item" type="text"
value="{{itemData.data.storageLocation}}" data-dtype="String" data-property="data.storageLocation"
value="{{itemData.system.storageLocation}}" data-dtype="String" data-property="system.storageLocation"
title="{{localize 'DS4.StorageLocation'}}">
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
{{/each}}
@ -160,14 +160,14 @@ documentType='item' type='equipment'}}
<ol class="ds4-embedded-document-list ds4-embedded-document-list--loot item-list">
{{#> systems/ds4/templates/sheets/actor/components/item-list-header.hbs hasQuantity=true type='loot'}}
{{!-- storage location --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.storageLocation"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.storageLocation"
title="{{localize 'DS4.SortByStorageLocation'}}">{{localize 'DS4.StorageLocation'}}</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-header.hbs}}
{{#each itemsByType.loot as |itemData id|}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-entry.hbs itemData=itemData hasQuantity=true}}
{{!-- storage location --}}
<input class="ds4-embedded-document-list__editable change-item" type="text"
value="{{itemData.data.storageLocation}}" data-dtype="String" data-property="data.storageLocation"
value="{{itemData.system.storageLocation}}" data-dtype="String" data-property="system.storageLocation"
title="{{localize 'DS4.StorageLocation'}}">
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
{{/each}}

View file

@ -6,24 +6,24 @@ SPDX-License-Identifier: MIT
--}}
<div class="ds4-profile">
{{#each data.data.profile as |profile-data-value profile-data-key|}}
{{#each data.system.profile as |profile-data-value profile-data-key|}}
{{#if (and (ne profile-data-key 'biography') (ne profile-data-key 'specialCharacteristics'))}}
<div class="ds4-profile__entry">
<label class="ds4-profile__entry-label" for="data.profile.{{profile-data-key}}">
<label class="ds4-profile__entry-label" for="system.profile.{{profile-data-key}}">
{{lookup ../config.i18n.characterProfile profile-data-key}}
</label>
<input class="ds4-profile__entry-input" type="text" name="data.profile.{{profile-data-key}}"
<input class="ds4-profile__entry-input" type="text" name="system.profile.{{profile-data-key}}"
value="{{profile-data-value}}"
data-dtype="{{lookup ../config.i18n.characterProfileDTypes profile-data-key}}" />
</div>
{{/if}}
{{/each}}
<div class="ds4-profile__entry">
<label class="ds4-profile__entry-label" for="data.profile.specialCharacteristics">
<label class="ds4-profile__entry-label" for="system.profile.specialCharacteristics">
{{lookup config.i18n.characterProfile 'specialCharacteristics'}}
</label>
<textarea class="ds4-profile__entry-input ds4-profile__entry-input--multiline"
name="data.profile.specialCharacteristics" data-dtype="String"
rows="4">{{data.data.profile.specialCharacteristics}}</textarea>
name="system.profile.specialCharacteristics" data-dtype="String"
rows="4">{{data.system.profile.specialCharacteristics}}</textarea>
</div>
</div>

View file

@ -12,13 +12,13 @@ SPDX-License-Identifier: MIT
<ol class="ds4-embedded-document-list ds4-embedded-document-list--talent item-list">
{{#> systems/ds4/templates/sheets/actor/components/item-list-header.hbs type='talent'}}
{{!-- rank --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.rank.total"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.rank.total"
title="{{localize 'DS4.SortByTalentRank'}}">{{localize 'DS4.TalentRank'}}</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-header.hbs}}
{{#each itemsByType.talent as |itemData id|}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-entry.hbs itemData=itemData}}
{{!-- rank --}}
<div>{{toRomanNumerals itemData.data.rank.total}}</div>
<div>{{toRomanNumerals itemData.system.rank.total}}</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
{{/each}}
</ol>

View file

@ -50,12 +50,12 @@ titleKey=titleKey}}
{{#> systems/ds4/templates/sheets/actor/components/item-list-header.hbs isEquipable=true hideDescription=true
type='spell'}}
{{!-- spell type --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.spellType"
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.spellType"
title="{{localize 'DS4.SortBySpellType'}}">{{localize 'DS4.SpellTypeAbbr'}}</div>
{{!-- spell modifier --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.spellModifier.complex"
data-data-path2="data.spellModifier.numerical" title="{{localize 'DS4.SortBySpellModifier'}}">{{localize
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="system.spellModifier.complex"
data-data-path2="system.spellModifier.numerical" title="{{localize 'DS4.SortBySpellModifier'}}">{{localize
'DS4.SpellModifierAbbr'}}</div>
{{!-- max. distance --}}
@ -73,23 +73,23 @@ titleKey=titleKey}}
hideDescription=true}}
{{!-- spell type --}}
<img class="ds4-embedded-document-list__image"
src="{{lookup @root/config.icons.spellTypes itemData.data.spellType}}"
title="{{lookup @root/config.i18n.spellTypes itemData.data.spellType}}" />
src="{{lookup @root/config.icons.spellTypes itemData.system.spellType}}"
title="{{lookup @root/config.i18n.spellTypes itemData.system.spellType}}" />
{{!-- spell modifier --}}
<div title="{{localize 'DS4.SpellModifier'}}">{{#if (eq itemData.data.spellModifier.complex
'')}}{{itemData.data.spellModifier.numerical}}{{else}}{{itemData.data.spellModifier.complex}}{{/if}}</div>
<div title="{{localize 'DS4.SpellModifier'}}">{{#if (eq itemData.system.spellModifier.complex
'')}}{{itemData.system.spellModifier.numerical}}{{else}}{{itemData.system.spellModifier.complex}}{{/if}}</div>
{{!-- max. distance --}}
{{> distanceUnit titleKey='DS4.SpellDistance' unitDatum=itemData.data.maxDistance
{{> distanceUnit titleKey='DS4.SpellDistance' unitDatum=itemData.system.maxDistance
config=@root/config}}
{{!-- duration --}}
{{> temporalUnit titleKey='DS4.SpellDuration' unitDatum=itemData.data.duration config=@root/config}}
{{> temporalUnit titleKey='DS4.SpellDuration' unitDatum=itemData.system.duration config=@root/config}}
{{!-- cooldown duration --}}
<div title="{{localize 'DS4.CooldownDuration'}}">{{lookup @root/config.i18n.cooldownDurations
itemData.data.cooldownDuration}}</div>
itemData.system.cooldownDuration}}</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
{{/each}}

Some files were not shown because too many files have changed in this diff Show more