201 lines
7.5 KiB
JavaScript
201 lines
7.5 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const registry = new Map();
|
|
|
|
function isObject(value) {
|
|
return Boolean(value && typeof value === "object");
|
|
}
|
|
|
|
function finiteOr(value, fallback) {
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n : fallback;
|
|
}
|
|
|
|
function toList(value) {
|
|
if (Array.isArray(value)) return value.filter(Boolean).map(String);
|
|
if (value == null || value === "") return [];
|
|
return [String(value)];
|
|
}
|
|
|
|
function hasOwnFn(obj, key) {
|
|
return typeof obj?.[key] === "function";
|
|
}
|
|
|
|
function resolveActionSpec(action) {
|
|
if (!action) return null;
|
|
return typeof action === "string" ? getTarinaiActionSpec(action) : action;
|
|
}
|
|
|
|
function buildActionContext(tarinai, world, ctx = {}, spec = null) {
|
|
const base = isObject(ctx) ? { ...ctx } : {};
|
|
if (base.target === undefined && spec && hasOwnFn(spec, "selectTarget")) {
|
|
base.target = spec.selectTarget(tarinai, world, base);
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function normalizeTarinaiActionSpec(input = {}) {
|
|
if (!isObject(input)) throw new TypeError("ActionSpec must be an object");
|
|
const id = String(input.id || input.actionId || "").trim();
|
|
if (!id) throw new TypeError("ActionSpec.id is required");
|
|
|
|
// Public lifecycle: canStart -> selectTarget -> start -> tick -> finish/fail -> text.
|
|
// Legacy hooks (`condition`, `run`, `update`) remain accepted so old code can be
|
|
// migrated incrementally, but newly migrated actions should use the lifecycle names.
|
|
const spec = {
|
|
...input,
|
|
id,
|
|
kind: String(input.kind || "need_action"),
|
|
need: String(input.need || "fulfill"),
|
|
subNeed: String(input.subNeed || ""),
|
|
label: String(input.label || input.phrase || id),
|
|
phrase: String(input.phrase || input.label || id),
|
|
weight: finiteOr(input.weight, 1),
|
|
priority: finiteOr(input.priority, 0),
|
|
tags: toList(input.tags),
|
|
timerBacked: Boolean(input.timerBacked),
|
|
interruptible: input.interruptible !== false,
|
|
};
|
|
|
|
const originalCanStart = hasOwnFn(input, "canStart") ? input.canStart : null;
|
|
const originalCondition = hasOwnFn(input, "condition") ? input.condition : null;
|
|
const originalSelectTarget = hasOwnFn(input, "selectTarget") ? input.selectTarget : null;
|
|
const originalStart = hasOwnFn(input, "start") ? input.start : null;
|
|
const originalRun = hasOwnFn(input, "run") ? input.run : null;
|
|
const originalTick = hasOwnFn(input, "tick") ? input.tick : null;
|
|
const originalUpdate = hasOwnFn(input, "update") ? input.update : null;
|
|
const originalFinish = hasOwnFn(input, "finish") ? input.finish : null;
|
|
const originalFail = hasOwnFn(input, "fail") ? input.fail : null;
|
|
const originalText = hasOwnFn(input, "text") ? input.text : null;
|
|
|
|
spec.selectTarget = function selectTarget(tarinai, world, ctx = {}) {
|
|
if (originalSelectTarget) return originalSelectTarget.call(spec, tarinai, world, ctx);
|
|
return ctx.target ?? tarinai?.target ?? null;
|
|
};
|
|
|
|
spec.canStart = function canStart(tarinai, world, ctx = {}) {
|
|
if (originalCanStart) return Boolean(originalCanStart.call(spec, tarinai, world, ctx));
|
|
if (originalCondition) return Boolean(originalCondition.call(spec, tarinai, world, ctx));
|
|
return true;
|
|
};
|
|
|
|
spec.start = function start(tarinai, world, ctx = {}) {
|
|
if (originalStart) return originalStart.call(spec, tarinai, world, ctx);
|
|
if (originalRun) return originalRun.call(spec, tarinai, world, ctx);
|
|
return true;
|
|
};
|
|
|
|
spec.tick = function tick(tarinai, world, dt = 0, needs = null, ctx = {}) {
|
|
if (originalTick) return originalTick.call(spec, tarinai, world, dt, needs, ctx);
|
|
if (originalUpdate) return originalUpdate.call(spec, tarinai, world, dt, needs, ctx);
|
|
return undefined;
|
|
};
|
|
|
|
spec.finish = function finish(tarinai, world, ctx = {}) {
|
|
return originalFinish ? originalFinish.call(spec, tarinai, world, ctx) : true;
|
|
};
|
|
|
|
spec.fail = function fail(tarinai, world, ctx = {}) {
|
|
return originalFail ? originalFail.call(spec, tarinai, world, ctx) : false;
|
|
};
|
|
|
|
spec.text = function text(tarinai, world, ctx = {}) {
|
|
if (originalText) return String(originalText.call(spec, tarinai, world, ctx) || "");
|
|
if (ctx.phase === "finished") return spec.phrase;
|
|
return spec.label;
|
|
};
|
|
|
|
Object.defineProperty(spec, "__actionSpec", {
|
|
value: true,
|
|
enumerable: false,
|
|
configurable: false,
|
|
});
|
|
return spec;
|
|
}
|
|
|
|
function registerTarinaiActionSpecs(definitions = []) {
|
|
const specs = [];
|
|
for (const definition of definitions || []) {
|
|
const spec = normalizeTarinaiActionSpec(definition);
|
|
registry.set(spec.id, spec);
|
|
specs.push(spec);
|
|
}
|
|
return specs;
|
|
}
|
|
|
|
function registerTarinaiActionSpec(definition = {}) {
|
|
const spec = normalizeTarinaiActionSpec(definition);
|
|
registry.set(spec.id, spec);
|
|
return spec;
|
|
}
|
|
|
|
function getTarinaiActionSpec(id = "") {
|
|
return registry.get(String(id || "")) || null;
|
|
}
|
|
|
|
function listTarinaiActionSpecs() {
|
|
return Array.from(registry.values());
|
|
}
|
|
|
|
function selectTarinaiActionTarget(action, tarinai, world, ctx = {}) {
|
|
const spec = resolveActionSpec(action);
|
|
if (!spec) return null;
|
|
return hasOwnFn(spec, "selectTarget") ? spec.selectTarget(tarinai, world, ctx) : (ctx.target ?? tarinai?.target ?? null);
|
|
}
|
|
|
|
function canStartTarinaiAction(action, tarinai, world, ctx = {}) {
|
|
const spec = resolveActionSpec(action);
|
|
if (!spec) return false;
|
|
return hasOwnFn(spec, "canStart") ? Boolean(spec.canStart(tarinai, world, isObject(ctx) ? ctx : {})) : true;
|
|
}
|
|
|
|
function startTarinaiAction(action, tarinai, world, ctx = {}) {
|
|
const spec = resolveActionSpec(action);
|
|
if (!spec) return false;
|
|
const canCtx = isObject(ctx) ? ctx : {};
|
|
if (hasOwnFn(spec, "canStart") && !spec.canStart(tarinai, world, canCtx)) return false;
|
|
const nextCtx = buildActionContext(tarinai, world, canCtx, spec);
|
|
return hasOwnFn(spec, "start") ? spec.start(tarinai, world, nextCtx) : spec.run?.(tarinai, world, nextCtx);
|
|
}
|
|
|
|
function tickTarinaiAction(action, tarinai, world, dt = 0, needs = null, ctx = {}) {
|
|
const spec = resolveActionSpec(action);
|
|
if (!spec) return undefined;
|
|
const nextCtx = buildActionContext(tarinai, world, ctx, spec);
|
|
return hasOwnFn(spec, "tick") ? spec.tick(tarinai, world, dt, needs, nextCtx) : spec.update?.(tarinai, world, dt, needs, nextCtx);
|
|
}
|
|
|
|
function finishTarinaiAction(action, tarinai, world, ctx = {}) {
|
|
const spec = resolveActionSpec(action);
|
|
return hasOwnFn(spec, "finish") ? spec.finish(tarinai, world, ctx) : true;
|
|
}
|
|
|
|
function failTarinaiAction(action, tarinai, world, ctx = {}) {
|
|
const spec = resolveActionSpec(action);
|
|
return hasOwnFn(spec, "fail") ? spec.fail(tarinai, world, ctx) : false;
|
|
}
|
|
|
|
function textTarinaiAction(action, tarinai, world, ctx = {}) {
|
|
const spec = resolveActionSpec(action);
|
|
if (!spec) return "";
|
|
return hasOwnFn(spec, "text") ? String(spec.text(tarinai, world, isObject(ctx) ? ctx : {}) || "") : String(spec.label || spec.phrase || spec.id || "");
|
|
}
|
|
|
|
Object.assign(global, {
|
|
normalizeTarinaiActionSpec,
|
|
createTarinaiActionSpec: normalizeTarinaiActionSpec,
|
|
registerTarinaiActionSpec,
|
|
registerTarinaiActionSpecs,
|
|
getTarinaiActionSpec,
|
|
listTarinaiActionSpecs,
|
|
selectTarinaiActionTarget,
|
|
canStartTarinaiAction,
|
|
startTarinaiAction,
|
|
tickTarinaiAction,
|
|
finishTarinaiAction,
|
|
failTarinaiAction,
|
|
textTarinaiAction,
|
|
TarinaiActionSpecRegistry: registry,
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|