Merge branch 'v10' into 'main'
V10 compatibility See merge request dungeonslayers/ds4!224
This commit is contained in:
commit
22e0fa6781
112 changed files with 42988 additions and 26573 deletions
.eslintignore.gitignore.prettierignorejsconfig.jsonjsconfig.json.licensepackage.jsonsystem.json
packs
creatures.jsonitems.jsonlanguages-and-scripts.jsonracial-abilities.jsonspecial-creature-abilities.jsonspells.jsontalents.json
spec
src
apps
dice
documents
active-effect.jsactive-effect.tstoken-document.js
global.d.tsactor
chat-message.jschat-message.tscommon
item
alphabet
armor
equipment
item-data-source-base.tsitem.jsitem.tslanguage
loot
proxy.jsracial-ability
shield
special-creature-ability
spell
talent
weapon
handlebars
hooks
macros
migration
settings.jsui
utils
templates/sheets/actor
|
@ -6,3 +6,5 @@
|
|||
/.pnp.cjs
|
||||
/.pnp.loader.mjs
|
||||
/.yarn/
|
||||
client
|
||||
common
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -32,3 +32,7 @@ junit.xml
|
|||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
|
||||
# foundry
|
||||
/client
|
||||
/common
|
||||
|
|
|
@ -8,3 +8,5 @@
|
|||
/.pnp.loader.mjs
|
||||
/.yarn/
|
||||
/.vscode/
|
||||
client
|
||||
common
|
||||
|
|
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "es2022",
|
||||
"target": "ES2022"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["src", "client", "common"]
|
||||
}
|
3
jsconfig.json.license
Normal file
3
jsconfig.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||
|
||||
SPDX-License-Identifier: MIT
|
12
package.json
12
package.json
|
@ -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",
|
||||
|
|
42127
packs/creatures.json
42127
packs/creatures.json
File diff suppressed because it is too large
Load diff
14224
packs/items.json
14224
packs/items.json
File diff suppressed because it is too large
Load diff
|
@ -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
2640
packs/spells.json
2640
packs/spells.json
File diff suppressed because one or more lines are too long
3498
packs/talents.json
3498
packs/talents.json
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,4 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@league-of-foundry-developers/foundry-vtt-types"]
|
||||
},
|
||||
"include": ["../src", "./"]
|
||||
}
|
||||
|
|
|
@ -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) {
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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.
|
||||
*/
|
|
@ -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")})`;
|
||||
|
|
@ -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[],
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
196
src/documents/active-effect.js
Normal file
196
src/documents/active-effect.js
Normal 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
|
||||
*/
|
|
@ -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 };
|
491
src/documents/actor/actor.js
Normal file
491
src/documents/actor/actor.js
Normal 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
|
||||
*/
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
23
src/documents/actor/character/character.js
Normal file
23
src/documents/actor/character/character.js
Normal 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"];
|
||||
}
|
||||
}
|
|
@ -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" } };
|
||||
}
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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);
|
31
src/documents/chat-message.js
Normal file
31
src/documents/chat-message.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
16
src/documents/common/roll-options.js
Normal file
16
src/documents/common/roll-options.js
Normal 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 {}
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
57
src/documents/item/item.js
Normal file
57
src/documents/item/item.js
Normal 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 }));
|
||||
}
|
||||
}
|
|
@ -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 }));
|
||||
}
|
||||
}
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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" } };
|
||||
}
|
||||
|
|
|
@ -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" } };
|
||||
}
|
18
src/documents/item/talent/talent.js
Normal file
18
src/documents/item/talent/talent.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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" } };
|
||||
}
|
|
@ -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" } };
|
||||
}
|
|
@ -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
21
src/global.d.ts
vendored
|
@ -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 {};
|
|
@ -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
35
src/hooks/hotbar-drop.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import { migration } from "../migration/migration";
|
||||
|
||||
export function registerForReadyHook(): void {
|
||||
export function registerForReadyHook() {
|
||||
Hooks.once("ready", () => {
|
||||
migration.migrate();
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
);
|
||||
}
|
|
@ -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
62
src/macros/roll-check.js
Normal 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 }));
|
||||
}
|
|
@ -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 }));
|
||||
}
|
|
@ -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"));
|
|
@ -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,
|
|
@ -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,
|
|
@ -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,
|
|
@ -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,
|
|
@ -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,
|
|
@ -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,
|
|
@ -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
61
src/migration/008.js
Normal 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,
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
250
src/migration/migrationHelpers.js
Normal file
250
src/migration/migrationHelpers.js
Normal 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 });
|
||||
};
|
||||
}
|
|
@ -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 });
|
||||
};
|
||||
}
|
|
@ -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
61
src/ui/notifications.js
Normal 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,
|
||||
});
|
|
@ -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
61
src/utils/utils.js
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
16
system.json
16
system.json
|
@ -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",
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 --}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue