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>
306 lines
8.8 KiB
JavaScript
306 lines
8.8 KiB
JavaScript
/**
|
||
* 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 };
|