Add spells rolls and enhance CSS styling

- 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>
This commit is contained in:
2026-01-21 13:56:09 +01:00
parent 7283f5f15b
commit abea77906d
15 changed files with 519 additions and 2283 deletions
+184 -7
View File
@@ -18,10 +18,15 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet {
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,
},
}
@@ -158,43 +163,215 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet {
await this.document.system.rollInitiative()
}
static #onArmorHitPointsPlus(event, target) {
static async #onArmorHitPointsPlus(event, target) {
let armorHP = this.actor.system.combat.armorHitPoints
armorHP += 1
this.actor.update({ "system.combat.armorHitPoints": armorHP })
}
static #onArmorHitPointsMinus(event, target) {
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 #onManaPointsPlus(event, target) {
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 #onManaPointsMinus(event, target) {
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 #onHpPlus(event, target) {
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 #onHpMinus(event, target) {
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 #onCreateEquipment(event, target) {
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) {
+2 -26
View File
@@ -159,38 +159,14 @@ export default class PrismRPGActor extends Actor {
case "weapon-damage-medium":
case "weapon-attack": {
let weapon = this.items.find((i) => i.type === "weapon" && i.id === rollKey)
let skill
let skills = this.items.filter((i) => i.type === "skill" && i.name.toLowerCase() === weapon.name.toLowerCase())
if (skills.length > 0) {
skill = this.getBestWeaponClassSkill(skills, rollType, 1.0)
} else {
skills = this.items.filter((i) => i.type === "skill" && i.name.toLowerCase().replace(" skill", "") === weapon.name.toLowerCase())
if (skills.length > 0) {
skill = this.getBestWeaponClassSkill(skills, rollType, 1.0)
} else {
skills = this.items.filter((i) => i.type === "skill" && i.system.weaponClass === weapon.system.weaponClass)
if (skills.length > 0) {
skill = this.getBestWeaponClassSkill(skills, rollType, 0.5)
} else {
skills = this.items.filter((i) => i.type === "skill" && i.system.weaponClass.includes(SYSTEM.WEAPON_CATEGORIES[weapon.system.weaponClass]))
if (skills.length > 0) {
skill = this.getBestWeaponClassSkill(skills, rollType, 0.25)
} else {
ui.notifications.warn(game.i18n.localize("PRISMRPG.Notifications.skillNotFound"))
return
}
}
}
}
if (!weapon || !skill) {
console.error("Weapon or skill not found", weapon, skill)
if (!weapon) {
console.error("Weapon not found", weapon, skill)
ui.notifications.warn(game.i18n.localize("PRISMRPG.Notifications.skillNotFound"))
return
}
// Create a plain object for rollTarget to ensure weapon data is preserved
rollTarget = {
...skill.toObject(),
weapon: weapon.toObject(),
rollKey: rollKey,
combat: foundry.utils.duplicate(this.system.combat),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+51 -51
View File
@@ -89,6 +89,11 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel {
max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
schema.actionPoints = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
schema.biodata = new fields.SchemaField({
level: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 }),
alignment: new fields.StringField({ required: true, nullable: false, initial: "" }),
@@ -165,6 +170,21 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel {
prepareDerivedData() {
super.prepareDerivedData();
// Calculate action points max based on level
const level = this.biodata.level
let actionPointsMax = 4
if (level >= 3 && level <= 5) {
actionPointsMax = 5
} else if (level >= 6 && level <= 8) {
actionPointsMax = 6
} else if (level >= 9 && level <= 10) {
actionPointsMax = 7
}
// Set max action points (but don't override if already set to a higher value)
if (this.actionPoints.max < actionPointsMax) {
this.actionPoints.max = actionPointsMax
}
// Calculate sub-attributes from parent characteristics
// Sub-attribute = lowest ability modifier between the two parent characteristics
for (let subAttrKey in SYSTEM.SUB_ATTRIBUTES) {
@@ -241,66 +261,46 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel {
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
/**
* Rolls initiative for the character: 1d20 + initiative modifier
* @param {string} combatId - Optional combat ID to update
* @param {string} combatantId - Optional combatant ID to update
* @returns {Promise<Roll|null>} The initiative roll or null if cancelled
*/
async rollInitiative(combatId = undefined, combatantId = undefined) {
const hasTarget = false
let actorClass = this.biodata.class;
// Get the initiative sub-attribute modifier
const initiativeModifier = this.subAttributes.initiative.value
let wisDef = SYSTEM.CHARACTERISTICS_TABLES.wis.find((c) => c.value === this.characteristics.wis.value)
let maxInit = Number(wisDef.init_cap) || 1000
// Create the roll formula: 1d20 + initiative modifier
const formula = `1d20 + ${initiativeModifier}`
let roll = await PrismRPGRoll.promptInitiative({
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
combatId,
combatantId,
actorClass,
maxInit,
// Roll the initiative
let initRoll = new Roll(formula)
await initRoll.evaluate()
// Create the chat message
let msg = await initRoll.toMessage({
flavor: `${game.i18n.localize("PRISMRPG.Label.initiative")} - ${this.parent.name}`,
speaker: ChatMessage.getSpeaker({ actor: this.parent })
})
if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
async rollProgressionDice(combatId, combatantId, rollProgressionCount) {
// Get all weapons from the actor
let weapons = this.parent.items.filter(i => i.type === "weapon" && i.system.weaponType === "melee")
let weaponsChoices = weapons.map(w => { return { id: w.id, name: `${w.name} (${w.system.combatProgressionDice.toUpperCase()})`, combatProgressionDice: w.system.combatProgressionDice.toUpperCase() } })
let rangeWeapons = this.parent.items.filter(i => i.type === "weapon" && i.system.weaponType === "ranged")
for (let w of rangeWeapons) {
weaponsChoices.push({ id: `${w.id}simpleAim`, name: `${w.name} (Simple Aim: ${w.system.speed.simpleAim.toUpperCase()})`, combatProgressionDice: w.system.speed.simpleAim.toUpperCase() })
weaponsChoices.push({ id: `${w.id}carefulAim`, name: `${w.name} (Careful Aim: ${w.system.speed.carefulAim.toUpperCase()})`, combatProgressionDice: w.system.speed.carefulAim.toUpperCase() })
weaponsChoices.push({ id: `${w.id}focusedAim`, name: `${w.name} (Focused Aim: ${w.system.speed.focusedAim.toUpperCase()})`, combatProgressionDice: w.system.speed.focusedAim.toUpperCase() })
// Wait for 3D dice animation if enabled
if (game?.dice3d) {
await game.dice3d.waitFor3DAnimationByMessageID(msg.id)
}
if (this.biodata.magicUser || this.biodata.clericUser) {
let spells = this.parent.items.filter(i => i.type === "spell" || i.type === "miracle")
for (let s of spells) {
let title = ""
let formula = ""
if (s.type === "spell") {
let dice = PrismRPGUtils.getLethargyDice(s.system.level)
title = `${s.name} (Casting time: ${s.system.castingTime}, Lethargy: ${dice})`
formula = `${s.system.castingTime}+${dice}`
} else {
title = `${s.name} (Prayer time: ${s.system.prayerTime})`
formula = `${s.system.prayerTime}`
}
weaponsChoices.push({ id: s.id, name: title, combatProgressionDice: formula })
// Update the combatant's initiative if in combat
if (combatId && combatantId) {
let combat = game.combats.get(combatId)
if (combat) {
await combat.updateEmbeddedDocuments("Combatant", [{
_id: combatantId,
initiative: initRoll.total
}])
}
}
let roll = await PrismRPGRoll.promptCombatAction({
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
weaponsChoices,
combatId,
combatantId,
rollProgressionCount,
type: "progression",
})
return initRoll
}
}
+5
View File
@@ -177,6 +177,11 @@ export default class PrismRPGUtils {
return str ? str.toUpperCase() : '';
})
Handlebars.registerHelper('replace', function (str, search, replacement) {
if (!str) return '';
return str.replace(search, replacement);
})
Handlebars.registerHelper('isEnabled', function (configKey) {
return game.settings.get("bol", configKey);
})