2025-12-19 05:19:22 +00:00
|
|
|
import { expect } from "@esm-bundle/chai";
|
|
|
|
|
import { VoxelGrid } from "../../src/grid/VoxelGrid.js";
|
|
|
|
|
import { RuinGenerator } from "../../src/generation/RuinGenerator.js";
|
|
|
|
|
|
|
|
|
|
describe("System: Procedural Generation (Scatter)", () => {
|
|
|
|
|
let grid;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
grid = new VoxelGrid(20, 5, 20);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 1: scatterCover should place objects on valid floors", () => {
|
|
|
|
|
const gen = new RuinGenerator(grid, 12345);
|
|
|
|
|
|
|
|
|
|
// 1. Generate empty rooms first
|
|
|
|
|
// Note: RuinGenerator runs applyTextures() automatically at the end of generate(),
|
|
|
|
|
// so floor IDs will be 200+, not 1.
|
|
|
|
|
gen.generate(1, 10, 10);
|
|
|
|
|
|
|
|
|
|
// 2. Count empty floor tiles (Air above Solid)
|
|
|
|
|
let floorCount = 0;
|
|
|
|
|
for (let x = 0; x < 20; x++) {
|
|
|
|
|
for (let z = 0; z < 20; z++) {
|
|
|
|
|
// Check for Air at y=1
|
|
|
|
|
const isAirAbove = grid.getCell(x, 1, z) === 0;
|
|
|
|
|
// Check for ANY solid block at y=0 (Floor ID is likely 200+)
|
|
|
|
|
const isSolidBelow = grid.getCell(x, 0, z) !== 0;
|
|
|
|
|
|
|
|
|
|
if (isAirAbove && isSolidBelow) {
|
|
|
|
|
floorCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Scatter Cover (ID 10) at 50% density
|
|
|
|
|
gen.scatterCover(10, 0.5);
|
|
|
|
|
|
|
|
|
|
// 4. Count Cover
|
|
|
|
|
let coverCount = 0;
|
|
|
|
|
for (let x = 0; x < 20; x++) {
|
|
|
|
|
for (let z = 0; z < 20; z++) {
|
|
|
|
|
if (grid.getCell(x, 1, z) === 10) {
|
|
|
|
|
coverCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expect roughly 50% of the floor to be covered
|
2025-12-19 23:35:29 +00:00
|
|
|
// Allow a wider margin (30-70%) for small sample sizes where RNG variance is higher
|
|
|
|
|
const expectedMin = Math.floor(floorCount * 0.3);
|
|
|
|
|
const expectedMax = Math.ceil(floorCount * 0.7);
|
2025-12-19 05:19:22 +00:00
|
|
|
|
|
|
|
|
expect(floorCount).to.be.greaterThan(0, "No floors were generated!");
|
|
|
|
|
expect(coverCount).to.be.within(
|
|
|
|
|
expectedMin,
|
|
|
|
|
expectedMax,
|
2025-12-19 23:35:29 +00:00
|
|
|
`Cover count ${coverCount} not within 30-70% of floor count ${floorCount}`
|
2025-12-19 05:19:22 +00:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 2: scatterCover should NOT place objects in mid-air", () => {
|
|
|
|
|
const gen = new RuinGenerator(grid, 12345);
|
|
|
|
|
gen.generate();
|
|
|
|
|
gen.scatterCover(10, 1.0); // 100% density to force errors if logic is wrong
|
|
|
|
|
|
|
|
|
|
// Scan for floating cover
|
|
|
|
|
for (let x = 0; x < 20; x++) {
|
|
|
|
|
for (let z = 0; z < 20; z++) {
|
|
|
|
|
for (let y = 1; y < 4; y++) {
|
|
|
|
|
if (grid.getCell(x, y, z) === 10) {
|
|
|
|
|
const below = grid.getCell(x, y - 1, z);
|
|
|
|
|
// Cover must have something solid below it
|
|
|
|
|
expect(below).to.not.equal(
|
|
|
|
|
0,
|
|
|
|
|
`Found floating cover at ${x},${y},${z}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|