import AwEActorSheet from "./base-actor-sheet.mjs" import { SYSTEM } from "../../config/system.mjs" export default class AwECharacterSheet extends AwEActorSheet { /** @override */ static DEFAULT_OPTIONS = { classes: ["character"], position: { width: 960, height: 780 }, window: { contentClasses: ["character-content"] }, actions: { createAbility: AwECharacterSheet.#onCreateAbility, createWeapon: AwECharacterSheet.#onCreateWeapon, createKit: AwECharacterSheet.#onCreateKit, createEquipment: AwECharacterSheet.#onCreateEquipment, flowPointsPlus: AwECharacterSheet.#onFlowPointsPlus, flowPointsMinus: AwECharacterSheet.#onFlowPointsMinus, rollField: AwECharacterSheet.#onRollField, rollWeapon: AwECharacterSheet.#onRollWeapon, rollDamage: AwECharacterSheet.#onRollDamage, toggleCondition: AwECharacterSheet.#onToggleCondition, useKit: AwECharacterSheet.#onUseKit, useAbility: AwECharacterSheet.#onUseAbility, dailyReset: AwECharacterSheet.#onDailyReset, longRest: AwECharacterSheet.#onLongRest } } /** @override */ static PARTS = { header: { template: "systems/fvtt-adventures-with-emmy/templates/character-header.hbs" }, tabs: { template: "templates/generic/tab-navigation.hbs" }, main: { template: "systems/fvtt-adventures-with-emmy/templates/character-main.hbs" }, biography: { template: "systems/fvtt-adventures-with-emmy/templates/character-biography.hbs" }, equipment: { template: "systems/fvtt-adventures-with-emmy/templates/character-equipment.hbs" } } /** @override */ tabGroups = { sheet: "main" } /** * Prepare an array of form header tabs. * @returns {Record>} The tab objects. */ #getTabs() { const tabs = { main: { id: "main", group: "sheet", icon: "fa-solid fa-user", label: "AWEMMY.Sheet.Tab.Main" }, biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "AWEMMY.Sheet.Tab.Biography" }, equipment: { id: "equipment", group: "sheet", icon: "fa-solid fa-backpack", label: "AWEMMY.Sheet.Tab.Equipment" } } for (const v of Object.values(tabs)) { v.active = this.tabGroups[v.group] === v.id v.cssClass = v.active ? "active" : "" } return tabs } /** @override */ async _prepareContext() { const context = await super._prepareContext() context.tabs = this.#getTabs() return context } /** @override */ async _preparePartContext(partId, context) { const doc = this.document switch (partId) { case "main": context.tab = context.tabs.main context.abilities = doc.itemTypes.ability.map(item => ({ id: item.id, uuid: item.uuid, name: item.name, img: item.img, system: item.system, costLabel: game.i18n.localize(SYSTEM.ABILITY_COST[item.system.cost]?.label ?? item.system.cost), usedToday: item.system.usedToday })) context.hasUsedAbilities = context.abilities.some(a => a.usedToday) context.conditions = Object.values(SYSTEM.CONDITIONS).map(c => ({ ...c, label: game.i18n.localize(c.label), img: `systems/fvtt-adventures-with-emmy/assets/conditions/${c.id}.svg`, active: doc.statuses.has(c.id) })) break case "biography": context.tab = context.tabs.biography context.fields = doc.itemTypes.field.map(item => ({ id: item.id, uuid: item.uuid, name: item.name, img: item.img, system: item.system, keyAttrLabel: game.i18n.localize(SYSTEM.ATTRIBUTES[item.system.keyAttribute]?.label ?? item.system.keyAttribute), keyAttr2Label: item.system.keyAttribute2 ? game.i18n.localize(SYSTEM.ATTRIBUTES[item.system.keyAttribute2]?.label ?? item.system.keyAttribute2) : null })) context.specializations = (doc.itemTypes.specialization ?? []).map(item => { const fieldMatch = doc.itemTypes.field.some(f => AwECharacterSheet.#slugify(f.name) === AwECharacterSheet.#slugify(item.system.fieldName) ) const attrOverrideLabel = item.system.keyAttributeOverride ? game.i18n.localize(SYSTEM.ATTRIBUTES[item.system.keyAttributeOverride]?.label ?? item.system.keyAttributeOverride) : null return { id: item.id, uuid: item.uuid, name: item.name, img: item.img, system: item.system, fieldMatch, attrOverrideLabel } }) context.archetypes = doc.itemTypes.archetype context.backgrounds = doc.itemTypes.background context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( doc.system.description, { async: true } ) context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML( doc.system.notes, { async: true } ) break case "equipment": context.tab = context.tabs.equipment context.kits = doc.itemTypes.kit context.weapons = doc.itemTypes.weapon context.equipments = doc.itemTypes.equipment break } return context } /** @override */ async _onDrop(event) { if (!this.isEditable) return const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) if (data.type === "Item") { const item = await fromUuid(data.uuid) return this._onDropItem(item) } } /** @override */ async _onDropItem(item) { if (!item) return // field/background/specialization: max 1 (replace existing); archetype: multiple allowed if (item.type === "field" || item.type === "background" || item.type === "specialization") { const existing = this.document.itemTypes[item.type] if (existing.length > 0) { ui.notifications.info(game.i18n.format("AWEMMY.Character.ItemReplaced", { name: existing[0].name })) await existing[0].delete() } return this.document.createEmbeddedDocuments("Item", [item.toObject()]) } if (item.type === "archetype") { return this.document.createEmbeddedDocuments("Item", [item.toObject()]) } return super._onDropItem(item) } /** * Create a new ability item. * @param {Event} event - The triggering event. * @param {HTMLElement} target - The target element. */ static #onCreateAbility(event, target) { const type = "ability" this.document.createEmbeddedDocuments("Item", [{ name: CONFIG.Item.documentClass.defaultName({ type }), type }]) } /** * Create a new weapon item. * @param {Event} event - The triggering event. * @param {HTMLElement} target - The target element. */ static #onCreateWeapon(event, target) { const type = "weapon" this.document.createEmbeddedDocuments("Item", [{ name: CONFIG.Item.documentClass.defaultName({ type }), type }]) } /** * Create a new kit item. * @param {Event} event - The triggering event. * @param {HTMLElement} target - The target element. */ static #onCreateKit(event, target) { const type = "kit" this.document.createEmbeddedDocuments("Item", [{ name: CONFIG.Item.documentClass.defaultName({ type }), type }]) } /** * Create a new equipment item. * @param {Event} event - The triggering event. * @param {HTMLElement} target - The target element. */ static #onCreateEquipment(event, target) { const type = "equipment" this.document.createEmbeddedDocuments("Item", [{ name: CONFIG.Item.documentClass.defaultName({ type }), type }]) } /** * Increase flow points by 1. * @param {Event} event - The triggering event. * @param {HTMLElement} target - The target element. */ static #onFlowPointsPlus(event, target) { const current = this.actor.system.flowPoints.value this.actor.update({ "system.flowPoints.value": current + 1 }) } /** * Decrease flow points by 1. * @param {Event} event - The triggering event. * @param {HTMLElement} target - The target element. */ static #onFlowPointsMinus(event, target) { const current = this.actor.system.flowPoints.value this.actor.update({ "system.flowPoints.value": Math.max(0, current - 1) }) } /** * Roll the key attribute check from a Field item. * If a matching Specialization has a keyAttributeOverride, it takes priority. * @param {PointerEvent} event The triggering event. * @param {HTMLElement} target The target element. */ static async #onRollField(event, target) { const itemId = target.closest("[data-item-id]")?.dataset.itemId const item = this.document.items.get(itemId) if (!item) return // Check for a specialization that matches this field and overrides the key attribute const spec = this.document.itemTypes.specialization?.find(s => AwECharacterSheet.#slugify(s.system.fieldName) === AwECharacterSheet.#slugify(item.name) ) const attrId = spec?.system.keyAttributeOverride || target.dataset.attributeId || item.system.keyAttribute await this.document.rollAttribute(attrId, { sourceItemName: item.name, sourceItemImg: item.img }) } static async #onRollWeapon(event, target) { const itemId = target.closest("[data-item-id]")?.dataset.itemId const item = this.document.items.get(itemId) if (!item) return await this.document.rollWeapon(item) } static async #onRollDamage(event, target) { const itemId = target.closest("[data-item-id]")?.dataset.itemId const item = this.document.items.get(itemId) if (!item) return await this.document.rollDamage(item) } /** Slugify a string for loose name matching (lowercase, trim, spaces→dash, strip non-alphanum). */ static #slugify(str) { return (str ?? "").toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") } static async #onToggleCondition(event, target) { const conditionId = target.dataset.conditionId await this.document.toggleStatusEffect(conditionId) } static async #onUseKit(event, target) { const itemId = target.closest("[data-item-id]")?.dataset.itemId await this.document.useKit(itemId) } static async #onUseAbility(event, target) { const itemId = target.closest("[data-item-id]")?.dataset.itemId await this.document.useAbility(itemId) } static async #onDailyReset(event, target) { const actor = this.document const dailyAbilities = actor.itemTypes.ability.filter(i => i.system.usedToday) if (!dailyAbilities.length) return const updates = dailyAbilities.map(i => ({ _id: i.id, "system.usedToday": false })) await actor.updateEmbeddedDocuments("Item", updates) ui.notifications.info(game.i18n.localize("AWEMMY.Ability.DailyResetDone")) } static async #onLongRest(event, target) { const actor = this.document const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.localize("AWEMMY.Rest.LongRest") }, content: `

${game.i18n.format("AWEMMY.Rest.LongRestConfirm", { name: actor.name })}

`, yes: { label: game.i18n.localize("AWEMMY.Rest.Rest"), icon: "fa-solid fa-moon" }, no: { label: game.i18n.localize("AWEMMY.Rest.Cancel") } }) if (!confirmed) return await actor.longRest() } }