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:
Jose Luis
2026-03-20 16:02:44 +01:00
parent e4cf35701e
commit c836ccbb21
24 changed files with 479 additions and 989 deletions

View File

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