169 lines
5.8 KiB
JavaScript
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;
|
|
}
|
|
})();
|