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>
233 lines
6.2 KiB
JavaScript
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 }
|
|
});
|
|
}
|
|
}
|