feat: module interaction system with wiring panel

Add a new "module" interaction type where doors/devices define ports
(in/out) and a JS verify function. Players wire their gadget's I/O
to the module's ports via a canvas-rendered wiring panel, then execute
to verify the circuit logic.

- New wiringPanel.js: full wiring UI with keyboard nav, bezier wires,
  mini circuit evaluator, and verify execution
- AND-gate example door in Circuit Lab (tile 9,1)
- Editor support: module type with ports editor and JS verify textarea
- Integrated into gameMode, worldInput, worldRenderer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 17:59:25 +01:00
parent 6ba3fa457a
commit f9492bff4c
6 changed files with 671 additions and 4 deletions

View File

@@ -467,7 +467,7 @@ function render() {
ctx.strokeStyle = selectedEntity?.type === 'interaction' && selectedEntity.index === i ? '#fff' : '#ffdd44';
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);
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);
});
@@ -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>';
}
} else if (t === 'interaction') {
html += propSelect('Type', 'type', ent.type, ['sign','workshop','terminal','door']); // puzzle_door hidden for now
html += propSelect('Type', 'type', ent.type, ['sign','workshop','terminal','door','module']); // puzzle_door hidden for now
html += propText('Label', 'label', ent.label || '');
html += propTextarea('Dialog', 'dialog', (ent.dialog || []).join('\n'));
if (ent.type === 'puzzle_door') {
html += propText('Puzzle ID', 'puzzleId', ent.puzzleId || '');
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;
@@ -899,6 +909,14 @@ function applyPropChange(prop, value, inputType) {
ent.dialog = value.split('\n').filter(l => l.trim());
} else if (prop === 'requiredOutputs') {
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') {
// Store as game ID (pallet-town → town)
ent.targetMap = value === 'pallet-town' ? 'town' : value;
@@ -998,7 +1016,19 @@ function generateMapsJS() {
if (inter.dialog) line += `, dialog: ${JSON.stringify(inter.dialog)}`;
if (inter.puzzleId) line += `, puzzleId: '${inter.puzzleId}'`;
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 += ` ]\n};\n\n`;