tarinai/js/tarinai_action_spec.js
2026-06-24 13:48:22 +09:00

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);