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:
173
js/world/gameMode.js
Normal file
173
js/world/gameMode.js
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user