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