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:
305
js/world/maps.js
Normal file
305
js/world/maps.js
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* maps.js - Tile-based world map definitions
|
||||
*
|
||||
* Tile types:
|
||||
* 0 = floor
|
||||
* 1 = wall
|
||||
* 2 = grass
|
||||
* 3 = workshop table
|
||||
* 4 = puzzle door locked
|
||||
* 5 = puzzle door open
|
||||
* 6 = NPC spot
|
||||
* 7 = path
|
||||
* 8 = water
|
||||
* 9 = terminal
|
||||
*/
|
||||
|
||||
// Helper to fill a rectangular area with a tile type
|
||||
function fillRect(tiles, x, y, w, h, tileType) {
|
||||
for (let row = y; row < y + h; row++) {
|
||||
for (let col = x; col < x + w; col++) {
|
||||
if (row >= 0 && row < tiles.length && col >= 0 && col < tiles[0].length) {
|
||||
tiles[row][col] = tileType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to place a horizontal line
|
||||
function hline(tiles, x, y, length, tileType) {
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (y >= 0 && y < tiles.length && x + i >= 0 && x + i < tiles[0].length) {
|
||||
tiles[y][x + i] = tileType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to place a vertical line
|
||||
function vline(tiles, x, y, length, tileType) {
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (x >= 0 && x < tiles[0].length && y + i >= 0 && y + i < tiles.length) {
|
||||
tiles[y + i][x] = tileType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MAP 1: CIRCUIT LAB (20×15)
|
||||
* Indoor tech lab with workstations, terminals, and a puzzle door
|
||||
*/
|
||||
function createLabMap() {
|
||||
const width = 20;
|
||||
const height = 15;
|
||||
|
||||
// Start with all floor
|
||||
const tiles = Array(height).fill(null).map(() => Array(width).fill(0));
|
||||
|
||||
// Border walls
|
||||
for (let i = 0; i < width; i++) {
|
||||
tiles[0][i] = 1; // top
|
||||
tiles[height - 1][i] = 1; // bottom
|
||||
}
|
||||
for (let i = 0; i < height; i++) {
|
||||
tiles[i][0] = 1; // left
|
||||
tiles[i][width - 1] = 1; // right
|
||||
}
|
||||
|
||||
// Internal wall structure - create lab rooms
|
||||
// Vertical divider wall down the middle area
|
||||
vline(tiles, 13, 1, 10, 1);
|
||||
|
||||
// Horizontal divider
|
||||
hline(tiles, 1, 7, 12, 1);
|
||||
|
||||
// Workshop area - left side with tables
|
||||
tiles[4][3] = 3; // workshop table 1
|
||||
tiles[4][5] = 3; // workshop table 2
|
||||
tiles[4][7] = 3; // workshop table 3
|
||||
tiles[6][3] = 3; // workshop table 4
|
||||
tiles[6][5] = 3; // workshop table 5
|
||||
|
||||
// Terminal near workshop area
|
||||
tiles[5][10] = 9; // terminal
|
||||
|
||||
// Professor NPC spot at top
|
||||
tiles[2][10] = 6; // NPC spawn location
|
||||
|
||||
// Puzzle door leading to back room (top-right area)
|
||||
tiles[2][16] = 4; // locked puzzle door
|
||||
|
||||
// Door opening in the internal wall for navigation
|
||||
tiles[7][7] = 0; // create passage
|
||||
|
||||
// Exit point at bottom (to town)
|
||||
tiles[13][10] = 0; // clear exit path
|
||||
|
||||
return {
|
||||
id: 'lab',
|
||||
name: 'Circuit Lab',
|
||||
width: width,
|
||||
height: height,
|
||||
tiles: tiles,
|
||||
spawn: { x: 10, y: 12 },
|
||||
|
||||
exits: [
|
||||
{ x: 10, y: 13, targetMap: 'town', targetX: 15, targetY: 1 }
|
||||
],
|
||||
|
||||
npcs: [
|
||||
{
|
||||
id: 'professor',
|
||||
type: 0,
|
||||
x: 10,
|
||||
y: 2,
|
||||
facing: 'down',
|
||||
dialog: [
|
||||
'Welcome to the Circuit Lab!',
|
||||
'I\'m the Professor. We study logic gates here.',
|
||||
'Try using the workshop tables to design circuits.',
|
||||
'Once you\'ve created some components, you can use them to solve puzzles.'
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
interactions: [
|
||||
{ x: 3, y: 4, type: 'workshop', action: 'openWorkshop', label: 'Workshop Table' },
|
||||
{ x: 5, y: 4, type: 'workshop', action: 'openWorkshop', label: 'Workshop Table' },
|
||||
{ x: 7, y: 4, type: 'workshop', action: 'openWorkshop', label: 'Workshop Table' },
|
||||
{ x: 10, y: 5, type: 'terminal', action: 'openTerminal', label: 'Terminal' },
|
||||
{ x: 16, y: 2, type: 'puzzle_door', puzzleId: 'lab_door_1', requiredOutputs: [1, 0, 1, 1], label: 'Locked Door' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MAP 2: NEON TOWN (30×20)
|
||||
* Outdoor town with buildings, NPCs, water feature, and puzzle areas
|
||||
*/
|
||||
function createTownMap() {
|
||||
const width = 30;
|
||||
const height = 20;
|
||||
|
||||
// Start with grass
|
||||
const tiles = Array(height).fill(null).map(() => Array(width).fill(2));
|
||||
|
||||
// Add some paths (lighter grass/paths)
|
||||
hline(tiles, 0, 10, width, 7); // horizontal path
|
||||
vline(tiles, 15, 0, height, 7); // vertical path
|
||||
|
||||
// Water feature on the left (pond)
|
||||
fillRect(tiles, 2, 5, 5, 6, 8);
|
||||
|
||||
// Lab entrance at top
|
||||
tiles[0][15] = 0; // entrance floor
|
||||
tiles[1][15] = 6; // NPC spawn for entrance
|
||||
|
||||
// Building 1 (top-left) - House structure
|
||||
fillRect(tiles, 5, 2, 7, 5, 1); // walls
|
||||
fillRect(tiles, 6, 3, 5, 3, 0); // interior floor
|
||||
tiles[4][8] = 0; // door to building 1
|
||||
|
||||
// Building 2 (top-right) - Shop
|
||||
fillRect(tiles, 20, 2, 7, 5, 1); // walls
|
||||
fillRect(tiles, 21, 3, 5, 3, 0); // interior floor
|
||||
tiles[4][23] = 0; // door to building 2
|
||||
|
||||
// Building 3 (bottom-left) - Guard post
|
||||
fillRect(tiles, 5, 14, 7, 5, 1); // walls
|
||||
fillRect(tiles, 6, 15, 5, 3, 0); // interior floor
|
||||
tiles[13][8] = 0; // door to guard post
|
||||
|
||||
// Building 4 (bottom-right) - Town Hall
|
||||
fillRect(tiles, 20, 14, 7, 5, 1); // walls
|
||||
fillRect(tiles, 21, 15, 5, 3, 0); // interior floor
|
||||
tiles[13][23] = 0; // door to town hall
|
||||
|
||||
// Merchant NPC in center town square
|
||||
tiles[10][15] = 6; // NPC spawn
|
||||
|
||||
// Guard NPC at guard post entrance
|
||||
tiles[13][8] = 6; // NPC spawn (overlays door, but NPC takes priority)
|
||||
|
||||
// Puzzle door to eastern area (locked)
|
||||
tiles[10][28] = 4; // locked puzzle door
|
||||
|
||||
return {
|
||||
id: 'town',
|
||||
name: 'Neon Town',
|
||||
width: width,
|
||||
height: height,
|
||||
tiles: tiles,
|
||||
spawn: { x: 15, y: 2 },
|
||||
|
||||
exits: [
|
||||
{ x: 15, y: 0, targetMap: 'lab', targetX: 10, targetY: 13 }
|
||||
],
|
||||
|
||||
npcs: [
|
||||
{
|
||||
id: 'merchant',
|
||||
type: 0,
|
||||
x: 15,
|
||||
y: 10,
|
||||
facing: 'down',
|
||||
dialog: [
|
||||
'Welcome to Neon Town!',
|
||||
'I trade in rare logic components.',
|
||||
'Show me what circuits you\'ve designed, and maybe we can make a deal.',
|
||||
'Some items are only available if you\'ve solved certain puzzles.'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'guard',
|
||||
type: 0,
|
||||
x: 8,
|
||||
y: 13,
|
||||
facing: 'right',
|
||||
dialog: [
|
||||
'I guard the eastern territories.',
|
||||
'You need to solve the puzzle at the gate before you can pass.',
|
||||
'Bring me a component that produces the right output pattern!'
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
interactions: [
|
||||
{ x: 8, y: 4, type: 'door', action: 'openBuilding', label: 'House', buildingId: 'house_1' },
|
||||
{ x: 23, y: 4, type: 'door', action: 'openBuilding', label: 'Shop', buildingId: 'shop_1' },
|
||||
{ x: 8, y: 13, type: 'door', action: 'openBuilding', label: 'Guard Post', buildingId: 'guardpost_1' },
|
||||
{ x: 23, y: 13, type: 'door', action: 'openBuilding', label: 'Town Hall', buildingId: 'townhall_1' },
|
||||
{ x: 28, y: 10, type: 'puzzle_door', puzzleId: 'town_gate_1', requiredOutputs: [0, 1, 1, 0], label: 'Locked Gate' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Map registry
|
||||
const maps = {
|
||||
lab: createLabMap(),
|
||||
town: createTownMap()
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a complete map by ID
|
||||
*/
|
||||
export function getMap(id) {
|
||||
return maps[id] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tile type at position
|
||||
*/
|
||||
export function getTile(mapId, x, y) {
|
||||
const map = maps[mapId];
|
||||
if (!map) return null;
|
||||
if (x < 0 || x >= map.width || y < 0 || y >= map.height) return null;
|
||||
return map.tiles[y][x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interaction at position (if any)
|
||||
*/
|
||||
export function getInteraction(mapId, x, y) {
|
||||
const map = maps[mapId];
|
||||
if (!map) return null;
|
||||
return map.interactions.find(inter => inter.x === x && inter.y === y) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPC at position (if any)
|
||||
*/
|
||||
export function getNPC(mapId, x, y) {
|
||||
const map = maps[mapId];
|
||||
if (!map) return null;
|
||||
return map.npcs.find(npc => npc.x === x && npc.y === y) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exit at position (if any)
|
||||
*/
|
||||
export function getExit(mapId, x, y) {
|
||||
const map = maps[mapId];
|
||||
if (!map) return null;
|
||||
return map.exits.find(exit => exit.x === x && exit.y === y) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tile is walkable
|
||||
* Walls (1), water (8), workshop tables (3), terminals (9), locked puzzle doors (4), and NPCs are not walkable
|
||||
*/
|
||||
export function isWalkable(mapId, x, y) {
|
||||
const tile = getTile(mapId, x, y);
|
||||
|
||||
// Out of bounds
|
||||
if (tile === null) return false;
|
||||
|
||||
// Non-walkable tiles
|
||||
const nonWalkable = [1, 3, 4, 8, 9];
|
||||
if (nonWalkable.includes(tile)) return false;
|
||||
|
||||
// Check for NPC
|
||||
if (getNPC(mapId, x, y)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export { maps };
|
||||
Reference in New Issue
Block a user