From c26574d2d15b5ff0dba5062ee0f74cec041cfb1b Mon Sep 17 00:00:00 2001 From: Alexander Minges Date: Fri, 2 May 2025 11:21:09 +0200 Subject: [PATCH 01/56] Fix expand rolls in chat in v13 --- templates/dice/roll.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/dice/roll.hbs b/templates/dice/roll.hbs index 8afcbd10..9b9fc28f 100644 --- a/templates/dice/roll.hbs +++ b/templates/dice/roll.hbs @@ -4,7 +4,7 @@ SPDX-FileCopyrightText: 2021 Johannes Loher SPDX-License-Identifier: MIT --}} -
+
{{#if flavor}}
{{flavor}}
{{/if}} From 2a797ed8ed0b1dd39a53612662a82c5050285b60 Mon Sep 17 00:00:00 2001 From: Alexander Minges Date: Fri, 2 May 2025 11:53:38 +0200 Subject: [PATCH 02/56] Update class imports to use fully qualified Foundry paths --- src/apps/active-effect-config.js | 2 +- src/apps/actor/base-sheet.js | 2 +- src/apps/actor/character-sheet.js | 9 ++++++--- src/apps/actor/creature-sheet.js | 9 ++++++--- src/apps/item-sheet.js | 11 +++++++---- src/dice/check-factory.js | 2 +- src/dice/roll.js | 2 +- src/handlebars/handlebars-partials.js | 2 +- src/hooks/init.js | 23 ++++++++++++++++------- src/utils/utils.js | 2 +- 10 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/apps/active-effect-config.js b/src/apps/active-effect-config.js index e0acae02..fc02074d 100644 --- a/src/apps/active-effect-config.js +++ b/src/apps/active-effect-config.js @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -export class DS4ActiveEffectConfig extends ActiveEffectConfig { +export class DS4ActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig { /** @override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { diff --git a/src/apps/actor/base-sheet.js b/src/apps/actor/base-sheet.js index 75a74b11..39d22cc9 100644 --- a/src/apps/actor/base-sheet.js +++ b/src/apps/actor/base-sheet.js @@ -16,7 +16,7 @@ import { disableOverriddenFields } from "../sheet-helpers"; /** * The base sheet class for all {@link DS4Actor}s. */ -export class DS4ActorSheet extends ActorSheet { +export class DS4ActorSheet extends foundry.appv1.sheets.ActorSheet { /** @override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { diff --git a/src/apps/actor/character-sheet.js b/src/apps/actor/character-sheet.js index 368fe176..2d33b262 100644 --- a/src/apps/actor/character-sheet.js +++ b/src/apps/actor/character-sheet.js @@ -17,9 +17,12 @@ export class DS4CharacterActorSheet extends DS4ActorSheet { /** @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, - }); + context.data.system.profile.biography = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + context.data.system.profile.biography, + { + async: true, + }, + ); return context; } } diff --git a/src/apps/actor/creature-sheet.js b/src/apps/actor/creature-sheet.js index fe89dec0..21acb003 100644 --- a/src/apps/actor/creature-sheet.js +++ b/src/apps/actor/creature-sheet.js @@ -17,9 +17,12 @@ export class DS4CreatureActorSheet extends DS4ActorSheet { /** @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, - }); + context.data.system.baseInfo.description = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + context.data.system.baseInfo.description, + { + async: true, + }, + ); return context; } } diff --git a/src/apps/item-sheet.js b/src/apps/item-sheet.js index 1fc6f0aa..e9b84533 100644 --- a/src/apps/item-sheet.js +++ b/src/apps/item-sheet.js @@ -12,7 +12,7 @@ import { disableOverriddenFields } from "./sheet-helpers"; /** * The Sheet class for DS4 Items */ -export class DS4ItemSheet extends ItemSheet { +export class DS4ItemSheet extends foundry.appv1.sheets.ItemSheet { /** @override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { @@ -33,9 +33,12 @@ export class DS4ItemSheet extends ItemSheet { /** @override */ async getData(options = {}) { const superContext = await super.getData(options); - superContext.data.system.description = await TextEditor.enrichHTML(superContext.data.system.description, { - async: true, - }); + superContext.data.system.description = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + superContext.data.system.description, + { + async: true, + }, + ); const context = { ...superContext, config: DS4, diff --git a/src/dice/check-factory.js b/src/dice/check-factory.js index 6063ca98..7070e7e2 100644 --- a/src/dice/check-factory.js +++ b/src/dice/check-factory.js @@ -166,7 +166,7 @@ async function askForInteractiveRollData(checkTargetNumber, options = {}, { temp }), id, }; - const renderedHtml = await renderTemplate(usedTemplate, templateData); + const renderedHtml = await foundry.applications.handlebars.renderTemplate(usedTemplate, templateData); const dialogPromise = new Promise((resolve) => { new DialogWithListeners( diff --git a/src/dice/roll.js b/src/dice/roll.js index bc632a07..833f6b19 100644 --- a/src/dice/roll.js +++ b/src/dice/roll.js @@ -29,6 +29,6 @@ export class DS4Roll extends Roll { isCoup: isPrivate ? null : isCoup, isFumble: isPrivate ? null : isFumble, }; - return renderTemplate(template, chatData); + return foundry.applications.handlebars.renderTemplate(template, chatData); } } diff --git a/src/handlebars/handlebars-partials.js b/src/handlebars/handlebars-partials.js index 747c13c4..70cd1e35 100644 --- a/src/handlebars/handlebars-partials.js +++ b/src/handlebars/handlebars-partials.js @@ -56,5 +56,5 @@ export async function registerHandlebarsPartials() { "systems/ds4/templates/sheets/shared/components/control-button-group.hbs", "systems/ds4/templates/sheets/shared/components/rollable-image.hbs", ]; - await loadTemplates(templatePaths); + await foundry.applications.handlebars.loadTemplates(templatePaths); } diff --git a/src/hooks/init.js b/src/hooks/init.js index 199b32bd..a57f4a6e 100644 --- a/src/hooks/init.js +++ b/src/hooks/init.js @@ -65,16 +65,25 @@ async function init() { registerSystemSettings(); - DocumentSheetConfig.unregisterSheet(Actor, "core", ActorSheet); - DocumentSheetConfig.registerSheet(Actor, "ds4", DS4CharacterActorSheet, { + foundry.applications.apps.DocumentSheetConfig.unregisterSheet(Actor, "core", foundry.appv1.sheets.ActorSheet); + foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, "ds4", DS4CharacterActorSheet, { types: ["character"], makeDefault: true, }); - DocumentSheetConfig.registerSheet(Actor, "ds4", DS4CreatureActorSheet, { types: ["creature"], makeDefault: true }); - DocumentSheetConfig.unregisterSheet(Item, "core", ItemSheet); - DocumentSheetConfig.registerSheet(Item, "ds4", DS4ItemSheet, { makeDefault: true }); - DocumentSheetConfig.unregisterSheet(ActiveEffect, "core", ActiveEffectConfig); - DocumentSheetConfig.registerSheet(ActiveEffect, "ds4", DS4ActiveEffectConfig, { makeDefault: true }); + foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, "ds4", DS4CreatureActorSheet, { + types: ["creature"], + makeDefault: true, + }); + foundry.applications.apps.DocumentSheetConfig.unregisterSheet(Item, "core", foundry.appv1.sheets.ItemSheet); + foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, "ds4", DS4ItemSheet, { makeDefault: true }); + foundry.applications.apps.DocumentSheetConfig.unregisterSheet( + ActiveEffect, + "core", + foundry.applications.sheets.ActiveEffectConfig, + ); + foundry.applications.apps.DocumentSheetConfig.registerSheet(ActiveEffect, "ds4", DS4ActiveEffectConfig, { + makeDefault: true, + }); preloadFonts(); await registerHandlebarsPartials(); diff --git a/src/utils/utils.js b/src/utils/utils.js index f7fec6cb..ae2f52eb 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -40,7 +40,7 @@ export function getCanvas() { * @returns {Game} */ export function getGame() { - enforce(game instanceof Game, "Game is not initialized yet."); + enforce(game instanceof foundry.Game, "Game is not initialized yet."); return game; } From cd44db079f19b4d34ae8fb6e88ad4c6cb4bbb9f9 Mon Sep 17 00:00:00 2001 From: Alexander Minges Date: Sat, 12 Jul 2025 20:44:03 +0200 Subject: [PATCH 03/56] feat!: port DS4 actor sheets to ApplicationV2 Convert DS4ActorSheet, DS4CharacterActorSheet, and DS4CreatureActorSheet from ApplicationV1 to ApplicationV2 architecture. Update all templates to use data-action attributes. Implement manual tab system and preserve all existing functionality including item management, effects, and rolling system. BREAKING CHANGE: Requires FoundryVTT ApplicationV2 system --- src/apps/actor/base-sheet.js | 973 ++++++++++-------- src/apps/actor/character-sheet.js | 33 +- src/apps/actor/creature-sheet.js | 33 +- templates/sheets/actor/character-sheet.hbs | 12 +- templates/sheets/actor/components/check.hbs | 2 +- .../actor/components/effect-list-entry.hbs | 4 +- .../actor/components/item-list-entry.hbs | 14 +- .../actor/components/item-list-header.hbs | 8 +- .../actor/components/items-overview.hbs | 26 +- templates/sheets/actor/creature-sheet.hbs | 12 +- .../sheets/actor/tabs/character-abilities.hbs | 2 +- templates/sheets/actor/tabs/spells.hbs | 4 +- .../sheets/shared/components/add-button.hbs | 2 +- .../components/control-button-group.hbs | 4 +- .../shared/components/rollable-image.hbs | 5 +- 15 files changed, 626 insertions(+), 508 deletions(-) diff --git a/src/apps/actor/base-sheet.js b/src/apps/actor/base-sheet.js index 39d22cc9..764608aa 100644 --- a/src/apps/actor/base-sheet.js +++ b/src/apps/actor/base-sheet.js @@ -1,496 +1,603 @@ // SPDX-FileCopyrightText: 2021 Johannes Loher -// SPDX-FileCopyrightText: 2021 Oliver Rümpelein -// SPDX-FileCopyrightText: 2021 Gesina Schwalbe -// SPDX-FileCopyrightText: 2021 Siegfried Krug // // SPDX-License-Identifier: MIT -import { DS4 } from "../../config"; -import { DS4ActiveEffect } from "../../documents/active-effect"; -import { isCheck } from "../../documents/actor/actor-data-properties-base"; -import { getDS4Settings } from "../../settings"; -import { notifications } from "../../ui/notifications"; -import { enforce, getCanvas, getGame } from "../../utils/utils"; -import { disableOverriddenFields } from "../sheet-helpers"; + /** - * The base sheet class for all {@link DS4Actor}s. + * The base sheet class for DS4 Actor Sheets */ -export class DS4ActorSheet extends foundry.appv1.sheets.ActorSheet { - /** @override */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["sheet", "ds4-actor-sheet"], - height: 645, - scrollY: [".ds4-sheet-body"], - tabs: [{ navSelector: ".ds4-sheet-tab-nav", contentSelector: ".ds4-sheet-body", initial: "values" }], - dragDrop: [ - { dragSelector: ".item-list .item", dropSelector: null }, - { dragSelector: ".effect-list .effect", dropSelector: null }, - { dragSelector: ".ds4-check", dropSelector: null }, - ], - width: 650, - }); +export class DS4ActorSheet extends foundry.applications.api.DocumentSheetV2 { + static DEFAULT_OPTIONS = { + classes: ["sheet", "ds4-actor-sheet"], + tag: "form", + form: { + handler: DS4ActorSheet.prototype._onSubmitForm, + submitOnChange: true, + closeOnSubmit: false, + }, + position: { + width: 720, + height: 680, + }, + window: { + resizable: true, + }, + actions: { + rollCheck: DS4ActorSheet.prototype._onRollCheck, + rollItem: DS4ActorSheet.prototype._onRollItem, + controlItem: DS4ActorSheet.prototype._onControlItem, + createItem: DS4ActorSheet.prototype._onCreateItem, + editItem: DS4ActorSheet.prototype._onEditItem, + deleteItem: DS4ActorSheet.prototype._onDeleteItem, + changeItem: DS4ActorSheet.prototype._onChangeItem, + controlEffect: DS4ActorSheet.prototype._onControlEffect, + createEffect: DS4ActorSheet.prototype._onCreateEffect, + editEffect: DS4ActorSheet.prototype._onEditEffect, + deleteEffect: DS4ActorSheet.prototype._onDeleteEffect, + changeEffect: DS4ActorSheet.prototype._onChangeEffect, + sortItems: DS4ActorSheet.prototype._onSortItems, + changeTab: DS4ActorSheet.prototype._onChangeTab, + edititem: DS4ActorSheet.prototype._onEditItem, + deleteitem: DS4ActorSheet.prototype._onDeleteItem, + createitem: DS4ActorSheet.prototype._onCreateItem, + editeffect: DS4ActorSheet.prototype._onEditEffect, + deleteeffect: DS4ActorSheet.prototype._onDeleteEffect, + createeffect: DS4ActorSheet.prototype._onCreateEffect, + }, + }; + + static TABS = {}; + + get title() { + return `${this.document.name} [${game.i18n.localize("DS4.ActorSheet")}]`; } /** @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.type}-sheet.hbs`; + const templatePath = !game.user?.isGM && this.document.limited + ? "systems/ds4/templates/sheets/actor/limited-sheet.hbs" + : `systems/ds4/templates/sheets/actor/${this.document.type}-sheet.hbs`; + + return templatePath; } /** @override */ - async getData(options = {}) { - const itemsByType = Object.fromEntries( - Object.entries(this.actor.itemTypes).map(([itemType, items]) => { - return [itemType, [...items].sort((a, b) => (a.sort || 0) - (b.sort || 0))]; - }), - ); + async _renderHTML(context, options) { + return await foundry.applications.handlebars.renderTemplate(this.template, context); + } - const enrichedEffects = [...this.actor.allApplicableEffects()].map((effect) => { - return { - ...effect.toObject(), - sourceName: effect.parent instanceof Item ? effect.parent.name : effect.sourceName, - factor: effect.factor, - active: effect.active, - uuid: effect.uuid, - }; - }); + /** @override */ + _replaceHTML(result, content, options) { + content.innerHTML = result; + } - const context = { - ...this.addTooltipsToData(await super.getData(options)), - config: DS4, - itemsByType, - enrichedEffects, - settings: getDS4Settings(), + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + + // Validate document exists + if (!this.document) { + throw new Error("Document not available for sheet rendering"); + } + + + + // Add document data + context.data = this.document; + context.system = this.document.system; + context.source = this.document.toObject(); + context.cssClass = this.document.constructor.name.toLowerCase(); + context.editable = this.isEditable; + context.owner = this.document.isOwner; + context.limited = this.document.limited; + + // Add configuration + context.config = CONFIG.DS4; + context.settings = { + showSlayerPoints: game.settings.get("ds4", "showSlayerPoints") || false, }; + + + + // Add items organized by type + context.itemsByType = {}; + if (this.document.items && this.document.items.size > 0) { + for (const item of this.document.items) { + const type = item.type; + if (!context.itemsByType[type]) context.itemsByType[type] = []; + context.itemsByType[type].push(item); + } + } + + // Add enriched effects + context.enrichedEffects = []; + if (this.document.effects && this.document.effects.size > 0) { + for (const effect of this.document.effects) { + const enrichedEffect = effect.toObject(); + enrichedEffect.sourceName = effect.sourceName; + context.enrichedEffects.push(enrichedEffect); + } + } + + // Add tooltips to data + this.addTooltipsToData(context); + return context; } /** - * Adds tooltips to the attributes, traits, and combatValues of the given context object. - * @param {object} context - * @protected + * Add tooltips to the given context data */ addTooltipsToData(context) { - const valueGroups = [context.data.system.attributes, context.data.system.traits, context.data.system.combatValues]; + const data = context.data; - valueGroups.forEach((valueGroup) => { - Object.values(valueGroup).forEach((attribute) => { - attribute.tooltip = this.getTooltipForValue(attribute); - }); - }); - return context; + // Add tooltips to attributes + if (data.system.attributes) { + for (const value of Object.values(data.system.attributes)) { + value.tooltip = this.getTooltipForValue(value); + } + } + + // Add tooltips to traits + if (data.system.traits) { + for (const value of Object.values(data.system.traits)) { + value.tooltip = this.getTooltipForValue(value); + } + } + + // Add tooltips to combat values + if (data.system.combatValues) { + for (const value of Object.values(data.system.combatValues)) { + value.tooltip = this.getTooltipForValue(value); + } + } } /** - * Generates a tooltip for a given attribute, trait, or combatValue. - * @param {import("../../documents/common/common-data").ModifiableDataBaseTotal} value The value to get a tooltip for - * @returns {string} The tooltip - * @protected + * Get a tooltip for a value + * @param {object} value - The value to get a tooltip for + * @returns {string} The tooltip string */ getTooltipForValue(value) { - return `${value.base} (${getGame().i18n.localize("DS4.TooltipBaseValue")}) + ${ - value.mod - } (${getGame().i18n.localize("DS4.TooltipModifier")}) ➞ ${getGame().i18n.localize("DS4.TooltipEffects")} ➞ ${ - value.total - }`; - } + const base = value.base ?? 0; + const modifier = value.mod ?? 0; + const effects = value.effects ?? 0; - /** - * @param {JQuery} html - * @override - */ - activateListeners(html) { - super.activateListeners(html); - - if (!this.options.editable) return; - - html.find(".control-item").on("click", this.onControlItem.bind(this)); - html.find(".change-item").on("change", this.onChangeItem.bind(this)); - - html.find(".control-effect").on("click", this.onControlEffect.bind(this)); - html.find(".change-effect").on("change", this.onChangeEffect.bind(this)); - - html.find(".rollable-item").on("click", this.onRollItem.bind(this)); - html.find(".rollable-check").on("click", this.onRollCheck.bind(this)); - - html.find(".sort-items").on("click", this.onSortItems.bind(this)); - - disableOverriddenFields(this.form, this.actor.overrides, (key) => `[name="${key}"]`); - for (const item of this.actor.items) { - disableOverriddenFields( - this.form, - item.overrides, - (key) => `[data-item-uuid="${item.uuid}"] .change-item[data-property="${key}"]`, - ); - } - } - - /** - * Handles a click on an element of this sheet to control an embedded item of the actor corresponding to this sheet. - * - * @param {JQuery.ClickEvent} event The originating click event - * @protected - */ - onControlItem(event) { - event.preventDefault(); - const a = event.currentTarget; - switch (a.dataset["action"]) { - case "create": - return this.onCreateItem(event); - case "edit": - return this.onEditItem(event); - case "delete": - return this.onDeleteItem(event); - } - } - - /** - * Creates a new embedded item using the initial data defined in the HTML dataset of the clicked element. - * - * @param {JQuery.ClickEvent} event The originating click event - * @protected - */ - onCreateItem(event) { - const { type } = foundry.utils.deepClone(event.currentTarget.dataset); - const name = getGame().i18n.localize(`DS4.New${type.capitalize()}Name`); - const itemData = { name, type }; - Item.create(itemData, { parent: this.actor }); - } - - /** - * Opens the sheet of the embedded item corresponding to the clicked element. - * - * @param {JQuery.ClickEvent} event The originating click event - * @protected - */ - async onEditItem(event) { - const li = event.currentTarget.closest(embeddedDocumentListEntryProperties.Item.selector); - const uuid = li.dataset[embeddedDocumentListEntryProperties.Item.uuidDataAttribute]; - const item = await fromUuid(uuid); - enforce( - item && item.parent === this.actor, - getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { uuid, actor: this.actor.name }), - ); - item.sheet?.render(true); - } - - /** - * Deletes the embedded item corresponding to the clicked element. - * - * @param {JQuery.ClickEvent} event The originating click event - * @protected - */ - async onDeleteItem(event) { - const li = event.currentTarget.closest(embeddedDocumentListEntryProperties.Item.selector); - const uuid = li.dataset[embeddedDocumentListEntryProperties.Item.uuidDataAttribute]; - const item = await fromUuid(uuid); - enforce( - item && item.parent === this.actor, - getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { uuid, actor: this.actor.name }), - ); - item.delete(); - $(li).slideUp(200, () => this.render(false)); - } - - /** - * 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 {JQuery.ClickEvent} event The originating click event - * @protected - */ - onChangeItem(event) { - return this.onChangeEmbeddedDocument(event, "Item"); - } - - /** - * Handles a click on an element of this sheet to control an embedded effect of the actor corresponding to this - * sheet. - * - * @param {JQuery.ClickEvent} event The originating click event - * @protected - */ - onControlEffect(event) { - event.preventDefault(); - const a = event.currentTarget; - switch (a.dataset["action"]) { - case "create": - return this.onCreateEffect(); - case "edit": - return this.onEditEffect(event); - case "delete": - return this.onDeleteEffect(event); - } - } - - /** - * Creates a new embedded effect. - * - * @param {JQuery.ClickEvent} event The originating click event - * @protected - */ - onCreateEffect() { - DS4ActiveEffect.createDefault(this.actor); - } - - /** - * Opens the sheet of the embedded effect corresponding to the clicked element. - * - * @param {JQuery.ClickEvent} event The originating click event - * @protected - */ - async onEditEffect(event) { - const li = event.currentTarget.closest(embeddedDocumentListEntryProperties.ActiveEffect.selector); - const uuid = li.dataset[embeddedDocumentListEntryProperties.ActiveEffect.uuidDataAttribute]; - const effect = await fromUuid(uuid); - enforce( - effect && (effect.parent === this.actor || effect.parent.parent === this.actor), - getGame().i18n.format("DS4.ErrorActorDoesNotHaveEffect", { uuid, actor: this.actor.name }), - ); - effect.sheet?.render(true); - } - - /** - * Deletes the embedded item corresponding to the clicked element. - * - * @param {JQuery.ClickEvent} event The originating click event - * @protected - */ - async onDeleteEffect(event) { - const li = event.currentTarget.closest(embeddedDocumentListEntryProperties.ActiveEffect.selector); - const uuid = li.dataset[embeddedDocumentListEntryProperties.ActiveEffect.uuidDataAttribute]; - const effect = await fromUuid(uuid); - enforce( - effect && (effect.parent === this.actor || effect.parent.parent === this.actor), - getGame().i18n.format("DS4.ErrorActorDoesNotHaveEffect", { uuid, actor: this.actor.name }), - ); - effect.delete(); - $(li).slideUp(200, () => this.render(false)); - } - - /** - * 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 {JQuery.ClickEvent} event The originating click event - * @protected - */ - onChangeEffect(event) { - return this.onChangeEmbeddedDocument(event, "ActiveEffect"); - } - - /** - * 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 {JQuery.ChangeEvent} event The originating click event - * @param {"Item" | "ActiveEffect"} documentName The name of the embedded document to be changed. - * @protected - */ - async onChangeEmbeddedDocument(event, documentName) { - event.preventDefault(); - const element = $(event.currentTarget).get(0); - if (element.disabled) return; - - const documentElement = element.closest(embeddedDocumentListEntryProperties[documentName].selector); - const uuid = documentElement.dataset[embeddedDocumentListEntryProperties[documentName].uuidDataAttribute]; - const property = element.dataset["property"]; - enforce(property !== undefined, TypeError("HTML element does not provide 'data-property' attribute")); - - const newValue = this.parseValue(element); - - const document = await fromUuid(uuid); - - if (documentName === "Item") { - enforce( - document && document.parent === this.actor, - getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { uuid, actor: this.actor.name }), - ); + if (effects === 0) { + return game.i18n.format("DS4.TooltipWithoutEffects", { base, modifier }); } else { - enforce( - document && (document.parent === this.actor || document.parent.parent === this.actor), - getGame().i18n.format("DS4.ErrorActorDoesNotHaveEffect", { uuid, actor: this.actor.name }), - ); - } - document.update({ [property]: newValue }); - } - - /** - * Parses the value of the given {@link HTMLInputElement} depending on the element's type - * The value is parsed to: - * - checkbox: `boolean`, if the attribute `data-inverted` is set to a truthy value, the parsed value is inverted - * - text input: `string` - * - number: `number` - * - * @param {HTMLInputElement} element The input element to parse the value from - * @returns {boolean | string | number} The parsed data - * @protected - */ - parseValue(element) { - switch (element.type) { - case "checkbox": { - const inverted = Boolean(element.dataset["inverted"]); - const value = element.checked; - return inverted ? !value : value; - } - case "text": { - const value = element.value; - return value; - } - case "number": { - const value = Number(element.value.trim()); - return value; - } - default: { - throw new TypeError("Binding of item property to this type of HTML element not supported; given: " + element); - } + return game.i18n.format("DS4.TooltipWithEffects", { base, modifier, effects }); } } /** - * Handle clickable item rolls. - * @param {JQuery.ClickEvent} event The originating click event - * @protected + * Handle form submission + * @param {Event} event - The form submission event + * @param {HTMLFormElement} form - The form element + * @param {FormDataExtended} formData - The form data */ - async onRollItem(event) { - event.preventDefault(); - const li = event.currentTarget.closest(embeddedDocumentListEntryProperties.Item.selector); - const uuid = li.dataset[embeddedDocumentListEntryProperties.Item.uuidDataAttribute]; - const item = await fromUuid(uuid); - enforce( - item && item.parent === this.actor, - getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { uuid, actor: this.actor.name }), - ); - item.roll().catch((e) => notifications.error(e, { log: true })); + async _onSubmitForm(event, form, formData) { + const submitData = this._prepareSubmitData(event, form, formData); + await this.document.update(submitData); } /** - * Handle clickable check rolls. - * @param {JQuery.ClickEvent} event The originating click event - * @protected + * Prepare data for form submission + * @param {Event} event - The form submission event + * @param {HTMLFormElement} form - The form element + * @param {FormDataExtended} formData - The form data + * @returns {object} The prepared submit data */ - onRollCheck(event) { - event.preventDefault(); - event.currentTarget.blur(); - const check = event.currentTarget.dataset["check"]; - this.actor.rollCheck(check).catch((e) => notifications.error(e, { log: true })); + _prepareSubmitData(event, form, formData) { + const submitData = super._prepareSubmitData(event, form, formData); + + // Parse values for specific fields + for (const [key, value] of Object.entries(submitData)) { + if (key.includes("system.") && typeof value === "string") { + submitData[key] = this.parseValue(value); + } + } + + return submitData; } /** - * @param {DragEvent} event - * @override + * Parse a value from a form field + * @param {string} value - The value to parse + * @returns {*} The parsed value */ - _onDragStart(event) { - const target = event.currentTarget; - if (!(target instanceof HTMLElement)) return super._onDragStart(event); + parseValue(value) { + if (value === "") return null; - const check = target.dataset.check; - if (!check) return super._onDragStart(event); + const numericValue = Number(value); + if (!isNaN(numericValue)) return numericValue; - enforce(isCheck(check), getGame().i18n.format("DS4.ErrorCannotDragMissingCheck", { check })); + // Try to parse as a formula + try { + const roll = new Roll(value); + return roll.evaluate({ async: false }).total; + } catch (error) { + return value; + } + } - const dragData = { - actorId: this.actor.id, - sceneId: this.actor.isToken ? getCanvas().scene?.id : null, - tokenId: this.actor.isToken ? this.actor.token?.id : null, - type: "Check", - data: check, + /** + * Handle rolling a check + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onRollCheck(event, target) { + const checkKey = target.dataset.check; + if (!checkKey) return; + + const actor = this.document; + const check = actor.system.checks[checkKey]; + + if (check !== undefined) { + const roll = new Roll("1d20"); + const rollResult = await roll.evaluate({ async: true }); + + const success = rollResult.total <= check; + const resultText = success ? "DS4.CheckSuccess" : "DS4.CheckFailure"; + + const messageData = { + speaker: ChatMessage.getSpeaker({ actor }), + flavor: game.i18n.format("DS4.CheckRollFlavor", { + check: game.i18n.localize(`DS4.Check${checkKey.capitalize()}`), + target: check, + }), + content: game.i18n.localize(resultText), + type: CONST.CHAT_MESSAGE_TYPES.ROLL, + rolls: [rollResult], + }; + + await ChatMessage.create(messageData); + } + } + + /** + * Handle rolling an item + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onRollItem(event, target) { + const itemId = target.closest("[data-item-id]")?.dataset.itemId; + if (!itemId) return; + + const item = this.document.items.get(itemId); + if (item?.system.rollable) { + await item.roll(); + } + } + + /** + * Handle item control actions + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onControlItem(_event, target) { + const action = target.dataset.action; + const itemId = target.closest("[data-item-id]")?.dataset.itemId; + + if (!itemId) return; + + const item = this.document.items.get(itemId); + if (!item) return; + + switch (action) { + case "edit": + await this._onEditItem(event, target); + break; + case "delete": + await this._onDeleteItem(event, target); + break; + } + } + + /** + * Handle creating a new item + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onCreateItem(event, target) { + const itemType = target.dataset.type; + if (!itemType) return; + + const itemData = { + name: game.i18n.localize(`DS4.ItemType${itemType.capitalize()}`), + type: itemType, }; - event.dataTransfer?.setData("text/plain", JSON.stringify(dragData)); + await this.document.createEmbeddedDocuments("Item", [itemData]); } /** - * Sort items according to the item list header that has been clicked. - * @param {JQuery.ClickEvent} event The originating click event - * @protected + * Handle editing an item + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element */ - onSortItems(event) { - event.preventDefault(); - const target = event.currentTarget; - const type = target.parentElement?.dataset["type"]; - enforce(type !== undefined, `Could not find property 'type' in the dataset of the parent of ${target}`); - 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.sort - b.sort); + async _onEditItem(event, target) { + const itemId = target.closest("[data-item-id]")?.dataset.itemId; + if (!itemId) return; - /** - * @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); + const item = this.document.items.get(itemId); + if (item) { + await item.sheet.render(true); + } + } - 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); + /** + * Handle deleting an item + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onDeleteItem(event, target) { + const itemId = target.closest("[data-item-id]")?.dataset.itemId; + if (!itemId) return; + + const item = this.document.items.get(itemId); + if (!item) return; + + const confirmed = await Dialog.confirm({ + title: game.i18n.localize("DS4.DialogDeleteItemTitle"), + content: game.i18n.format("DS4.DialogDeleteItemContent", { item: item.name }), + defaultYes: false, + }); + + if (confirmed) { + await item.delete(); + } + } + + /** + * Handle changing an item property + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onChangeItem(event, target) { + const itemId = target.closest("[data-item-id]")?.dataset.itemId; + const property = target.dataset.property; + + if (!itemId || !property) return; + + const item = this.document.items.get(itemId); + if (!item) return; + + let value = target.value; + + // Handle different input types + if (target.type === "checkbox") { + value = target.checked; + if (target.dataset.inverted === "true") { + value = !value; } - - return comparison; - }; - - const sortedItems = [...items].sort(sortFunction(false)); - const wasSortedAlready = !sortedItems.find((item, index) => item !== items[index]); - - if (wasSortedAlready) { - sortedItems.sort(sortFunction(true)); + } else if (target.dataset.dtype === "Number") { + value = Number(value); } - const updates = sortedItems.map((item, i) => ({ + await item.update({ [property]: value }); + } + + /** + * Handle effect control actions + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onControlEffect(event, target) { + const action = target.dataset.action; + const effectId = target.closest("[data-effect-id]")?.dataset.effectId; + + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (!effect) return; + + switch (action) { + case "edit": + await this._onEditEffect(event, target); + break; + case "delete": + await this._onDeleteEffect(event, target); + break; + } + } + + /** + * Handle creating a new effect + */ + async _onCreateEffect() { + const effectData = { + name: game.i18n.localize("DS4.EffectNew"), + icon: "icons/svg/aura.svg", + }; + + await this.document.createEmbeddedDocuments("ActiveEffect", [effectData]); + } + + /** + * Handle editing an effect + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onEditEffect(event, target) { + const effectId = target.closest("[data-effect-id]")?.dataset.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + await effect.sheet.render(true); + } + } + + /** + * Handle deleting an effect + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onDeleteEffect(event, target) { + const effectId = target.closest("[data-effect-id]")?.dataset.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (!effect) return; + + const confirmed = await Dialog.confirm({ + title: game.i18n.localize("DS4.DialogDeleteEffectTitle"), + content: game.i18n.format("DS4.DialogDeleteEffectContent", { effect: effect.name }), + defaultYes: false, + }); + + if (confirmed) { + await effect.delete(); + } + } + + /** + * Handle changing an effect property + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onChangeEffect(event, target) { + const effectId = target.closest("[data-effect-id]")?.dataset.effectId; + const property = target.dataset.property; + + if (!effectId || !property) return; + + const effect = this.document.effects.get(effectId); + if (!effect) return; + + let value = target.value; + + // Handle different input types + if (target.type === "checkbox") { + value = target.checked; + if (target.dataset.inverted === "true") { + value = !value; + } + } else if (target.dataset.dtype === "Number") { + value = Number(value); + } + + await effect.update({ [property]: value }); + } + + /** + * Handle sorting items + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onSortItems(event, target) { + const dataPath = target.dataset.dataPath; + const itemType = target.closest("[data-type]")?.dataset.type; + + if (!dataPath || !itemType) return; + + const items = this.document.items.filter(item => item.type === itemType); + const sortedItems = this.sortItems(items, dataPath); + + const updates = sortedItems.map((item, index) => ({ _id: item.id, - sort: (i + 1) * CONST.SORT_INTEGER_DENSITY, + sort: index * 100, })); - this.actor.updateEmbeddedDocuments("Item", updates); + await this.document.updateEmbeddedDocuments("Item", updates); + } + + /** + * Sort items by a given property path + * @param {Item[]} items - The items to sort + * @param {string} dataPath - The property path to sort by + * @returns {Item[]} The sorted items + */ + sortItems(items, dataPath) { + return items.sort((a, b) => { + const aValue = foundry.utils.getProperty(a, dataPath); + const bValue = foundry.utils.getProperty(b, dataPath); + + if (typeof aValue === "string" && typeof bValue === "string") { + return aValue.localeCompare(bValue); + } else if (typeof aValue === "number" && typeof bValue === "number") { + return aValue - bValue; + } else { + return 0; + } + }); + } + + /** + * Handle tab changes manually for custom tab behavior + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onChangeTab(event, target) { + event.preventDefault(); + const tab = target.dataset.tab; + if (!tab) return; + + // Find tab navigation elements + const nav = target.closest(".ds4-sheet-tab-nav"); + const sheet = this.element.querySelector(".ds4-sheet-body"); + + if (!nav || !sheet) return; + + // Update navigation active state + nav.querySelectorAll(".ds4-sheet-tab-nav__item").forEach(item => { + item.classList.remove("active"); + }); + target.classList.add("active"); + + // Update tab content visibility + sheet.querySelectorAll(".ds4-sheet-tab").forEach(tabContent => { + tabContent.classList.remove("active"); + }); + + const activeTab = sheet.querySelector(`.ds4-sheet-tab[data-tab="${tab}"]`); + if (activeTab) { + activeTab.classList.add("active"); + } + } + + /** @override */ + async _onRender(context, options) { + await super._onRender(context, options); + + // Initialize first tab as active + this._initializeTabs(); + } + + /** + * Initialize tab state - show first tab, hide others + */ + _initializeTabs() { + const nav = this.element.querySelector(".ds4-sheet-tab-nav"); + const sheet = this.element.querySelector(".ds4-sheet-body"); + + if (!nav || !sheet) { + return; + } + + // Get all tab navigation items and tab content + const navItems = nav.querySelectorAll(".ds4-sheet-tab-nav__item"); + const tabContents = sheet.querySelectorAll(".ds4-sheet-tab"); + + // Remove active class from all items first + navItems.forEach(item => item.classList.remove("active")); + tabContents.forEach(content => content.classList.remove("active")); + + // Set first tab navigation as active + const firstNavItem = navItems[0]; + if (firstNavItem) { + firstNavItem.classList.add("active"); + + // Set corresponding tab content as active + const firstTab = firstNavItem.dataset.tab; + if (firstTab) { + const activeTabContent = sheet.querySelector(`.ds4-sheet-tab[data-tab="${firstTab}"]`); + if (activeTabContent) { + activeTabContent.classList.add("active"); + } + } + } } } - -/** - * This object contains information about specific properties embedded document list entries for each different type. - */ -const embeddedDocumentListEntryProperties = Object.freeze({ - ActiveEffect: { - selector: ".effect", - uuidDataAttribute: "effectUuid", - }, - Item: { - selector: ".item", - uuidDataAttribute: "itemUuid", - }, -}); - -/** - * 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()); -}; - -/** - * 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; -}; diff --git a/src/apps/actor/character-sheet.js b/src/apps/actor/character-sheet.js index 2d33b262..d8b993a1 100644 --- a/src/apps/actor/character-sheet.js +++ b/src/apps/actor/character-sheet.js @@ -2,27 +2,32 @@ // // SPDX-License-Identifier: MIT -import { DS4ActorSheet } from "./base-sheet"; +import { DS4ActorSheet } from "./base-sheet.js"; /** * The Sheet class for DS4 Character Actors */ export class DS4CharacterActorSheet extends DS4ActorSheet { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["sheet", "ds4-actor-sheet", "ds4-character-sheet"], - }); - } + static DEFAULT_OPTIONS = { + ...super.DEFAULT_OPTIONS, + classes: ["sheet", "ds4-actor-sheet", "ds4-character-sheet"], + }; /** @override */ - async getData(options = {}) { - const context = await super.getData(options); - context.data.system.profile.biography = await foundry.applications.ux.TextEditor.implementation.enrichHTML( - context.data.system.profile.biography, - { - async: true, - }, - ); + async _prepareContext(options) { + const context = await super._prepareContext(options); + + // Enrich biography HTML content + if (context.data.system.profile.biography) { + context.data.system.profile.biography = await TextEditor.enrichHTML( + context.data.system.profile.biography, + { + async: true, + relativeTo: this.document, + } + ); + } + return context; } } diff --git a/src/apps/actor/creature-sheet.js b/src/apps/actor/creature-sheet.js index 21acb003..6849b8e6 100644 --- a/src/apps/actor/creature-sheet.js +++ b/src/apps/actor/creature-sheet.js @@ -2,27 +2,32 @@ // // SPDX-License-Identifier: MIT -import { DS4ActorSheet } from "./base-sheet"; +import { DS4ActorSheet } from "./base-sheet.js"; /** * The Sheet class for DS4 Creature Actors */ export class DS4CreatureActorSheet extends DS4ActorSheet { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["sheet", "ds4-actor-sheet", "ds4-creature-sheet"], - }); - } + static DEFAULT_OPTIONS = { + ...super.DEFAULT_OPTIONS, + classes: ["sheet", "ds4-actor-sheet", "ds4-creature-sheet"], + }; /** @override */ - async getData(options = {}) { - const context = await super.getData(options); - context.data.system.baseInfo.description = await foundry.applications.ux.TextEditor.implementation.enrichHTML( - context.data.system.baseInfo.description, - { - async: true, - }, - ); + async _prepareContext(options) { + const context = await super._prepareContext(options); + + // Enrich description HTML content + if (context.data.system.baseInfo.description) { + context.data.system.baseInfo.description = await TextEditor.enrichHTML( + context.data.system.baseInfo.description, + { + async: true, + relativeTo: this.document, + } + ); + } + return context; } } diff --git a/templates/sheets/actor/character-sheet.hbs b/templates/sheets/actor/character-sheet.hbs index 556b4c1e..33322d79 100644 --- a/templates/sheets/actor/character-sheet.hbs +++ b/templates/sheets/actor/character-sheet.hbs @@ -14,12 +14,12 @@ SPDX-License-Identifier: MIT {{!-- Sheet Tab Navigation --}} diff --git a/templates/sheets/actor/components/check.hbs b/templates/sheets/actor/components/check.hbs index 44e50266..ecd8669c 100644 --- a/templates/sheets/actor/components/check.hbs +++ b/templates/sheets/actor/components/check.hbs @@ -12,7 +12,7 @@ SPDX-License-Identifier: MIT !-- @param check-label: The label for the check --}} -