feat: add Pokemon-style world mode with workshop integration

Two-mode game: explore a tile-based cyberpunk world, then enter
Workshop mode (the existing circuit editor) to craft components.

New modules (js/world/):
- sprites.js: programmatic pixel-art renderer (16x16 tiles, 3x scale)
- maps.js: tile-based map definitions (lab + town)
- worldState.js: player position, inventory, dialog, puzzle state
- worldRenderer.js: camera-following world renderer on shared canvas
- worldInput.js: WASD movement, E interaction, dialog system
- gameMode.js: central mode switcher (world ↔ workshop)

Changes to existing code:
- app.js: boots into world mode, registers circuit editor for workshop
- renderer.js: circuit draw loop now stoppable (start/stopCircuitLoop)
- index.html: added "Back to World" button for workshop mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 15:52:13 +01:00
parent bbde11dfc7
commit e4cf35701e
9 changed files with 1903 additions and 14 deletions

173
js/world/gameMode.js Normal file
View File

@@ -0,0 +1,173 @@
// 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;
warpToMap(targetMap, targetX, targetY);
console.log(`[gameMode] warped to ${targetMap} (${targetX}, ${targetY})`);
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';
}