mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-23 04:08:03 +00:00
fix: update module imports to latest versions for improved functionality and consistency
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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)})`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<roomId, pathfinder>
|
||||
this.grids = new Map(); // Map<roomId, grid>
|
||||
this.roomBounds = new Map(); // Map<roomId, {x, y, width, height, mapWidth, mapHeight}>
|
||||
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user