tarinai/js/tarinai_action_spec.js
2026-07-16 22:12:03 +09:00

158 lines
5.7 KiB
JavaScript

"use strict";
(function (global) {
const registry = new Map();
const isObject = global.TarinaiCoreHelpers?.isPlainObject || (value => Boolean(value && typeof value === "object"));
const finiteOr = global.TarinaiCoreHelpers?.finiteOr || ((value, fallback) => {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
});
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.
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),
timerBacked: Boolean(input.timerBacked),
};
const originalCanStart = hasOwnFn(input, "canStart") ? input.canStart : null;
const originalSelectTarget = hasOwnFn(input, "selectTarget") ? input.selectTarget : null;
const originalStart = hasOwnFn(input, "start") ? input.start : null;
const originalTick = hasOwnFn(input, "tick") ? input.tick : 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));
return true;
};
spec.start = function start(tarinai, world, ctx = {}) {
if (originalStart) return originalStart.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);
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;
};
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 getTarinaiActionSpec(id = "") {
return registry.get(String(id || "")) || 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 spec.start(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 spec.tick(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, {
registerTarinaiActionSpecs,
getTarinaiActionSpec,
canStartTarinaiAction,
startTarinaiAction,
tickTarinaiAction,
finishTarinaiAction,
failTarinaiAction,
textTarinaiAction,
});
})(typeof window !== "undefined" ? window : globalThis);