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:
261
js/world/maps.js
261
js/world/maps.js
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user