diff --git a/public/break_escape/js/core/game.js b/public/break_escape/js/core/game.js index 64810b6..98e21a3 100644 --- a/public/break_escape/js/core/game.js +++ b/public/break_escape/js/core/game.js @@ -1,5 +1,5 @@ import { initializeRooms, calculateWorldBounds, calculateRoomPositions, createRoom, revealRoom, updatePlayerRoom, rooms } from './rooms.js?v=17'; -import { createPlayer, updatePlayerMovement, movePlayerToPoint, facePlayerToward, player } from './player.js?v=16'; +import { createPlayer, updatePlayerMovement, movePlayerToPoint, facePlayerToward, player } from './player.js?v=17'; import { initializePathfinder } from './pathfinding.js?v=7'; import { initializeInventory, processInitialInventoryItems } from '../systems/inventory.js?v=9'; import { checkObjectInteractions, setGameInstance, isObjectInInteractionRange } from '../systems/interactions.js?v=31'; diff --git a/public/break_escape/js/core/player.js b/public/break_escape/js/core/player.js index 51f4424..35373f4 100644 --- a/public/break_escape/js/core/player.js +++ b/public/break_escape/js/core/player.js @@ -842,21 +842,17 @@ export function movePlayerToPoint(x, y) { const requestId = ++playerPathRequestId; const pathfindingManager = window.pathfindingManager; - const roomId = window.currentPlayerRoom; - if (pathfindingManager && roomId) { + if (pathfindingManager && pathfindingManager.worldPathfinder) { // Use the body centre (feet collider) as the start position so all LOS // and pathfinding queries are relative to what actually collides with walls. const { x: px, y: py } = playerBodyPos(); - const room = window.rooms?.[roomId]; - const boxCount = room?.wallCollisionBoxes?.length ?? 0; - console.log(`πŸ–±οΈ movePlayerToPoint: feet(${px.toFixed(0)},${py.toFixed(0)}) β†’ target(${x.toFixed(0)},${y.toFixed(0)}) room=${roomId} wallBoxes=${boxCount}`); + console.log(`πŸ–±οΈ movePlayerToPoint: feet(${px.toFixed(0)},${py.toFixed(0)}) β†’ target(${x.toFixed(0)},${y.toFixed(0)})`); - // Prefer a direct route when line-of-sight is clear. - // Use physics-body LOS (checks actual wallCollisionBoxes) rather than - // the tile-grid approximation, so we don't walk into thin wall strips. - if (pathfindingManager.hasPhysicsLineOfSight(roomId, px, py, x, y)) { + // Prefer a direct route when the full width of the player body has clear + // physics LOS to the destination (world-aware: checks all rooms' wall boxes). + if (pathfindingManager.hasWorldPhysicsLineOfSight(px, py, x, y)) { console.log(' β†’ Direct LOS clear β€” going straight'); drawPathDebug(px, py, [{ x, y }], null); playerFollowingPath = false; @@ -864,45 +860,40 @@ export function movePlayerToPoint(x, y) { targetPoint = { x, y }; isMoving = true; } else { - // Snap the destination to the nearest walkable tile in case the click landed - // inside an obstacle (table, wall) β€” EasyStar cannot path to impassable tiles - const snappedDest = pathfindingManager.findNearestWalkableTile(roomId, x, y) || { x, y }; + // Snap destination to nearest walkable world-grid cell if the click + // landed inside an obstacle β€” EasyStar cannot path to blocked cells. + const snappedDest = pathfindingManager.findNearestWalkableWorldCell(x, y) || { x, y }; if (snappedDest.x !== x || snappedDest.y !== y) { console.log(` β†’ Dest snapped from (${x.toFixed(0)},${y.toFixed(0)}) to (${snappedDest.x.toFixed(0)},${snappedDest.y.toFixed(0)})`); } console.log(' β†’ LOS blocked β€” requesting EasyStar path...'); - // Route around obstacles via EasyStar β€” result arrives asynchronously - pathfindingManager.findPath(roomId, px, py, snappedDest.x, snappedDest.y, (path) => { - // Ignore result if the player has already clicked somewhere else + // Route via the unified world grid (works across room boundaries) + pathfindingManager.findWorldPath(px, py, snappedDest.x, snappedDest.y, (path) => { + // Ignore if player has already clicked somewhere else if (requestId !== playerPathRequestId) return; if (path && path.length > 0) { console.log(` β†’ EasyStar returned ${path.length} raw waypoints`); - // Smooth using the body-centre position at callback time so that - // stale async results are still safe. + // Smooth using current feet position (safe even with async delay) const { x: cx, y: cy } = playerBodyPos(); - const smoothed = pathfindingManager.smoothPathForPlayer( - roomId, cx, cy, path - ); + const smoothed = pathfindingManager.smoothWorldPathForPlayer(cx, cy, path); console.log(` β†’ Smoothed to ${smoothed.length} waypoints:`, smoothed.map((p, i) => `[${i}](${p.x.toFixed(0)},${p.y.toFixed(0)})`).join(' β†’ ')); drawPathDebug(cx, cy, smoothed, path); - playerFinalGoal = { x, y }; // remember original destination for LOS shortcutting + playerFinalGoal = { x, y }; // original click for LOS shortcut playerPath = smoothed; playerPathIndex = 0; playerFollowingPath = true; - // Start moving toward the first waypoint targetPoint = playerPath[playerPathIndex++]; isMoving = true; } else { - console.warn(` ⚠️ EasyStar returned no path β€” falling back to straight line to snapped dest`); + console.warn(' ⚠️ EasyStar returned no path β€” falling back to snapped dest'); const { x: cx, y: cy } = playerBodyPos(); drawPathDebug(cx, cy, [snappedDest], null); - // No path found β€” fall back to straight-line toward snapped position playerFollowingPath = false; targetPoint = snappedDest; isMoving = true; @@ -910,7 +901,7 @@ export function movePlayerToPoint(x, y) { }); } } else { - // Pathfinding not yet available β€” go direct + // World grid not yet available β€” go direct playerFollowingPath = false; targetPoint = { x, y }; isMoving = true; @@ -1262,14 +1253,12 @@ function updatePlayerMouseMovement() { // the remaining waypoints and head straight there, giving smooth arrival. if (playerFollowingPath && playerFinalGoal) { const pm = window.pathfindingManager; - const rid = window.currentPlayerRoom; - if (pm && rid && pm.hasPhysicsLineOfSight(rid, px, py, playerFinalGoal.x, playerFinalGoal.y)) { + if (pm && pm.hasWorldPhysicsLineOfSight(px, py, playerFinalGoal.x, playerFinalGoal.y)) { targetPoint = playerFinalGoal; playerFinalGoal = null; playerPath = []; playerPathIndex = 0; playerFollowingPath = false; - // Debug: show the direct leg if (window.pathfindingDebug) console.log(`βœ‚οΈ LOS shortcut to final goal (${targetPoint.x.toFixed(0)},${targetPoint.y.toFixed(0)})`); } } diff --git a/public/break_escape/js/core/rooms.js b/public/break_escape/js/core/rooms.js index 4c5e894..56ba1d4 100644 --- a/public/break_escape/js/core/rooms.js +++ b/public/break_escape/js/core/rooms.js @@ -56,11 +56,11 @@ import { } from '../utils/constants.js?v=8'; // Import the new system modules -import { initializeDoors, createDoorSpritesForRoom, checkDoorTransitions, updateDoorSpritesVisibility } from '../systems/doors.js?v=2'; +import { initializeDoors, createDoorSpritesForRoom, checkDoorTransitions, updateDoorSpritesVisibility } from '../systems/doors.js?v=3'; import { initializeObjectPhysics, setupChairCollisions, setupExistingChairsWithNewRoom, calculateChairSpinDirection, updateSwivelChairRotation, updateSpriteDepth } from '../systems/object-physics.js'; import { initializePlayerEffects, createPlayerBumpEffect, createPlantBumpEffect } from '../systems/player-effects.js'; import { initializeCollision, createWallCollisionBoxes, removeTilesUnderDoor, removeWallTilesForDoorInRoom, removeWallTilesAtWorldPosition } from '../systems/collision.js'; -import { NPCPathfindingManager } from '../systems/npc-pathfinding.js?v=10'; +import { NPCPathfindingManager } from '../systems/npc-pathfinding.js?v=14'; import NPCSpriteManager from '../systems/npc-sprites.js?v=3'; export let rooms = {}; diff --git a/public/break_escape/js/systems/collision.js b/public/break_escape/js/systems/collision.js index 20260e6..2ff4571 100644 --- a/public/break_escape/js/systems/collision.js +++ b/public/break_escape/js/systems/collision.js @@ -7,7 +7,7 @@ */ import { TILE_SIZE } from '../utils/constants.js'; -import { getOppositeDirection, calculateDoorPositionsForRoom } from './doors.js?v=2'; +import { getOppositeDirection, calculateDoorPositionsForRoom } from './doors.js?v=3'; let gameRef = null; let rooms = null; diff --git a/public/break_escape/js/systems/doors.js b/public/break_escape/js/systems/doors.js index c39b4cf..61a23a2 100644 --- a/public/break_escape/js/systems/doors.js +++ b/public/break_escape/js/systems/doors.js @@ -618,6 +618,10 @@ function openDoor(doorSprite) { // Wait for game scene to be ready before proceeding // This prevents crashes when called immediately after minigame cleanup const finishOpeningDoor = () => { + // Disable the door's physics body immediately so LOS checks stop seeing it + // right away, even before the sprite is destroyed asynchronously below. + if (doorSprite.body) doorSprite.body.enable = false; + // Update pathfinding grid to mark door tiles as walkable if (window.pathfindingManager) { // Mark door walkable in the current room @@ -702,7 +706,18 @@ function openDoor(doorSprite) { if (doorSprite.interactionZone) { doorSprite.interactionZone.destroy(); } - + + // Rebuild the world grid now that the door body is fully gone. + // Use a 250ms delay so any table/wall delayedCall(0,...) callbacks + // from the newly-loaded connected room have already fired. + if (window.pathfindingManager) { + const pm = window.pathfindingManager; + const scene = pm.scene; + if (scene?.time) { + scene.time.delayedCall(250, () => pm.rebuildWorldGrid()); + } + } + props.open = true; }; diff --git a/public/break_escape/js/systems/interactions.js b/public/break_escape/js/systems/interactions.js index abe147e..395d429 100644 --- a/public/break_escape/js/systems/interactions.js +++ b/public/break_escape/js/systems/interactions.js @@ -2,7 +2,7 @@ import { INTERACTION_RANGE, INTERACTION_RANGE_SQ, INTERACTION_CHECK_INTERVAL } from '../utils/constants.js?v=8'; import { rooms } from '../core/rooms.js?v=17'; import { handleUnlock } from './unlock-system.js'; -import { handleDoorInteraction } from './doors.js?v=2'; +import { handleDoorInteraction } from './doors.js?v=3'; import { collectFingerprint, handleBiometricScan } from './biometrics.js'; import { addToInventory, removeFromInventory, createItemIdentifier } from './inventory.js?v=9'; import { playUISound, playGameSound } from './ui-sounds.js?v=1'; diff --git a/public/break_escape/js/systems/npc-behavior.js b/public/break_escape/js/systems/npc-behavior.js index 947ac80..f05deb0 100644 --- a/public/break_escape/js/systems/npc-behavior.js +++ b/public/break_escape/js/systems/npc-behavior.js @@ -22,7 +22,7 @@ */ import { TILE_SIZE } from '../utils/constants.js?v=8'; -import { NPCPathfindingManager } from './npc-pathfinding.js?v=10'; +import { NPCPathfindingManager } from './npc-pathfinding.js?v=14'; /** * NPCBehaviorManager - Manages all NPC behaviors diff --git a/public/break_escape/js/systems/npc-pathfinding.js b/public/break_escape/js/systems/npc-pathfinding.js index bc6fd43..ef883fa 100644 --- a/public/break_escape/js/systems/npc-pathfinding.js +++ b/public/break_escape/js/systems/npc-pathfinding.js @@ -14,7 +14,7 @@ * @module npc-pathfinding */ -import { TILE_SIZE, GRID_SIZE } from '../utils/constants.js?v=8'; +import { TILE_SIZE, GRID_SIZE, PATHFINDING_STEP } from '../utils/constants.js?v=10'; const PATROL_EDGE_OFFSET = 2; // Distance from room edge (2 tiles) @@ -36,7 +36,13 @@ export class NPCPathfindingManager { this.pathfinders = new Map(); // Map this.grids = new Map(); // Map this.roomBounds = new Map(); // Map - + + // Unified world-level pathfinding grid (finer resolution, spans all rooms) + this.worldGrid = null; + this.worldPathfinder = null; + this.worldGridBounds = null; // {minX, minY, cols, rows, step} + this._worldGridDebugGraphics = null; + console.log('βœ… NPCPathfindingManager initialized'); } @@ -109,6 +115,12 @@ export class NPCPathfindingManager { // Call window.refreshPathfindingGrid() from the console if you want to // overlay physics bodies onto the grid manually for debugging. + // Rebuild the unified world grid AFTER a short delay so that all + // delayedCall(0, ...) callbacks (table setSize/setOffset, wall immovable + // assignment, etc.) have had a chance to fire first. Without the delay + // the grid sees full sprite bounds instead of the trimmed collision strips. + this.scene.time.delayedCall(200, () => this.rebuildWorldGrid()); + } catch (error) { console.error(`❌ Failed to initialize pathfinding for room ${roomId}:`, error); console.error('Error stack:', error.stack); @@ -531,15 +543,280 @@ export class NPCPathfindingManager { pathfinder.setGrid(grid); console.log(`βœ… Marked ${markedCount} door tiles as walkable in ${roomId} at (${tileX}, ${tileY}) direction: ${direction}`); + + // Unblock the corresponding cells in the world grid too + const doorWorldX = bounds.worldX + tileX * TILE_SIZE; + const doorWorldY = bounds.worldY + tileY * TILE_SIZE; + const doorW = (direction === 'north' || direction === 'south') ? TILE_SIZE * 2 : TILE_SIZE; + const doorH = (direction === 'east' || direction === 'west') ? TILE_SIZE * 2 : TILE_SIZE; + this.markWorldCellsWalkable(doorWorldX, doorWorldY, doorW, doorH); + } + + // ========================================================================= + // UNIFIED WORLD GRID + // One EasyStar grid at PATHFINDING_STEP (16px) resolution covering all rooms. + // Built from every immovable/static physics body in the scene, so walls, + // furniture and locked doors are all blocked automatically. + // Rebuilt each time a new room is initialised and when a door is opened. + // ========================================================================= + + /** + * (Re)build the unified world-level EasyStar grid from all physics bodies. + * Called automatically at the end of initializeRoomPathfinding(). + */ + rebuildWorldGrid() { + const scene = this.scene; + if (!scene?.physics?.world) return; + + const wb = scene.physics.world.bounds; + if (!wb || wb.width === 0 || wb.height === 0) { + console.warn('⚠️ rebuildWorldGrid: physics world bounds not set yet'); + return; + } + + const step = PATHFINDING_STEP; + const minX = wb.x; + const minY = wb.y; + const cols = Math.ceil(wb.width / step) + 2; // +2 safety margin + const rows = Math.ceil(wb.height / step) + 2; + + const grid = Array.from({ length: rows }, () => new Array(cols).fill(0)); + + // Mark every grid cell that substantially overlaps an immovable physics body. + // Subtracting 1 before flooring the right/bottom edge prevents a body whose + // edge sits exactly on a boundary from blocking the adjacent walkable cell. + const markBlocked = (left, top, right, bottom) => { + const cx1 = Math.max(0, Math.floor((left - minX) / step)); + const cy1 = Math.max(0, Math.floor((top - minY) / step)); + const cx2 = Math.min(cols - 1, Math.floor((right - minX - 1) / step)); + const cy2 = Math.min(rows - 1, Math.floor((bottom - minY - 1) / step)); + for (let cy = cy1; cy <= cy2; cy++) { + for (let cx = cx1; cx <= cx2; cx++) grid[cy][cx] = 1; + } + }; + + // Phaser static bodies (StaticGroups etc.) + scene.physics.world.staticBodies.iterate(body => { + if (body.enable) markBlocked(body.left, body.top, body.right, body.bottom); + }); + + // Dynamic-but-immovable bodies: wall strips, locked doors, furniture + scene.physics.world.bodies.iterate(body => { + if (body.enable && body.immovable) { + markBlocked(body.left, body.top, body.right, body.bottom); + } + }); + + this.worldGridBounds = { minX, minY, cols, rows, step }; + this.worldGrid = grid; + + const pf = new EasyStar.js(); + pf.setGrid(grid); + pf.setAcceptableTiles([0]); + pf.enableDiagonals(); + this.worldPathfinder = pf; + + const blocked = grid.reduce((n, row) => n + row.filter(v => v === 1).length, 0); + console.log(`βœ… World grid rebuilt: ${cols}Γ—${rows} @${step}px β€” ${blocked} blocked`); } /** - * Update the pathfinding grid by marking any tile that overlaps a real - * Phaser physics body in room.wallCollisionBoxes as impassable. - * - * Call this after createWallCollisionBoxes() finishes for the room so the - * EasyStar grid matches the actual physics geometry (thin wall strips at tile - * edges that may only partially cover border tiles). + * Find a path via the unified world grid (works across any rooms). + * Callback receives an array of world {x,y} waypoints, or null on failure. + */ + findWorldPath(startX, startY, endX, endY, callback) { + if (!this.worldPathfinder || !this.worldGridBounds) { + console.warn('⚠️ findWorldPath: world grid not ready'); + callback(null); + return; + } + const { minX, minY, cols, rows, step } = this.worldGridBounds; + const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); + const toCX = wx => clamp(Math.floor((wx - minX) / step), 0, cols - 1); + const toCY = wy => clamp(Math.floor((wy - minY) / step), 0, rows - 1); + const toWorld = (cx, cy) => ({ + x: minX + cx * step + step / 2, + y: minY + cy * step + step / 2 + }); + + this.worldPathfinder.findPath(toCX(startX), toCY(startY), toCX(endX), toCY(endY), (tilePath) => { + callback(tilePath?.length > 0 ? tilePath.map(p => toWorld(p.x, p.y)) : null); + }); + this.worldPathfinder.calculate(); + } + + /** + * Find the nearest walkable cell in the world grid to a world position. + * Returns the Euclidean-closest walkable cell, or null if none within maxRadius. + */ + findNearestWalkableWorldCell(worldX, worldY, maxRadius = 8) { + if (!this.worldGrid || !this.worldGridBounds) return null; + const { minX, minY, cols, rows, step } = this.worldGridBounds; + + const centerCX = Math.floor((worldX - minX) / step); + const centerCY = Math.floor((worldY - minY) / step); + + if (centerCY >= 0 && centerCY < rows && centerCX >= 0 && centerCX < cols && + this.worldGrid[centerCY][centerCX] === 0) { + return { x: minX + centerCX * step + step / 2, y: minY + centerCY * step + step / 2 }; + } + + let best = null, bestDistSq = Infinity; + for (let r = 1; r <= maxRadius; r++) { + for (let dy = -r; dy <= r; dy++) { + for (let dx = -r; dx <= r; dx++) { + if (Math.abs(dx) !== r && Math.abs(dy) !== r) continue; + const cx = centerCX + dx, cy = centerCY + dy; + if (cy < 0 || cy >= rows || cx < 0 || cx >= cols) continue; + if (this.worldGrid[cy][cx] !== 0) continue; + const wx = minX + cx * step + step / 2; + const wy = minY + cy * step + step / 2; + const distSq = (wx - worldX) ** 2 + (wy - worldY) ** 2; + if (distSq < bestDistSq) { bestDistSq = distSq; best = { x: wx, y: wy }; } + } + } + if (best !== null) break; + } + return best; + } + + /** + * Mark a rectangular area as walkable in the world grid. + * Used when a door is unlocked (removes the door body's blocked cells). + */ + markWorldCellsWalkable(worldX, worldY, width, height) { + if (!this.worldGrid || !this.worldGridBounds) return; + const { minX, minY, cols, rows, step } = this.worldGridBounds; + const cx1 = Math.max(0, Math.floor((worldX - minX) / step)); + const cy1 = Math.max(0, Math.floor((worldY - minY) / step)); + const cx2 = Math.min(cols - 1, Math.ceil((worldX + width - minX) / step)); + const cy2 = Math.min(rows - 1, Math.ceil((worldY + height - minY) / step)); + for (let cy = cy1; cy <= cy2; cy++) { + for (let cx = cx1; cx <= cx2; cx++) this.worldGrid[cy][cx] = 0; + } + this.worldPathfinder?.setGrid(this.worldGrid); + } + + /** + * Physics LOS check that works across ALL loaded rooms. + * Uses the same immovable-body sources as rebuildWorldGrid(), so walls AND + * furniture are both tested. Uses the same fat 3-ray sweep as hasPhysicsLineOfSight(). + */ + hasWorldPhysicsLineOfSight(x1, y1, x2, y2, bodyMargin = 2, bodyHalfWidth = 9) { + const scene = this.scene; + if (!scene?.physics?.world) return true; + + const dx = x2 - x1, dy = y2 - y1; + const len = Math.sqrt(dx * dx + dy * dy); + const px = len > 0.001 ? -dy / len : 1; + const py = len > 0.001 ? dx / len : 0; + + const rays = [ + { ax: x1, ay: y1, bx: x2, by: y2 }, + { ax: x1 + px * bodyHalfWidth, ay: y1 + py * bodyHalfWidth, bx: x2 + px * bodyHalfWidth, by: y2 + py * bodyHalfWidth }, + { ax: x1 - px * bodyHalfWidth, ay: y1 - py * bodyHalfWidth, bx: x2 - px * bodyHalfWidth, by: y2 - py * bodyHalfWidth }, + ]; + + const testBody = (body) => { + if (!body.enable) return false; + const left = body.left - bodyMargin; + const right = body.right + bodyMargin; + const top = body.top - bodyMargin; + const bottom = body.bottom + bodyMargin; + for (const ray of rays) { + if (_segmentIntersectsAABB(ray.ax, ray.ay, ray.bx, ray.by, left, top, right, bottom)) { + if (isDebug()) console.log(`🧱 worldLOS BLOCKED (${left.toFixed(0)},${top.toFixed(0)})β†’(${right.toFixed(0)},${bottom.toFixed(0)})`); + return true; + } + } + return false; + }; + + // Static bodies (Phaser StaticGroups etc.) + let blocked = false; + scene.physics.world.staticBodies.iterate(body => { + if (!blocked && testBody(body)) blocked = true; + }); + if (blocked) return false; + + // Dynamic-but-immovable bodies: wall strips, furniture, locked doors + scene.physics.world.bodies.iterate(body => { + if (!blocked && body.immovable && testBody(body)) blocked = true; + }); + + return !blocked; + } + + /** + * Greedy string-pull using hasWorldPhysicsLineOfSight β€” no roomId needed. + * Should be used for all player path smoothing. + */ + smoothWorldPathForPlayer(startX, startY, path) { + if (!path || path.length <= 1) return path; + const debug = isDebug(); + const smoothed = []; + let cx = startX, cy = startY, i = 0; + + while (i < path.length) { + let farthest = i; + for (let j = path.length - 1; j > i; j--) { + if (this.hasWorldPhysicsLineOfSight(cx, cy, path[j].x, path[j].y)) { + farthest = j; break; + } + } + if (debug) { + const p = path[farthest]; + console.log(` smooth: (${cx.toFixed(0)},${cy.toFixed(0)}) β†’ [${farthest}](${p.x.toFixed(0)},${p.y.toFixed(0)})`); + } + smoothed.push(path[farthest]); + cx = path[farthest].x; cy = path[farthest].y; + i = farthest + 1; + } + return smoothed.length > 0 ? smoothed : path; + } + + /** + * Draw a colour-coded overlay of the unified world grid. + * Red = blocked, faint green = walkable. + */ + drawWorldGridDebug(scene) { + this.clearWorldGridDebug(); + if (!this.worldGrid || !this.worldGridBounds || !scene) { + console.warn('drawWorldGridDebug: world grid not ready β€” try window.refreshPathfindingGrid() first'); + return null; + } + const { minX, minY, cols, rows, step } = this.worldGridBounds; + const g = scene.add.graphics(); + g.setDepth(851); + this._worldGridDebugGraphics = g; + + for (let cy = 0; cy < rows; cy++) { + for (let cx = 0; cx < cols; cx++) { + const wx = minX + cx * step; + const wy = minY + cy * step; + if (this.worldGrid[cy][cx] === 1) { + g.fillStyle(0xff2222, 0.45); + } else { + g.fillStyle(0x22ff44, 0.08); + } + g.fillRect(wx + 1, wy + 1, step - 2, step - 2); + } + } + console.log(`πŸ—ΊοΈ World grid debug: ${cols}Γ—${rows} @${step}px`); + return g; + } + + clearWorldGridDebug() { + if (this._worldGridDebugGraphics) { + this._worldGridDebugGraphics.destroy(); + this._worldGridDebugGraphics = null; + } + } + + /** + * Optional: bake actual physics bodies onto the per-room EasyStar tile grid. + * Not called automatically (see comment in initializeRoomPathfinding). + * Available for debugging via window.refreshPathfindingGrid(). * * @param {string} roomId * @returns {number} Number of newly-blocked tiles (0 if nothing to do) @@ -849,7 +1126,8 @@ export class NPCPathfindingManager { while (i < path.length) { let farthest = i; for (let j = path.length - 1; j > i; j--) { - if (this.hasPhysicsLineOfSight(roomId, cx, cy, path[j].x, path[j].y)) { + // Use world-aware LOS so cross-room segments are checked correctly + if (this.hasWorldPhysicsLineOfSight(cx, cy, path[j].x, path[j].y)) { farthest = j; break; } @@ -918,38 +1196,30 @@ function _segmentIntersectsAABB(x1, y1, x2, y2, left, top, right, bottom) { window.NPCPathfindingManager = NPCPathfindingManager; /** - * Console helpers β€” call these from the browser console to visualise the - * pathfinding grid for the room the player is currently in. + * Console helpers for visualising/debugging the unified world pathfinding grid. * * Usage: - * window.showPathfindingGrid() // draw grid overlay - * window.hidePathfindingGrid() // remove it - * window.refreshPathfindingGrid() // rebake physics bodies then redraw + * window.showPathfindingGrid() // draw world grid overlay (red=blocked, green=walkable) + * window.hidePathfindingGrid() // remove overlay + * window.refreshPathfindingGrid() // rebuild from current physics bodies then redraw */ window.showPathfindingGrid = () => { - const pm = window.pathfindingManager; - const rid = window.currentPlayerRoom; - // The main game scene is always scenes[0]; the .find() approach fails because - // Phaser marks a scene as inactive during the same frame it is running. - const scene = window.game?.scene?.scenes?.[0]; - if (!pm || !rid || !scene) { - console.warn('showPathfindingGrid: pathfindingManager / currentPlayerRoom / scene not ready', - { pm: !!pm, rid, scene: !!scene }); - return; - } - pm.drawGridDebug(rid, scene); - console.log(`πŸ—ΊοΈ Grid overlay shown for room "${rid}" β€” red=blocked, green=walkable`); + const pm = window.pathfindingManager; + const scene = pm?.scene; + if (!pm || !scene) { console.warn('showPathfindingGrid: not ready', { pm: !!pm, scene: !!scene }); return; } + pm.drawWorldGridDebug(scene); + console.log('πŸ—ΊοΈ World grid overlay shown β€” red=blocked, green=walkable'); }; window.hidePathfindingGrid = () => { - window.pathfindingManager?.clearGridDebug(); + const pm = window.pathfindingManager; + pm?.clearGridDebug(); + pm?.clearWorldGridDebug(); }; window.refreshPathfindingGrid = () => { - const pm = window.pathfindingManager; - const rid = window.currentPlayerRoom; - if (!pm || !rid) { console.warn('refreshPathfindingGrid: not ready'); return; } - const n = pm.refreshGridFromPhysicsBodies(rid); - console.log(`refreshPathfindingGrid: baked ${n} tiles from physics bodies in room "${rid}"`); + const pm = window.pathfindingManager; + if (!pm) { console.warn('refreshPathfindingGrid: not ready'); return; } + pm.rebuildWorldGrid(); window.showPathfindingGrid(); }; diff --git a/public/break_escape/js/systems/unlock-system.js b/public/break_escape/js/systems/unlock-system.js index 8b66dcf..e8e2d0e 100644 --- a/public/break_escape/js/systems/unlock-system.js +++ b/public/break_escape/js/systems/unlock-system.js @@ -9,7 +9,7 @@ import { DOOR_ALIGN_OVERLAP } from '../utils/constants.js'; import { rooms } from '../core/rooms.js'; -import { unlockDoor } from './doors.js?v=2'; +import { unlockDoor } from './doors.js?v=3'; import { startLockpickingMinigame, startKeySelectionMinigame, startPinMinigame, startPasswordMinigame } from './minigame-starters.js'; import { playUISound } from './ui-sounds.js?v=1'; diff --git a/public/break_escape/js/utils/constants.js b/public/break_escape/js/utils/constants.js index 7281ee6..3a596c7 100644 --- a/public/break_escape/js/utils/constants.js +++ b/public/break_escape/js/utils/constants.js @@ -13,6 +13,10 @@ export const VISUAL_TOP_TILES = 2; // Top 2 rows are visual wall overlay export const GRID_UNIT_WIDTH_PX = GRID_UNIT_WIDTH_TILES * TILE_SIZE; // 160px export const GRID_UNIT_HEIGHT_PX = GRID_UNIT_HEIGHT_TILES * TILE_SIZE; // 128px +// Pathfinding grid resolution (px per cell). Smaller than TILE_SIZE so the +// player can navigate gaps narrower than a full 32px tile. +export const PATHFINDING_STEP = 8; + export const MOVEMENT_SPEED = 150; export const RUN_SPEED_MULTIPLIER = 1.5; // Speed multiplier when holding shift export const RUN_ANIMATION_MULTIPLIER = 1.5; // Animation speed multiplier when holding shift