diff --git a/js/world/maps.js b/js/world/maps.js index 79e8d99..cfb1546 100644 --- a/js/world/maps.js +++ b/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; } diff --git a/js/world/sprites.js b/js/world/sprites.js index 25cfb48..ba680aa 100644 --- a/js/world/sprites.js +++ b/js/world/sprites.js @@ -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); } /** diff --git a/js/world/worldRenderer.js b/js/world/worldRenderer.js index 2b88320..f356d91 100644 --- a/js/world/worldRenderer.js +++ b/js/world/worldRenderer.js @@ -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