- 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>
272 lines
8.3 KiB
JavaScript
272 lines
8.3 KiB
JavaScript
/**
|
||
* maps.js - PNG-based world maps with wall coordinate arrays
|
||
*
|
||
* Each map has a pre-rendered PNG background image and defines:
|
||
* - 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 ====================
|
||
|
||
/**
|
||
* 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',
|
||
image: 'map:lab',
|
||
widthTiles: 10,
|
||
heightTiles: 12,
|
||
spawn: { x: 4, y: 10 },
|
||
wallSet: buildWallSet(10, 12, labWalls),
|
||
|
||
exits: [
|
||
{ x: 4, y: 11, targetMap: 'town', targetX: 10, targetY: 7 },
|
||
{ x: 5, y: 11, targetMap: 'town', targetX: 10, targetY: 7 }
|
||
],
|
||
|
||
npcs: [
|
||
{
|
||
id: 'professor',
|
||
x: 5, y: 1,
|
||
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!'
|
||
]
|
||
}
|
||
],
|
||
|
||
interactions: [
|
||
// 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 (machine at top-right)
|
||
{ x: 8, y: 2, type: 'puzzle_door', puzzleId: 'lab_door_1',
|
||
requiredOutputs: [1, 0, 1, 1], label: 'Locked Door' }
|
||
]
|
||
};
|
||
|
||
/**
|
||
* 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: 10, y: 8 },
|
||
wallSet: buildWallSet(20, 18, townWalls),
|
||
|
||
exits: [
|
||
// Door into lab building
|
||
{ x: 10, y: 7, targetMap: 'lab', targetX: 4, targetY: 10 }
|
||
],
|
||
|
||
npcs: [
|
||
{
|
||
id: 'merchant',
|
||
x: 8, y: 10,
|
||
facing: 'right',
|
||
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: 11, y: 12,
|
||
facing: 'down',
|
||
dialog: [
|
||
'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: [
|
||
// Left house door
|
||
{ x: 3, y: 5, type: 'door', label: 'House',
|
||
dialog: ['The door is locked.', 'Nobody seems to be home.'] },
|
||
// 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 = {
|
||
lab: labMap,
|
||
town: townMap
|
||
};
|
||
|
||
// ==================== Public API ====================
|
||
|
||
export function getMap(id) {
|
||
return maps[id] || null;
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
if (x < 0 || x >= map.widthTiles || y < 0 || y >= map.heightTiles) return true;
|
||
return map.wallSet.has(`${x},${y}`);
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Backward compat
|
||
export function getTile(mapId, x, y) {
|
||
return isWall(mapId, x, y) ? 1 : 0;
|
||
}
|
||
|
||
export { maps };
|