ENhance actor sheet with roll messages

This commit is contained in:
2026-03-07 16:09:00 +01:00
parent 8dec307ced
commit 63da2ef664
32 changed files with 888 additions and 22 deletions
+158 -2
View File
@@ -1,4 +1,5 @@
import AwERoll from "./roll.mjs"
import { SYSTEM } from "../config/system.mjs"
export default class AwEActor extends Actor {
/** @override */
@@ -48,11 +49,15 @@ export default class AwEActor extends Actor {
bonus: f.system.knowledgeBonus ?? ""
})).filter(f => f.bonus !== "") ?? []
return AwERoll.prompt({
const { conditionBonus, conditionLabels } = this.#buildConditionOptions()
const roll = await AwERoll.prompt({
attributeKey: attrId,
modifier: attribute.mod ?? 0,
attributeBonus: attribute.bonus ?? 0,
knowledgeBonuses,
conditionBonus,
conditionLabels,
actorId: this.id,
actorName: this.name,
actorImage: this.img,
@@ -62,6 +67,28 @@ export default class AwEActor extends Actor {
damageType: weaponItem.system.damageType,
...options
})
// Remove consumed conditions
if (roll && this.statuses.has("edge")) await this.toggleStatusEffect("edge")
return roll
}
/**
* Roll weapon damage directly (no attack roll).
* @param {Item} weaponItem - The weapon item.
* @returns {Promise<Roll>}
*/
async rollDamage(weaponItem) {
const formula = weaponItem.system.damageFormula
if (!formula) return ui.notifications.warn(game.i18n.localize("AWEMMY.Weapon.NoDamageFormula"))
const roll = new Roll(formula)
await roll.evaluate()
const typeStr = weaponItem.system.damageType ? ` (${weaponItem.system.damageType})` : ""
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: `<strong>${weaponItem.name}</strong> — ${game.i18n.localize("AWEMMY.Weapon.DamageRoll")}${typeStr}`
})
return roll
}
/**
@@ -80,15 +107,144 @@ export default class AwEActor extends Actor {
bonus: f.system.knowledgeBonus ?? ""
})).filter(f => f.bonus !== "") ?? []
return AwERoll.prompt({
const { conditionBonus, conditionLabels } = this.#buildConditionOptions()
const roll = await AwERoll.prompt({
attributeKey: attributeId,
modifier: attribute.mod ?? 0,
attributeBonus: attribute.bonus ?? 0,
knowledgeBonuses,
conditionBonus,
conditionLabels,
actorId: this.id,
actorName: this.name,
actorImage: this.img,
...options
})
// Remove consumed conditions
if (roll && this.statuses.has("edge")) await this.toggleStatusEffect("edge")
return roll
}
#buildConditionOptions() {
let conditionBonus = 0
const conditionLabels = []
if (this.statuses.has("edge")) {
conditionBonus += 2
conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Edge"), bonus: 2 })
}
if (this.statuses.has("prone")) {
conditionBonus -= 2
conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Prone"), bonus: -2 })
}
if (this.statuses.has("jumbled")) {
conditionBonus -= 2
conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Jumbled"), bonus: -2 })
}
return { conditionBonus, conditionLabels }
}
/**
* Use a kit item: decrement charges and post a chat message.
* @param {string} kitId - The kit item ID.
*/
async useKit(kitId) {
const item = this.items.get(kitId)
if (!item) return
const charges = item.system.charges
if (charges.value <= 0) {
ui.notifications.warn(game.i18n.format("AWEMMY.Kit.Depleted", { name: item.name }))
return
}
await item.update({ "system.charges.value": charges.value - 1 })
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: this }),
content: `<p>${game.i18n.format("AWEMMY.Kit.Used", { name: item.name, value: charges.value - 1, max: charges.max })}</p>`
})
}
/**
* Use an ability item: check daily/FP constraints, deduct FP, mark used, post chat card.
* @param {string} abilityId - The ability item ID.
*/
async useAbility(abilityId) {
const item = this.items.get(abilityId)
if (!item) return
const sys = item.system
if (sys.usedToday) {
ui.notifications.warn(game.i18n.format("AWEMMY.Ability.AlreadyUsed", { name: item.name }))
return
}
if (sys.flowPointCost > 0) {
const fp = this.system.flowPoints.value
if (fp < sys.flowPointCost) {
ui.notifications.warn(game.i18n.format("AWEMMY.Ability.NotEnoughFP", { name: item.name, cost: sys.flowPointCost, current: fp }))
return
}
await this.update({ "system.flowPoints.value": fp - sys.flowPointCost })
}
const isDaily = sys.frequency?.toLowerCase().includes("day")
if (isDaily) await item.update({ "system.usedToday": true })
const abilityTypeLabel = game.i18n.localize(SYSTEM.ABILITY_TYPE[sys.abilityType]?.label ?? sys.abilityType)
const costLabel = game.i18n.localize(SYSTEM.ABILITY_COST[sys.cost]?.label ?? sys.cost)
const enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(sys.description ?? "", { async: true, relativeTo: item })
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-adventures-with-emmy/templates/ability-use.hbs",
{
name: item.name,
img: item.img,
costLabel,
abilityTypeLabel,
traits: sys.traits ?? [],
frequency: sys.frequency,
trigger: sys.trigger,
requirements: sys.requirements,
flowPointCost: sys.flowPointCost || 0,
description: enrichedDescription
}
)
await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor: this }), content })
}
/**
* Perform a long rest: restore HP, reset daily abilities, refill kits, post chat card.
* No confirmation dialog — caller is responsible for confirming if needed.
*/
async longRest() {
const sys = this.system
const updates = {}
const summary = []
const hpMissing = sys.hp.max - sys.hp.value
if (hpMissing > 0) {
updates["system.hp.value"] = sys.hp.max
summary.push(game.i18n.format("AWEMMY.Rest.HPRestored", { amount: hpMissing, max: sys.hp.max }))
}
if (Object.keys(updates).length > 0) await this.update(updates)
const dailyAbilities = this.itemTypes.ability.filter(i => i.system.usedToday)
if (dailyAbilities.length) {
await this.updateEmbeddedDocuments("Item", dailyAbilities.map(i => ({ _id: i.id, "system.usedToday": false })))
summary.push(game.i18n.format("AWEMMY.Rest.AbilitiesReset", { count: dailyAbilities.length }))
}
const depleted = this.itemTypes.kit.filter(i => i.system.charges.value < i.system.charges.max)
if (depleted.length) {
await this.updateEmbeddedDocuments("Item", depleted.map(i => ({ _id: i.id, "system.charges.value": i.system.charges.max })))
summary.push(game.i18n.format("AWEMMY.Rest.KitsReplenished", { count: depleted.length }))
}
const bulletList = summary.map(s => `<li>${s}</li>`).join("")
const content = `
<div class="awemmy-rest-message">
<h3><i class="fa-solid fa-moon"></i> ${game.i18n.format("AWEMMY.Rest.LongRestTitle", { name: this.name })}</h3>
${summary.length ? `<ul>${bulletList}</ul>` : `<p>${game.i18n.localize("AWEMMY.Rest.AlreadyRested")}</p>`}
</div>`
await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor: this }), content })
}
}