fix: player size, unplayable walls, NPC interaction, canvas sizing

- Player sprite: render 32x32 char at 1 tile (TILE_PX) instead of
  2x2 tiles (32*SCALE), matching NPC size on the game grid
- Wall data: completely rebuilt for both lab and town maps based on
  actual PNG layouts. Previous walls blocked spawn point, NPC access,
  and all exits. Now uses Set for O(1) collision lookups
- NPC dialog: was unreachable due to wall layout, causing player to
  interact with workshop tiles instead. Fixed by opening corridors
- Canvas: use window.innerWidth/Height directly instead of
  offsetWidth which gave wrong values before CSS recompute

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 16:19:49 +01:00
parent 75001e10e7
commit 9b2a25856e
3 changed files with 149 additions and 130 deletions

View File

@@ -2,18 +2,61 @@
* maps.js - PNG-based world maps with wall coordinate arrays
*
* Each map has a pre-rendered PNG background image and defines:
* - walls: coordinate-based collision data { row: [col1, col2, ...] }
* - walls: Set of "x,y" strings for collision (built from coordinate data)
* - npcs, interactions, exits as position-based objects
*
* Map images are drawn at 3x scale (16px native → 48px on screen)
*/
// ==================== Wall builder helpers ====================
function buildWallSet(widthTiles, heightTiles, wallData) {
const set = new Set();
for (const [row, cols] of Object.entries(wallData)) {
for (const col of cols) {
set.add(`${col},${row}`);
}
}
return set;
}
function wallRange(from, to) {
const cols = [];
for (let c = from; c <= to; c++) cols.push(c);
return cols;
}
// ==================== Map definitions ====================
/**
* MAP: LAB (10×12 tiles — lab.png is 160×192)
* Pokemon professor's lab interior
* LAB (10×12 tiles — lab.png 160×192)
*
* Visual layout from PNG:
* Row 0: Top wall — machinery/equipment
* Row 1: Machines on left, big computer right
* Row 2: Open area, desk/machine on far right
* Row 3-4: Table in center-left area
* Row 5: Open corridor
* Row 6-7: Two rows of bookshelves with gap in middle (col 4)
* Row 8-9: Open floor
* Row 10: Open floor near exit
* Row 11: Bottom wall, door at cols 4-5
*/
const labWalls = {
0: wallRange(0, 9), // solid top wall
1: [0, 1, 7, 8, 9], // machines on sides
2: [0, 7, 8, 9], // desk/machine right
3: [0, 2, 3, 9], // table center-left
4: [0, 2, 3, 9], // table continues
5: [0, 9], // open corridor
6: [0, 1, 2, 6, 7, 8, 9], // bookshelves, gap at 3-5
7: [0, 1, 2, 6, 7, 8, 9], // bookshelves, gap at 3-5
8: [0, 9], // open
9: [0, 9], // open
10: [0, 9], // open
11: [0, 1, 2, 3, 6, 7, 8, 9] // bottom wall, door at 4-5
};
const labMap = {
id: 'lab',
name: 'Circuit Lab',
@@ -21,34 +64,17 @@ const labMap = {
widthTiles: 10,
heightTiles: 12,
spawn: { x: 4, y: 10 },
// 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]
},
wallSet: buildWallSet(10, 12, labWalls),
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 }
{ x: 4, y: 11, targetMap: 'town', targetX: 10, targetY: 7 },
{ x: 5, y: 11, targetMap: 'town', targetX: 10, targetY: 7 }
],
npcs: [
{
id: 'professor',
x: 4, y: 3,
x: 5, y: 1,
facing: 'down',
dialog: [
'Welcome to the Circuit Lab!',
@@ -60,95 +86,101 @@ const labMap = {
],
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',
// Workshop tables (the table at rows 3-4)
{ x: 2, y: 3, type: 'workshop', label: 'Workshop Table' },
{ x: 3, y: 3, type: 'workshop', label: 'Workshop Table' },
{ x: 2, y: 4, type: 'workshop', label: 'Workshop Table' },
{ x: 3, y: 4, type: 'workshop', label: 'Workshop Table' },
// Bookshelves (interact from adjacent)
{ x: 1, y: 6, type: 'sign', label: 'Bookshelf',
dialog: ['A collection of logic circuit manuals.'] },
{ x: 7, y: 6, type: 'sign', label: 'Bookshelf',
dialog: ['Advanced boolean algebra textbooks.'] },
// Machines at top
{ x: 8, y: 1, 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',
// Puzzle door (machine at top-right)
{ x: 8, y: 2, type: 'puzzle_door', puzzleId: 'lab_door_1',
requiredOutputs: [1, 0, 1, 1], label: 'Locked Door' }
]
};
/**
* MAP: TOWN (20×18 tiles — pallet-town.png is 320×288)
* Pokemon-style starting town with houses and paths
* TOWN (20×18 tiles — pallet-town.png 320×288)
*
* Visual layout from PNG:
* Row 0: Top border — trees
* Row 1: Trees, fence
* Row 2-5: Left house (cols 1-5), Right big building (cols 12-18)
* Row 6: Open area, house doors
* Row 7-8: Sign, fences, open path area
* Row 9-10: Garden/flowers left, path center, fence right
* Row 11-13: Open area, another building right side
* Row 14-16: Water (left), trees (right), path center
* Row 17: Bottom border — trees/fence
*/
const townWalls = (() => {
const w = {};
function add(row, cols) {
if (!w[row]) w[row] = [];
w[row].push(...cols);
}
// Top border
add(0, wallRange(0, 19));
add(1, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 18, 19]);
// Left house (rows 2-5, cols 1-5)
for (let r = 2; r <= 4; r++) add(r, [0, 1, 2, 3, 4, 5, 19]);
add(5, [0, 1, 2, 4, 5, 19]); // door gap at col 3
// Right big building (rows 2-5, cols 12-18)
for (let r = 2; r <= 5; r++) add(r, [0, 12, 13, 14, 15, 16, 17, 18, 19]);
// Open path rows with side borders
add(6, [0, 19]);
add(7, [0, 19]);
add(8, [0, 1, 18, 19]);
// Garden/flowers left side + fence right
add(9, [0, 1, 2, 3, 4, 5, 18, 19]);
add(10, [0, 1, 2, 3, 4, 5, 18, 19]);
// Open area
add(11, [0, 1, 18, 19]);
add(12, [0, 1, 18, 19]);
// Building right + water left (rows 13-16)
add(13, [0, 1, 14, 15, 16, 17, 18, 19]);
add(14, [0, 1, 2, 3, 14, 15, 16, 17, 18, 19]);
add(15, [0, 1, 2, 3, 14, 15, 16, 17, 18, 19]);
add(16, [0, 1, 2, 3, 14, 15, 16, 17, 18, 19]);
// Bottom border
add(17, wallRange(0, 19));
return w;
})();
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;
})(),
spawn: { x: 10, y: 8 },
wallSet: buildWallSet(20, 18, townWalls),
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 }
// Door into lab building
{ x: 10, y: 7, targetMap: 'lab', targetX: 4, targetY: 10 }
],
npcs: [
{
id: 'merchant',
x: 9, y: 11,
facing: 'down',
x: 8, y: 10,
facing: 'right',
dialog: [
'Welcome to Neon Town!',
'I trade in rare logic components.',
@@ -158,30 +190,29 @@ const townMap = {
},
{
id: 'guide',
x: 14, y: 9,
facing: 'left',
x: 11, y: 12,
facing: 'down',
dialog: [
'The Circuit Lab is just up ahead.',
'Professor Oak.. I mean, the Professor can teach you about logic gates!',
'The Circuit Lab is in the big building up north.',
'Talk to the Professor — he\'ll teach you about logic gates!',
'Press TAB anytime to open your Workshop.'
]
}
],
interactions: [
// House 1 door
{ x: 5, y: 6, type: 'door', label: 'House',
// Left house door
{ x: 3, y: 5, 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 ↑'] }
// Sign in town center
{ x: 10, y: 9, type: 'sign', label: 'Sign',
dialog: ['Welcome to Neon Town!', 'Circuit Lab ↑'] },
// Right building sign
{ x: 12, y: 6, type: 'sign', label: 'Sign',
dialog: ['CIRCUIT LAB', 'Open for research!'] }
]
};
// ==================== Map registry ====================
const maps = {
@@ -196,16 +227,13 @@ export function getMap(id) {
}
/**
* Check if a tile position is a wall
* Check if a tile position is a wall (using Set for O(1) lookup)
*/
export function isWall(mapId, x, y) {
const map = maps[mapId];
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);
return map.wallSet.has(`${x},${y}`);
}
/**
@@ -217,34 +245,25 @@ export function isWalkable(mapId, x, y) {
return true;
}
/**
* Get interaction at position
*/
export function getInteraction(mapId, x, y) {
const map = maps[mapId];
if (!map) return null;
return map.interactions.find(i => i.x === x && i.y === y) || null;
}
/**
* Get NPC at position
*/
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
*/
export function getExit(mapId, x, y) {
const map = maps[mapId];
if (!map) return null;
return map.exits.find(e => e.x === x && e.y === y) || null;
}
// No longer needed but keep for compat — returns null always
// Backward compat
export function getTile(mapId, x, y) {
return isWall(mapId, x, y) ? 1 : 0;
}

View File

@@ -122,8 +122,9 @@ export function drawPlayer(ctx, screenX, screenY, direction, walkFrame) {
return;
}
ctx.imageSmoothingEnabled = false;
// Character is 32x32 native = 2x2 tiles, draw at SCALE
ctx.drawImage(img, screenX, screenY, 32 * SCALE, 32 * SCALE);
// Character is 32x32 native but represents a 1-tile-wide, 2-tile-tall entity
// Draw at TILE_PX wide x TILE_PX tall (square, matching NPC size on grid)
ctx.drawImage(img, screenX, screenY, TILE_PX, TILE_PX);
}
/**

View File

@@ -21,8 +21,10 @@ export function initWorldRenderer() {
function resizeCanvas() {
if (!canvas) return;
canvas.width = canvas.offsetWidth || window.innerWidth;
canvas.height = canvas.offsetHeight || window.innerHeight;
// Always use full window size in world mode — don't rely on offsetWidth
// because CSS layout may not have recomputed yet on initial load
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// ==================== Camera ====================
@@ -69,7 +71,7 @@ export function renderWorld(timestamp) {
updateMovement(dt);
// Resize check
if (canvas.width !== canvas.offsetWidth || canvas.height !== canvas.offsetHeight) {
if (canvas.width !== window.innerWidth || canvas.height !== window.innerHeight) {
resizeCanvas();
}
@@ -94,15 +96,12 @@ export function renderWorld(timestamp) {
}
// === Layer 3: Player ===
// Character sprite is 32x32 native (2 tiles tall)
// Position so bottom half aligns with player tile, top half overlaps above
const playerScreen = tileToScreen(
worldState.player.x + worldState.player.px,
worldState.player.y + worldState.player.py
);
// Offset upward by 1 tile since character is 2 tiles tall
const playerDrawX = playerScreen.x;
const playerDrawY = playerScreen.y - TILE_PX;
const playerDrawY = playerScreen.y;
const walkFrame = worldState.player.moving
? (Math.floor(Date.now() / 150) % 2) + 1 // alternates 1, 2