Files
logic-gates/js/world/gameMode.js
Jose Luis 1d494d8ef3 feat: return-to-door system for map exits
When entering an interior (e.g. lab from town), the game saves the
player's current position as a return point. When exiting, if the
exit has no explicit targetX/targetY, the system pops the stored
return point and warps back to that exact position.

This means interior exits just need targetMap — the player always
returns to the specific door they entered from, not a hardcoded
position. Falls back to the destination map's spawn if no return
point is stored.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 17:02:51 +01:00

204 lines
6.4 KiB
JavaScript

// gameMode.js - Central coordinator: switches between World and Workshop modes
import { worldState, setPlayerPosition, warpToMap, solvePuzzle, isPuzzleSolved } from './worldState.js';
import { initWorldRenderer, startWorldLoop, stopWorldLoop } from './worldRenderer.js';
import { initWorldInput, destroyWorldInput, setInteractionHandler } from './worldInput.js';
import { getMap } from './maps.js';
// Circuit editor stop function (to stop its render loop when switching modes)
import { stopCircuitLoop } from '../renderer.js';
// Circuit editor modules (registered from app.js to avoid circular deps)
let circuitEditorInit = null;
let circuitEditorDestroy = null;
let currentMode = 'none'; // 'world' | 'workshop'
/**
* Register the circuit editor's init/destroy functions.
* Called from app.js so we don't create circular imports.
*/
export function registerCircuitEditor(initFn, destroyFn) {
circuitEditorInit = initFn;
circuitEditorDestroy = destroyFn;
}
/**
* Boot the game — start in world mode
*/
export function startGame() {
// Set spawn
const map = getMap(worldState.currentMap);
if (map && map.spawn) {
setPlayerPosition(map.spawn.x, map.spawn.y);
}
// Wire up interaction handler
setInteractionHandler(handleInteraction);
// Enter world mode
enterWorldMode();
}
// ==================== Mode switching ====================
export function enterWorldMode() {
if (currentMode === 'world') return;
// Tear down workshop if active
if (currentMode === 'workshop') {
stopCircuitLoop();
if (circuitEditorDestroy) circuitEditorDestroy();
hideWorkshopUI();
}
currentMode = 'world';
worldState.mode = 'world';
showWorldUI();
initWorldRenderer();
initWorldInput();
startWorldLoop();
console.log('[gameMode] entered world mode');
}
export function enterWorkshopMode() {
if (currentMode === 'workshop') return;
// Tear down world
if (currentMode === 'world') {
stopWorldLoop();
destroyWorldInput();
hideWorldUI();
}
currentMode = 'workshop';
worldState.mode = 'workshop';
showWorkshopUI();
if (circuitEditorInit) circuitEditorInit();
console.log('[gameMode] entered workshop mode');
}
export function getCurrentMode() { return currentMode; }
// ==================== Interaction handler ====================
function handleInteraction(event) {
switch (event.type) {
case 'enterWorkshop':
enterWorkshopMode();
break;
case 'puzzleDoor': {
const inter = event.data;
if (isPuzzleSolved(inter.puzzleId)) {
// Already solved — could open door, show message, etc.
return;
}
// For now, show a hint dialog. Later: open puzzle UI
worldState.dialog = {
lines: [
'This door requires a logic circuit to open.',
`Required output pattern: [${inter.requiredOutputs.join(', ')}]`,
'Craft a component in your Workshop (TAB)!'
],
currentLine: 0,
speakerName: 'System'
};
worldState.mode = 'dialog';
break;
}
case 'mapExit': {
const { targetMap, targetX, targetY } = event.data;
const p = worldState.player;
// Save return point: where the player is NOW (one tile back from the exit)
// so when they leave the target map, they return here
worldState.returnPoints.push({
fromMap: worldState.currentMap,
fromX: p.x,
fromY: p.y
});
// If exit has explicit coordinates, use them
// Otherwise, check for a stored return point for the target map
if (targetX != null && targetY != null) {
warpToMap(targetMap, targetX, targetY);
} else {
// Pop the most recent return point for this map
const retIdx = worldState.returnPoints.findLastIndex(
rp => rp.fromMap === targetMap
);
if (retIdx >= 0) {
const ret = worldState.returnPoints[retIdx];
worldState.returnPoints.splice(retIdx, 1);
warpToMap(ret.fromMap, ret.fromX, ret.fromY);
} else {
// Fallback: use map spawn point
const destMap = getMap(targetMap);
const sx = destMap?.spawn?.x ?? 0;
const sy = destMap?.spawn?.y ?? 0;
warpToMap(targetMap, sx, sy);
}
}
console.log(`[gameMode] warped to ${worldState.currentMap} (${worldState.player.x}, ${worldState.player.y})`);
break;
}
case 'openInventory':
// TODO: inventory UI
console.log('[gameMode] inventory:', worldState.inventory);
break;
}
}
// ==================== UI visibility ====================
function showWorldUI() {
// Hide workshop-specific elements
const toolbar = document.getElementById('toolbar');
const wavePanel = document.getElementById('waveform-panel');
const canvas = document.getElementById('canvas');
if (toolbar) toolbar.style.display = 'none';
if (wavePanel) wavePanel.style.display = 'none';
if (canvas) {
canvas.style.top = '0';
canvas.style.cursor = 'default';
}
// Show back-to-world button (hidden since we're IN world)
const backBtn = document.getElementById('back-to-world-btn');
if (backBtn) backBtn.style.display = 'none';
}
function hideWorldUI() {
// Nothing special to hide — canvas stays
}
function showWorkshopUI() {
const toolbar = document.getElementById('toolbar');
const canvas = document.getElementById('canvas');
if (toolbar) toolbar.style.display = 'flex';
if (canvas) {
canvas.style.top = '56px';
canvas.style.cursor = 'default';
}
// Show back-to-world button
const backBtn = document.getElementById('back-to-world-btn');
if (backBtn) backBtn.style.display = 'flex';
}
function hideWorkshopUI() {
const toolbar = document.getElementById('toolbar');
if (toolbar) toolbar.style.display = 'none';
const backBtn = document.getElementById('back-to-world-btn');
if (backBtn) backBtn.style.display = 'none';
}