2025-12-22 20:57:04 +00:00
|
|
|
import { expect } from "@esm-bundle/chai";
|
|
|
|
|
import { CaveGenerator } from "../../src/generation/CaveGenerator.js";
|
|
|
|
|
import { VoxelGrid } from "../../src/grid/VoxelGrid.js";
|
|
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
// Mock OffscreenCanvas for texture generation
|
|
|
|
|
if (typeof OffscreenCanvas === "undefined") {
|
|
|
|
|
class MockCanvas {
|
|
|
|
|
constructor(width, height) {
|
|
|
|
|
this.width = width;
|
|
|
|
|
this.height = height;
|
|
|
|
|
}
|
|
|
|
|
getContext() {
|
|
|
|
|
return {
|
|
|
|
|
createImageData: (w, h) => ({
|
|
|
|
|
data: new Uint8ClampedArray(w * h * 4),
|
|
|
|
|
}),
|
|
|
|
|
putImageData: () => {},
|
|
|
|
|
drawImage: () => {},
|
|
|
|
|
fillStyle: "",
|
|
|
|
|
fillRect: () => {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
globalThis.OffscreenCanvas = MockCanvas;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:57:04 +00:00
|
|
|
describe("Generation: CaveGenerator", () => {
|
|
|
|
|
let grid;
|
|
|
|
|
let generator;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
grid = new VoxelGrid(20, 10, 20);
|
|
|
|
|
generator = new CaveGenerator(grid, 12345);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 1: Should initialize with texture generators", () => {
|
|
|
|
|
expect(generator.floorGen).to.exist;
|
|
|
|
|
expect(generator.wallGen).to.exist;
|
|
|
|
|
expect(generator.generatedAssets).to.have.property("palette");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 2: preloadTextures should generate texture palette", () => {
|
|
|
|
|
generator.preloadTextures();
|
|
|
|
|
|
|
|
|
|
// Should have wall variations (100-109)
|
|
|
|
|
expect(generator.generatedAssets.palette[100]).to.exist;
|
|
|
|
|
expect(generator.generatedAssets.palette[109]).to.exist;
|
|
|
|
|
|
|
|
|
|
// Should have floor variations (200-209)
|
|
|
|
|
expect(generator.generatedAssets.palette[200]).to.exist;
|
|
|
|
|
expect(generator.generatedAssets.palette[209]).to.exist;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 3: generate should create foundation layer", () => {
|
|
|
|
|
generator.generate(0.5, 2);
|
|
|
|
|
|
|
|
|
|
// Foundation (y=0) should be solid
|
|
|
|
|
for (let x = 0; x < grid.size.x; x++) {
|
|
|
|
|
for (let z = 0; z < grid.size.z; z++) {
|
|
|
|
|
expect(grid.getCell(x, 0, z)).to.not.equal(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
it("CoA 4: generate should create extruded walls", () => {
|
2025-12-22 20:57:04 +00:00
|
|
|
generator.generate(0.5, 2);
|
|
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
// Check a spot that is a wall (has block at y=5 for example)
|
|
|
|
|
// It should be solid all the way down to y=0
|
|
|
|
|
let foundWall = false;
|
2025-12-22 20:57:04 +00:00
|
|
|
for (let x = 0; x < grid.size.x; x++) {
|
|
|
|
|
for (let z = 0; z < grid.size.z; z++) {
|
2026-01-07 17:00:10 +00:00
|
|
|
const midY = Math.floor(grid.size.y / 2);
|
|
|
|
|
if (grid.getCell(x, midY, z) !== 0) {
|
|
|
|
|
foundWall = true;
|
|
|
|
|
// Valid wall stack
|
|
|
|
|
expect(grid.getCell(x, 0, z)).to.not.equal(0);
|
|
|
|
|
expect(grid.getCell(x, 1, z)).to.not.equal(0);
|
|
|
|
|
}
|
2025-12-22 20:57:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-07 17:00:10 +00:00
|
|
|
expect(foundWall).to.be.true;
|
2025-12-22 20:57:04 +00:00
|
|
|
});
|
|
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
it("CoA 5: smoothMap should apply 2D cellular automata", () => {
|
|
|
|
|
// 5x5 Map
|
|
|
|
|
// 1 1 1 0 0
|
|
|
|
|
// 1 0 1 0 0
|
|
|
|
|
// 1 1 1 0 0
|
|
|
|
|
// 0 0 0 0 0
|
|
|
|
|
// 0 0 0 0 0
|
|
|
|
|
// Center (2, 2) has neighbors: (1,1)=0, (1,2)=1, (1,3)=0, (2,1)=1, (2,3)=1, (3,1)=0, (3,2)=0, (3,3)=0
|
|
|
|
|
// Total 1s = 3. Less than 4 -> should become 0.
|
|
|
|
|
|
|
|
|
|
// Actually let's just test simple behavior: dense area stays dense, sparse area clears.
|
|
|
|
|
let map = [
|
|
|
|
|
[1, 1, 1, 1, 1],
|
|
|
|
|
[1, 1, 1, 1, 1],
|
|
|
|
|
[1, 1, 1, 1, 1],
|
|
|
|
|
[0, 0, 0, 0, 0],
|
|
|
|
|
[0, 0, 0, 0, 0],
|
|
|
|
|
];
|
|
|
|
|
// Mock dimensions temporarily for this test if smoothMap uses this.width/depth
|
|
|
|
|
// implementation uses map.length so it might be fine or we need to override width/depth
|
|
|
|
|
// My implementation used `this.width`. I should mock it or use the real grid size if I constructed it right.
|
|
|
|
|
// The real grid is 20x10x20.
|
|
|
|
|
// Let's use a full size map initialized to match grid size to avoid bounds errors.
|
|
|
|
|
|
|
|
|
|
map = [];
|
|
|
|
|
for (let x = 0; x < 20; x++) {
|
|
|
|
|
map[x] = [];
|
|
|
|
|
for (let z = 0; z < 20; z++) {
|
|
|
|
|
map[x][z] = x < 10 && z < 10 ? 1 : 0; // Block of walls
|
2025-12-22 20:57:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
// Creating a hole in the wall block
|
|
|
|
|
map[5][5] = 0;
|
|
|
|
|
// Surrounded by 1s (8 neighbors). Should become 1.
|
2025-12-22 20:57:04 +00:00
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
const newMap = generator.smoothMap(map);
|
|
|
|
|
// (5,5) should be filled
|
|
|
|
|
expect(newMap[5][5]).to.equal(1);
|
2025-12-22 20:57:04 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 6: applyTextures should assign floor and wall IDs", () => {
|
2026-01-07 17:00:10 +00:00
|
|
|
// Setup manual grid state replicating generate() output
|
|
|
|
|
// Wall at 5,5 (Column)
|
|
|
|
|
for (let y = 0; y < grid.size.y; y++) {
|
|
|
|
|
grid.setCell(5, y, 5, 100);
|
2025-12-22 20:57:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
// Floor at 6,6 (Only y=0)
|
|
|
|
|
grid.setCell(6, 0, 6, 100);
|
|
|
|
|
// y=1 is air
|
|
|
|
|
grid.setCell(6, 1, 6, 0);
|
2025-12-22 20:57:04 +00:00
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
generator.applyTextures();
|
2025-12-22 20:57:04 +00:00
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
// Wall Check
|
|
|
|
|
const wallId = grid.getCell(5, 5, 5);
|
2025-12-22 20:57:04 +00:00
|
|
|
expect(wallId).to.be.greaterThanOrEqual(100);
|
|
|
|
|
expect(wallId).to.be.lessThanOrEqual(109);
|
2026-01-07 17:00:10 +00:00
|
|
|
|
|
|
|
|
// Floor Check
|
|
|
|
|
const floorId = grid.getCell(6, 0, 6);
|
|
|
|
|
expect(floorId).to.be.greaterThanOrEqual(200);
|
|
|
|
|
expect(floorId).to.be.lessThanOrEqual(209);
|
2025-12-22 20:57:04 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 7: generate should scatter cover objects", () => {
|
|
|
|
|
generator.generate(0.5, 2);
|
|
|
|
|
|
|
|
|
|
// Check for cover objects (ID 10)
|
|
|
|
|
let coverCount = 0;
|
|
|
|
|
for (let x = 0; x < grid.size.x; x++) {
|
|
|
|
|
for (let z = 0; z < grid.size.z; z++) {
|
|
|
|
|
for (let y = 0; y < grid.size.y; y++) {
|
|
|
|
|
if (grid.getCell(x, y, z) === 10) {
|
|
|
|
|
coverCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Should have some cover objects
|
|
|
|
|
expect(coverCount).to.be.greaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 8: generate with same seed should produce consistent results", () => {
|
|
|
|
|
const grid1 = new VoxelGrid(20, 10, 20);
|
|
|
|
|
const gen1 = new CaveGenerator(grid1, 12345);
|
|
|
|
|
gen1.generate(0.5, 2);
|
|
|
|
|
|
|
|
|
|
const grid2 = new VoxelGrid(20, 10, 20);
|
|
|
|
|
const gen2 = new CaveGenerator(grid2, 12345);
|
|
|
|
|
gen2.generate(0.5, 2);
|
|
|
|
|
|
|
|
|
|
// Same seed should produce same results
|
|
|
|
|
expect(grid1.cells).to.deep.equal(grid2.cells);
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-07 17:00:10 +00:00
|
|
|
it("CoA 9: ensureConnectivity should remove disconnected small regions", () => {
|
|
|
|
|
// Create a map with two disconnected regions
|
|
|
|
|
// Region A: 3x3 (Total 9)
|
|
|
|
|
// Region B: 1x1 (Total 1)
|
|
|
|
|
let map = [];
|
|
|
|
|
for (let x = 0; x < 20; x++) {
|
|
|
|
|
map[x] = [];
|
|
|
|
|
for (let z = 0; z < 20; z++) {
|
|
|
|
|
map[x][z] = 1; // Walls everywhere
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Region A (Large)
|
|
|
|
|
for (let x = 1; x <= 3; x++) {
|
|
|
|
|
for (let z = 1; z <= 3; z++) {
|
|
|
|
|
map[x][z] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Region B (Small, disconnected)
|
|
|
|
|
map[10][10] = 0;
|
|
|
|
|
|
|
|
|
|
const connectedMap = generator.ensureConnectivity(map);
|
|
|
|
|
|
|
|
|
|
// Region A should remain
|
|
|
|
|
expect(connectedMap[2][2]).to.equal(0);
|
|
|
|
|
|
|
|
|
|
// Region B should be filled
|
|
|
|
|
expect(connectedMap[10][10]).to.equal(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 10: scatterWallDetails should place objects on wall faces", () => {
|
|
|
|
|
// Setup a single wall column at 5,5 from y=0 to y=10
|
|
|
|
|
// And air at 6,5 (adjacent)
|
|
|
|
|
for (let y = 0; y < grid.size.y; y++) {
|
|
|
|
|
grid.setCell(5, y, 5, 100);
|
|
|
|
|
grid.setCell(6, y, 5, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Force seed/density to ensure placement
|
|
|
|
|
// Or just check that it CAN place
|
|
|
|
|
// Let's rely on randomness with a loop or high density
|
|
|
|
|
generator.scatterWallDetails(11, 1.0, 2); // 100% density for test
|
|
|
|
|
|
|
|
|
|
// Check y=2 to y=height-2 (safe range)
|
|
|
|
|
let foundDetail = false;
|
|
|
|
|
for (let y = 2; y < grid.size.y - 1; y++) {
|
|
|
|
|
const id = grid.getCell(6, y, 5);
|
|
|
|
|
if (id === 11) foundDetail = true;
|
|
|
|
|
}
|
|
|
|
|
expect(foundDetail).to.be.true;
|
|
|
|
|
|
|
|
|
|
// Ensure it didn't place below minHeight (y=1)
|
|
|
|
|
expect(grid.getCell(6, 1, 5)).to.equal(0);
|
|
|
|
|
});
|
|
|
|
|
});
|