// wiringPanel.js — Wiring panel for connecting gadget ports to module ports // The player wires a gadget's I/O to a module door's ports, then executes // to verify the circuit satisfies the module's logic. import { worldState, solvePuzzle, isPuzzleSolved, startDialog } from './worldState.js'; import { showNotification } from './inventory.js'; // ==================== State ==================== let panelOpen = false; let moduleInter = null; // the full interaction object from the map let gadget = null; // the selected gadget from backpack let wires = []; // [{ moduleIdx, gadgetIdx }] let cursor = { side: 'module', index: 0 }; let selectedModule = null; // index or null (first port of a pending wire) let selectedGadget = null; let result = null; // { message, color } or null let resultTimer = 0; // ==================== Public API ==================== export function isWiringOpen() { return panelOpen; } /** * Open the wiring panel for a module interaction with a chosen gadget. * @param {Object} inter - The module interaction from the map * @param {Object} gad - The gadget from inventory */ export function openWiringPanel(inter, gad) { panelOpen = true; moduleInter = inter; gadget = gad; wires = []; cursor = { side: 'module', index: 0 }; selectedModule = null; selectedGadget = null; result = null; worldState.mode = 'wiring'; console.log(`[wiring] opened for module "${inter.label}" with gadget "${gad.name}"`); } export function closeWiringPanel() { panelOpen = false; moduleInter = null; gadget = null; wires = []; worldState.mode = 'world'; } // ==================== Gadget port helpers ==================== /** Build a flat list of gadget ports (inputs first, then outputs) */ function getGadgetPorts() { if (!gadget) return []; const ports = []; const inputIds = gadget.inputIds || []; const outputIds = gadget.outputIds || []; for (let i = 0; i < inputIds.length; i++) { ports.push({ name: `In ${i + 1}`, dir: 'in', gateId: inputIds[i] }); } for (let i = 0; i < outputIds.length; i++) { ports.push({ name: `Out ${i + 1}`, dir: 'out', gateId: outputIds[i] }); } return ports; } // ==================== Input handling ==================== export function handleWiringInput(key) { if (!panelOpen) return false; const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); switch (key) { case 'ArrowUp': case 'w': case 'W': cursor.index = Math.max(0, cursor.index - 1); break; case 'ArrowDown': case 's': case 'S': { const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1; cursor.index = Math.min(Math.max(max, 0), cursor.index + 1); break; } case 'ArrowLeft': case 'ArrowRight': case 'a': case 'A': case 'd': case 'D': cursor.side = cursor.side === 'module' ? 'gadget' : 'module'; { const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1; cursor.index = Math.min(cursor.index, Math.max(max, 0)); } break; case 'e': case 'E': case ' ': selectPort(); break; case 'Enter': executeWiring(); break; case 'Backspace': case 'Delete': case 'x': case 'X': removeWireAtCursor(); break; case 'Escape': closeWiringPanel(); break; default: return false; } return true; } // ==================== Wire management ==================== function selectPort() { if (cursor.side === 'module') { selectedModule = cursor.index; if (selectedGadget !== null) tryCreateWire(); } else { selectedGadget = cursor.index; if (selectedModule !== null) tryCreateWire(); } } function tryCreateWire() { const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); const mPort = mPorts[selectedModule]; const gPort = gPorts[selectedGadget]; if (!mPort || !gPort) { selectedModule = null; selectedGadget = null; return; } // Validation: module-out → gadget-in, OR gadget-out → module-in const valid = (mPort.dir === 'out' && gPort.dir === 'in') || (mPort.dir === 'in' && gPort.dir === 'out'); if (!valid) { result = { message: '✗ Invalid! Wire out→in only', color: '#ff4444' }; resultTimer = Date.now(); selectedModule = null; selectedGadget = null; return; } // Remove any existing wire on either port wires = wires.filter(w => w.moduleIdx !== selectedModule && w.gadgetIdx !== selectedGadget); wires.push({ moduleIdx: selectedModule, gadgetIdx: selectedGadget }); result = { message: `✓ Wired ${mPort.name} ↔ ${gPort.name}`, color: '#00e599' }; resultTimer = Date.now(); selectedModule = null; selectedGadget = null; } function removeWireAtCursor() { const key = cursor.side === 'module' ? 'moduleIdx' : 'gadgetIdx'; const before = wires.length; wires = wires.filter(w => w[key] !== cursor.index); if (wires.length < before) { result = { message: 'Wire removed', color: '#ffaa00' }; resultTimer = Date.now(); } } // ==================== Circuit evaluation ==================== /** * Mini circuit evaluator — runs a cloned gadget circuit with given input values. * Returns an object mapping OUTPUT gate IDs → computed values. */ function evaluateGadgetCircuit(gates, connections, inputValues) { // inputValues = { gateId: 0|1 } const evalGates = JSON.parse(JSON.stringify(gates)); // Set input gate values for (const g of evalGates) { if (g.type === 'INPUT' && inputValues[g.id] !== undefined) { g.value = inputValues[g.id]; } } // Fixed-point iteration (max 20 passes) for (let iter = 0; iter < 20; iter++) { let changed = false; for (const g of evalGates) { if (g.type === 'INPUT' || g.type === 'CLOCK') continue; // Gather inputs from connections const inCount = (g.type === 'NOT' || g.type === 'OUTPUT') ? 1 : 2; const ins = []; for (let p = 0; p < inCount; p++) { const conn = connections.find(c => c.to === g.id && c.toPort === p); if (conn) { const src = evalGates.find(s => s.id === conn.from); ins.push(src ? (src.value || 0) : 0); } else { ins.push(0); } } let val = 0; switch (g.type) { case 'AND': val = (ins[0] && ins[1]) ? 1 : 0; break; case 'OR': val = (ins[0] || ins[1]) ? 1 : 0; break; case 'NOT': val = ins[0] ? 0 : 1; break; case 'NAND': val = (ins[0] && ins[1]) ? 0 : 1; break; case 'NOR': val = (ins[0] || ins[1]) ? 0 : 1; break; case 'XOR': val = (ins[0] !== ins[1]) ? 1 : 0; break; case 'OUTPUT': val = ins[0] || 0; break; default: val = g.value || 0; } if (val !== g.value) { g.value = val; changed = true; } } if (!changed) break; } return evalGates; } /** * Execute the wiring — build a test() function from the wires, * then run the module's verify function to check if the gadget passes. */ function executeWiring() { const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); // Build the test function that the verify code will call. // test(moduleOutputValues) → moduleInputValues function test(moduleOutputs) { // moduleOutputs = { A: 0, B: 1, ... } — values for module's "out" ports // Map module output values → gadget input gates via wires const inputValues = {}; for (const wire of wires) { const mp = mPorts[wire.moduleIdx]; const gp = gPorts[wire.gadgetIdx]; if (mp.dir === 'out' && gp.dir === 'in') { // Module provides value → gadget receives it inputValues[gp.gateId] = moduleOutputs[mp.name] || 0; } } // Evaluate the gadget circuit const evaluated = evaluateGadgetCircuit(gadget.gates, gadget.connections, inputValues); // Read gadget output gates → map back to module input ports via wires const moduleInputs = {}; for (const wire of wires) { const mp = mPorts[wire.moduleIdx]; const gp = gPorts[wire.gadgetIdx]; if (mp.dir === 'in' && gp.dir === 'out') { const outGate = evaluated.find(g => g.id === gp.gateId); moduleInputs[mp.name] = outGate ? (outGate.value || 0) : 0; } } return moduleInputs; } // Run the verify function try { const verifyFn = new Function('return ' + moduleInter.verify)(); const passed = verifyFn(test); if (passed) { result = { message: '⚡ VERIFIED! Module unlocked!', color: '#00ff88' }; resultTimer = Date.now(); // Mark as solved if it has an ID if (moduleInter.moduleId) { solvePuzzle(moduleInter.moduleId); } // Close after a brief delay and show dialog setTimeout(() => { closeWiringPanel(); showNotification('Module unlocked!', '⚡', '#00ff88'); startDialog([ `⚡ "${gadget.name}" passed the verification!`, 'The module hums to life and the door unlocks.' ], 'System'); }, 800); } else { result = { message: '✗ Verification failed — wrong logic', color: '#ff4444' }; resultTimer = Date.now(); } } catch (e) { result = { message: `Error: ${e.message}`, color: '#ff4444' }; resultTimer = Date.now(); console.error('[wiring] verify error:', e); } } // ==================== Rendering ==================== const PANEL_BG = 'rgba(10, 12, 20, 0.95)'; const PANEL_BORDER = '#00e599'; const PORT_OUT_COLOR = '#ff6644'; const PORT_IN_COLOR = '#44aaff'; const WIRE_COLOR = '#ffdd44'; const SELECTED_COLOR = '#ffffff'; const CURSOR_COLOR = '#00ffcc'; export function drawWiringPanel(ctx, canvasW, canvasH) { if (!panelOpen || !moduleInter || !gadget) return; const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); // Panel dimensions const pw = Math.min(640, canvasW - 40); const ph = Math.min(480, canvasH - 40); const px = (canvasW - pw) / 2; const py = (canvasH - ph) / 2; // Dim background ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(0, 0, canvasW, canvasH); // Panel background ctx.fillStyle = PANEL_BG; ctx.strokeStyle = PANEL_BORDER; ctx.lineWidth = 2; roundRect(ctx, px, py, pw, ph, 8); ctx.fill(); ctx.stroke(); // Title ctx.fillStyle = '#00e599'; ctx.font = 'bold 16px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText(`⚡ WIRING PANEL — ${moduleInter.label || 'Module'}`, px + pw / 2, py + 12); // Gadget name ctx.fillStyle = '#ff44aa'; ctx.font = '12px "Segoe UI", system-ui, sans-serif'; ctx.fillText(`Gadget: ${gadget.icon || '🔧'} ${gadget.name}`, px + pw / 2, py + 34); // Column headers const colY = py + 60; const leftX = px + 30; const rightX = px + pw - 30; const portStartY = colY + 30; const portSpacing = 40; ctx.font = 'bold 13px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; // Module ports header ctx.fillStyle = '#aaa'; ctx.fillText('MODULE PORTS', leftX + 80, colY); // Gadget ports header ctx.fillText('GADGET PORTS', rightX - 80, colY); // Draw column separator ctx.strokeStyle = '#333'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px + pw / 2, colY + 15); ctx.lineTo(px + pw / 2, py + ph - 50); ctx.stroke(); // Port positions for wire drawing const modulePortPositions = []; const gadgetPortPositions = []; // Draw module ports ctx.font = '13px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'left'; for (let i = 0; i < mPorts.length; i++) { const port = mPorts[i]; const yPos = portStartY + i * portSpacing; const isOut = port.dir === 'out'; const dotX = leftX; const dotRadius = 8; // Port dot position (for wires) const wireX = leftX + 170; modulePortPositions.push({ x: wireX, y: yPos + 6 }); // Highlight if cursor is here const isCursor = cursor.side === 'module' && cursor.index === i; const isSelected = selectedModule === i; // Background highlight if (isCursor) { ctx.fillStyle = 'rgba(0, 255, 200, 0.1)'; ctx.fillRect(leftX - 10, yPos - 6, 190, portSpacing - 8); } // Port dot ctx.beginPath(); ctx.arc(dotX, yPos + 6, dotRadius, 0, Math.PI * 2); ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR); ctx.fill(); if (isCursor) { ctx.strokeStyle = CURSOR_COLOR; ctx.lineWidth = 2; ctx.stroke(); } // Direction arrow inside dot ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6); // Port label ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.font = '13px "Segoe UI", system-ui, sans-serif'; ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd'; ctx.fillText(`${port.name}`, dotX + 14, yPos); // Direction tag ctx.font = '10px monospace'; ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR; ctx.fillText(isOut ? 'OUT' : 'IN', dotX + 14, yPos + 16); // Wire connected indicator const wire = wires.find(w => w.moduleIdx === i); if (wire) { ctx.fillStyle = WIRE_COLOR; ctx.fillText('⚡', dotX + 50, yPos + 16); } } // Draw gadget ports ctx.textAlign = 'right'; for (let i = 0; i < gPorts.length; i++) { const port = gPorts[i]; const yPos = portStartY + i * portSpacing; const isOut = port.dir === 'out'; const dotX = rightX; const dotRadius = 8; const wireX = rightX - 170; gadgetPortPositions.push({ x: wireX, y: yPos + 6 }); const isCursor = cursor.side === 'gadget' && cursor.index === i; const isSelected = selectedGadget === i; // Background highlight if (isCursor) { ctx.fillStyle = 'rgba(0, 255, 200, 0.1)'; ctx.fillRect(rightX - 180, yPos - 6, 190, portSpacing - 8); } // Port dot ctx.beginPath(); ctx.arc(dotX, yPos + 6, dotRadius, 0, Math.PI * 2); ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR); ctx.fill(); if (isCursor) { ctx.strokeStyle = CURSOR_COLOR; ctx.lineWidth = 2; ctx.stroke(); } // Direction arrow ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6); // Port label ctx.textAlign = 'right'; ctx.textBaseline = 'top'; ctx.font = '13px "Segoe UI", system-ui, sans-serif'; ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd'; ctx.fillText(`${port.name}`, dotX - 14, yPos); // Direction tag ctx.font = '10px monospace'; ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR; ctx.fillText(isOut ? 'OUT' : 'IN', dotX - 14, yPos + 16); // Wire connected indicator const wire = wires.find(w => w.gadgetIdx === i); if (wire) { ctx.fillStyle = WIRE_COLOR; ctx.fillText('⚡', dotX - 60, yPos + 16); } } // Draw wires ctx.strokeStyle = WIRE_COLOR; ctx.lineWidth = 2; ctx.setLineDash([6, 4]); for (const wire of wires) { const mp = modulePortPositions[wire.moduleIdx]; const gp = gadgetPortPositions[wire.gadgetIdx]; if (mp && gp) { ctx.beginPath(); ctx.moveTo(mp.x, mp.y); // Bezier curve for nice look const midX = (mp.x + gp.x) / 2; ctx.bezierCurveTo(midX, mp.y, midX, gp.y, gp.x, gp.y); ctx.stroke(); } } ctx.setLineDash([]); // Pending wire (half-selected) if (selectedModule !== null && selectedGadget === null) { const mp = modulePortPositions[selectedModule]; if (mp) { ctx.beginPath(); ctx.arc(mp.x, mp.y, 5, 0, Math.PI * 2); ctx.fillStyle = SELECTED_COLOR; ctx.fill(); } } if (selectedGadget !== null && selectedModule === null) { const gp = gadgetPortPositions[selectedGadget]; if (gp) { ctx.beginPath(); ctx.arc(gp.x, gp.y, 5, 0, Math.PI * 2); ctx.fillStyle = SELECTED_COLOR; ctx.fill(); } } // Result message if (result && (Date.now() - resultTimer < 3000)) { ctx.fillStyle = result.color; ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillText(result.message, px + pw / 2, py + ph - 35); } // Controls hint ctx.fillStyle = '#555'; ctx.font = '11px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillText('↑↓←→: Navigate | E: Select/Wire | X: Remove | Enter: Execute | ESC: Close', px + pw / 2, py + ph - 10); } // ==================== Helpers ==================== function roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); }