Files
fvtt-cthulhu-eternal/module/utils.mjs
T
2026-06-14 23:25:24 +02:00

742 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import CthulhuEternalRoll from "./documents/roll.mjs"
import { SystemManager } from './applications/hud/system-manager.js'
import { SYSTEM } from "./config/system.mjs"
export default class CthulhuEternalUtils {
static registerSettings() {
game.settings.register("fvtt-cthulhu-eternal", "settings-era", {
name: game.i18n.localize("CTHULHUETERNAL.Settings.era"),
hint: game.i18n.localize("CTHULHUETERNAL.Settings.eraHint"),
default: "jazz",
scope: "world",
type: String,
choices: SYSTEM.AVAILABLE_SETTINGS,
config: true,
onChange: _ => window.location.reload()
});
game.settings.register("fvtt-cthulhu-eternal", "settings-nudge", {
name: game.i18n.localize("CTHULHUETERNAL.Settings.nudge"),
hint: game.i18n.localize("CTHULHUETERNAL.Settings.nudgeHint"),
default: true,
scope: "world",
type: Boolean,
config: true
});
game.settings.register("fvtt-cthulhu-eternal", "roll-opposed-store", {
name: "Roll Opposed Store",
hint: "Whether to store opposed roll results for later use",
default: { roll1: null, roll2: null },
scope: "world",
type: Object,
config: false
});
}
static async loadCompendiumData(compendium) {
const pack = game.packs.get(compendium)
return await pack?.getDocuments() ?? []
}
static async loadCompendium(compendium, filter = item => true) {
let compendiumData = await CthulhuEternalUtils.loadCompendiumData(compendium)
return compendiumData.filter(filter)
}
static registerHandlebarsHelpers() {
Handlebars.registerHelper('isNull', function (val) {
return val == null;
});
Handlebars.registerHelper('exists', function (val) {
return val != null && val !== undefined;
});
Handlebars.registerHelper('isEmpty', function (list) {
if (list) return list.length === 0;
else return false;
});
Handlebars.registerHelper('notEmpty', function (list) {
return list.length > 0;
});
Handlebars.registerHelper('isNegativeOrNull', function (val) {
return val <= 0;
});
Handlebars.registerHelper('isNegative', function (val) {
return val < 0;
});
Handlebars.registerHelper('isPositive', function (val) {
return val > 0;
});
Handlebars.registerHelper('equals', function (val1, val2) {
return val1 === val2;
});
Handlebars.registerHelper('neq', function (val1, val2) {
return val1 !== val2;
});
Handlebars.registerHelper('gt', function (val1, val2) {
return val1 > val2;
})
Handlebars.registerHelper('lt', function (val1, val2) {
return val1 < val2;
})
Handlebars.registerHelper('gte', function (val1, val2) {
return val1 >= val2;
})
Handlebars.registerHelper('lte', function (val1, val2) {
return val1 <= val2;
})
Handlebars.registerHelper('and', function (val1, val2) {
return val1 && val2;
})
Handlebars.registerHelper('or', function (val1, val2) {
return val1 || val2;
})
Handlebars.registerHelper('or3', function (val1, val2, val3) {
return val1 || val2 || val3;
})
Handlebars.registerHelper('for', function (from, to, incr, block) {
let accum = '';
for (let i = from; i < to; i += incr)
accum += block.fn(i);
return accum;
})
Handlebars.registerHelper('not', function (cond) {
return !cond;
})
Handlebars.registerHelper('count', function (list) {
return list.length;
})
Handlebars.registerHelper('countKeys', function (obj) {
return Object.keys(obj).length;
})
Handlebars.registerHelper('split', function (str, separator, keep) {
return str.split(separator)[keep];
})
// If you need to add Handlebars helpers, here are a few useful examples:
Handlebars.registerHelper('concat', function () {
let outStr = '';
for (let arg in arguments) {
if (typeof arguments[arg] != 'object') {
outStr += arguments[arg];
}
}
return outStr;
})
Handlebars.registerHelper('add', function (a, b) {
return parseInt(a) + parseInt(b);
});
Handlebars.registerHelper('mul', function (a, b) {
return parseInt(a) * parseInt(b);
})
Handlebars.registerHelper('sub', function (a, b) {
return parseInt(a) - parseInt(b);
})
Handlebars.registerHelper('abbrev2', function (a) {
return a.substring(0, 2);
})
Handlebars.registerHelper('abbrev3', function (a) {
return a.substring(0, 3);
})
Handlebars.registerHelper('valueAtIndex', function (arr, idx) {
return arr[idx];
})
Handlebars.registerHelper('includesKey', function (items, type, key) {
return items.filter(i => i.type === type).map(i => i.system.key).includes(key);
})
Handlebars.registerHelper('includes', function (array, val) {
return array.includes(val);
})
Handlebars.registerHelper('eval', function (expr) {
return eval(expr);
})
Handlebars.registerHelper('isOwnerOrGM', function (actor) {
return actor.isOwner || game.isGM;
})
Handlebars.registerHelper('upperFirst', function (text) {
if (typeof text !== 'string') return text
return text.charAt(0).toUpperCase() + text.slice(1)
})
Handlebars.registerHelper('upperFirstOnly', function (text) {
if (typeof text !== 'string') return text
return text.charAt(0).toUpperCase()
})
Handlebars.registerHelper('isCreature', function (key) {
return key === "creature" || key === "daemon";
})
// Handle v12 removal of this helper
Handlebars.registerHelper('select', function (selected, options) {
const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected));
const rgx = new RegExp(' value=[\"\']' + escapedValue + '[\"\']');
const html = options.fn(this);
return html.replace(rgx, "$& selected");
});
}
/* -------------------------------------------- */
static removeChatMessageId(messageId) {
if (messageId) {
game.messages.get(messageId)?.delete();
}
}
static findChatMessageId(current) {
return HawkmoonUtility.getChatMessageId(HawkmoonUtility.findChatMessage(current));
}
static getChatMessageId(node) {
return node?.attributes.getNamedItem('data-message-id')?.value;
}
static findChatMessage(current) {
return HawkmoonUtility.findNodeMatching(current, it => it.classList.contains('chat-message') && it.attributes.getNamedItem('data-message-id'))
}
static findNodeMatching(current, predicate) {
if (current) {
if (predicate(current)) {
return current;
}
return HawkmoonUtility.findNodeMatching(current.parentElement, predicate);
}
return undefined;
}
/* -------------------------------------------- */
static getWeaponSkill(actor, weapon, era) {
let skill
if (weapon.system.hasDirectSkill) {
let skillName = weapon.name
skill = { type: "skill", name: skillName, system: { base: 0, bonus: weapon.system.directSkillValue, skillTotal: weapon.system.directSkillValue } }
} else {
let skillName = game.i18n.localize(SYSTEM.WEAPON_SKILL_MAPPING[era][weapon.system.weaponType])
skill = actor.items.find(i => i.type === "skill" && i.name.toLowerCase() === skillName.toLowerCase())
if (!skill) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.NoWeaponSkill"))
return
}
}
return skill
}
/* -------------------------------------------- */
static async applySANType(rollMessage, event) {
let rollData = rollMessage.getFlag("fvtt-cthulhu-eternal", "rollData")
if (!rollData) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noRollDataFound"))
return
}
let actor = game.actors.get(rollData.actorId)
if (!actor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noActorFound"))
return
}
let sanType = event.currentTarget.dataset.sanType;
if (!sanType) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noSanTypeFound"))
return
}
rollData.sanType = sanType
await actor.system.applySANConsequences(rollData)
// Delete the roll message
await rollMessage.delete()
}
static async applySANLoss(rollMessage, event) {
let rollData = rollMessage.getFlag("fvtt-cthulhu-eternal", "rollData")
if (!rollData) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noRollDataFound"))
return
}
let actor = game.actors.get(rollData.actorId)
if (!actor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noActorFound"))
return
}
// Get the san loss from the event : data-san-value
let sanLoss = event.currentTarget.dataset.sanValue
let r = new Roll(sanLoss.toString())
await r.evaluate()
rollData.sanLoss = -r.total
await actor.system.modifySAN(rollData)
// Delete the roll message
await rollMessage.delete()
}
static async healingRoll(rollMessage) {
let rollData = rollMessage.rolls[0]?.options?.rollData
let healingFormula = rollData.rollItem.system.healingFormula
let healingMsg = "CTHULHUETERNAL.Label.healingRoll"
if (rollData.resultType === "successCritical") {
healingFormula += " * 2"
}
if (rollData.resultType === "failureCritical") {
healingMsg = "CTHULHUETERNAL.Label.healingRollFailure"
}
// Now display the result in chat message
let roll = new Roll(healingFormula)
await roll.evaluate()
roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: rollData.actorId }),
flavor: `${game.i18n.localize(healingMsg)} : ${roll.total}`,
rolls: [roll],
options: {
rollData: rollData,
resultType: rollData.resultType
}
})
}
static async opposedRollManagement(rollMessage, event) {
let rollData = rollMessage.getFlag("fvtt-cthulhu-eternal", "rollData")
if (!rollData) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noRollDataFound"))
return
}
// Get the store
let store = game.settings.get("fvtt-cthulhu-eternal", "roll-opposed-store")
if (!store.roll1) {
store.roll1 = {
rollData: rollData,
messageId: rollMessage.id
}
await game.settings.set("fvtt-cthulhu-eternal", "roll-opposed-store", store)
ui.notifications.info(game.i18n.localize("CTHULHUETERNAL.Notifications.opposedRollFirstStored"))
}
else if (!store.roll2) {
store.roll2 = {
rollData: rollData,
messageId: rollMessage.id
}
await game.settings.set("fvtt-cthulhu-eternal", "roll-opposed-store", store)
ui.notifications.info(game.i18n.localize("CTHULHUETERNAL.Notifications.opposedRollSecondStored"))
// Now perform the opposed roll resolution
await this.resolveOpposedRolls(store.roll1, store.roll2)
// Clear the store
store.roll1 = null
store.roll2 = null
await game.settings.set("fvtt-cthulhu-eternal", "roll-opposed-store", store)
}
else {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.opposedRollStoreFull"))
}
}
static async resolveOpposedRolls(roll1, roll2) {
// Get actors
let actor1 = game.actors.get(roll1.rollData.actorId)
let actor2 = game.actors.get(roll2.rollData.actorId)
if (!actor1 || !actor2) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noActorFound"))
return
}
// Determine winner
let winner = null
let loser = null
// If there critical success/failure, apply them first (remark : this d100 results)
roll1.rollData.rollCompare = roll1.rollData.rollResult
roll2.rollData.rollCompare = roll2.rollData.rollResult
if (roll1.rollData.resultType === "successCritical") {
roll1.rollData.rollCompare = -roll1.rollData.rollResult
}
if (roll2.rollData.resultType === "failureCritical") {
roll2.rollData.rollCompare = 100 + roll2.rollData.rollResult
}
if (roll2.rollData.resultType === "successCritical") {
roll2.rollData.rollCompare = -roll2.rollData.rollResult
}
if (roll1.rollData.resultType === "failureCritical") {
roll1.rollData.rollCompare = roll1.rollData.rollResult * 2
}
if (roll1.rollData.isSuccess && roll2.rollData.isFailure) {
winner = { actor: actor1, rollData: roll1.rollData, messageId: roll1.messageId }
loser = { actor: actor2, rollData: roll2.rollData, messageId: roll2.messageId }
}
else if (roll2.rollData.isSuccess && roll1.rollData.isFailure) {
winner = { actor: actor2, rollData: roll2.rollData, messageId: roll2.messageId }
loser = { actor: actor1, rollData: roll1.rollData, messageId: roll1.messageId }
}
else if (roll1.rollData.rollCompare < roll2.rollData.rollCompare) {
winner = { actor: actor1, rollData: roll1.rollData, messageId: roll1.messageId }
loser = { actor: actor2, rollData: roll2.rollData, messageId: roll2.messageId }
}
else {
winner = { actor: actor2, rollData: roll2.rollData, messageId: roll2.messageId }
loser = { actor: actor1, rollData: roll1.rollData, messageId: roll1.messageId }
}
// Check if winner was attacking with a weapon that can apply damage
let canApplyDamage = winner && winner.rollData?.weapon && winner.rollData.weapon.system
// Prepare data for the template
let msgData = {
winner: {
actor: {
name: winner.actor.name,
img: winner.actor.img
},
rollData: {
rollResult: winner.rollData.rollResult || winner.rollData.total,
isCritical: winner.rollData.isCritical,
weapon: winner.rollData.weapon
}
},
loser: {
actor: {
name: loser.actor.name,
img: loser.actor.img
},
rollData: {
rollResult: loser.rollData.rollResult || loser.rollData.total,
isCritical: loser.rollData.isCritical
}
},
canApplyDamage: canApplyDamage
}
// Render the template
let content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-cthulhu-eternal/templates/chat-opposed-result.hbs",
msgData
)
// Display the result in chat
let chatMsg = await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: winner.actor.id }),
content: content
})
// Store the winner's roll data for damage roll if applicable
if (canApplyDamage) {
await chatMsg.setFlag("fvtt-cthulhu-eternal", "rollData", winner.rollData)
}
}
static translateRangeUnit(range) {
if (typeof range === 'string') {
return game.i18n.localize(`CTHULHUETERNAL.Label.${range}`)
} else if (typeof range === 'number') {
return range
} else {
console.warn("CTHULHU ETERNAL | translateRange called with an unknown type", range)
return range
}
}
static translateRange(range) {
// If the range is a string, replace STR with FOR
if (typeof range === 'string') {
return range.replace(/STR/g, "FOR").replace(/str/g, "for")
}
return range
}
static async registerBabeleTranslations(babele) {
babele.registerConverters({
'translateRangeUnit': (originalValue) => {
return CthulhuEternalUtils.translateRangeUnit(originalValue)
},
'translateRange': (originalValue) => {
return CthulhuEternalUtils.translateRange(originalValue)
}
})
}
static async damageRoll(rollMessage, formula = null) {
let rollData = rollMessage.rolls[0]?.options?.rollData
if (!rollData) {
rollData = rollMessage.getFlag("fvtt-cthulhu-eternal", "rollData")
}
if (!rollData) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Label.noRollDataFound"))
return
}
let actor = game.actors.get(rollData.actorId)
if (!actor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Label.noActorFound"))
return
}
rollData.weapon.resultType = rollData.resultType // Keep the result type from the roll message
rollData.weapon.selectiveFireChoice = rollData.selectiveFireChoice // Keep the selected fire choice from the roll message
rollData.weapon.damageFormula = formula || rollData.weapon.system.damage
actor.system.roll("damage", rollData.weapon)
}
static async wornWeaponCheck(rollMessage) {
let rollData = rollMessage.rolls[0]?.options?.rollData
if (!rollData) {
rollData = rollMessage.getFlag("fvtt-cthulhu-eternal", "rollData")
}
if (!rollData) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Label.noRollDataFound"))
return
}
let actor = game.actors.get(rollData.actorId)
if (!actor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Label.noActorFound"))
return
}
let weapon = rollData.weapon
if (!weapon) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Label.noWeaponFound"))
return
}
// Lance un jet de chance (50%)
let luckRoll = new Roll("1d100")
await luckRoll.evaluate()
let isSuccess = luckRoll.total <= 50
let resultMsg = ""
if (isSuccess) {
// Succès - l'arme reste worn
resultMsg = game.i18n.format("CTHULHUETERNAL.Label.wornWeaponCheckSuccess", {
weapon: weapon.name,
roll: luckRoll.total
})
} else {
// Échec - l'arme devient junk
resultMsg = game.i18n.format("CTHULHUETERNAL.Label.wornWeaponCheckFailure", {
weapon: weapon.name,
roll: luckRoll.total
})
// Mettre à jour l'état de l'arme
await actor.updateEmbeddedDocuments("Item", [{
_id: weapon._id,
"system.state": "junk"
}])
ui.notifications.warn(game.i18n.format("CTHULHUETERNAL.Notifications.WeaponBecameJunk", { weapon: weapon.name }))
}
// Créer un message de chat avec le résultat
await ChatMessage.create({
user: game.user.id,
content: `<div class="cthulhu-eternal-roll">
<h4>${game.i18n.localize("CTHULHUETERNAL.Label.wornWeaponCheckTitle")}</h4>
<p>${resultMsg}</p>
</div>`,
speaker: ChatMessage.getSpeaker({ actor: actor }),
})
}
static async nudgeToSuccess(rollMessage) {
if (!game.settings.get("fvtt-cthulhu-eternal", "settings-nudge")) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Settings.nudgeDisabled"))
return
}
const rollOptions = rollMessage.rolls[0]?.options
if (!rollOptions) return
const actor = game.actors.get(rollOptions.actorId)
if (!actor) return
const rollTotal = rollMessage.rolls[0].total
const targetScore = rollOptions.rollData?.targetScore ?? rollOptions.targetScore
const wpCostToSucceed = Math.ceil((rollTotal - targetScore) / 5)
if (actor.system.wp.value < wpCostToSucceed) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Label.notEnoughWP"))
return
}
const dialogContext = foundry.utils.duplicate(rollOptions)
dialogContext.nudgedValue = targetScore
dialogContext.wpCost = wpCostToSucceed
dialogContext.isNudgedRoll = true
dialogContext.isNudge = false
const roll = new CthulhuEternalRoll(String(targetScore))
await roll.evaluate()
roll.options = dialogContext
roll.displayRollResult(roll, dialogContext, dialogContext.rollData)
roll.toMessage()
actor.system.modifyWP(-wpCostToSucceed)
// Original roll was a failure with skill progression — nudge succeeded, so unmark progression
if (rollOptions.skillMarkedForProgress && rollOptions.rollItem?._id) {
const skillItem = actor.items.get(rollOptions.rollItem._id)
if (skillItem) await skillItem.update({ "system.rollFailed": false })
}
await rollMessage.delete()
}
static async nudgeRoll(rollMessage) {
if (!game.settings.get("fvtt-cthulhu-eternal", "settings-nudge")) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Settings.nudgeDisabled"))
return
}
let dialogContext = rollMessage.rolls[0]?.options
let actor = game.actors.get(dialogContext.actorId)
const rollTotal = rollMessage.rolls[0].total
dialogContext.wpValue = actor.system.wp.value
// Rule: 1 WP can decrease the roll by 15. Only decrease is allowed.
dialogContext.minValue = Math.max(rollTotal - (dialogContext.wpValue * 5), 1)
dialogContext.maxValue = rollTotal
dialogContext.rollResultIndex = rollTotal - dialogContext.minValue // index of current value in nudgeOptions
dialogContext.wpCost = 0
dialogContext.nudgedValue = rollTotal // default: no change for the select: values from minValue to maxValue (current roll)
dialogContext.nudgeOptions = Array.from({ length: dialogContext.maxValue - dialogContext.minValue + 1 }, (_, i) => dialogContext.minValue + i)
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/nudge-dialog.hbs", dialogContext)
const title = game.i18n.localize("CTHULHUETERNAL.Roll.nudgeRoll")
const rollContext = await foundry.applications.api.DialogV2.wait({
window: { title: title },
classes: ["fvtt-cthulhu-eternal"],
content,
buttons: [
{
action: "apply",
label: game.i18n.localize("CTHULHUETERNAL.Roll.applyNudge"),
callback: (event, button, dialog) => {
const output = Array.from(button.form.elements).reduce((obj, input) => {
if (input.name) obj[input.name] = input.value
return obj
}, {})
// Compute nudgedValue from the selected index (selectOptions uses 0-based indices for arrays)
dialogContext.nudgedValue = dialogContext.minValue + Number(output.modifiedValue)
dialogContext.wpCost = Math.ceil(Math.abs(rollTotal - dialogContext.nudgedValue) / 5)
return output
},
},
{
action: "cancel",
label: game.i18n.localize("CTHULHUETERNAL.Roll.cancel"),
callback: (event, button, dialog) => { }
}
],
actions: {
},
rejectClose: false, // Click on Close button will not launch an error
render: (event, dialog) => {
$(".nudged-score-select").change(event => {
dialogContext.nudgedValue = dialogContext.minValue + Number(event.target.value)
dialogContext.wpCost = Math.ceil(Math.abs(rollMessage.rolls[0].total - dialogContext.nudgedValue) / 5)
$("#nudged-wp-cost").val(dialogContext.wpCost)
})
}
})
// If the user cancels the dialog, exit
if (rollContext === null || dialogContext.wpCost === 0) {
return
}
if (actor.system.wp.value < dialogContext.wpCost) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Label.notEnoughWP"))
return
}
const roll = new CthulhuEternalRoll(String(dialogContext.nudgedValue))
await roll.evaluate()
roll.options = dialogContext
roll.options.isNudgedRoll = true
roll.options.isNudge = false
roll.displayRollResult(roll, dialogContext, dialogContext.rollData)
roll.toMessage()
actor.system.modifyWP(-dialogContext.wpCost)
// If original roll had skill progression and nudge converts it to success, unmark progression
const nudgedTargetScore = dialogContext.rollData?.targetScore ?? dialogContext.targetScore
if (dialogContext.skillMarkedForProgress && dialogContext.nudgedValue <= nudgedTargetScore && dialogContext.rollItem?._id) {
const skillItem = actor.items.get(dialogContext.rollItem._id)
if (skillItem) await skillItem.update({ "system.rollFailed": false })
}
// Delete the initial roll message
await rollMessage.delete()
}
static setupCSSRootVariables() {
const era = game.settings.get("fvtt-cthulhu-eternal", "settings-era")
let eraCSS = SYSTEM.ERA_CSS[era];
if (!eraCSS) eraCSS = SYSTEM.ERA_CSS["jazz"];
document.documentElement.style.setProperty('--font-size-standard', eraCSS.baseFontSize);
document.documentElement.style.setProperty('--font-size-title', eraCSS.titleFontSize);
document.documentElement.style.setProperty('--font-size-result', eraCSS.titleFontSize);
document.documentElement.style.setProperty('--font-primary', eraCSS.primaryFont);
document.documentElement.style.setProperty('--font-secondary', eraCSS.secondaryFont);
document.documentElement.style.setProperty('--font-title', eraCSS.titleFont);
document.documentElement.style.setProperty('--img-icon-color-filter', eraCSS.imgFilter);
document.documentElement.style.setProperty('--background-image-base', `linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)), url("../assets/ui/${era}_background_main.webp")`);
}
static getTarget() {
if (game.user.targets && game.user.targets.size === 1) {
for (let target of game.user.targets) {
return target
}
}
return undefined;
}
static applyWounds(message, event) {
let woundData = message.getFlag("fvtt-cthulhu-eternal", "woundData")
if (!woundData) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noRollDataFound"))
return
}
let actor = game.actors.get(woundData.actorId)
if (!actor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noActorFound"))
return
}
this.removeChatMessageId(message.id)
// Resolve the target actor: prefer canvas token (works for both combat and scene tokens)
const targetTokenId = event.currentTarget.dataset.tokenId
const targetActorId = event.currentTarget.dataset.actorId
let targetActor
if (targetTokenId) {
const token = canvas.tokens.get(targetTokenId)
targetActor = token?.actor
}
if (!targetActor && targetActorId) {
targetActor = game.actors.get(targetActorId)
}
// Legacy fallback: combatant lookup
if (!targetActor) {
const combatantId = event.currentTarget.dataset.combatantId
if (combatantId && game.combat) {
const combatant = game.combat.combatants.get(combatantId)
targetActor = combatant?.token?.actor || game.actors.get(combatant?.actorId)
}
}
if (!targetActor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noTargetActorFound"))
return
}
targetActor.applyWounds(woundData)
}
}