Files
FoundryVTT/src/macro_arcanePool_applyEffects.js
centron\schwoerer 5669aa75ca track HP in chat
2025-11-14 09:25:31 +01:00

169 lines
5.8 KiB
JavaScript

(async () => {
try {
const FLAG_SCOPE = "world";
const FLAG_ROOT = "arcanePool";
const flagPath = (key) => `${FLAG_ROOT}.${key}`;
const getFlag = (doc, key) => doc.getFlag(FLAG_SCOPE, flagPath(key));
const setFlag = (doc, key, value) => doc.setFlag(FLAG_SCOPE, flagPath(key), value);
const readBuffFlag = (doc, key) => doc.getFlag(FLAG_SCOPE, flagPath(key));
// Normalize arguments
const rawArgs = Array.isArray(args) ? args : [];
const options = rawArgs[0] ?? {};
const actorId = options.actorId ?? actor?.id;
if (!actorId) {
console.warn("Arcane Pool AE: Missing actor id");
return false;
}
const targetActor = game.actors.get(actorId);
if (!targetActor) {
console.warn(`Arcane Pool AE: Actor ${actorId} not found`);
return false;
}
// Merge config updates when provided
const currentConfig = getFlag(targetActor, "config") ?? {};
const configUpdates = {
weaponId: options.weaponId,
enhancementBonus: options.enhancementBonus,
poolCost: options.poolCost,
enduringBlade: options.enduringBlade,
ghostBlade: options.ghostBlade,
};
const sanitizedUpdates = Object.fromEntries(
Object.entries(configUpdates).filter(([, value]) => value !== undefined)
);
const updatedConfig = {
...currentConfig,
...sanitizedUpdates,
};
if (!foundry.utils.isEmpty(updatedConfig) && !foundry.utils.isEqual(updatedConfig, currentConfig)) {
await setFlag(targetActor, "config", updatedConfig);
}
const config = getFlag(targetActor, "config");
const weaponId = config?.weaponId;
if (!weaponId) {
console.warn("Arcane Pool AE: No weaponId in config");
return false;
}
const weapon = targetActor.items.get(weaponId);
if (!weapon) {
console.warn(`Arcane Pool AE: Weapon ${weaponId} not found on actor ${targetActor.name}`);
return false;
}
// Locate primary attack action (default "Attack")
const attacks = weapon.system?.actions ?? [];
const attackIndex = attacks.findIndex((action) => (action?.name ?? "").toLowerCase() === "attack");
const attackPath = attackIndex >= 0 ? `system.actions.${attackIndex}` : null;
// Cache baseline weapon data to allow clean restoration
let baseline = getFlag(weapon, "baseline");
const baselineWeaponId = baseline?.weaponId;
if (!baseline || baselineWeaponId !== weaponId) {
baseline = {
weaponId,
enh: weapon.system?.enh ?? 0,
damageParts: foundry.utils.duplicate(weapon.system?.damage?.parts ?? []),
attackAbility: attackPath
? {
critRange: foundry.utils.getProperty(weapon.system, `${attackPath}.ability.critRange`) ?? null,
critMult: foundry.utils.getProperty(weapon.system, `${attackPath}.ability.critMult`) ?? null,
}
: null,
};
await setFlag(weapon, "baseline", baseline);
}
const activeBuffs = targetActor.items.filter(
(item) => item.type === "buff" && item.system?.active && readBuffFlag(item, "tag")
);
const baseBuff = activeBuffs.find((buff) => readBuffFlag(buff, "role") === "base");
// Restore baseline if base buff is inactive
if (!baseBuff) {
const restoreData = {
"system.enh": baseline.enh ?? 0,
"system.damage.parts": baseline.damageParts ?? [],
"flags.arcanePool.active": false,
"flags.arcanePool.activeProperties": [],
};
if (baseline.attackAbility && attackPath) {
if (baseline.attackAbility.critRange !== null) {
restoreData[`${attackPath}.ability.critRange`] = baseline.attackAbility.critRange;
}
if (baseline.attackAbility.critMult !== null) {
restoreData[`${attackPath}.ability.critMult`] = baseline.attackAbility.critMult;
}
}
await weapon.update(restoreData);
return true;
}
const enhancementBonus =
Number(config?.enhancementBonus ?? readBuffFlag(baseBuff, "enhancementBonus") ?? 0) || 0;
// Build new damage array starting from baseline
const newDamage = foundry.utils.duplicate(baseline.damageParts ?? []);
const activeProperties = [];
for (const buff of activeBuffs) {
if (buff.id === baseBuff.id) continue;
const propertyKey = readBuffFlag(buff, "property");
if (!propertyKey) continue;
activeProperties.push(propertyKey);
const damageFormula = readBuffFlag(buff, "damageFormula");
if (!damageFormula) continue;
const parts = damageFormula.split("+").map((segment) => segment.trim()).filter(Boolean);
for (const part of parts) {
const match = part.match(/([^[]+)\[([^]+)]/);
if (!match) continue;
const [, dice, damageType] = match;
newDamage.push([dice.trim(), damageType.trim()]);
}
}
const updateData = {
"system.enh": (baseline.enh ?? 0) + enhancementBonus,
"system.damage.parts": newDamage,
"flags.arcanePool.active": true,
"flags.arcanePool.activeProperties": activeProperties,
"flags.arcanePool.lastApplied": Date.now(),
};
// Keen, Speed, and other non-damage properties currently rely on player toggles.
// Additional automation can be added by extending property handlers here.
if (baseline.attackAbility && attackPath) {
if (baseline.attackAbility.critRange !== null) {
updateData[`${attackPath}.ability.critRange`] = baseline.attackAbility.critRange;
}
if (baseline.attackAbility.critMult !== null) {
updateData[`${attackPath}.ability.critMult`] = baseline.attackAbility.critMult;
}
}
await weapon.update(updateData);
return true;
} catch (error) {
console.error("Arcane Pool AE: Failed to apply effects", error);
ui.notifications.error("Failed to synchronize Arcane Pool effects.");
return false;
}
})();