import { expect } from "@esm-bundle/chai"; import sinon from "sinon"; import { GameLoop } from "../../../src/core/GameLoop.js"; import { createGameLoopSetup, cleanupGameLoop, createRunData, createMockGameStateManagerForDeployment, createMockMissionManager, } from "./helpers.js"; describe("Core: GameLoop - Deployment", function () { this.timeout(30000); let gameLoop; let container; beforeEach(() => { const setup = createGameLoopSetup(); gameLoop = setup.gameLoop; container = setup.container; gameLoop.init(container); }); afterEach(() => { cleanupGameLoop(gameLoop, container); }); it("CoA 3: Deployment Phase should separate zones and allow manual placement", async () => { const runData = createRunData({ squad: [{ id: "u1", classId: "CLASS_VANGUARD" }], }); // Mock gameStateManager for deployment phase const mockGameStateManager = createMockGameStateManagerForDeployment(); if (!mockGameStateManager.rosterManager) { mockGameStateManager.rosterManager = { roster: [] }; } gameLoop.gameStateManager = mockGameStateManager; // Mock MissionManager with no enemy_spawns (will use default) const mockMissionManager = createMockMissionManager([]); mockMissionManager.setUnitManager = sinon.stub(); mockMissionManager.setTurnSystem = sinon.stub(); mockMissionManager.setupActiveMission = sinon.stub(); gameLoop.missionManager = mockMissionManager; // startLevel should now prepare the map but NOT spawn units immediately await gameLoop.startLevel(runData, { startAnimation: false }); // 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 await 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 5: finalizeDeployment should spawn enemies from mission enemy_spawns", async () => { const runData = createRunData({ squad: [{ id: "u1", classId: "CLASS_VANGUARD" }], }); // Mock gameStateManager for deployment phase const mockGameStateManager = createMockGameStateManagerForDeployment(); if (!mockGameStateManager.rosterManager) { mockGameStateManager.rosterManager = { roster: [] }; } gameLoop.gameStateManager = mockGameStateManager; // Mock MissionManager with enemy_spawns and required methods // Use ENEMY_DEFAULT which exists in the test environment const mockMissionManager = createMockMissionManager([ { enemy_def_id: "ENEMY_DEFAULT", count: 2 }, ]); mockMissionManager.setUnitManager = sinon.stub(); mockMissionManager.setTurnSystem = sinon.stub(); mockMissionManager.setupActiveMission = sinon.stub(); gameLoop.missionManager = mockMissionManager; await gameLoop.startLevel(runData, { startAnimation: false }); // Copy enemy spawn zone before finalizeDeployment modifies it const eZone = [...gameLoop.enemySpawnZone]; // Finalize deployment should spawn enemies from mission definition await gameLoop.finalizeDeployment(); const enemies = gameLoop.unitManager.getUnitsByTeam("ENEMY"); // Should have spawned 2 enemies (or as many as possible given spawn zone size) expect(enemies.length).to.be.greaterThan(0); expect(enemies.length).to.be.at.most(2); // Verify enemies are in their zone enemies.forEach((enemy) => { const enemyPos = enemy.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 6: finalizeDeployment should fall back to default if no enemy_spawns", async () => { const runData = createRunData({ squad: [{ id: "u1", classId: "CLASS_VANGUARD" }], }); // Mock gameStateManager for deployment phase const mockGameStateManager = createMockGameStateManagerForDeployment(); if (!mockGameStateManager.rosterManager) { mockGameStateManager.rosterManager = { roster: [] }; } gameLoop.gameStateManager = mockGameStateManager; // Mock MissionManager with no enemy_spawns and required methods const mockMissionManager = createMockMissionManager([]); mockMissionManager.setUnitManager = sinon.stub(); mockMissionManager.setTurnSystem = sinon.stub(); mockMissionManager.setupActiveMission = sinon.stub(); gameLoop.missionManager = mockMissionManager; await gameLoop.startLevel(runData, { startAnimation: false }); // Finalize deployment should fall back to default behavior const consoleWarnSpy = sinon.spy(console, "warn"); await gameLoop.finalizeDeployment(); // Should have warned about missing enemy_spawns expect(consoleWarnSpy.calledWith(sinon.match(/No enemy_spawns defined/))).to .be.true; const enemies = gameLoop.unitManager.getUnitsByTeam("ENEMY"); // Should still spawn at least one enemy (default behavior) expect(enemies.length).to.be.greaterThan(0); consoleWarnSpy.restore(); }); it("CoA 7: deployUnit should restore Explorer progression from roster", async () => { const runData = createRunData({ squad: [{ id: "u1", classId: "CLASS_VANGUARD", name: "Test Explorer" }], }); // Mock gameStateManager with roster containing progression data const mockGameStateManager = createMockGameStateManagerForDeployment(); // Ensure rosterManager exists if (!mockGameStateManager.rosterManager) { mockGameStateManager.rosterManager = { roster: [] }; } mockGameStateManager.rosterManager.roster = [ { id: "u1", classId: "CLASS_VANGUARD", name: "Test Explorer", status: "READY", activeClassId: "CLASS_VANGUARD", classMastery: { CLASS_VANGUARD: { level: 5, xp: 250, skillPoints: 3, unlockedNodes: ["ROOT", "NODE_1"], }, }, }, ]; gameLoop.gameStateManager = mockGameStateManager; await gameLoop.startLevel(runData, { startAnimation: false }); const unitDef = runData.squad[0]; const validTile = gameLoop.playerSpawnZone[0]; const unit = gameLoop.deployUnit(unitDef, validTile); expect(unit).to.exist; expect(unit.type).to.equal("EXPLORER"); expect(unit.rosterId).to.equal("u1"); // Verify progression was restored expect(unit.classMastery).to.exist; expect(unit.classMastery.CLASS_VANGUARD).to.exist; expect(unit.classMastery.CLASS_VANGUARD.level).to.equal(5); expect(unit.classMastery.CLASS_VANGUARD.xp).to.equal(250); expect(unit.classMastery.CLASS_VANGUARD.skillPoints).to.equal(3); expect(unit.classMastery.CLASS_VANGUARD.unlockedNodes).to.include("ROOT"); expect(unit.classMastery.CLASS_VANGUARD.unlockedNodes).to.include("NODE_1"); expect(unit.activeClassId).to.equal("CLASS_VANGUARD"); // Verify stats were recalculated based on level // Level 5 means 4 level-ups, so health should be higher than base expect(unit.baseStats.health).to.be.greaterThan(100); // Base is 100 }); it("CoA 8: finalizeDeployment should spawn mission objects with placement_strategy", async () => { const runData = createRunData({ squad: [{ id: "u1", classId: "CLASS_VANGUARD" }], }); // Mock gameStateManager for deployment phase const mockGameStateManager = createMockGameStateManagerForDeployment(); if (!mockGameStateManager.rosterManager) { mockGameStateManager.rosterManager = { roster: [] }; } gameLoop.gameStateManager = mockGameStateManager; // Mock MissionManager with mission_objects const mockMissionManager = createMockMissionManager([]); mockMissionManager.getActiveMission = sinon.stub().resolves({ enemy_spawns: [], mission_objects: [ { object_id: "OBJ_SIGNAL_RELAY", placement_strategy: "center_of_enemy_room", }, ], }); mockMissionManager.setUnitManager = sinon.stub(); mockMissionManager.setTurnSystem = sinon.stub(); mockMissionManager.setupActiveMission = sinon.stub(); gameLoop.missionManager = mockMissionManager; await gameLoop.startLevel(runData, { startAnimation: false }); // Finalize deployment should spawn mission objects await gameLoop.finalizeDeployment(); // Verify mission object was spawned expect(gameLoop.missionObjects.has("OBJ_SIGNAL_RELAY")).to.be.true; const objPos = gameLoop.missionObjects.get("OBJ_SIGNAL_RELAY"); expect(objPos).to.exist; expect(objPos).to.have.property("x"); expect(objPos).to.have.property("y"); expect(objPos).to.have.property("z"); // Verify visual mesh was created const mesh = gameLoop.missionObjectMeshes.get("OBJ_SIGNAL_RELAY"); expect(mesh).to.exist; expect(mesh.position.x).to.equal(objPos.x); expect(mesh.position.z).to.equal(objPos.z); }); it("CoA 9: finalizeDeployment should spawn mission objects with explicit position", async () => { const runData = createRunData({ squad: [{ id: "u1", classId: "CLASS_VANGUARD" }], }); // Mock gameStateManager for deployment phase const mockGameStateManager = createMockGameStateManagerForDeployment(); if (!mockGameStateManager.rosterManager) { mockGameStateManager.rosterManager = { roster: [] }; } gameLoop.gameStateManager = mockGameStateManager; await gameLoop.startLevel(runData, { startAnimation: false }); // Find a valid walkable position const validTile = gameLoop.enemySpawnZone[0]; const walkableY = gameLoop.movementSystem.findWalkableY( validTile.x, validTile.z, validTile.y ); // Mock MissionManager with mission_objects using explicit position const mockMissionManager = createMockMissionManager([]); mockMissionManager.getActiveMission = sinon.stub().resolves({ enemy_spawns: [], mission_objects: [ { object_id: "OBJ_DATA_TERMINAL", position: { x: validTile.x, y: walkableY, z: validTile.z }, }, ], }); mockMissionManager.setUnitManager = sinon.stub(); mockMissionManager.setTurnSystem = sinon.stub(); mockMissionManager.setupActiveMission = sinon.stub(); gameLoop.missionManager = mockMissionManager; // Finalize deployment should spawn mission objects await gameLoop.finalizeDeployment(); // Verify mission object was spawned at the specified position expect(gameLoop.missionObjects.has("OBJ_DATA_TERMINAL")).to.be.true; const objPos = gameLoop.missionObjects.get("OBJ_DATA_TERMINAL"); expect(objPos.x).to.equal(validTile.x); expect(objPos.z).to.equal(validTile.z); }); it("CoA 10: checkMissionObjectInteraction should dispatch INTERACT event when unit moves to object", async () => { const runData = createRunData({ squad: [{ id: "u1", classId: "CLASS_VANGUARD" }], }); // Mock gameStateManager for deployment phase const mockGameStateManager = createMockGameStateManagerForDeployment(); if (!mockGameStateManager.rosterManager) { mockGameStateManager.rosterManager = { roster: [] }; } gameLoop.gameStateManager = mockGameStateManager; await gameLoop.startLevel(runData, { startAnimation: false }); // Use a player spawn zone position so we can deploy a unit there const validTile = gameLoop.playerSpawnZone[0]; const walkableY = gameLoop.movementSystem.findWalkableY( validTile.x, validTile.z, validTile.y ); // Manually add a mission object for testing at the same position const objPos = { x: validTile.x, y: walkableY, z: validTile.z }; gameLoop.missionObjects.set("OBJ_TEST_RELAY", objPos); gameLoop.createMissionObjectMesh("OBJ_TEST_RELAY", objPos); // Create a unit at the object position (in player spawn zone, so deployUnit will work) const unitDef = runData.squad[0]; const unit = gameLoop.deployUnit(unitDef, validTile); expect(unit).to.exist; // Ensure unit was deployed // Mock MissionManager to spy on onGameEvent const mockMissionManager = createMockMissionManager([]); const interactSpy = sinon.spy(); mockMissionManager.onGameEvent = interactSpy; gameLoop.missionManager = mockMissionManager; // Check interaction (simulating movement to object position) gameLoop.checkMissionObjectInteraction(unit); // Verify INTERACT event was dispatched expect(interactSpy.calledOnce).to.be.true; expect(interactSpy.firstCall.args[0]).to.equal("INTERACT"); expect(interactSpy.firstCall.args[1].objectId).to.equal("OBJ_TEST_RELAY"); expect(interactSpy.firstCall.args[1].unitId).to.equal(unit.id); }); it("CoA 11: findObjectPlacement should find valid positions for different strategies", async () => { const runData = createRunData({ squad: [{ id: "u1", classId: "CLASS_VANGUARD" }], }); // Mock gameStateManager for deployment phase const mockGameStateManager = createMockGameStateManagerForDeployment(); if (!mockGameStateManager.rosterManager) { mockGameStateManager.rosterManager = { roster: [] }; } gameLoop.gameStateManager = mockGameStateManager; await gameLoop.startLevel(runData, { startAnimation: false }); // Test center_of_enemy_room strategy const enemyPos = gameLoop.findObjectPlacement("center_of_enemy_room"); expect(enemyPos).to.exist; expect(enemyPos).to.have.property("x"); expect(enemyPos).to.have.property("y"); expect(enemyPos).to.have.property("z"); // Test center_of_player_room strategy const playerPos = gameLoop.findObjectPlacement("center_of_player_room"); expect(playerPos).to.exist; expect(playerPos).to.have.property("x"); expect(playerPos).to.have.property("y"); expect(playerPos).to.have.property("z"); // Test middle_room strategy const middlePos = gameLoop.findObjectPlacement("middle_room"); expect(middlePos).to.exist; expect(middlePos).to.have.property("x"); expect(middlePos).to.have.property("y"); expect(middlePos).to.have.property("z"); // Test random_walkable strategy const randomPos = gameLoop.findObjectPlacement("random_walkable"); expect(randomPos).to.exist; expect(randomPos).to.have.property("x"); expect(randomPos).to.have.property("y"); expect(randomPos).to.have.property("z"); // Test invalid strategy (should return null or fallback) const invalidPos = gameLoop.findObjectPlacement("invalid_strategy"); // Should either return null or fallback to random_walkable if (invalidPos) { expect(invalidPos).to.have.property("x"); expect(invalidPos).to.have.property("y"); expect(invalidPos).to.have.property("z"); } }); });