aether-shards/test/core/GameLoop.test.js

147 lines
4.6 KiB
JavaScript

import { expect } from "@esm-bundle/chai";
import sinon from "sinon";
import * as THREE from "three";
import { GameLoop } from "../../src/core/GameLoop.js";
describe("Core: GameLoop (Integration)", function () {
// Increase timeout for WebGL/Shader compilation overhead
this.timeout(30000);
let gameLoop;
let container;
beforeEach(() => {
// Create a mounting point
container = document.createElement("div");
document.body.appendChild(container);
gameLoop = new GameLoop();
});
afterEach(() => {
gameLoop.stop();
if (container.parentNode) {
container.parentNode.removeChild(container);
}
// Cleanup Three.js resources if possible to avoid context loss limits
if (gameLoop.renderer) {
gameLoop.renderer.dispose();
gameLoop.renderer.forceContextLoss();
}
});
it("CoA 1: init() should setup Three.js scene, camera, and renderer", () => {
gameLoop.init(container);
expect(gameLoop.scene).to.be.instanceOf(THREE.Scene);
expect(gameLoop.camera).to.be.instanceOf(THREE.PerspectiveCamera);
expect(gameLoop.renderer).to.be.instanceOf(THREE.WebGLRenderer);
// Verify renderer is attached to DOM
expect(container.querySelector("canvas")).to.exist;
});
it("CoA 2: startLevel() should initialize grid, visuals, and generate world", async () => {
gameLoop.init(container);
const runData = {
seed: 12345,
depth: 1,
squad: [],
};
await gameLoop.startLevel(runData);
// Grid should be populated
expect(gameLoop.grid).to.exist;
// Check center of map (likely not empty for RuinGen) or at least check valid bounds
expect(gameLoop.grid.size.x).to.equal(20);
// VoxelManager should be initialized
expect(gameLoop.voxelManager).to.exist;
// Should have visual meshes
expect(gameLoop.scene.children.length).to.be.greaterThan(0);
});
it("CoA 3: Deployment Phase should separate zones and allow manual placement", async () => {
gameLoop.init(container);
const runData = {
seed: 12345, // Deterministic seed
depth: 1,
squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
};
// startLevel should now prepare the map but NOT spawn units immediately
await gameLoop.startLevel(runData);
// 1. Verify Spawn Zones Generated
// The generator/loop should identify valid tiles for player start and enemy start
expect(gameLoop.playerSpawnZone).to.be.an("array").that.is.not.empty;
expect(gameLoop.enemySpawnZone).to.be.an("array").that.is.not.empty;
// 2. Verify Zone Separation
// Create copies to ensure we don't test against mutated arrays later
const pZone = [...gameLoop.playerSpawnZone];
const eZone = [...gameLoop.enemySpawnZone];
const overlap = pZone.some((pTile) =>
eZone.some((eTile) => eTile.x === pTile.x && eTile.z === pTile.z)
);
expect(overlap).to.be.false;
// 3. Test Manual Deployment (User Selection)
const unitDef = runData.squad[0];
const validTile = pZone[0]; // Pick first valid tile from player zone
// Expect a method to manually place a unit from the roster onto a specific tile
const unit = gameLoop.deployUnit(unitDef, validTile);
expect(unit).to.exist;
expect(unit.position.x).to.equal(validTile.x);
expect(unit.position.z).to.equal(validTile.z);
// Verify visual mesh created
const mesh = gameLoop.unitMeshes.get(unit.id);
expect(mesh).to.exist;
expect(mesh.position.x).to.equal(validTile.x);
// 4. Test Enemy Spawning (Finalize Deployment)
// This triggers the actual start of combat/AI
gameLoop.finalizeDeployment();
const enemies = gameLoop.unitManager.getUnitsByTeam("ENEMY");
expect(enemies.length).to.be.greaterThan(0);
// Verify enemies are in their zone
// Note: finalizeDeployment removes used spots from gameLoop.enemySpawnZone,
// so we check against our copy `eZone`.
const enemyPos = enemies[0].position;
const isInZone = eZone.some(
(t) => t.x === enemyPos.x && t.z === enemyPos.z
);
expect(
isInZone,
`Enemy spawned at ${enemyPos.x},${enemyPos.z} which is not in enemy zone`
).to.be.true;
});
it("CoA 4: stop() should halt animation loop", (done) => {
gameLoop.init(container);
gameLoop.isRunning = true;
// Spy on animate
const spy = sinon.spy(gameLoop, "animate");
gameLoop.stop();
// Wait a short duration to ensure loop doesn't fire
// Using setTimeout instead of requestAnimationFrame for reliability in headless env
setTimeout(() => {
expect(gameLoop.isRunning).to.be.false;
done();
}, 50);
});
});