Fixed bug with vision when standing in doorway

This commit is contained in:
2026-02-07 13:04:49 +11:00
parent 02f850da35
commit da544438e1
3 changed files with 91 additions and 3 deletions

View File

@@ -2,7 +2,7 @@ import type { World, EntityId, Action, SimEvent, Actor, CombatantActor, Collecti
import { calculateDamage } from "../gameplay/CombatLogic"; import { calculateDamage } from "../gameplay/CombatLogic";
import { isBlocked, tryDestructTile } from "../world/world-logic"; import { isBlocked, tryDestructTile } from "../world/world-logic";
import { isDestructibleByWalk } from "../../core/terrain"; import { isDestructibleByWalk, TileType } from "../../core/terrain";
import { GAME_CONFIG } from "../../core/config/GameConfig"; import { GAME_CONFIG } from "../../core/config/GameConfig";
import { type EntityAccessor } from "../EntityAccessor"; import { type EntityAccessor } from "../EntityAccessor";
import { AISystem } from "../ecs/AISystem"; import { AISystem } from "../ecs/AISystem";
@@ -102,8 +102,25 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number },
const events: SimEvent[] = [{ type: "moved", actorId: actor.id, from, to }]; const events: SimEvent[] = [{ type: "moved", actorId: actor.id, from, to }];
const tileIdx = ny * w.width + nx; const tileIdx = ny * w.width + nx;
if (isDestructibleByWalk(w.tiles[tileIdx])) { const tile = w.tiles[tileIdx];
if (isDestructibleByWalk(tile)) {
// Only open if it's currently closed.
// tryDestructTile toggles, so we must be specific for doors.
if (tile === TileType.DOOR_CLOSED) {
tryDestructTile(w, nx, ny); tryDestructTile(w, nx, ny);
} else if (tile !== TileType.DOOR_OPEN) {
// For other destructibles like grass
tryDestructTile(w, nx, ny);
}
}
// Handle "from" tile - Close door if we just left it and no one else is there
const fromIdx = from.y * w.width + from.x;
if (w.tiles[fromIdx] === TileType.DOOR_OPEN) {
const actorsLeft = accessor.getActorsAt(from.x, from.y);
if (actorsLeft.length === 0) {
w.tiles[fromIdx] = TileType.DOOR_CLOSED;
}
} }
if (actor.category === "combatant" && actor.isPlayer) { if (actor.category === "combatant" && actor.isPlayer) {

View File

@@ -13,6 +13,7 @@ export class FovManager {
private visibleStrength!: Float32Array; private visibleStrength!: Float32Array;
private worldWidth: number = 0; private worldWidth: number = 0;
private worldHeight: number = 0; private worldHeight: number = 0;
private currentOrigin: { x: number; y: number } = { x: 0, y: 0 };
initialize(world: World) { initialize(world: World) {
this.worldWidth = world.width; this.worldWidth = world.width;
@@ -22,6 +23,10 @@ export class FovManager {
this.visibleStrength = new Float32Array(world.width * world.height); this.visibleStrength = new Float32Array(world.width * world.height);
this.fov = new FOV.PreciseShadowcasting((x: number, y: number) => { this.fov = new FOV.PreciseShadowcasting((x: number, y: number) => {
// Best practice: Origin is always transparent to itself,
// otherwise vision is blocked if standing on an opaque tile (like a doorway).
if (x === this.currentOrigin.x && y === this.currentOrigin.y) return true;
if (!inBounds(world, x, y)) return false; if (!inBounds(world, x, y)) return false;
const idx = y * world.width + x; const idx = y * world.width + x;
return !blocksSight(world.tiles[idx]); return !blocksSight(world.tiles[idx]);
@@ -29,6 +34,7 @@ export class FovManager {
} }
compute(world: World, origin: { x: number; y: number }) { compute(world: World, origin: { x: number; y: number }) {
this.currentOrigin = origin;
this.visible.fill(0); this.visible.fill(0);
this.visibleStrength.fill(0); this.visibleStrength.fill(0);

View File

@@ -0,0 +1,65 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock Phaser
vi.mock('phaser', () => ({
default: {
Math: {
Clamp: (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
}
}
}));
import { FovManager } from '../FovManager';
import { TileType } from '../../core/terrain';
import { type World } from '../../core/types';
describe('FovManager Repro', () => {
let fovManager: FovManager;
let world: World;
beforeEach(() => {
world = {
width: 11,
height: 11,
tiles: new Array(11 * 11).fill(TileType.EMPTY),
exit: { x: 10, y: 10 },
trackPath: []
};
fovManager = new FovManager();
});
it('should see through a doorway when standing in it (open door)', () => {
// Create a vertical wall at x=5 with a door at (5,5)
for (let y = 0; y < 11; y++) {
if (y === 5) {
world.tiles[y * 11 + 5] = TileType.DOOR_OPEN;
} else {
world.tiles[y * 11 + 5] = TileType.WALL;
}
}
fovManager.initialize(world);
fovManager.compute(world, { x: 5, y: 5 });
expect(fovManager.isVisible(4, 5)).toBe(true);
expect(fovManager.isVisible(6, 5)).toBe(true);
});
it('should NOT be blind when standing on an opaque tile (like a closed door) AFTER FIX', () => {
// Create a horizontal wall with a closed door at (5,5)
for (let x = 0; x < 11; x++) {
if (x === 5) {
world.tiles[5 * 11 + x] = TileType.DOOR_CLOSED;
} else {
world.tiles[5 * 11 + x] = TileType.WALL;
}
}
fovManager.initialize(world);
fovManager.compute(world, { x: 5, y: 5 });
// AFTER FIX: should see tiles on both sides of the door
expect(fovManager.isVisible(5, 4)).toBe(true);
expect(fovManager.isVisible(5, 6)).toBe(true);
});
});