Add reaction engine and action skeletons
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { executeApplyStatusAction } from "../actions/apply-status-action.js";
|
||||
import { executeTeleportAction } from "../actions/teleport-action.js";
|
||||
import { executeCastSpellAction } from "../actions/cast-spell-action.js";
|
||||
import { executeUseInventoryItemAction } from "../actions/use-inventory-item-action.js";
|
||||
|
||||
const ACTION_HANDLERS = Object.freeze({
|
||||
applyStatus: executeApplyStatusAction,
|
||||
teleport: executeTeleportAction,
|
||||
castSpellFromToken: executeCastSpellAction,
|
||||
useInventoryItem: executeUseInventoryItemAction
|
||||
});
|
||||
|
||||
export async function executeReactionActions(reaction, context) {
|
||||
let anySuccess = false;
|
||||
let anyFailureConsumes = false;
|
||||
|
||||
for (const action of reaction.actions ?? []) {
|
||||
if (!action?.enabled) continue;
|
||||
|
||||
const handler = ACTION_HANDLERS[action.type];
|
||||
if (!handler) continue;
|
||||
|
||||
const result = await handler(action, {
|
||||
...context,
|
||||
reaction,
|
||||
reactionOrigin: `Module.configurable-reactions.Reaction.${reaction.id}`
|
||||
});
|
||||
|
||||
if (result.success) anySuccess = true;
|
||||
else if (shouldConsumeOnFailure(reaction, action)) anyFailureConsumes = true;
|
||||
|
||||
if (result.stopProcessing) break;
|
||||
}
|
||||
|
||||
return {
|
||||
success: anySuccess,
|
||||
shouldConsume: anySuccess || anyFailureConsumes
|
||||
};
|
||||
}
|
||||
|
||||
function shouldConsumeOnFailure(reaction, action) {
|
||||
if (typeof action.consumeOnFailure === "boolean") return action.consumeOnFailure;
|
||||
return reaction.consumption?.consumeOnFailure === true;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MODULE_ID, SETTINGS } from "../constants.js";
|
||||
|
||||
export async function getAssignedReactionsForContext(context) {
|
||||
const reactions = game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? [];
|
||||
const assignments = game.settings.get(MODULE_ID, SETTINGS.ASSIGNMENTS) ?? [];
|
||||
|
||||
const actorUuid = context.targetActor?.uuid ?? context.actor?.uuid ?? null;
|
||||
const tokenUuid = context.targetTokenDocument?.uuid ?? context.tokenDocument?.uuid ?? null;
|
||||
|
||||
const matching = assignments.filter(assignment => {
|
||||
if (assignment.mode === "actor" && assignment.actorUuid === actorUuid) return true;
|
||||
if (assignment.mode === "token" && assignment.tokenUuid === tokenUuid) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
return matching
|
||||
.map(assignment => reactions.find(reaction => reaction.id === assignment.reactionId))
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { checkDamageCondition } from "../conditions/damage-condition.js";
|
||||
import { checkHpAfterDamageCondition } from "../conditions/hp-threshold-condition.js";
|
||||
|
||||
const CONDITION_HANDLERS = Object.freeze({
|
||||
damage: checkDamageCondition,
|
||||
hpAfterDamage: checkHpAfterDamageCondition
|
||||
});
|
||||
|
||||
export async function checkConditions(reaction, context) {
|
||||
for (const [conditionType, config] of Object.entries(reaction.conditions ?? {})) {
|
||||
if (!config?.enabled) continue;
|
||||
|
||||
const handler = CONDITION_HANDLERS[conditionType];
|
||||
if (!handler) return false;
|
||||
|
||||
const passed = await handler(config, context);
|
||||
if (!passed) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { MODULE_ID } from "../constants.js";
|
||||
|
||||
export async function checkConsumptionAvailable(reaction, context) {
|
||||
const consumption = reaction.consumption;
|
||||
if (!consumption?.enabled || consumption.mode === "none") return true;
|
||||
|
||||
const document = getUsageDocument(context);
|
||||
if (!document) return true;
|
||||
|
||||
const usage = document.getFlag(MODULE_ID, "usage") ?? {};
|
||||
const reactionUsage = usage[reaction.id] ?? {};
|
||||
|
||||
if (consumption.mode === "perCombat") {
|
||||
if (!game.combat?.id) return true;
|
||||
return reactionUsage.combatId !== game.combat.id || (reactionUsage.uses ?? 0) < consumption.maxUses;
|
||||
}
|
||||
|
||||
if (consumption.mode === "perRound") {
|
||||
if (!game.combat?.id) return true;
|
||||
return reactionUsage.combatId !== game.combat.id || reactionUsage.round !== game.combat.round || (reactionUsage.uses ?? 0) < consumption.maxUses;
|
||||
}
|
||||
|
||||
if (consumption.mode === "perTurn") {
|
||||
if (!game.combat?.id) return true;
|
||||
return reactionUsage.combatId !== game.combat.id || reactionUsage.round !== game.combat.round || reactionUsage.turn !== game.combat.turn || (reactionUsage.uses ?? 0) < consumption.maxUses;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function consumeReactionUse(reaction, context) {
|
||||
const consumption = reaction.consumption;
|
||||
if (!consumption?.enabled || consumption.mode === "none") return;
|
||||
|
||||
const document = getUsageDocument(context);
|
||||
if (!document) return;
|
||||
|
||||
const usage = foundry.utils.deepClone(document.getFlag(MODULE_ID, "usage") ?? {});
|
||||
const current = usage[reaction.id] ?? {};
|
||||
|
||||
usage[reaction.id] = {
|
||||
combatId: game.combat?.id ?? null,
|
||||
round: game.combat?.round ?? null,
|
||||
turn: game.combat?.turn ?? null,
|
||||
uses: (current.uses ?? 0) + 1,
|
||||
lastUsedAt: Date.now()
|
||||
};
|
||||
|
||||
await document.setFlag(MODULE_ID, "usage", usage);
|
||||
}
|
||||
|
||||
function getUsageDocument(context) {
|
||||
return context.targetTokenDocument ?? context.tokenDocument ?? context.targetActor ?? context.actor ?? null;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MODULE_ID } from "../constants.js";
|
||||
import { getAssignedReactionsForContext } from "./assignment-resolver.js";
|
||||
import { checkConditions } from "./condition-runner.js";
|
||||
import { checkConsumptionAvailable, consumeReactionUse } from "./consumption-manager.js";
|
||||
import { executeReactionActions } from "./action-runner.js";
|
||||
|
||||
export async function handleTrigger(triggerType, context) {
|
||||
if (!game.user.isGM) return;
|
||||
|
||||
const assignedReactions = await getAssignedReactionsForContext(context);
|
||||
|
||||
for (const reaction of assignedReactions) {
|
||||
try {
|
||||
if (!reaction?.enabled) continue;
|
||||
if (reaction.trigger?.type !== triggerType) continue;
|
||||
if (!await checkConsumptionAvailable(reaction, context)) continue;
|
||||
if (!await checkConditions(reaction, context)) continue;
|
||||
|
||||
const result = await executeReactionActions(reaction, context);
|
||||
if (result.shouldConsume) await consumeReactionUse(reaction, context);
|
||||
} catch (error) {
|
||||
console.error(`${MODULE_ID} | Failed to handle reaction`, reaction, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user