abea77906d
- Add spell roll functionality to character sheets - Enhance CSS and LESS styling for better visual presentation - Update character templates and models - Remove old backup files (roll-old.mjs, roll.mjs.backup) - Improve character combat and equipment templates - Update utility functions and actor documents Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
425 lines
15 KiB
JavaScript
425 lines
15 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,
|
|
postItemToChat: PrismRPGCharacterSheet.#onPostItemToChat,
|
|
},
|
|
}
|
|
|
|
/** @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
|
|
]
|
|
break
|
|
case "skills":
|
|
context.tab = context.tabs.skills
|
|
context.skills = doc.itemTypes.skill
|
|
context.racialAbilities = doc.itemTypes["racial-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.equipments = doc.itemTypes.equipment
|
|
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 #onCreateEquipment(event, target) {
|
|
}
|
|
|
|
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
|
|
}
|