diff --git a/src/game/maze-objects.ts b/src/game/maze-objects.ts index 854f955..8060f7c 100644 --- a/src/game/maze-objects.ts +++ b/src/game/maze-objects.ts @@ -234,8 +234,12 @@ export function isOpenFloor(cells: readonly (readonly Ds1Cell[])[], cx: number, const cell = row[cx] if (!cell) return false - // Must contain floor - const hasFloor = cell.floors.length > 0 && cell.floors.some(f => !f.hidden && (f.prop1 !== 0 || f.style !== 0 || f.sequence !== 0 || true)) + // Must contain floor *art*. A `Ds1Floor` whose `prop1`, `style` and `sequence` + // are all zero is an empty slot, not a tile: DS1 stores one entry per floor + // layer for every cell, so a one-layer map has a zero-filled entry everywhere + // the level does not paint. The `|| true` that used to close this condition + // made the whole content test vacuous, so objects were placed on void cells. + const hasFloor = cell.floors.length > 0 && cell.floors.some(f => !f.hidden && (f.prop1 !== 0 || f.style !== 0 || f.sequence !== 0)) if (!hasFloor) return false // Must not have a solid, visible wall diff --git a/tests/maze-objects.test.ts b/tests/maze-objects.test.ts index 1e259b7..ff44275 100644 --- a/tests/maze-objects.test.ts +++ b/tests/maze-objects.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest' import type { Ds1, Ds1Cell, Ds1Object } from '../src/formats/ds1.ts' import { populateDungeonObjects, + isOpenFloor, CANONICAL_SHRINES, CHEST_IDS_BY_ACT, RACK_IDS_BY_ACT, @@ -482,3 +483,56 @@ describe('generateMaze end-to-end dynamic object integration', () => { expect(foundStatic).toBe(true) }) }) + +/** + * Regression cover for the `|| true` that used to close `isOpenFloor`'s content + * test (issue #58 item 2). With it, every `Ds1Floor` entry counted as floor art, + * so objects were seeded on cells the level never paints. + */ +describe('isOpenFloor content check', () => { + /** One cell grid with the given floor entries and no walls. */ + function grid(floors: Ds1Cell['floors']): Ds1Cell[][] { + return [[{ walls: [], floors, shadows: [], substitutions: [] }]] + } + + const painted = { prop1: 1, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false } + const empty = { prop1: 0, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false } + + test('a painted floor cell is open floor', () => { + expect(isOpenFloor(grid([painted]), 0, 0)).toBe(true) + }) + + test('a cell whose only floor entry is all-zero is not open floor', () => { + expect(isOpenFloor(grid([empty]), 0, 0)).toBe(false) + }) + + test('style or sequence alone still counts as art', () => { + expect(isOpenFloor(grid([{ ...empty, style: 3 }]), 0, 0)).toBe(true) + expect(isOpenFloor(grid([{ ...empty, sequence: 7 }]), 0, 0)).toBe(true) + }) + + test('a hidden painted floor does not rescue an otherwise empty cell', () => { + expect(isOpenFloor(grid([{ ...painted, hidden: true }]), 0, 0)).toBe(false) + }) + + test('a multi-layer cell is open floor when any layer carries art', () => { + expect(isOpenFloor(grid([empty, painted]), 0, 0)).toBe(true) + expect(isOpenFloor(grid([empty, empty]), 0, 0)).toBe(false) + }) + + test('objects are not seeded into a level whose floors are all empty slots', () => { + const { level, rooms } = createSyntheticDungeon() + for (const row of level.cells) { + for (const cell of row) { + ;(cell.floors as Ds1Cell['floors'][number][]).splice( + 0, + cell.floors.length, + { prop1: 0, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false }, + ) + } + } + const result = populateDungeonObjects(rooms, 0, 0, level, 'Act 1 - Cave', 2, { density: 1.0 }, new Rng(7)) + expect(result.placedObjects.length).toBe(0) + }) +}) +