// gameMode.js - Central coordinator: switches between World and Workshop modes import { worldState, setPlayerPosition, warpToMap, isPuzzleSolved, solvePuzzle, startDialog } from './worldState.js'; import { initWorldRenderer, startWorldLoop, stopWorldLoop } from './worldRenderer.js'; import { initWorldInput, destroyWorldInput, setInteractionHandler } from './worldInput.js'; import { getMap } from './maps.js'; import { saveGadget, openBackpack, getGadgets, openNamingScreen, showNotification } from './inventory.js'; // Circuit editor stop function (to stop its render loop when switching modes) import { stopCircuitLoop } from '../renderer.js'; import { state as circuitState } from '../state.js'; // Circuit editor modules (registered from app.js to avoid circular deps) let circuitEditorInit = null; let circuitEditorDestroy = null; let currentMode = 'none'; // 'world' | 'workshop' /** * Register the circuit editor's init/destroy functions. * Called from app.js so we don't create circular imports. */ export function registerCircuitEditor(initFn, destroyFn) { circuitEditorInit = initFn; circuitEditorDestroy = destroyFn; } /** * Boot the game — start in world mode */ export function startGame() { // Set spawn const map = getMap(worldState.currentMap); if (map && map.spawn) { setPlayerPosition(map.spawn.x, map.spawn.y); } // Wire up interaction handler setInteractionHandler(handleInteraction); // Wire save-gadget button const saveGadgetBtn = document.getElementById('save-gadget-btn'); if (saveGadgetBtn) { saveGadgetBtn.addEventListener('click', handleSaveGadget); } // Enter world mode enterWorldMode(); } // ==================== Mode switching ==================== export function enterWorldMode() { if (currentMode === 'world') return; // Tear down workshop if active if (currentMode === 'workshop') { stopCircuitLoop(); if (circuitEditorDestroy) circuitEditorDestroy(); hideWorkshopUI(); } currentMode = 'world'; worldState.mode = 'world'; showWorldUI(); initWorldRenderer(); initWorldInput(); startWorldLoop(); console.log('[gameMode] entered world mode'); } export function enterWorkshopMode() { if (currentMode === 'workshop') return; // Tear down world if (currentMode === 'world') { stopWorldLoop(); destroyWorldInput(); hideWorldUI(); } currentMode = 'workshop'; worldState.mode = 'workshop'; showWorkshopUI(); if (circuitEditorInit) circuitEditorInit(); console.log('[gameMode] entered workshop mode'); } export function getCurrentMode() { return currentMode; } // ==================== Interaction handler ==================== function handleInteraction(event) { switch (event.type) { case 'enterWorkshop': enterWorkshopMode(); break; case 'puzzleDoor': { const inter = event.data; if (isPuzzleSolved(inter.puzzleId)) { startDialog(['This door is already unlocked.'], 'System'); return; } // Open backpack to let player choose a gadget const gadgets = getGadgets(); if (gadgets.length === 0) { startDialog([ 'This door requires a logic circuit to open.', `Required output: [${inter.requiredOutputs.join(', ')}]`, 'Craft a circuit in your Workshop (TAB) and save it as a gadget!' ], 'System'); return; } // Open backpack with a "use" callback that tests the gadget openBackpack((gadget) => { const result = testGadgetOnPuzzle(gadget, inter); if (result) { solvePuzzle(inter.puzzleId); startDialog([ `🎉 "${gadget.name}" solved the puzzle!`, 'The door unlocks with a satisfying click.' ], 'System'); } else { startDialog([ `"${gadget.name}" didn't produce the right output.`, `Required: [${inter.requiredOutputs.join(', ')}]`, 'Try a different gadget or tweak your circuit!' ], 'System'); } }); break; } case 'mapExit': { // Every exit MUST have targetX/targetY — bidirectional door links. // No spawn fallback. Spawn is only for the initial game start. const { targetMap, targetX, targetY } = event.data; warpToMap(targetMap, targetX, targetY); console.log(`[gameMode] warped to ${targetMap} (${targetX}, ${targetY})`); break; } case 'openInventory': // TODO: inventory UI console.log('[gameMode] inventory:', worldState.inventory); break; } } // ==================== Save Gadget ==================== function handleSaveGadget() { // Gather current circuit from the editor state const gates = circuitState.gates; const connections = circuitState.connections; const inputGates = gates.filter(g => g.type === 'INPUT'); const outputGates = gates.filter(g => g.type === 'OUTPUT'); if (inputGates.length === 0 || outputGates.length === 0) { showNotification('Need at least 1 INPUT and 1 OUTPUT!', '⚠️', '#ff5555'); return; } // Switch to world render temporarily to show the naming screen on canvas // (workshop mode uses its own render loop, so we overlay on the canvas) openNamingScreen( '🎒 Name your gadget', `Gadget ${getGadgets().length + 1}`, (name) => { if (!name) return; // cancelled const component = { id: name.toLowerCase().replace(/[^a-z0-9_]/g, '_').replace(/_+/g, '_'), name, inputCount: inputGates.length, outputCount: outputGates.length, inputIds: inputGates.map(g => g.id), outputIds: outputGates.map(g => g.id), gates: JSON.parse(JSON.stringify(gates)), connections: JSON.parse(JSON.stringify(connections)) }; const result = saveGadget(component); if (result.success) { showNotification(`"${name}" saved to backpack!`, '🎒', '#ff44aa'); } else { showNotification(result.error, '⚠️', '#ff5555'); } } ); } // ==================== Puzzle testing ==================== /** * Test a gadget against a puzzle door's required outputs. * Runs the gadget's internal circuit with all possible input combos * and checks if outputs match. */ function testGadgetOnPuzzle(gadget, puzzleInter) { const required = puzzleInter.requiredOutputs; if (!required || !gadget.gates || !gadget.connections) return false; // Clone internal circuit for evaluation const gates = JSON.parse(JSON.stringify(gadget.gates)); const conns = gadget.connections; const inputIds = gadget.inputIds || []; const outputIds = gadget.outputIds || []; // Test with all inputs = 0, then all = 1, etc. // For simplicity: test all 2^n input combos and collect outputs for each const n = inputIds.length; const allOutputs = []; for (let combo = 0; combo < (1 << n); combo++) { // Reset gates const evalGates = JSON.parse(JSON.stringify(gates)); // Set inputs for (let i = 0; i < n; i++) { const inputGate = evalGates.find(g => g.id === inputIds[i]); if (inputGate) inputGate.value = (combo >> i) & 1; } // Evaluate (fixed-point iteration) for (let iter = 0; iter < 20; iter++) { let changed = false; for (const g of evalGates) { if (g.type === 'INPUT' || g.type === 'CLOCK') continue; const inCount = g.type === 'NOT' || g.type === 'OUTPUT' ? 1 : 2; const gInputs = []; for (let j = 0; j < inCount; j++) { const conn = conns.find(c => c.to === g.id && c.toPort === j); if (conn) { const src = evalGates.find(s => s.id === conn.from); gInputs.push(src ? (src.value || 0) : 0); } else { gInputs.push(0); } } let result = 0; switch (g.type) { case 'AND': result = (gInputs[0] && gInputs[1]) ? 1 : 0; break; case 'OR': result = (gInputs[0] || gInputs[1]) ? 1 : 0; break; case 'NOT': result = gInputs[0] ? 0 : 1; break; case 'NAND': result = (gInputs[0] && gInputs[1]) ? 0 : 1; break; case 'NOR': result = (gInputs[0] || gInputs[1]) ? 0 : 1; break; case 'XOR': result = (gInputs[0] !== gInputs[1]) ? 1 : 0; break; case 'OUTPUT': result = gInputs[0] || 0; break; default: result = 0; } if (result !== g.value) { g.value = result; changed = true; } } if (!changed) break; } // Collect outputs for (const outId of outputIds) { const outGate = evalGates.find(g => g.id === outId); allOutputs.push(outGate ? (outGate.value || 0) : 0); } } // Compare: the required outputs should match the output pattern // Simple comparison: check if required matches any combo's outputs // Or: required is the full truth table (outputs for combo 0, then combo 1, etc.) if (allOutputs.length === required.length) { return allOutputs.every((v, i) => v === required[i]); } // Fallback: check if any single combo's outputs match const outPerCombo = outputIds.length; for (let combo = 0; combo < (1 << n); combo++) { const comboOutputs = allOutputs.slice(combo * outPerCombo, (combo + 1) * outPerCombo); if (comboOutputs.length === required.length && comboOutputs.every((v, i) => v === required[i])) { return true; } } return false; } // ==================== UI visibility ==================== function showWorldUI() { // Hide workshop-specific elements const toolbar = document.getElementById('toolbar'); const wavePanel = document.getElementById('waveform-panel'); const canvas = document.getElementById('canvas'); if (toolbar) toolbar.style.display = 'none'; if (wavePanel) wavePanel.style.display = 'none'; if (canvas) { canvas.style.top = '0'; canvas.style.cursor = 'default'; } // Hide game buttons (we're IN world, not workshop) const backBtn = document.getElementById('back-to-world-btn'); if (backBtn) backBtn.style.display = 'none'; const saveBtn = document.getElementById('save-gadget-btn'); if (saveBtn) saveBtn.style.display = 'none'; } function hideWorldUI() { // Nothing special to hide — canvas stays } function showWorkshopUI() { const toolbar = document.getElementById('toolbar'); const canvas = document.getElementById('canvas'); if (toolbar) toolbar.style.display = 'flex'; if (canvas) { canvas.style.top = '56px'; canvas.style.cursor = 'default'; } // Move game buttons into toolbar-right so they sit inline with Export/Import const toolbarRight = document.querySelector('.toolbar-right'); const saveBtn = document.getElementById('save-gadget-btn'); const backBtn = document.getElementById('back-to-world-btn'); if (toolbarRight && saveBtn) { toolbarRight.insertBefore(saveBtn, toolbarRight.firstChild); saveBtn.style.display = 'inline-block'; } if (toolbarRight && backBtn) { toolbarRight.insertBefore(backBtn, toolbarRight.firstChild); backBtn.style.display = 'inline-block'; } } function hideWorkshopUI() { const toolbar = document.getElementById('toolbar'); if (toolbar) toolbar.style.display = 'none'; // Hide game buttons and move them back out of toolbar const backBtn = document.getElementById('back-to-world-btn'); if (backBtn) { backBtn.style.display = 'none'; document.body.insertBefore(backBtn, document.body.firstChild); } const saveBtn = document.getElementById('save-gadget-btn'); if (saveBtn) { saveBtn.style.display = 'none'; document.body.insertBefore(saveBtn, document.body.firstChild); } }