diff --git a/src/engine/simulation/simulation.ts b/src/engine/simulation/simulation.ts index 6495072..b30db07 100644 --- a/src/engine/simulation/simulation.ts +++ b/src/engine/simulation/simulation.ts @@ -2,7 +2,7 @@ import type { World, EntityId, Action, SimEvent, Actor, CombatantActor, Collecti import { calculateDamage } from "../gameplay/CombatLogic"; 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 { type EntityAccessor } from "../EntityAccessor"; 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 tileIdx = ny * w.width + nx; - if (isDestructibleByWalk(w.tiles[tileIdx])) { - tryDestructTile(w, nx, ny); + 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); + } 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) { diff --git a/src/rendering/FovManager.ts b/src/rendering/FovManager.ts index 555f865..b4c0e47 100644 --- a/src/rendering/FovManager.ts +++ b/src/rendering/FovManager.ts @@ -13,6 +13,7 @@ export class FovManager { private visibleStrength!: Float32Array; private worldWidth: number = 0; private worldHeight: number = 0; + private currentOrigin: { x: number; y: number } = { x: 0, y: 0 }; initialize(world: World) { this.worldWidth = world.width; @@ -22,6 +23,10 @@ export class FovManager { this.visibleStrength = new Float32Array(world.width * world.height); 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; const idx = y * world.width + x; return !blocksSight(world.tiles[idx]); @@ -29,6 +34,7 @@ export class FovManager { } compute(world: World, origin: { x: number; y: number }) { + this.currentOrigin = origin; this.visible.fill(0); this.visibleStrength.fill(0); diff --git a/src/rendering/__tests__/FovManager.repro.test.ts b/src/rendering/__tests__/FovManager.repro.test.ts new file mode 100644 index 0000000..b8b259a --- /dev/null +++ b/src/rendering/__tests__/FovManager.repro.test.ts @@ -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); + }); +});