Compare commits
2 Commits
e7b18afd1a
...
f9492bff4c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9492bff4c | ||
|
|
6ba3fa457a |
36
editor.html
36
editor.html
@@ -467,7 +467,7 @@ function render() {
|
|||||||
ctx.strokeStyle = selectedEntity?.type === 'interaction' && selectedEntity.index === i ? '#fff' : '#ffdd44';
|
ctx.strokeStyle = selectedEntity?.type === 'interaction' && selectedEntity.index === i ? '#fff' : '#ffdd44';
|
||||||
ctx.lineWidth = selectedEntity?.type === 'interaction' && selectedEntity.index === i ? 2.5 : 1.5;
|
ctx.lineWidth = selectedEntity?.type === 'interaction' && selectedEntity.index === i ? 2.5 : 1.5;
|
||||||
ctx.strokeRect(inter.x * TILE_PX, inter.y * TILE_PX, TILE_PX, TILE_PX);
|
ctx.strokeRect(inter.x * TILE_PX, inter.y * TILE_PX, TILE_PX, TILE_PX);
|
||||||
const icon = inter.type === 'workshop' ? '🔧' : inter.type === 'puzzle_door' ? '🔒' : inter.type === 'terminal' ? '💻' : '📋';
|
const icon = inter.type === 'workshop' ? '🔧' : inter.type === 'puzzle_door' ? '🔒' : inter.type === 'module' ? '⚡' : inter.type === 'terminal' ? '💻' : '📋';
|
||||||
drawLabel(ctx, icon, inter.x, inter.y);
|
drawLabel(ctx, icon, inter.x, inter.y);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -856,13 +856,23 @@ function updateProps() {
|
|||||||
html += '<div style="color:var(--red);font-size:10px;padding:2px 0;">⚠️ Set target X/Y! Every exit needs explicit coordinates.</div>';
|
html += '<div style="color:var(--red);font-size:10px;padding:2px 0;">⚠️ Set target X/Y! Every exit needs explicit coordinates.</div>';
|
||||||
}
|
}
|
||||||
} else if (t === 'interaction') {
|
} else if (t === 'interaction') {
|
||||||
html += propSelect('Type', 'type', ent.type, ['sign','workshop','puzzle_door','terminal','door']);
|
html += propSelect('Type', 'type', ent.type, ['sign','workshop','terminal','door','module']); // puzzle_door hidden for now
|
||||||
html += propText('Label', 'label', ent.label || '');
|
html += propText('Label', 'label', ent.label || '');
|
||||||
html += propTextarea('Dialog', 'dialog', (ent.dialog || []).join('\n'));
|
html += propTextarea('Dialog', 'dialog', (ent.dialog || []).join('\n'));
|
||||||
if (ent.type === 'puzzle_door') {
|
if (ent.type === 'puzzle_door') {
|
||||||
html += propText('Puzzle ID', 'puzzleId', ent.puzzleId || '');
|
html += propText('Puzzle ID', 'puzzleId', ent.puzzleId || '');
|
||||||
html += propText('Req. Outputs', 'requiredOutputs', (ent.requiredOutputs || []).join(','));
|
html += propText('Req. Outputs', 'requiredOutputs', (ent.requiredOutputs || []).join(','));
|
||||||
}
|
}
|
||||||
|
if (ent.type === 'module') {
|
||||||
|
html += propText('Module ID', 'moduleId', ent.moduleId || '');
|
||||||
|
// Ports editor — compact format: "A:out, B:out, C:in"
|
||||||
|
const portsStr = (ent.ports || []).map(p => `${p.name}:${p.dir}`).join(', ');
|
||||||
|
html += propText('Ports', 'ports', portsStr);
|
||||||
|
html += `<div class="prop-row" style="font-size:10px;color:#888;">Format: A:out, B:out, C:in</div>`;
|
||||||
|
// Verify code editor
|
||||||
|
const verifyCode = ent.verify || `(test) => {\n return test({A:0, B:0}).C === 0\n && test({A:1, B:1}).C === 1;\n}`;
|
||||||
|
html += `<div class="prop-row"><label>Verify (JS)</label><textarea data-prop="verify" style="font-family:monospace;font-size:11px;min-height:100px;white-space:pre;">${esc(verifyCode)}</textarea></div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
@@ -899,6 +909,14 @@ function applyPropChange(prop, value, inputType) {
|
|||||||
ent.dialog = value.split('\n').filter(l => l.trim());
|
ent.dialog = value.split('\n').filter(l => l.trim());
|
||||||
} else if (prop === 'requiredOutputs') {
|
} else if (prop === 'requiredOutputs') {
|
||||||
ent.requiredOutputs = value.split(',').map(Number);
|
ent.requiredOutputs = value.split(',').map(Number);
|
||||||
|
} else if (prop === 'ports') {
|
||||||
|
// Parse "A:out, B:out, C:in" format
|
||||||
|
ent.ports = value.split(',').map(s => s.trim()).filter(Boolean).map(s => {
|
||||||
|
const [name, dir] = s.split(':').map(p => p.trim());
|
||||||
|
return { name: name || '?', dir: dir || 'out', bits: 1 };
|
||||||
|
});
|
||||||
|
} else if (prop === 'verify') {
|
||||||
|
ent.verify = value;
|
||||||
} else if (prop === 'targetMap') {
|
} else if (prop === 'targetMap') {
|
||||||
// Store as game ID (pallet-town → town)
|
// Store as game ID (pallet-town → town)
|
||||||
ent.targetMap = value === 'pallet-town' ? 'town' : value;
|
ent.targetMap = value === 'pallet-town' ? 'town' : value;
|
||||||
@@ -998,7 +1016,19 @@ function generateMapsJS() {
|
|||||||
if (inter.dialog) line += `, dialog: ${JSON.stringify(inter.dialog)}`;
|
if (inter.dialog) line += `, dialog: ${JSON.stringify(inter.dialog)}`;
|
||||||
if (inter.puzzleId) line += `, puzzleId: '${inter.puzzleId}'`;
|
if (inter.puzzleId) line += `, puzzleId: '${inter.puzzleId}'`;
|
||||||
if (inter.requiredOutputs) line += `, requiredOutputs: [${inter.requiredOutputs}]`;
|
if (inter.requiredOutputs) line += `, requiredOutputs: [${inter.requiredOutputs}]`;
|
||||||
line += ` },\n`;
|
if (inter.moduleId) line += `,\n moduleId: '${inter.moduleId}'`;
|
||||||
|
if (inter.ports && inter.ports.length > 0) {
|
||||||
|
line += `,\n ports: [\n`;
|
||||||
|
for (const p of inter.ports) {
|
||||||
|
line += ` { name: '${p.name}', dir: '${p.dir}', bits: ${p.bits || 1} },\n`;
|
||||||
|
}
|
||||||
|
line += ` ]`;
|
||||||
|
}
|
||||||
|
if (inter.verify) {
|
||||||
|
// Output verify as raw JS (not a string) so it's executable
|
||||||
|
line += `,\n verify: \`${inter.verify.replace(/`/g, '\\`')}\``;
|
||||||
|
}
|
||||||
|
line += `\n },\n`;
|
||||||
out += line;
|
out += line;
|
||||||
}
|
}
|
||||||
out += ` ]\n};\n\n`;
|
out += ` ]\n};\n\n`;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => {
|
() => {
|
||||||
initRenderer();
|
initRenderer();
|
||||||
initEvents();
|
initEvents();
|
||||||
initPuzzleUI();
|
// initPuzzleUI(); // HIDDEN: puzzle mode disabled for now
|
||||||
if (loadFromStorage()) {
|
if (loadFromStorage()) {
|
||||||
updateComponentButtons();
|
updateComponentButtons();
|
||||||
evaluateAll();
|
evaluateAll();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { initWorldRenderer, startWorldLoop, stopWorldLoop } from './worldRendere
|
|||||||
import { initWorldInput, destroyWorldInput, setInteractionHandler } from './worldInput.js';
|
import { initWorldInput, destroyWorldInput, setInteractionHandler } from './worldInput.js';
|
||||||
import { getMap } from './maps.js';
|
import { getMap } from './maps.js';
|
||||||
import { saveGadget, openBackpack, getGadgets, openNamingScreen, showNotification } from './inventory.js';
|
import { saveGadget, openBackpack, getGadgets, openNamingScreen, showNotification } from './inventory.js';
|
||||||
|
import { openWiringPanel } from './wiringPanel.js';
|
||||||
|
|
||||||
// Circuit editor stop function (to stop its render loop when switching modes)
|
// Circuit editor stop function (to stop its render loop when switching modes)
|
||||||
import { stopCircuitLoop } from '../renderer.js';
|
import { stopCircuitLoop } from '../renderer.js';
|
||||||
@@ -144,6 +145,31 @@ function handleInteraction(event) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'module': {
|
||||||
|
const inter = event.data;
|
||||||
|
// Already solved?
|
||||||
|
if (inter.moduleId && isPuzzleSolved(inter.moduleId)) {
|
||||||
|
startDialog(['This module is already unlocked.'], 'System');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Need gadgets
|
||||||
|
const mGadgets = getGadgets();
|
||||||
|
if (mGadgets.length === 0) {
|
||||||
|
const portDesc = (inter.ports || []).map(p => `${p.name} (${p.dir})`).join(', ');
|
||||||
|
startDialog([
|
||||||
|
`This module requires a gadget to operate.`,
|
||||||
|
`Ports: ${portDesc}`,
|
||||||
|
'Craft a circuit in your Workshop (TAB) and save it as a gadget!'
|
||||||
|
], 'System');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Open backpack → on "Use", open wiring panel
|
||||||
|
openBackpack((gadget) => {
|
||||||
|
openWiringPanel(inter, gadget);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'openInventory':
|
case 'openInventory':
|
||||||
// TODO: inventory UI
|
// TODO: inventory UI
|
||||||
console.log('[gameMode] inventory:', worldState.inventory);
|
console.log('[gameMode] inventory:', worldState.inventory);
|
||||||
|
|||||||
@@ -48,6 +48,23 @@ const labMap = {
|
|||||||
{ x: 1, y: 7, type: 'sign', label: 'Bookshelf', dialog: ["A collection of logic circuit manuals."] },
|
{ x: 1, y: 7, type: 'sign', label: 'Bookshelf', dialog: ["A collection of logic circuit manuals."] },
|
||||||
{ x: 7, y: 7, type: 'sign', label: 'Bookshelf', dialog: ["Advanced boolean algebra textbooks."] },
|
{ x: 7, y: 7, type: 'sign', label: 'Bookshelf', dialog: ["Advanced boolean algebra textbooks."] },
|
||||||
{ x: 0, y: 1, type: 'terminal', label: 'Terminal', dialog: ["Circuit analysis terminal.","Connect components to solve puzzles."] },
|
{ x: 0, y: 1, type: 'terminal', label: 'Terminal', dialog: ["Circuit analysis terminal.","Connect components to solve puzzles."] },
|
||||||
|
// Module door example: requires AND(A, B) → C
|
||||||
|
{
|
||||||
|
x: 9, y: 1, type: 'module',
|
||||||
|
moduleId: 'lab_and_door',
|
||||||
|
label: 'AND Gate Door',
|
||||||
|
ports: [
|
||||||
|
{ name: 'A', dir: 'out', bits: 1 },
|
||||||
|
{ name: 'B', dir: 'out', bits: 1 },
|
||||||
|
{ name: 'C', dir: 'in', bits: 1 }
|
||||||
|
],
|
||||||
|
verify: `(test) => {
|
||||||
|
return test({A:0, B:0}).C === 0
|
||||||
|
&& test({A:0, B:1}).C === 0
|
||||||
|
&& test({A:1, B:0}).C === 0
|
||||||
|
&& test({A:1, B:1}).C === 1;
|
||||||
|
}`
|
||||||
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
577
js/world/wiringPanel.js
Normal file
577
js/world/wiringPanel.js
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { worldState, advanceDialog, startDialog } from './worldState.js';
|
|||||||
import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.js';
|
import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.js';
|
||||||
import { toggleDebug } from './worldRenderer.js';
|
import { toggleDebug } from './worldRenderer.js';
|
||||||
import { isBackpackOpen, openBackpack, handleBackpackInput, isNamingActive, handleNamingInput } from './inventory.js';
|
import { isBackpackOpen, openBackpack, handleBackpackInput, isNamingActive, handleNamingInput } from './inventory.js';
|
||||||
|
import { isWiringOpen, handleWiringInput } from './wiringPanel.js';
|
||||||
|
|
||||||
const keysDown = new Set();
|
const keysDown = new Set();
|
||||||
let interactionHandler = null;
|
let interactionHandler = null;
|
||||||
@@ -33,6 +34,13 @@ function onKeyDown(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wiring panel — route all input there
|
||||||
|
if (isWiringOpen()) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleWiringInput(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Backpack open — route all input there
|
// Backpack open — route all input there
|
||||||
if (isBackpackOpen()) {
|
if (isBackpackOpen()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -201,6 +209,9 @@ function performInteraction() {
|
|||||||
case 'puzzle_door':
|
case 'puzzle_door':
|
||||||
if (interactionHandler) interactionHandler({ type: 'puzzleDoor', data: inter });
|
if (interactionHandler) interactionHandler({ type: 'puzzleDoor', data: inter });
|
||||||
break;
|
break;
|
||||||
|
case 'module':
|
||||||
|
if (interactionHandler) interactionHandler({ type: 'module', data: inter });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (inter.dialog) startDialog(inter.dialog, '');
|
if (inter.dialog) startDialog(inter.dialog, '');
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { worldState } from './worldState.js';
|
|||||||
import { getMap, getInteraction, getNPC, getExit, isWall } from './maps.js';
|
import { getMap, getInteraction, getNPC, getExit, isWall } from './maps.js';
|
||||||
import { updateMovement } from './worldInput.js';
|
import { updateMovement } from './worldInput.js';
|
||||||
import { drawBackpack, getGadgets, drawNamingScreen, drawNotification } from './inventory.js';
|
import { drawBackpack, getGadgets, drawNamingScreen, drawNotification } from './inventory.js';
|
||||||
|
import { isWiringOpen, drawWiringPanel } from './wiringPanel.js';
|
||||||
|
|
||||||
let canvas = null;
|
let canvas = null;
|
||||||
let ctx = null;
|
let ctx = null;
|
||||||
@@ -145,7 +146,12 @@ export function renderWorld(timestamp) {
|
|||||||
drawBackpack(ctx, canvas.width, canvas.height);
|
drawBackpack(ctx, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Layer 7: Naming screen (on top of everything including backpack) ===
|
// === Layer 7: Wiring panel overlay ===
|
||||||
|
if (isWiringOpen()) {
|
||||||
|
drawWiringPanel(ctx, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Layer 8: Naming screen (on top of everything) ===
|
||||||
drawNamingScreen(ctx, canvas.width, canvas.height);
|
drawNamingScreen(ctx, canvas.width, canvas.height);
|
||||||
|
|
||||||
// === Layer 8: Notification toast ===
|
// === Layer 8: Notification toast ===
|
||||||
|
|||||||
Reference in New Issue
Block a user