refactor: migrate world rendering from programmatic sprites to PNG assets
Replace pixel-art drawing with pre-rendered PNG map backgrounds and character/NPC sprite images from pokemon-js reference. Maps now use coordinate-based wall arrays instead of tile grids. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
459
js/world/maps.js
459
js/world/maps.js
@@ -1,271 +1,233 @@
|
||||
/**
|
||||
* maps.js - Tile-based world map definitions
|
||||
* maps.js - PNG-based world maps with wall coordinate arrays
|
||||
*
|
||||
* 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
|
||||
* Each map has a pre-rendered PNG background image and defines:
|
||||
* - walls: coordinate-based collision data { row: [col1, col2, ...] }
|
||||
* - npcs, interactions, exits as position-based objects
|
||||
*
|
||||
* Map images are drawn at 3x scale (16px native → 48px on screen)
|
||||
*/
|
||||
|
||||
// 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 definitions ====================
|
||||
|
||||
/**
|
||||
* MAP 1: CIRCUIT LAB (20×15)
|
||||
* Indoor tech lab with workstations, terminals, and a puzzle door
|
||||
* MAP: LAB (10×12 tiles — lab.png is 160×192)
|
||||
* Pokemon professor's lab interior
|
||||
*/
|
||||
function createLabMap() {
|
||||
const width = 20;
|
||||
const height = 15;
|
||||
const labMap = {
|
||||
id: 'lab',
|
||||
name: 'Circuit Lab',
|
||||
image: 'map:lab',
|
||||
widthTiles: 10,
|
||||
heightTiles: 12,
|
||||
spawn: { x: 4, y: 10 },
|
||||
|
||||
// Start with all floor
|
||||
const tiles = Array(height).fill(null).map(() => Array(width).fill(0));
|
||||
// Walls: { row: [col, col, ...] }
|
||||
// Row 0-1: top shelves/machines
|
||||
walls: {
|
||||
0: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
2: [0, 1, 2, 7, 8, 9],
|
||||
3: [0, 3, 4, 5, 6, 9],
|
||||
4: [0, 3, 4, 5, 6, 9],
|
||||
5: [0, 3, 4, 5, 6, 9],
|
||||
6: [0, 9],
|
||||
7: [0, 9],
|
||||
8: [0, 1, 2, 7, 8, 9],
|
||||
9: [0, 1, 8, 9],
|
||||
10: [0, 1, 2, 3, 6, 7, 8, 9],
|
||||
11: [0, 1, 2, 3, 4, 6, 7, 8, 9]
|
||||
},
|
||||
|
||||
// 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
|
||||
}
|
||||
exits: [
|
||||
// Exit at bottom center — door to town
|
||||
{ x: 4, y: 11, targetMap: 'town', targetX: 12, targetY: 10 },
|
||||
{ x: 5, y: 11, targetMap: 'town', targetX: 12, targetY: 10 }
|
||||
],
|
||||
|
||||
// Internal wall structure - create lab rooms
|
||||
// Vertical divider wall down the middle area
|
||||
vline(tiles, 13, 1, 10, 1);
|
||||
npcs: [
|
||||
{
|
||||
id: 'professor',
|
||||
x: 4, y: 3,
|
||||
facing: 'down',
|
||||
dialog: [
|
||||
'Welcome to the Circuit Lab!',
|
||||
'I\'m the Professor. We study logic gates here.',
|
||||
'Use the workshop tables to design circuits.',
|
||||
'Press TAB to open the Workshop anytime!'
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// 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()
|
||||
interactions: [
|
||||
// Workshop tables (the big table in the middle of lab)
|
||||
{ x: 3, y: 5, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 4, y: 5, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 5, y: 5, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 6, y: 5, type: 'workshop', label: 'Workshop Table' },
|
||||
// Machine/bookshelf
|
||||
{ x: 8, y: 2, type: 'terminal', label: 'Terminal',
|
||||
dialog: ['Circuit analysis terminal.', 'Connect components to solve puzzles.'] },
|
||||
// Puzzle door in top-right area
|
||||
{ x: 8, y: 3, type: 'puzzle_door', puzzleId: 'lab_door_1',
|
||||
requiredOutputs: [1, 0, 1, 1], label: 'Locked Door' }
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a complete map by ID
|
||||
* MAP: TOWN (20×18 tiles — pallet-town.png is 320×288)
|
||||
* Pokemon-style starting town with houses and paths
|
||||
*/
|
||||
const townMap = {
|
||||
id: 'town',
|
||||
name: 'Neon Town',
|
||||
image: 'map:pallet-town',
|
||||
widthTiles: 20,
|
||||
heightTiles: 18,
|
||||
spawn: { x: 9, y: 9 },
|
||||
|
||||
// Walls based on pallet-town visual layout
|
||||
// Trees around border, houses, fences, water
|
||||
walls: (() => {
|
||||
const w = {};
|
||||
// Helper to add walls
|
||||
function addWall(row, cols) {
|
||||
if (!w[row]) w[row] = [];
|
||||
w[row].push(...cols);
|
||||
}
|
||||
function addRange(row, from, to) {
|
||||
const cols = [];
|
||||
for (let c = from; c <= to; c++) cols.push(c);
|
||||
addWall(row, cols);
|
||||
}
|
||||
function addRect(rowStart, rowEnd, colStart, colEnd) {
|
||||
for (let r = rowStart; r <= rowEnd; r++) addRange(r, colStart, colEnd);
|
||||
}
|
||||
|
||||
// Top border (trees/fence) — rows 0-1
|
||||
addRange(0, 0, 19);
|
||||
addRange(1, 0, 19);
|
||||
|
||||
// Left border trees
|
||||
for (let r = 2; r <= 17; r++) addWall(r, [0, 1]);
|
||||
|
||||
// Right border trees
|
||||
for (let r = 2; r <= 17; r++) addWall(r, [18, 19]);
|
||||
|
||||
// Bottom border
|
||||
addRange(17, 0, 19);
|
||||
|
||||
// House 1 (top-left area) — roughly rows 3-6, cols 3-7
|
||||
addRect(3, 5, 3, 7);
|
||||
|
||||
// House 2 (top-right area) — rows 3-6, cols 12-16
|
||||
addRect(3, 5, 12, 16);
|
||||
|
||||
// Fence segments
|
||||
addRange(7, 2, 7);
|
||||
addRange(7, 12, 17);
|
||||
|
||||
// Water/pond (bottom-left)
|
||||
addRect(13, 15, 2, 5);
|
||||
|
||||
// Some trees/obstacles in bottom area
|
||||
addWall(16, [2, 3, 4, 5, 6, 7]);
|
||||
addWall(16, [12, 13, 14, 15, 16, 17]);
|
||||
|
||||
return w;
|
||||
})(),
|
||||
|
||||
exits: [
|
||||
// North exit — goes to lab
|
||||
{ x: 12, y: 9, targetMap: 'lab', targetX: 4, targetY: 10 },
|
||||
// Route 1 south (future)
|
||||
// { x: 9, y: 17, targetMap: 'route1', targetX: 10, targetY: 0 }
|
||||
],
|
||||
|
||||
npcs: [
|
||||
{
|
||||
id: 'merchant',
|
||||
x: 9, y: 11,
|
||||
facing: 'down',
|
||||
dialog: [
|
||||
'Welcome to Neon Town!',
|
||||
'I trade in rare logic components.',
|
||||
'Craft some circuits in the Lab workshop!',
|
||||
'Some doors need special output patterns to open.'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'guide',
|
||||
x: 14, y: 9,
|
||||
facing: 'left',
|
||||
dialog: [
|
||||
'The Circuit Lab is just up ahead.',
|
||||
'Professor Oak.. I mean, the Professor can teach you about logic gates!',
|
||||
'Press TAB anytime to open your Workshop.'
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
interactions: [
|
||||
// House 1 door
|
||||
{ x: 5, y: 6, type: 'door', label: 'House',
|
||||
dialog: ['The door is locked.', 'Nobody seems to be home.'] },
|
||||
// House 2 door
|
||||
{ x: 14, y: 6, type: 'door', label: 'House',
|
||||
dialog: ['This is the component shop.', 'Coming soon!'] },
|
||||
// Sign
|
||||
{ x: 10, y: 8, type: 'sign', label: 'Sign',
|
||||
dialog: ['Welcome to Neon Town!', 'Circuit Lab ↑'] }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
// ==================== Map registry ====================
|
||||
|
||||
const maps = {
|
||||
lab: labMap,
|
||||
town: townMap
|
||||
};
|
||||
|
||||
// ==================== Public API ====================
|
||||
|
||||
export function getMap(id) {
|
||||
return maps[id] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tile type at position
|
||||
* Check if a tile position is a wall
|
||||
*/
|
||||
export function getTile(mapId, x, y) {
|
||||
export function isWall(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];
|
||||
if (!map) return true;
|
||||
// Out of bounds = wall
|
||||
if (x < 0 || x >= map.widthTiles || y < 0 || y >= map.heightTiles) return true;
|
||||
const row = map.walls[y];
|
||||
if (!row) return false;
|
||||
return row.includes(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interaction at position (if any)
|
||||
* Check if a tile is walkable (not a wall and no NPC blocking)
|
||||
*/
|
||||
export function isWalkable(mapId, x, y) {
|
||||
if (isWall(mapId, x, y)) return false;
|
||||
if (getNPC(mapId, x, y)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interaction at position
|
||||
*/
|
||||
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;
|
||||
return map.interactions.find(i => i.x === x && i.y === y) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPC at position (if any)
|
||||
* Get NPC at position
|
||||
*/
|
||||
export function getNPC(mapId, x, y) {
|
||||
const map = maps[mapId];
|
||||
@@ -274,32 +236,17 @@ export function getNPC(mapId, x, y) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exit at position (if any)
|
||||
* Get exit at position
|
||||
*/
|
||||
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;
|
||||
return map.exits.find(e => e.x === x && e.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;
|
||||
// No longer needed but keep for compat — returns null always
|
||||
export function getTile(mapId, x, y) {
|
||||
return isWall(mapId, x, y) ? 1 : 0;
|
||||
}
|
||||
|
||||
export { maps };
|
||||
|
||||
Reference in New Issue
Block a user