import PrismRPGActorSheet from "./base-actor-sheet.mjs" import PrismRPGRoll from "../../documents/roll.mjs" import { SYSTEM } from "../../config/system.mjs" export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { /** @override */ static DEFAULT_OPTIONS = { classes: ["character"], position: { width: 780, height: 780, }, window: { contentClasses: ["character-content"], }, actions: { createEquipment: PrismRPGCharacterSheet.#onCreateEquipment, rollInitiative: PrismRPGCharacterSheet.#onRollInitiative, armorHitPointsPlus: PrismRPGCharacterSheet.#onArmorHitPointsPlus, armorHitPointsMinus: PrismRPGCharacterSheet.#onArmorHitPointsMinus, armorPointsPlus: PrismRPGCharacterSheet.#onArmorPointsPlus, armorPointsMinus: PrismRPGCharacterSheet.#onArmorPointsMinus, actionPointsPlus: PrismRPGCharacterSheet.#onActionPointsPlus, actionPointsMinus: PrismRPGCharacterSheet.#onActionPointsMinus, manaPointsPlus: PrismRPGCharacterSheet.#onManaPointsPlus, manaPointsMinus: PrismRPGCharacterSheet.#onManaPointsMinus, hpPlus: PrismRPGCharacterSheet.#onHpPlus, hpMinus: PrismRPGCharacterSheet.#onHpMinus, hpTempPlus: PrismRPGCharacterSheet.#onHpTempPlus, hpTempMinus: PrismRPGCharacterSheet.#onHpTempMinus, postItemToChat: PrismRPGCharacterSheet.#onPostItemToChat, useConsumable: PrismRPGCharacterSheet.#onUseConsumable, toggleContainerEquipped: PrismRPGCharacterSheet.#onToggleContainerEquipped, toggleEquipped: PrismRPGCharacterSheet.#onToggleEquipped, assignToContainer: PrismRPGCharacterSheet.#onAssignToContainer, removeFromContainer: PrismRPGCharacterSheet.#onRemoveFromContainer, }, } /** @override */ static PARTS = { main: { template: "systems/fvtt-prism-rpg/templates/character-main.hbs", }, tabs: { template: "templates/generic/tab-navigation.hbs", }, skills: { template: "systems/fvtt-prism-rpg/templates/character-skills.hbs", }, subattributes: { template: "systems/fvtt-prism-rpg/templates/character-subattributes.hbs", }, combat: { template: "systems/fvtt-prism-rpg/templates/character-combat.hbs", }, equipment: { template: "systems/fvtt-prism-rpg/templates/character-equipment.hbs", }, spells: { template: "systems/fvtt-prism-rpg/templates/character-spells.hbs", }, biography: { template: "systems/fvtt-prism-rpg/templates/character-biography.hbs", }, } /** @override */ tabGroups = { sheet: "skills", } /** * Prepare an array of form header tabs. * @returns {Record>} */ #getTabs() { let tabs = { skills: { id: "skills", group: "sheet", icon: "fa-solid fa-shapes", label: "PRISMRPG.Label.skills" }, subattributes: { id: "subattributes", group: "sheet", icon: "fa-solid fa-diagram-project", label: "PRISMRPG.Label.subattributes" }, combat: { id: "combat", group: "sheet", icon: "fa-solid fa-swords", label: "PRISMRPG.Label.combat" }, equipment: { id: "equipment", group: "sheet", icon: "fa-solid fa-backpack", label: "PRISMRPG.Label.equipment" }, biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "PRISMRPG.Label.biography" }, } if (this.actor.system.biodata.magicUser) { tabs.spells = { id: "spells", group: "sheet", icon: "fa-sharp-duotone fa-solid fa-wand-magic-sparkles", label: "PRISMRPG.Label.spells" } } 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() context.config = SYSTEM return context } /** @override */ async _preparePartContext(partId, context) { const doc = this.document switch (partId) { case "main": context.race = doc.itemTypes.race?.[0] || null const classes = doc.itemTypes.class || [] // Create 3 class slots context.classSlots = [ classes[0] || null, classes[1] || null, classes[2] || null ] // Burden computed values const bSubAttr = doc.system.burden.subAttribute const bSubVal = doc.system.subAttributes[bSubAttr]?.value ?? 0 const baseBurden = doc.itemTypes.race?.[0]?.system.baseBurden ?? 0 context.burdenMax = Math.max(0, baseBurden + bSubVal + doc.system.burden.other) // Equipped burden: only items with equipped=true count toward burden const equippableTypes = [ ...doc.itemTypes.weapon, ...doc.itemTypes.armor, ...doc.itemTypes.shield, ...doc.itemTypes.equipment, ...doc.itemTypes.container, ] const burdenEquipped = equippableTypes .filter(i => i.system.equipped) .reduce((sum, i) => sum + (i.system.encLoad ?? 0), 0) context.burdenUsed = burdenEquipped // Excess equipped burden reduces Movement Rating const excessBurden = Math.max(0, burdenEquipped - context.burdenMax) // Movement Rating computed value (excess burden adds to reduction) const mrSubAttr = doc.system.movementRating.subAttribute const mrSubVal = doc.system.subAttributes[mrSubAttr]?.value ?? 0 context.movementRatingValue = Math.max(0, 3 + mrSubVal + doc.system.movementRating.other - doc.system.movementRating.reduction - excessBurden ) context.excessBurden = excessBurden break case "skills": context.tab = context.tabs.skills context.skills = doc.itemTypes.skill context.racialAbilities = doc.itemTypes["racial-ability"] context.abilities = doc.itemTypes.ability context.vulnerabilities = doc.itemTypes.vulnerability ?? [] break case "subattributes": context.tab = context.tabs.subattributes break case "spells": context.tab = context.tabs.spells context.spells = doc.itemTypes.spell context.hasSpells = context.spells.length > 0 break case "combat": context.tab = context.tabs.combat context.weapons = doc.itemTypes.weapon context.armors = doc.itemTypes.armor context.shields = doc.itemTypes.shield break case "equipment": context.tab = context.tabs.equipment // All items that can be stored in containers const allStorable = [ ...doc.itemTypes.weapon, ...doc.itemTypes.armor, ...doc.itemTypes.shield, ...doc.itemTypes.equipment, ...doc.itemTypes.consumable, ...doc.itemTypes.loot, ] // Build a map: containerId → items[] const containerGroups = {} for (const container of doc.itemTypes.container) { containerGroups[container.id] = { container, items: [] } } for (const item of allStorable) { const cid = item.system.containerId if (cid && containerGroups[cid]) { containerGroups[cid].items.push(item) } } context.containerGroups = Object.values(containerGroups) // Items are "uncontained" if they have no containerId, or if their container was deleted const isUncontained = i => !i.system.containerId || !containerGroups[i.system.containerId] context.weapons = doc.itemTypes.weapon.filter(isUncontained) context.armors = [...doc.itemTypes.armor, ...doc.itemTypes.shield].filter(isUncontained) context.consumables = doc.itemTypes.consumable.filter(isUncontained) context.kits = doc.itemTypes.equipment.filter(i => i.system.isKit && isUncontained(i)) context.equipmentItems = doc.itemTypes.equipment.filter(i => !i.system.isKit && isUncontained(i)) context.loots = doc.itemTypes.loot.filter(isUncontained) context.containers = doc.itemTypes.container context.packBurdenMax = doc.itemTypes.container .filter(c => c.system.equipped) .reduce((sum, c) => sum + (c.system.packBurden ?? 0), 0) // Pack burden = items stored in an existing container context.packBurdenUsed = allStorable .filter(i => i.system.containerId && containerGroups[i.system.containerId]) .reduce((sum, i) => sum + (i.system.encLoad ?? 0), 0) break case "biography": context.tab = context.tabs.biography 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 } return context } // #region Drag-and-Drop Workflow /** * Callback actions which occur when a dragged element is dropped on a target. * @param {DragEvent} event The originating DragEvent * @protected */ async _onDrop(event) { if (!this.isEditable || !this.isEditMode) return const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) // Handle different data types if (data.type === "Item") { const item = await fromUuid(data.uuid) // Check if dropped onto a container row const containerEl = event.target.closest("[data-container-id]") if (containerEl && item?.parent === this.document) { const containerId = containerEl.dataset.containerId // Don't store containers inside containers if (item.type !== "container") { await item.update({ "system.containerId": containerId }) return } } return this._onDropItem(item) } } static async #onRollInitiative(event, target) { await this.document.system.rollInitiative() } static async #onArmorHitPointsPlus(event, target) { let armorHP = this.actor.system.combat.armorHitPoints armorHP += 1 this.actor.update({ "system.combat.armorHitPoints": armorHP }) } static async #onArmorHitPointsMinus(event, target) { let armorHP = this.actor.system.combat.armorHitPoints armorHP -= 1 this.actor.update({ "system.combat.armorHitPoints": Math.max(armorHP, 0) }) } static async #onManaPointsPlus(event, target) { let mana = this.actor.system.manaPoints.value mana += 1 this.actor.update({ "system.manaPoints.value": Math.min(mana, this.actor.system.manaPoints.max) }) } static async #onManaPointsMinus(event, target) { let mana = this.actor.system.manaPoints.value mana -= 1 this.actor.update({ "system.manaPoints.value": Math.max(mana, 0) }) } static async #onArmorPointsPlus(event, target) { let armor = this.actor.system.armorPoints.value armor += 1 this.actor.update({ "system.armorPoints.value": Math.min(armor, this.actor.system.armorPoints.max) }) } static async #onArmorPointsMinus(event, target) { let armor = this.actor.system.armorPoints.value armor -= 1 this.actor.update({ "system.armorPoints.value": Math.max(armor, 0) }) } static async#onActionPointsPlus(event, target) { let actionPoints = this.actor.system.actionPoints.value actionPoints += 1 this.actor.update({ "system.actionPoints.value": Math.min(actionPoints, this.actor.system.actionPoints.max) }) } static async#onActionPointsMinus(event, target) { let actionPoints = this.actor.system.actionPoints.value actionPoints -= 1 this.actor.update({ "system.actionPoints.value": Math.max(actionPoints, 0) }) } static async#onHpPlus(event, target) { let hp = this.actor.system.hp.value hp += 1 this.actor.update({ "system.hp.value": Math.min(hp, this.actor.system.hp.max) }) } static async#onHpMinus(event, target) { let hp = this.actor.system.hp.value hp -= 1 this.actor.update({ "system.hp.value": Math.max(hp, 0) }) } static async#onHpTempPlus(event, target) { const temp = this.actor.system.hp.temp this.actor.update({ "system.hp.temp": temp + 1 }) } static async#onHpTempMinus(event, target) { const temp = this.actor.system.hp.temp this.actor.update({ "system.hp.temp": Math.max(temp - 1, 0) }) } static async #onCreateEquipment(event, target) { const itemType = target.dataset.itemType ?? "equipment" const isKit = target.dataset.itemKit === "true" const typeLabel = game.i18n.localize(`TYPES.Item.${itemType}`) || itemType const itemData = { name: game.i18n.format("DOCUMENT.New", { type: typeLabel }), type: itemType, } if (isKit) itemData["system.isKit"] = true await this.document.createEmbeddedDocuments("Item", [itemData]) } static async #onUseConsumable(event, target) { const itemElement = target.closest("[data-item-id]") if (!itemElement) return const item = this.document.items.get(itemElement.dataset.itemId) if (!item) return if (item.system.uses <= 0) return const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.localize("PRISMRPG.Dialog.useConsumable") }, content: `

${game.i18n.format("PRISMRPG.Dialog.useConsumableContent", { name: item.name, uses: item.system.uses })}

`, }) if (!confirmed) return if (item.system.uses <= 1) { await item.delete() } else { await item.update({ "system.uses": item.system.uses - 1 }) } } static async #onToggleContainerEquipped(event, target) { const itemElement = target.closest("[data-item-id]") if (!itemElement) return const item = this.document.items.get(itemElement.dataset.itemId) if (!item) return await item.update({ "system.equipped": !item.system.equipped }) } static async #onToggleEquipped(event, target) { const itemElement = target.closest("[data-item-id]") if (!itemElement) return const item = this.document.items.get(itemElement.dataset.itemId) if (!item) return await item.update({ "system.equipped": !item.system.equipped }) } static async #onAssignToContainer(event, target) { const itemElement = target.closest("[data-item-id]") if (!itemElement) return const item = this.document.items.get(itemElement.dataset.itemId) if (!item || item.type === "container") return const containers = this.document.itemTypes.container if (!containers.length) { ui.notifications.warn(game.i18n.localize("PRISMRPG.Message.noContainers")) return } const options = containers.map(c => { const escapedName = foundry.utils.escapeHTML(c.name) return `` }).join("") const content = `
` const containerId = await foundry.applications.api.DialogV2.prompt({ window: { title: game.i18n.localize("PRISMRPG.Dialog.assignToContainer") }, classes: ["prismrpg"], content, ok: { callback: (event, button) => button.form.elements.containerId.value, }, }) if (containerId) await item.update({ "system.containerId": containerId }) } static async #onRemoveFromContainer(event, target) { const itemElement = target.closest("[data-item-id]") if (!itemElement) return const item = this.document.items.get(itemElement.dataset.itemId) if (!item) return await item.update({ "system.containerId": "" }) } static async #onPostItemToChat(event, target) { console.log("PRISM RPG | PostItemToChat action triggered", { event: event, target: target }) // Try to find the item element from the clicked target or its parents let itemElement = null // First try with the target (the actual clicked element) if (event.target) { itemElement = event.target.closest('[data-item-id]') } // If not found, try with currentTarget (the element with the action) if (!itemElement && event.currentTarget) { itemElement = event.currentTarget.closest('[data-item-id]') } // If still not found, try with the target parameter if (!itemElement && target) { itemElement = target.closest('[data-item-id]') } console.log("PRISM RPG | Found item element", { itemElement: itemElement }) if (!itemElement) { console.warn("PRISM RPG | Could not find item element for posting to chat") return } const itemId = itemElement.dataset.itemId if (!itemId) { console.warn("PRISM RPG | Item ID not found for posting to chat") return } const item = this.actor.items.get(itemId) if (!item) { console.warn("PRISM RPG | Item not found for posting to chat", { itemId: itemId }) return } // Create a chat message with the item data const speaker = ChatMessage.getSpeaker({ actor: this.actor }) const content = await this.formatItemForChat(item) await ChatMessage.create({ content: content, speaker: speaker, }) } async formatItemForChat(item) { // Format the item data for chat display const itemTypeClass = `${item.type}-item` let htmlContent = `

${item.name}

${game.i18n.localize(`TYPES.Item.${item.type}`) || item.type}
` // Add item image if available if (item.img && !item.img.includes('icons/svg/mystery-man.svg')) { htmlContent += `
${item.name}
` } // Add item description if available if (item.system.description && item.system.description.trim() !== '') { const enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.system.description, { async: true }) htmlContent += `
${enrichedDescription}
` } // Add specific item data based on item type htmlContent += `
` switch (item.type) { case 'weapon': htmlContent += `
Type: ${item.system.weaponType || 'Unknown'}
Damage: ${item.system.damage || 'N/A'}
APC: ${item.system.apc || 0}
` if (item.system.damageType) { const damageTypes = [] if (item.system.damageType.piercing) damageTypes.push('Piercing') if (item.system.damageType.bludgeoning) damageTypes.push('Bludgeoning') if (item.system.damageType.slashing) damageTypes.push('Slashing') if (damageTypes.length > 0) { htmlContent += `
Damage Type: ${damageTypes.join('/')}
` } } break case 'armor': htmlContent += `
Armor Type: ${item.system.armorType || 'Unknown'}
Armor Points: ${item.system.armorPoints || 0}
APC: ${item.system.apc || 0}
` break case 'shield': htmlContent += `
Shield Type: ${item.system.shieldType || 'Unknown'}
Armor Points: ${item.system.armorPoints || 0}
APC: ${item.system.apc || 0}
` break case 'skill': htmlContent += `
Modifier: ${item.system.modifier || 0}
Core Skill: ${item.system.isCoreSkill ? 'Yes' : 'No'}
` break case 'spell': htmlContent += `
Level: ${item.system.level || 'Unknown'}
Mana Cost: ${item.system.manaCost || 0}
Casting Time: ${item.system.castingTime || 'N/A'}
` break case 'miracle': htmlContent += `
Prayer Time: ${item.system.prayerTime || 'N/A'}
` break case 'equipment': htmlContent += `
Weight: ${item.system.weight || 'N/A'}
` break default: // For other item types, just show basic info htmlContent += `
Item Type: ${item.type}
` } htmlContent += `
` return htmlContent } _onRender(context, options) { // Inputs with class `item-quantity` const woundDescription = this.element.querySelectorAll('.wound-data') for (const input of woundDescription) { input.addEventListener("change", (e) => { e.preventDefault(); e.stopImmediatePropagation(); const newValue = e.currentTarget.value const index = e.currentTarget.dataset.index const fieldName = e.currentTarget.dataset.name let tab = foundry.utils.duplicate(this.actor.system.hp.wounds) tab[index][fieldName] = newValue console.log(tab, index, fieldName, newValue) this.actor.update({ "system.hp.wounds": tab }); }) } // Container drag-over highlight this.element.querySelectorAll("[data-container-id]").forEach(el => { el.addEventListener("dragover", (e) => { e.preventDefault() el.classList.add("drag-over") }) el.addEventListener("dragleave", () => el.classList.remove("drag-over")) el.addEventListener("drop", () => el.classList.remove("drag-over")) }) super._onRender(); } /** * Handles the roll action triggered by user interaction. * * @param {PointerEvent} event The event object representing the user interaction. * @param {HTMLElement} target The target element that triggered the roll. * * @returns {Promise} A promise that resolves when the roll action is complete. * * @throws {Error} Throws an error if the roll type is not recognized. * * @description This method checks the current mode (edit or not) and determines the type of roll * (save, resource, or damage) based on the target element's data attributes. It retrieves the * corresponding value from the document's system and performs the roll. */ async _onRoll(event, target) { if (this.isEditMode) return // Use closest to find the rollable element in case user clicked on a child const rollableElement = event.target.closest('.rollable') || event.target const rollType = rollableElement.dataset.rollType let rollKey = rollableElement.dataset.rollKey; let rollDice = rollableElement.dataset?.rollDice; this.actor.prepareRoll(rollType, rollKey, rollDice) } // #endregion }