tarinai/js/command_dispatcher.js
2026-07-18 13:06:02 +09:00

200 lines
8.5 KiB
JavaScript

"use strict";
// Layer: commands
// UI calls this facade instead of mutating cross-cutting world state directly.
// Keep this file small: commands normalize user intent, then delegate game rules to World/system modules.
(function (global) {
const DEFAULT_SPEEDS = Object.freeze([1, 2, 4]);
const DEFAULT_SIZE_ORDER = Object.freeze(["small", "medium", "large"]);
const DEFAULT_PICK_MODE_ORDER = Object.freeze(["free", "grid20", "grid40", "grid60"]);
function emit(worldRef, type, payload) {
worldRef?.emit?.(type, payload);
}
function ok(payload = {}) {
return { ok: true, ...payload };
}
function fail(reason, payload = {}) {
return { ok: false, reason, ...payload };
}
function selectTool(worldRef, toolId) {
if (!worldRef) return fail("missing_world");
const id = String(toolId || "observe");
if (worldRef.tool === id) return ok({ toolId: id, changed: false });
worldRef.tool = id;
if (id !== "shoot") {
worldRef.shootCursorActive = false;
worldRef.shootCursorOffsetX = 0;
worldRef.shootCursorOffsetY = 0;
}
emit(worldRef, "tool:selected", { toolId: id });
return ok({ toolId: id, changed: true });
}
function setSelection(worldRef, target) {
if (!worldRef) return fail("missing_world");
const next = target || null;
if (worldRef.selected === next) return ok({ selected: next, changed: false });
worldRef.selected = next;
emit(worldRef, "selection:changed", { selected: worldRef.selected });
return ok({ selected: worldRef.selected, changed: true });
}
function clearSelection(worldRef) {
return setSelection(worldRef, null);
}
function focusSelection(worldRef, target, options = {}) {
if (!worldRef) return fail("missing_world");
if (!target || target.dead) return fail("missing_target");
const result = setSelection(worldRef, target);
target.focusPulseTimer = Math.max(target.focusPulseTimer || 0, Number(options.pulse ?? 1.15) || 1.15);
if (options.selectObserve !== false) selectTool(worldRef, "observe");
if (options.center !== false && Number.isFinite(target.x) && Number.isFinite(target.y)) {
const visibleW = worldRef.visibleWorldW ? worldRef.visibleWorldW() : (worldRef.viewportW || worldRef.w || 0);
const visibleH = worldRef.visibleWorldH ? worldRef.visibleWorldH() : (worldRef.viewportH || worldRef.h || 0);
worldRef.cameraX = target.x - visibleW / 2;
worldRef.cameraY = target.y - visibleH / 2;
worldRef.clampCamera?.();
}
return ok({ selected: result.selected || target, target });
}
function toggleSelectedFavorite(worldRef, target = worldRef?.selected || null) {
if (!worldRef) return fail("missing_world");
if (!target || target.dead) return fail("missing_target");
target.favorite = !target.favorite;
if (target.favorite) global.TarinaiAchievements?.recordFavoriteTarinai?.({ world: worldRef, tarinai: target, source: "favorite-toggle" });
return ok({ selected: target, favorite: Boolean(target.favorite) });
}
function scalableToolSet() {
if (typeof global.scalableToolIds === "function") return new Set(global.scalableToolIds());
return new Set();
}
function setToolSize(worldRef, tool, size) {
if (!worldRef) return fail("missing_world");
const id = String(tool || worldRef.tool || "");
const next = String(size || "medium");
if (!id || !scalableToolSet().has(id)) return fail("not_scalable", { toolId: id });
if (!worldRef.toolSizes) worldRef.toolSizes = {};
worldRef.toolSizes[id] = next;
worldRef.toolSize = next;
return ok({ toolId: id, size: next });
}
function cycleToolSize(worldRef, tool, order = DEFAULT_SIZE_ORDER) {
if (!worldRef) return fail("missing_world");
const id = String(tool || worldRef.tool || "");
if (!id || !scalableToolSet().has(id)) return fail("not_scalable", { toolId: id });
const sizes = Array.isArray(order) && order.length ? order.map(String) : Array.from(DEFAULT_SIZE_ORDER);
const current = typeof global.normalizedToolSizeFor === "function"
? global.normalizedToolSizeFor(worldRef, id)
: ((worldRef.toolSizes && worldRef.toolSizes[id]) || worldRef.toolSize || "medium");
const idx = sizes.indexOf(current);
const next = sizes[(idx + 1 + sizes.length) % sizes.length];
return setToolSize(worldRef, id, next);
}
function normalizeToolPickMode(mode = "free") {
const value = String(mode || "free").toLowerCase();
if (value === "grid") return "grid40"; // compatibility with v39.16.34
return DEFAULT_PICK_MODE_ORDER.includes(value) ? value : "free";
}
function setToolPickMode(worldRef, mode = "free") {
if (!worldRef) return fail("missing_world");
const next = normalizeToolPickMode(mode);
worldRef.toolPickMode = next;
return ok({ mode: next });
}
function cycleToolPickMode(worldRef, order = DEFAULT_PICK_MODE_ORDER) {
if (!worldRef) return fail("missing_world");
const modes = Array.isArray(order) && order.length
? order.map(normalizeToolPickMode).filter((value, index, list) => list.indexOf(value) === index)
: Array.from(DEFAULT_PICK_MODE_ORDER);
const current = normalizeToolPickMode(worldRef.toolPickMode);
const index = modes.indexOf(current);
const next = modes[(index + 1 + modes.length) % modes.length] || "free";
return setToolPickMode(worldRef, next);
}
function togglePaused(worldRef) {
if (!worldRef) return fail("missing_world");
worldRef.paused = !worldRef.paused;
return ok({ paused: Boolean(worldRef.paused) });
}
function cycleSpeed(worldRef, speeds = DEFAULT_SPEEDS) {
if (!worldRef) return fail("missing_world");
const list = Array.isArray(speeds) && speeds.length ? speeds.map(Number).filter(Number.isFinite) : Array.from(DEFAULT_SPEEDS);
const i = list.indexOf(Number(worldRef.speed) || 1);
worldRef.speed = list[(i + 1 + list.length) % list.length];
return ok({ speed: worldRef.speed });
}
function primaryPointer(worldRef, x, y) {
if (!worldRef) return fail("missing_world");
const px = Number(x);
const py = Number(y);
if (!Number.isFinite(px) || !Number.isFinite(py)) return fail("invalid_point", { x, y });
if (typeof worldRef.handleClick !== "function") return fail("missing_pointer_handler");
if (worldRef.tool && worldRef.tool !== "observe") global.TarinaiHistory.capture(worldRef, `tool:${worldRef.tool}`);
worldRef.handleClick(px, py);
return ok({ x: px, y: py, toolId: worldRef.tool || "" });
}
function dispatch(worldRef, command = {}) {
switch (command.type) {
case "tool.select":
return selectTool(worldRef, command.toolId);
case "tool.size.cycle":
return cycleToolSize(worldRef, command.toolId, command.order);
case "tool.pick.toggle":
case "tool.pick.cycle":
return cycleToolPickMode(worldRef, command.order);
case "tool.pick.set":
return setToolPickMode(worldRef, command.mode);
case "selection.clear":
return clearSelection(worldRef);
case "selection.focus":
return focusSelection(worldRef, command.target || null, command.options || {});
case "selection.favorite.toggle":
return toggleSelectedFavorite(worldRef, command.target || worldRef?.selected || null);
case "simulation.paused.toggle":
return togglePaused(worldRef);
case "simulation.speed.cycle":
return cycleSpeed(worldRef, command.speeds);
case "pointer.primary":
return primaryPointer(worldRef, command.x, command.y);
case "history.undo": {
const deadBefore = Math.max(0, Number(worldRef?.deadCount || 0) || 0);
const result = global.TarinaiHistory.undo(worldRef);
if (!result.ok) return fail(result.reason || "undo_failed");
const deadAfter = Math.max(0, Number(worldRef?.deadCount || 0) || 0);
global.TarinaiAchievements?.recordHistoryAction?.("undo", { world: worldRef, deadRestored: Math.max(0, deadBefore - deadAfter), label: result.label || "" });
return ok({ action: "undo" });
}
case "history.redo": {
const result = global.TarinaiHistory.redo(worldRef);
if (!result.ok) return fail(result.reason || "redo_failed");
global.TarinaiAchievements?.recordHistoryAction?.("redo", { world: worldRef, label: result.label || "" });
return ok({ action: "redo" });
}
default:
return fail("unknown_command", { type: command.type || "" });
}
}
global.TarinaiCommands = Object.freeze({
dispatch,
setSelection,
});
})(typeof window !== "undefined" ? window : globalThis);