530 lines
20 KiB
JavaScript
530 lines
20 KiB
JavaScript
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,
|
|
},
|
|
}
|
|
|
|
/** @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<string, Partial<ApplicationTab>>}
|
|
*/
|
|
#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
|
|
context.weapons = doc.itemTypes.weapon
|
|
context.armors = [...doc.itemTypes.armor, ...doc.itemTypes.shield]
|
|
context.consumables = doc.itemTypes.consumable
|
|
context.kits = doc.itemTypes.equipment.filter(i => i.system.isKit)
|
|
context.equipmentItems = doc.itemTypes.equipment.filter(i => !i.system.isKit)
|
|
context.loots = doc.itemTypes.loot
|
|
context.containers = doc.itemTypes.container
|
|
context.packBurdenMax = doc.itemTypes.container
|
|
.filter(c => c.system.equipped)
|
|
.reduce((sum, c) => sum + (c.system.packBurden ?? 0), 0)
|
|
context.packBurdenUsed = [
|
|
...doc.itemTypes.equipment,
|
|
...doc.itemTypes.consumable,
|
|
...doc.itemTypes.loot,
|
|
...doc.itemTypes.container,
|
|
...doc.itemTypes.weapon,
|
|
...doc.itemTypes.armor,
|
|
...doc.itemTypes.shield,
|
|
].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)
|
|
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: `<p>${game.i18n.format("PRISMRPG.Dialog.useConsumableContent", { name: item.name, uses: item.system.uses })}</p>`,
|
|
})
|
|
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 #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 = `
|
|
<div class="prismrpg-item-chat-card ${itemTypeClass}">
|
|
<div class="item-header">
|
|
<h3>${item.name}</h3>
|
|
<span class="item-type">${game.i18n.localize(`TYPES.Item.${item.type}`) || item.type}</span>
|
|
</div>
|
|
`
|
|
|
|
// Add item image if available
|
|
if (item.img && !item.img.includes('icons/svg/mystery-man.svg')) {
|
|
htmlContent += `<div class="item-image"><img src="${item.img}" alt="${item.name}"></div>`
|
|
}
|
|
|
|
// 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 += `<div class="item-description">${enrichedDescription}</div>`
|
|
}
|
|
|
|
// Add specific item data based on item type
|
|
htmlContent += `<div class="item-details">`
|
|
|
|
switch (item.type) {
|
|
case 'weapon':
|
|
htmlContent += `
|
|
<div class="item-detail"><strong>Type:</strong> ${item.system.weaponType || 'Unknown'}</div>
|
|
<div class="item-detail"><strong>Damage:</strong> ${item.system.damage || 'N/A'}</div>
|
|
<div class="item-detail"><strong>APC:</strong> ${item.system.apc || 0}</div>
|
|
`
|
|
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 += `<div class="item-detail"><strong>Damage Type:</strong> ${damageTypes.join('/')}</div>`
|
|
}
|
|
}
|
|
break
|
|
|
|
case 'armor':
|
|
htmlContent += `
|
|
<div class="item-detail"><strong>Armor Type:</strong> ${item.system.armorType || 'Unknown'}</div>
|
|
<div class="item-detail"><strong>Armor Points:</strong> ${item.system.armorPoints || 0}</div>
|
|
<div class="item-detail"><strong>APC:</strong> ${item.system.apc || 0}</div>
|
|
`
|
|
break
|
|
|
|
case 'shield':
|
|
htmlContent += `
|
|
<div class="item-detail"><strong>Shield Type:</strong> ${item.system.shieldType || 'Unknown'}</div>
|
|
<div class="item-detail"><strong>Armor Points:</strong> ${item.system.armorPoints || 0}</div>
|
|
<div class="item-detail"><strong>APC:</strong> ${item.system.apc || 0}</div>
|
|
`
|
|
break
|
|
|
|
case 'skill':
|
|
htmlContent += `
|
|
<div class="item-detail"><strong>Modifier:</strong> ${item.system.modifier || 0}</div>
|
|
<div class="item-detail"><strong>Core Skill:</strong> ${item.system.isCoreSkill ? 'Yes' : 'No'}</div>
|
|
`
|
|
break
|
|
|
|
case 'spell':
|
|
htmlContent += `
|
|
<div class="item-detail"><strong>Level:</strong> ${item.system.level || 'Unknown'}</div>
|
|
<div class="item-detail"><strong>Mana Cost:</strong> ${item.system.manaCost || 0}</div>
|
|
<div class="item-detail"><strong>Casting Time:</strong> ${item.system.castingTime || 'N/A'}</div>
|
|
`
|
|
break
|
|
|
|
case 'miracle':
|
|
htmlContent += `
|
|
<div class="item-detail"><strong>Prayer Time:</strong> ${item.system.prayerTime || 'N/A'}</div>
|
|
`
|
|
break
|
|
|
|
case 'equipment':
|
|
htmlContent += `
|
|
<div class="item-detail"><strong>Weight:</strong> ${item.system.weight || 'N/A'}</div>
|
|
`
|
|
break
|
|
|
|
default:
|
|
// For other item types, just show basic info
|
|
htmlContent += `<div class="item-detail"><strong>Item Type:</strong> ${item.type}</div>`
|
|
}
|
|
|
|
htmlContent += `</div></div>`
|
|
|
|
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 });
|
|
})
|
|
}
|
|
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<void>} 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
|
|
}
|