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); }); });