Add Pokemon-style inventory where players save crafted circuits as "gadgets" in a backpack. Gadgets can be used on puzzle doors to solve them by testing their truth table against required outputs. New files: - js/world/inventory.js: gadget data model, backpack UI (list with scroll, action menu, detail panel), keyboard navigation Changes: - Workshop gets "Save as Gadget" button (pink, top-right) - I key opens backpack overlay in world mode - Puzzle doors open backpack to select a gadget to try - HUD shows gadget count instead of old component count - worldState gains gadgets[] array Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
312 lines
7.6 KiB
JavaScript
312 lines
7.6 KiB
JavaScript
/**
|
|
* 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: 4,
|
|
y: 10, // 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 (legacy — component IDs)
|
|
inventory: [],
|
|
|
|
// Gadget backpack — saved circuits as reusable items
|
|
gadgets: [], // array of gadget objects (see inventory.js)
|
|
|
|
// 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 = 4;
|
|
worldState.player.y = 10;
|
|
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.gadgets = [];
|
|
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)));
|
|
}
|