Files
logic-gates/js/world/worldInput.js
Jose Luis f9492bff4c 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>
2026-03-20 17:59:25 +01:00

233 lines
6.2 KiB
JavaScript

// worldInput.js - Keyboard input for world mode
import { worldState, advanceDialog, startDialog } from './worldState.js';
import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.js';
import { toggleDebug } from './worldRenderer.js';
import { isBackpackOpen, openBackpack, handleBackpackInput, isNamingActive, handleNamingInput } from './inventory.js';
import { isWiringOpen, handleWiringInput } from './wiringPanel.js';
const keysDown = new Set();
let interactionHandler = null;
export function setInteractionHandler(fn) { interactionHandler = fn; }
export function initWorldInput() {
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
}
export function destroyWorldInput() {
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('keyup', onKeyUp);
keysDown.clear();
}
// ---- Key handlers ----
function onKeyDown(e) {
const key = e.key;
keysDown.add(key);
// Naming screen — route all input there
if (isNamingActive()) {
e.preventDefault();
handleNamingInput(key);
return;
}
// Wiring panel — route all input there
if (isWiringOpen()) {
e.preventDefault();
handleWiringInput(key);
return;
}
// Backpack open — route all input there
if (isBackpackOpen()) {
e.preventDefault();
handleBackpackInput(key);
return;
}
// During dialog: advance on action keys
if (worldState.dialog) {
if (key === 'Enter' || key === ' ' || key === 'e' || key === 'E') {
e.preventDefault();
if (!advanceDialog()) {
// Dialog ended
}
}
return;
}
// Debug overlay toggle (F3)
if (key === 'F3') {
e.preventDefault();
toggleDebug();
return;
}
// Backpack toggle (I)
if (key === 'i' || key === 'I') {
e.preventDefault();
openBackpack(null);
return;
}
// Workshop shortcut (TAB)
if (key === 'Tab') {
e.preventDefault();
if (interactionHandler) interactionHandler({ type: 'enterWorkshop' });
return;
}
// Interaction (E / Enter / Space)
if (key === 'e' || key === 'E' || key === 'Enter' || key === ' ') {
e.preventDefault();
performInteraction();
return;
}
// Movement (handled in updateMovement via keysDown)
const dir = keyToDir(key);
if (dir) e.preventDefault();
}
function onKeyUp(e) {
keysDown.delete(e.key);
}
// ---- Direction mapping ----
function keyToDir(key) {
if (key === 'ArrowUp' || key === 'w' || key === 'W') return 'up';
if (key === 'ArrowDown' || key === 's' || key === 'S') return 'down';
if (key === 'ArrowLeft' || key === 'a' || key === 'A') return 'left';
if (key === 'ArrowRight' || key === 'd' || key === 'D') return 'right';
return null;
}
/** Get the currently pressed direction (prioritizes most recent) */
function getHeldDirection() {
// Check in order of specificity
for (const key of keysDown) {
const dir = keyToDir(key);
if (dir) return dir;
}
return null;
}
// ---- Movement ----
const MOVE_DURATION = 0.15; // seconds per tile
/**
* Called each frame by the renderer.
* Handles movement interpolation and starting new moves.
*/
export function updateMovement(dt) {
const p = worldState.player;
if (p.moving) {
// Advance interpolation
p._moveProgress = (p._moveProgress || 0) + dt / MOVE_DURATION;
if (p._moveProgress >= 1) {
// Snap to target
p.x = p._targetX;
p.y = p._targetY;
p.px = 0;
p.py = 0;
p.moving = false;
p._moveProgress = 0;
// Check map exit
checkMapExit();
// Continue moving if key held
const dir = getHeldDirection();
if (dir) tryMove(dir);
} else {
// Interpolate
p.px = (p._targetX - p._startX) * p._moveProgress;
p.py = (p._targetY - p._startY) * p._moveProgress;
}
} else {
// Not moving — check if direction key is held
const dir = getHeldDirection();
if (dir) tryMove(dir);
}
}
function tryMove(direction) {
const p = worldState.player;
p.direction = direction;
let tx = p.x, ty = p.y;
if (direction === 'up') ty--;
else if (direction === 'down') ty++;
else if (direction === 'left') tx--;
else if (direction === 'right') tx++;
if (!isWalkable(worldState.currentMap, tx, ty)) return;
// Start movement
p._startX = p.x;
p._startY = p.y;
p._targetX = tx;
p._targetY = ty;
p._moveProgress = 0;
p.moving = true;
}
// ---- Interaction ----
function performInteraction() {
if (worldState.player.moving) return;
const p = worldState.player;
let fx = p.x, fy = p.y;
if (p.direction === 'up') fy--;
else if (p.direction === 'down') fy++;
else if (p.direction === 'left') fx--;
else if (p.direction === 'right') fx++;
// NPC?
const npc = getNPC(worldState.currentMap, fx, fy);
if (npc && npc.dialog) {
startDialog(npc.dialog, npc.id);
return;
}
// Interaction tile?
const inter = getInteraction(worldState.currentMap, fx, fy);
if (!inter) return;
switch (inter.type) {
case 'workshop':
if (interactionHandler) interactionHandler({ type: 'enterWorkshop', data: inter });
break;
case 'puzzle_door':
if (interactionHandler) interactionHandler({ type: 'puzzleDoor', data: inter });
break;
case 'module':
if (interactionHandler) interactionHandler({ type: 'module', data: inter });
break;
default:
if (inter.dialog) startDialog(inter.dialog, '');
break;
}
}
// ---- Map transitions ----
function checkMapExit() {
const p = worldState.player;
const exit = getExit(worldState.currentMap, p.x, p.y);
if (exit && interactionHandler) {
interactionHandler({
type: 'mapExit',
data: { targetMap: exit.targetMap, targetX: exit.targetX, targetY: exit.targetY }
});
}
}