Files
logic-gates/js/world/worldState.js
Jose Luis b999fe855a feat: gadget backpack system — save circuits as items
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>
2026-03-20 17:30:30 +01:00

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)));
}