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
+199
View File
@@ -0,0 +1,199 @@
import { SYSTEM } from '../../config/system.mjs'
export let ActionHandler = null
Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
ActionHandler = class ActionHandler extends coreModule.api.ActionHandler {
/** @override */
async buildSystemActions(groupIds) {
this.actors = (!this.actor) ? this._getActors() : [this.actor]
this.actorType = this.actor?.type
if (this.actorType === 'character') {
await this.#buildCharacterActions()
} else if (this.actorType === 'creature') {
await this.#buildCreatureActions()
}
}
async #buildCharacterActions() {
await this.#buildAttributes()
await this.#buildHP()
await this.#buildFlow()
await this.#buildWeapons()
await this.#buildConditions()
await this.#buildAbilities()
await this.#buildKits()
await this.#buildUtility()
}
async #buildCreatureActions() {
await this.#buildAttributes()
await this.#buildHP()
}
async #buildAttributes() {
const actions = []
const attrKeys = ['agility', 'fitness', 'awareness', 'influence']
const labelKeys = {
agility: 'AWEMMY.Attribute.Agility',
fitness: 'AWEMMY.Attribute.Fitness',
awareness: 'AWEMMY.Attribute.Awareness',
influence: 'AWEMMY.Attribute.Influence'
}
for (const key of attrKeys) {
const attr = this.actor.system.attributes?.[key]
if (!attr) continue
const mod = attr.mod ?? 0
const modText = mod >= 0 ? `+${mod}` : `${mod}`
actions.push({
name: coreModule.api.Utils.i18n(labelKeys[key]),
id: key,
info1: { text: modText },
encodedValue: ['attribute', key].join(this.delimiter)
})
}
await this.addActions(actions, { id: 'attributes', type: 'system' })
}
async #buildHP() {
const hp = this.actor.system.hp
if (!hp) return
const tooltip = { content: `${hp.value} / ${hp.max}`, direction: 'LEFT' }
const actions = [
{
name: `${hp.value} / ${hp.max}`,
id: 'hp_display',
tooltip,
encodedValue: ['hp', 'display'].join(this.delimiter)
},
{
name: '+',
id: 'hp_add',
tooltip,
encodedValue: ['hp', 'add'].join(this.delimiter)
},
{
name: '',
id: 'hp_sub',
tooltip,
encodedValue: ['hp', 'sub'].join(this.delimiter)
}
]
await this.addActions(actions, { id: 'hp', type: 'system' })
}
async #buildFlow() {
const fp = this.actor.system.flowPoints
if (fp === undefined) return
const tooltip = { content: `FP: ${fp.value}`, direction: 'LEFT' }
const actions = [
{
name: `${fp.value} FP`,
id: 'flow_display',
tooltip,
encodedValue: ['flow', 'display'].join(this.delimiter)
},
{
name: '+',
id: 'flow_add',
tooltip,
encodedValue: ['flow', 'add'].join(this.delimiter)
},
{
name: '',
id: 'flow_sub',
tooltip,
encodedValue: ['flow', 'sub'].join(this.delimiter)
}
]
await this.addActions(actions, { id: 'flow', type: 'system' })
}
async #buildWeapons() {
const weapons = this.actor.itemTypes?.weapon ?? []
for (const weapon of weapons) {
const attrId = weapon.system.attackAttribute
const attr = this.actor.system.attributes?.[attrId]
const mod = attr?.mod ?? 0
const modText = mod >= 0 ? `+${mod}` : `${mod}`
const groupData = { id: `weapon_${weapon.id}`, name: weapon.name, type: 'system' }
this.addGroup(groupData, { id: 'weapons', type: 'system' }, true)
const actions = [{
name: weapon.name,
id: `weapon_${weapon.id}`,
info1: { text: modText },
encodedValue: ['weapon', weapon.id].join(this.delimiter)
}]
await this.addActions(actions, { id: `weapon_${weapon.id}`, type: 'system' })
}
}
async #buildConditions() {
const actions = []
for (const [key, cond] of Object.entries(SYSTEM.CONDITIONS)) {
const isActive = this.actor.statuses?.has(key) ?? false
actions.push({
name: coreModule.api.Utils.i18n(cond.label),
id: key,
info1: { text: isActive ? '✓' : '' },
encodedValue: ['condition', key].join(this.delimiter)
})
}
await this.addActions(actions, { id: 'conditions', type: 'system' })
}
async #buildAbilities() {
const abilities = this.actor.itemTypes?.ability ?? []
const actions = []
for (const ability of abilities) {
const sys = ability.system
const costLabel = game.i18n.localize(SYSTEM.ABILITY_COST[sys.cost]?.label ?? sys.cost)
const isUsed = sys.usedToday
actions.push({
name: ability.name,
id: ability.id,
info1: { text: isUsed ? `${costLabel}` : costLabel },
encodedValue: ['ability', ability.id].join(this.delimiter)
})
}
await this.addActions(actions, { id: 'abilities', type: 'system' })
}
async #buildKits() {
const kits = this.actor.itemTypes?.kit ?? []
const actions = []
for (const kit of kits) {
const charges = kit.system.charges
const chargesText = `${charges.value}/${charges.max}`
actions.push({
name: kit.name,
id: kit.id,
info1: { text: chargesText },
encodedValue: ['kit', kit.id].join(this.delimiter)
})
}
await this.addActions(actions, { id: 'kits', type: 'system' })
}
async #buildUtility() {
const actions = []
actions.push({
name: coreModule.api.Utils.i18n('AWEMMY.TAH.LongRest'),
id: 'longRest',
encodedValue: ['utility', 'longRest'].join(this.delimiter)
})
if (game.combat?.current?.tokenId === this.token?.id) {
actions.push({
name: coreModule.api.Utils.i18n('AWEMMY.TAH.EndTurn'),
id: 'endTurn',
encodedValue: ['utility', 'endTurn'].join(this.delimiter)
})
}
await this.addActions(actions, { id: 'utility', type: 'system' })
}
}
})
+14
View File
@@ -0,0 +1,14 @@
export const MODULE = { ID: 'token-action-hud-adventures-with-emmy' }
export const CORE_MODULE = { ID: 'token-action-hud-core' }
export const REQUIRED_CORE_MODULE_VERSION = '2.0'
export const GROUP = {
attributes: { id: 'attributes', name: 'AWEMMY.Character.Attributes', type: 'system' },
hp: { id: 'hp', name: 'AWEMMY.TAH.HP', type: 'system' },
flow: { id: 'flow', name: 'AWEMMY.TAH.Flow', type: 'system' },
weapons: { id: 'weapons', name: 'AWEMMY.TAH.Weapons', type: 'system' },
conditions: { id: 'conditions', name: 'AWEMMY.Condition.Panel', type: 'system' },
abilities: { id: 'abilities', name: 'AWEMMY.TAH.Abilities', type: 'system' },
kits: { id: 'kits', name: 'AWEMMY.TAH.Kits', type: 'system' },
utility: { id: 'utility', name: 'AWEMMY.TAH.Utility', type: 'system' }
}
+54
View File
@@ -0,0 +1,54 @@
import { GROUP } from './constants.js'
export let DEFAULTS = null
Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
const groups = foundry.utils.deepClone(GROUP)
Object.values(groups).forEach(group => {
group.name = coreModule.api.Utils.i18n(group.name)
group.listName = `Group: ${coreModule.api.Utils.i18n(group.listName ?? group.name)}`
})
const groupsArray = Object.values(groups)
DEFAULTS = {
layout: [
{
nestId: 'stats',
id: 'stats',
name: game.i18n.localize('AWEMMY.TAH.Stats'),
groups: [
{ ...groups.attributes, nestId: 'stats_attributes' },
{ ...groups.hp, nestId: 'stats_hp' },
{ ...groups.flow, nestId: 'stats_flow' }
]
},
{
nestId: 'combat',
id: 'combat',
name: game.i18n.localize('AWEMMY.TAH.Combat'),
groups: [
{ ...groups.weapons, nestId: 'combat_weapons' },
{ ...groups.conditions, nestId: 'combat_conditions' }
]
},
{
nestId: 'items',
id: 'items',
name: game.i18n.localize('AWEMMY.TAH.Items'),
groups: [
{ ...groups.abilities, nestId: 'items_abilities' },
{ ...groups.kits, nestId: 'items_kits' }
]
},
{
nestId: 'utility',
id: 'utility',
name: game.i18n.localize('AWEMMY.TAH.Utility'),
groups: [
{ ...groups.utility, nestId: 'utility_utility' }
]
}
],
groups: groupsArray
}
})
+92
View File
@@ -0,0 +1,92 @@
export let RollHandler = null
Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
RollHandler = class RollHandler extends coreModule.api.RollHandler {
/** @override */
async handleActionClick(event, encodedValue) {
const [actionTypeId, actionId] = encodedValue.split(this.delimiter ?? '|')
if (this.actor) {
await this.#handleAction(event, this.actor, this.token, actionTypeId, actionId)
return
}
const knownTypes = ['character', 'creature']
for (const token of canvas.tokens.controlled.filter(t => knownTypes.includes(t.actor?.type))) {
await this.#handleAction(event, token.actor, token, actionTypeId, actionId)
}
}
/** @override */
async handleActionHover(event, encodedValue) {}
/** @override */
async handleGroupClick(event, group) {}
async #handleAction(event, actor, token, actionTypeId, actionId) {
switch (actionTypeId) {
case 'attribute':
await actor.rollAttribute(actionId)
break
case 'hp':
await this.#handleHP(actor, actionId)
break
case 'flow':
await this.#handleFlow(actor, actionId)
break
case 'weapon':
await this.#handleWeapon(actor, actionId)
break
case 'condition':
await actor.toggleStatusEffect(actionId)
break
case 'ability':
await actor.useAbility(actionId)
break
case 'kit':
await actor.useKit(actionId)
break
case 'utility':
await this.#handleUtility(actor, token, actionId)
break
}
}
async #handleHP(actor, actionId) {
if (actionId === 'display') return
const hp = actor.system.hp
if (!hp) return
const newValue = actionId === 'add' ? hp.value + 1 : hp.value - 1
if (newValue < 0 || newValue > hp.max) return
await actor.update({ 'system.hp.value': newValue })
}
async #handleFlow(actor, actionId) {
if (actionId === 'display') return
const fp = actor.system.flowPoints
if (fp === undefined) return
const newValue = actionId === 'add' ? fp.value + 1 : Math.max(0, fp.value - 1)
await actor.update({ 'system.flowPoints.value': newValue })
}
async #handleWeapon(actor, actionId) {
const weapon = actor.items.get(actionId)
if (!weapon) return
await actor.rollWeapon(weapon)
}
async #handleUtility(actor, token, actionId) {
switch (actionId) {
case 'longRest':
await actor.longRest()
break
case 'endTurn':
if (game.combat?.current?.tokenId === token?.id) {
await game.combat.nextTurn()
}
break
}
}
}
})
+8
View File
@@ -0,0 +1,8 @@
/**
* Register module settings.
* Called by Token Action HUD Core to register Token Action HUD system module settings.
* @param {function} coreUpdate Token Action HUD Core update function
*/
export function register(coreUpdate) {
// No system-specific settings for now
}
+46
View File
@@ -0,0 +1,46 @@
import { ActionHandler } from './action-handler.js'
import { RollHandler as Core } from './roll-handler.js'
import { MODULE } from './constants.js'
import { DEFAULTS } from './defaults.js'
import * as systemSettings from './settings.js'
export let SystemManager = null
Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
SystemManager = class SystemManager extends coreModule.api.SystemManager {
/** @override */
getActionHandler() {
return new ActionHandler()
}
/** @override */
getAvailableRollHandlers() {
return { core: 'Adventures with Emmy' }
}
/** @override */
getRollHandler(rollHandlerId) {
switch (rollHandlerId) {
case 'core':
default:
return new Core()
}
}
/** @override */
async registerDefaults() {
return DEFAULTS
}
/** @override */
registerSettings(coreUpdate) {
systemSettings.register(coreUpdate)
}
/** @override */
registerStyles() {
return {}
}
}
})
@@ -86,6 +86,8 @@ export default class AwEActorSheet extends HandlebarsApplicationMixin(foundry.ap
*/
async _onRoll(event) {
if (this.isEditMode) return
// Skip if the element has a registered data-action (handled by the action system)
if (event.currentTarget.dataset.action) return
const attributeId = event.currentTarget.dataset.attributeId
if (!attributeId) return
await this.document.rollAttribute(attributeId)
@@ -233,6 +235,7 @@ export default class AwEActorSheet extends HandlebarsApplicationMixin(foundry.ap
static async #onItemDelete(event, target) {
const itemUuid = target.getAttribute("data-item-uuid")
const item = await fromUuid(itemUuid)
if (!item) return
await item.deleteDialog()
}
+63 -3
View File
@@ -20,7 +20,13 @@ export default class AwECharacterSheet extends AwEActorSheet {
flowPointsPlus: AwECharacterSheet.#onFlowPointsPlus,
flowPointsMinus: AwECharacterSheet.#onFlowPointsMinus,
rollField: AwECharacterSheet.#onRollField,
rollWeapon: AwECharacterSheet.#onRollWeapon
rollWeapon: AwECharacterSheet.#onRollWeapon,
rollDamage: AwECharacterSheet.#onRollDamage,
toggleCondition: AwECharacterSheet.#onToggleCondition,
useKit: AwECharacterSheet.#onUseKit,
useAbility: AwECharacterSheet.#onUseAbility,
dailyReset: AwECharacterSheet.#onDailyReset,
longRest: AwECharacterSheet.#onLongRest
}
}
@@ -84,7 +90,15 @@ export default class AwECharacterSheet extends AwEActorSheet {
name: item.name,
img: item.img,
system: item.system,
costLabel: game.i18n.localize(SYSTEM.ABILITY_COST[item.system.cost]?.label ?? item.system.cost)
costLabel: game.i18n.localize(SYSTEM.ABILITY_COST[item.system.cost]?.label ?? item.system.cost),
usedToday: item.system.usedToday
}))
context.hasUsedAbilities = context.abilities.some(a => a.usedToday)
context.conditions = Object.values(SYSTEM.CONDITIONS).map(c => ({
...c,
label: game.i18n.localize(c.label),
img: `systems/fvtt-adventures-with-emmy/assets/conditions/${c.id}.svg`,
active: doc.statuses.has(c.id)
}))
break
case "biography":
@@ -147,7 +161,10 @@ export default class AwECharacterSheet extends AwEActorSheet {
// field/background/specialization: max 1 (replace existing); archetype: multiple allowed
if (item.type === "field" || item.type === "background" || item.type === "specialization") {
const existing = this.document.itemTypes[item.type]
if (existing.length > 0) await existing[0].delete()
if (existing.length > 0) {
ui.notifications.info(game.i18n.format("AWEMMY.Character.ItemReplaced", { name: existing[0].name }))
await existing[0].delete()
}
return this.document.createEmbeddedDocuments("Item", [item.toObject()])
}
if (item.type === "archetype") {
@@ -248,8 +265,51 @@ export default class AwECharacterSheet extends AwEActorSheet {
await this.document.rollWeapon(item)
}
static async #onRollDamage(event, target) {
const itemId = target.closest("[data-item-id]")?.dataset.itemId
const item = this.document.items.get(itemId)
if (!item) return
await this.document.rollDamage(item)
}
/** Slugify a string for loose name matching (lowercase, trim, spaces→dash, strip non-alphanum). */
static #slugify(str) {
return (str ?? "").toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "")
}
static async #onToggleCondition(event, target) {
const conditionId = target.dataset.conditionId
await this.document.toggleStatusEffect(conditionId)
}
static async #onUseKit(event, target) {
const itemId = target.closest("[data-item-id]")?.dataset.itemId
await this.document.useKit(itemId)
}
static async #onUseAbility(event, target) {
const itemId = target.closest("[data-item-id]")?.dataset.itemId
await this.document.useAbility(itemId)
}
static async #onDailyReset(event, target) {
const actor = this.document
const dailyAbilities = actor.itemTypes.ability.filter(i => i.system.usedToday)
if (!dailyAbilities.length) return
const updates = dailyAbilities.map(i => ({ _id: i.id, "system.usedToday": false }))
await actor.updateEmbeddedDocuments("Item", updates)
ui.notifications.info(game.i18n.localize("AWEMMY.Ability.DailyResetDone"))
}
static async #onLongRest(event, target) {
const actor = this.document
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { title: game.i18n.localize("AWEMMY.Rest.LongRest") },
content: `<p>${game.i18n.format("AWEMMY.Rest.LongRestConfirm", { name: actor.name })}</p>`,
yes: { label: game.i18n.localize("AWEMMY.Rest.Rest"), icon: "fa-solid fa-moon" },
no: { label: game.i18n.localize("AWEMMY.Rest.Cancel") }
})
if (!confirmed) return
await actor.longRest()
}
}
@@ -55,7 +55,7 @@ export default class AwECreatureSheet extends AwEActorSheet {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description, { async: true }
this.document.system.description ?? "", { async: true }
)
return context
}