From 72d0f5d576519a8e7e0fd19830c870068a1e609f Mon Sep 17 00:00:00 2001 From: Kyle Banicevic Date: Sat, 7 Feb 2026 13:34:12 +1100 Subject: [PATCH] Fixed door saying open bugs --- src/engine/__tests__/DoorWalkthrough.test.ts | 126 +++++++++++++++++++ src/engine/simulation/simulation.ts | 3 + src/rendering/DungeonRenderer.ts | 4 +- 3 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 src/engine/__tests__/DoorWalkthrough.test.ts diff --git a/src/engine/__tests__/DoorWalkthrough.test.ts b/src/engine/__tests__/DoorWalkthrough.test.ts new file mode 100644 index 0000000..dd0a84f --- /dev/null +++ b/src/engine/__tests__/DoorWalkthrough.test.ts @@ -0,0 +1,126 @@ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { applyAction } from '../simulation/simulation'; +import { type World, type Actor, type EntityId } from '../../core/types'; +import { EntityAccessor } from '../EntityAccessor'; +import { ECSWorld } from '../ecs/World'; +import { TileType } from '../../core/terrain'; + +const createTestWorld = (): World => { + return { + width: 10, + height: 10, + tiles: new Array(100).fill(TileType.EMPTY), + exit: { x: 9, y: 9 }, + trackPath: [] + }; +}; + +describe('Multi-step Door Walkthrough Bug', () => { + let ecsWorld: ECSWorld; + let world: World; + + beforeEach(() => { + ecsWorld = new ECSWorld(); + world = createTestWorld(); + }); + + it('door should close after player walks through and moves away', () => { + const playerId = 1 as EntityId; + const player: Actor = { + id: playerId, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: { hp: 10, maxHp: 10 } as any, energy: 0 + } as any; + + ecsWorld.addComponent(playerId, "position", player.pos); + ecsWorld.addComponent(playerId, "player", {}); + ecsWorld.addComponent(playerId, "stats", { hp: 10, maxHp: 10 } as any); + ecsWorld.addComponent(playerId, "actorType", { type: "player" }); + ecsWorld.addComponent(playerId, "energy", { current: 0, speed: 100 }); + + const accessor = new EntityAccessor(world, playerId, ecsWorld); + + // Place a closed door at (4,3) + const doorIdx = 3 * 10 + 4; + world.tiles[doorIdx] = TileType.DOOR_CLOSED; + + // 1. Move onto the door + console.log("Step 1: Moving onto door at (4,3)"); + applyAction(world, playerId, { type: "move", dx: 1, dy: 0 }, accessor); + expect(player.pos).toEqual({ x: 4, y: 3 }); + expect(world.tiles[doorIdx]).toBe(TileType.DOOR_OPEN); + + // 2. Move off the door to (5,3) + console.log("Step 2: Moving off door to (5,3)"); + applyAction(world, playerId, { type: "move", dx: 1, dy: 0 }, accessor); + expect(player.pos).toEqual({ x: 5, y: 3 }); + + // This is where it's reported to stay open sometimes + console.log("Door tile state after Step 2:", world.tiles[doorIdx]); + expect(world.tiles[doorIdx]).toBe(TileType.DOOR_CLOSED); + + // 3. Move further away to (6,3) + console.log("Step 3: Moving further away to (6,3)"); + applyAction(world, playerId, { type: "move", dx: 1, dy: 0 }, accessor); + expect(player.pos).toEqual({ x: 6, y: 3 }); + expect(world.tiles[doorIdx]).toBe(TileType.DOOR_CLOSED); + }); + + it('door should close after player walks through it diagonally', () => { + const playerId = 1 as EntityId; + const player: Actor = { + id: playerId, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: { hp: 10, maxHp: 10 } as any, energy: 0 + } as any; + + ecsWorld.addComponent(playerId, "position", player.pos); + ecsWorld.addComponent(playerId, "player", {}); + ecsWorld.addComponent(playerId, "stats", { hp: 10, maxHp: 10 } as any); + ecsWorld.addComponent(playerId, "actorType", { type: "player" }); + ecsWorld.addComponent(playerId, "energy", { current: 0, speed: 100 }); + + const accessor = new EntityAccessor(world, playerId, ecsWorld); + + // Place a closed door at (4,4) + const doorIdx = 4 * 10 + 4; + world.tiles[doorIdx] = TileType.DOOR_CLOSED; + + // 1. Move onto the door diagonally + applyAction(world, playerId, { type: "move", dx: 1, dy: 1 }, accessor); + expect(player.pos).toEqual({ x: 4, y: 4 }); + expect(world.tiles[doorIdx]).toBe(TileType.DOOR_OPEN); + + // 2. Move off the door diagonally to (5,5) + applyAction(world, playerId, { type: "move", dx: 1, dy: 1 }, accessor); + expect(player.pos).toEqual({ x: 5, y: 5 }); + + expect(world.tiles[doorIdx]).toBe(TileType.DOOR_CLOSED); + }); + + it('door should stay open while player is standing on it (wait action)', () => { + const playerId = 1 as EntityId; + const player: Actor = { + id: playerId, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: { hp: 10, maxHp: 10 } as any, energy: 0 + } as any; + + ecsWorld.addComponent(playerId, "position", player.pos); + ecsWorld.addComponent(playerId, "player", {}); + ecsWorld.addComponent(playerId, "stats", { hp: 10, maxHp: 10 } as any); + ecsWorld.addComponent(playerId, "actorType", { type: "player" }); + ecsWorld.addComponent(playerId, "energy", { current: 0, speed: 100 }); + + const accessor = new EntityAccessor(world, playerId, ecsWorld); + + // Place a closed door at (4,3) + const doorIdx = 3 * 10 + 4; + world.tiles[doorIdx] = TileType.DOOR_CLOSED; + + // 1. Move onto the door + applyAction(world, playerId, { type: "move", dx: 1, dy: 0 }, accessor); + expect(player.pos).toEqual({ x: 4, y: 3 }); + expect(world.tiles[doorIdx]).toBe(TileType.DOOR_OPEN); + + // 2. Wait on the door + applyAction(world, playerId, { type: "wait" }, accessor); + expect(player.pos).toEqual({ x: 4, y: 3 }); + expect(world.tiles[doorIdx]).toBe(TileType.DOOR_OPEN); + }); +}); diff --git a/src/engine/simulation/simulation.ts b/src/engine/simulation/simulation.ts index b30db07..b2e8bfc 100644 --- a/src/engine/simulation/simulation.ts +++ b/src/engine/simulation/simulation.ts @@ -119,7 +119,10 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }, if (w.tiles[fromIdx] === TileType.DOOR_OPEN) { const actorsLeft = accessor.getActorsAt(from.x, from.y); if (actorsLeft.length === 0) { + console.log(`[simulation] Closing door at ${from.x},${from.y} - Actor ${actor.id} left`); w.tiles[fromIdx] = TileType.DOOR_CLOSED; + } else { + console.log(`[simulation] Door at ${from.x},${from.y} stays open - ${actorsLeft.length} actors remain`); } } diff --git a/src/rendering/DungeonRenderer.ts b/src/rendering/DungeonRenderer.ts index b468c45..067e0c5 100644 --- a/src/rendering/DungeonRenderer.ts +++ b/src/rendering/DungeonRenderer.ts @@ -238,9 +238,7 @@ export class DungeonRenderer { // Sync visual tile with logical tile (e.g. if grass was destroyed) if (tile.index !== worldTile) { - // We can safely update the index property for basic tile switching - // If we needed to change collision properties, we'd use putTileAt - tile.index = worldTile; + this.layer!.putTileAt(worldTile, tile.x, tile.y); } const isSeen = seen[i] === 1;