export function createScriptController({ setState }) { let actions = []; const saved = new Map(); let recording = false; let playing = false; let resolver = null; const publish = () => setState({ scriptRecording: recording, scriptPlaying: playing, scriptActions: actions.map((action, index) => ({ id: action.id, label: action.label, index, parallel: action.parallel, parallelGroup: action.parallelGroup, delay: action.delay || 0 })), savedScripts: [...saved.keys()], }); const api = { get recording() { return recording; }, get playing() { return playing; }, start() { if (!playing) { recording = true; publish(); } }, stop() { recording = false; publish(); }, toggle() { recording ? api.stop() : api.start(); }, add(label, run, options = {}) { if (!recording || typeof run !== "function") return false; actions.push({ id: crypto.randomUUID(), label, run, parallel: !!options.parallel, delay: Math.max(0, Number(options.delay) || 0) }); publish(); return true; }, toggleParallel(index) { if (!actions[index]) return; if (actions[index].parallelGroup) actions[index].parallelGroup = null; actions[index].parallel = !actions[index].parallel; publish(); }, setDelay(index, seconds) { if (!actions[index]) return; actions[index].delay = Math.max(0, Number(seconds) || 0); publish(); }, remove(index) { actions.splice(index, 1); publish(); }, clear() { actions = []; publish(); }, save(name) { const clean = String(name || "").trim(); if (!clean || !actions.length) return false; saved.set(clean, actions.slice()); publish(); return true; }, async run(name = null) { const sequence = name ? saved.get(name) : actions; if (!sequence?.length || playing) return; playing = true; recording = false; publish(); try { for (let i = 0; i < sequence.length;) { const action = sequence[i]; if (!action.parallel && !action.parallelGroup) { if (action.delay) await gameApiWait(action.delay); await action.run(); i++; continue; } const group = []; if (action.parallelGroup) { while (i < sequence.length && sequence[i].parallelGroup === action.parallelGroup) group.push(sequence[i++]); } else { while (i < sequence.length && sequence[i].parallel && !sequence[i].parallelGroup) group.push(sequence[i++]); } await Promise.all(group.map(async (entry) => { if (entry.delay) await gameApiWait(entry.delay); return entry.run(); })); } } finally { playing = false; publish(); } }, runSaved(name) { return api.run(name); }, wait(seconds) { return gameApiWait(seconds); }, load(serialized) { if (!resolver) throw new Error("Script loader is not ready"); const source = typeof serialized === "string" ? JSON.parse(serialized) : serialized; const entries = Array.isArray(source) ? source : source?.actions; if (!Array.isArray(entries) || !entries.length) throw new Error("Script must contain an actions array"); const loaded = entries.map((entry) => { const resolved = resolver(entry); if (!resolved?.run) throw new Error(`Unknown script action: ${entry?.action || entry?.type || "?"}`); return { id: crypto.randomUUID(), label: resolved.label || entry.label || entry.action, run: resolved.run, parallel: !!entry.parallel, parallelGroup: entry.parallelGroup || null, delay: Math.max(0, Number(entry.delay ?? entry.startAfter) || 0) }; }); actions = loaded; publish(); return true; }, }; // Bound by game.js after the simulation timeline exists. let gameApiWait = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); api.bindWait = (wait) => { gameApiWait = wait; }; api.bindResolver = (next) => { resolver = next; }; publish(); return api; }