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:
307
js/world/worldState.js
Normal file
307
js/world/worldState.js
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* worldState.js - World game state management
|
||||
*
|
||||
* Tracks player position, current map, dialog, inventory, puzzles, and other game state
|
||||
*/
|
||||
|
||||
// Default/initial world state
|
||||
export const worldState = {
|
||||
// Current mode
|
||||
mode: 'world', // 'world' | 'workshop' | 'dialog' | 'puzzle'
|
||||
|
||||
// Player
|
||||
player: {
|
||||
x: 10,
|
||||
y: 12, // tile position in current map
|
||||
px: 0,
|
||||
py: 0, // pixel offset for smooth movement (interpolation)
|
||||
direction: 'down', // 'up' | 'down' | 'left' | 'right'
|
||||
moving: false,
|
||||
frame: 0, // animation frame (0-3 for walking cycles)
|
||||
speed: 150 // milliseconds per tile movement
|
||||
},
|
||||
|
||||
// Map
|
||||
currentMap: 'lab',
|
||||
|
||||
// Camera
|
||||
camera: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
|
||||
// Dialog
|
||||
dialog: null, // { lines: [...], currentLine: 0, speakerName: '' } or null
|
||||
|
||||
// Inventory of crafted components
|
||||
inventory: [], // array of component IDs from customComponents (stored in circuit editor)
|
||||
|
||||
// Puzzle state
|
||||
solvedPuzzles: [], // array of puzzleIds that have been solved
|
||||
activePuzzle: null, // { puzzleId, requiredOutputs, doorX, doorY } or null when no puzzle active
|
||||
|
||||
// Game flags
|
||||
flags: {
|
||||
// Examples:
|
||||
// 'met_professor': false,
|
||||
// 'guard_talked': false,
|
||||
// 'merchant_met': false
|
||||
},
|
||||
|
||||
// Timing
|
||||
lastMoveTime: 0,
|
||||
animTimer: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset world state to initial defaults
|
||||
*/
|
||||
export function resetWorldState() {
|
||||
worldState.mode = 'world';
|
||||
worldState.player.x = 10;
|
||||
worldState.player.y = 12;
|
||||
worldState.player.px = 0;
|
||||
worldState.player.py = 0;
|
||||
worldState.player.direction = 'down';
|
||||
worldState.player.moving = false;
|
||||
worldState.player.frame = 0;
|
||||
worldState.currentMap = 'lab';
|
||||
worldState.camera.x = 0;
|
||||
worldState.camera.y = 0;
|
||||
worldState.dialog = null;
|
||||
worldState.inventory = [];
|
||||
worldState.solvedPuzzles = [];
|
||||
worldState.activePuzzle = null;
|
||||
worldState.flags = {};
|
||||
worldState.lastMoveTime = 0;
|
||||
worldState.animTimer = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player is currently in movement animation
|
||||
*/
|
||||
export function isPlayerMoving() {
|
||||
return worldState.player.moving;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set player position and reset movement state
|
||||
*/
|
||||
export function setPlayerPosition(x, y) {
|
||||
worldState.player.x = x;
|
||||
worldState.player.y = y;
|
||||
worldState.player.px = 0;
|
||||
worldState.player.py = 0;
|
||||
worldState.player.moving = false;
|
||||
worldState.player.frame = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a dialog sequence
|
||||
*/
|
||||
export function startDialog(lines, speakerName = '') {
|
||||
worldState.dialog = {
|
||||
lines: Array.isArray(lines) ? lines : [lines],
|
||||
currentLine: 0,
|
||||
speakerName: speakerName
|
||||
};
|
||||
worldState.mode = 'dialog';
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance dialog to next line
|
||||
* Returns false when dialog sequence ends and should be closed
|
||||
*/
|
||||
export function advanceDialog() {
|
||||
if (!worldState.dialog) return false;
|
||||
|
||||
worldState.dialog.currentLine++;
|
||||
|
||||
// Dialog finished
|
||||
if (worldState.dialog.currentLine >= worldState.dialog.lines.length) {
|
||||
worldState.dialog = null;
|
||||
worldState.mode = 'world';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current dialog line text
|
||||
*/
|
||||
export function getCurrentDialogLine() {
|
||||
if (!worldState.dialog) return '';
|
||||
return worldState.dialog.lines[worldState.dialog.currentLine] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add component to inventory
|
||||
*/
|
||||
export function addToInventory(componentId) {
|
||||
if (!worldState.inventory.includes(componentId)) {
|
||||
worldState.inventory.push(componentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove component from inventory
|
||||
*/
|
||||
export function removeFromInventory(componentId) {
|
||||
const idx = worldState.inventory.indexOf(componentId);
|
||||
if (idx !== -1) {
|
||||
worldState.inventory.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if component is in inventory
|
||||
*/
|
||||
export function hasInInventory(componentId) {
|
||||
return worldState.inventory.includes(componentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a puzzle as solved
|
||||
*/
|
||||
export function solvePuzzle(puzzleId) {
|
||||
if (!worldState.solvedPuzzles.includes(puzzleId)) {
|
||||
worldState.solvedPuzzles.push(puzzleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a puzzle has been solved
|
||||
*/
|
||||
export function isPuzzleSolved(puzzleId) {
|
||||
return worldState.solvedPuzzles.includes(puzzleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active puzzle that player is attempting
|
||||
*/
|
||||
export function setActivePuzzle(puzzleId, requiredOutputs, doorX, doorY) {
|
||||
worldState.activePuzzle = {
|
||||
puzzleId: puzzleId,
|
||||
requiredOutputs: requiredOutputs,
|
||||
doorX: doorX,
|
||||
doorY: doorY
|
||||
};
|
||||
worldState.mode = 'puzzle';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the active puzzle
|
||||
*/
|
||||
export function clearActivePuzzle() {
|
||||
worldState.activePuzzle = null;
|
||||
worldState.mode = 'world';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active puzzle
|
||||
*/
|
||||
export function getActivePuzzle() {
|
||||
return worldState.activePuzzle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a game flag
|
||||
*/
|
||||
export function setFlag(key, value) {
|
||||
worldState.flags[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a game flag
|
||||
*/
|
||||
export function getFlag(key, defaultValue = false) {
|
||||
return worldState.flags[key] !== undefined ? worldState.flags[key] : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a flag is true
|
||||
*/
|
||||
export function isFlagSet(key) {
|
||||
return getFlag(key) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move player by tile offset (for movement updates)
|
||||
* Returns true if movement started, false if blocked
|
||||
*/
|
||||
export function movePlayer(dx, dy, isWalkable) {
|
||||
if (worldState.player.moving) return false;
|
||||
|
||||
const newX = worldState.player.x + dx;
|
||||
const newY = worldState.player.y + dy;
|
||||
|
||||
// Check if new position is walkable
|
||||
if (!isWalkable(newX, newY)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update direction
|
||||
if (dx > 0) worldState.player.direction = 'right';
|
||||
if (dx < 0) worldState.player.direction = 'left';
|
||||
if (dy > 0) worldState.player.direction = 'down';
|
||||
if (dy < 0) worldState.player.direction = 'up';
|
||||
|
||||
// Start movement animation
|
||||
worldState.player.x = newX;
|
||||
worldState.player.y = newY;
|
||||
worldState.player.moving = true;
|
||||
worldState.player.frame = 0;
|
||||
worldState.lastMoveTime = Date.now();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update player movement animation
|
||||
* Call this in game loop, delta is time elapsed in ms
|
||||
*/
|
||||
export function updatePlayerAnimation(delta) {
|
||||
if (!worldState.player.moving) return;
|
||||
|
||||
const elapsed = Date.now() - worldState.lastMoveTime;
|
||||
const progress = Math.min(elapsed / worldState.player.speed, 1);
|
||||
|
||||
// Update pixel offset for smooth movement
|
||||
const tileSize = 32; // Assuming 32x32 tiles
|
||||
worldState.player.px = (worldState.player.direction === 'right' ? 1 : worldState.player.direction === 'left' ? -1 : 0) * tileSize * progress;
|
||||
worldState.player.py = (worldState.player.direction === 'down' ? 1 : worldState.player.direction === 'up' ? -1 : 0) * tileSize * progress;
|
||||
|
||||
// Update animation frame
|
||||
worldState.player.frame = Math.floor(progress * 4) % 4;
|
||||
|
||||
// Movement complete
|
||||
if (progress >= 1) {
|
||||
worldState.player.moving = false;
|
||||
worldState.player.px = 0;
|
||||
worldState.player.py = 0;
|
||||
worldState.player.frame = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warp player to a new map and position
|
||||
*/
|
||||
export function warpToMap(mapId, x, y) {
|
||||
worldState.currentMap = mapId;
|
||||
setPlayerPosition(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete world state snapshot (for debugging/saving)
|
||||
*/
|
||||
export function getWorldStateSnapshot() {
|
||||
return JSON.parse(JSON.stringify(worldState));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load world state from snapshot
|
||||
*/
|
||||
export function loadWorldStateSnapshot(snapshot) {
|
||||
Object.assign(worldState, JSON.parse(JSON.stringify(snapshot)));
|
||||
}
|
||||
Reference in New Issue
Block a user