import sinon from "sinon"; import { GameLoop } from "../../../src/core/GameLoop.js"; /** * Mock Data for Assets */ const MOCK_MANIFEST = { classes: ["classes/vanguard.json"], units: ["enemies/default.json"], }; const MOCK_VANGUARD = { id: "CLASS_VANGUARD", name: "Vanguard", base_stats: { health: 100, maxHealth: 100, ap: 3, maxAp: 3, movement: 5, defense: 10, resistance: 5, speed: 10, accuracy: 85, evasion: 5, critRate: 5, critDamage: 1.5, }, growth_rates: { health: 1.0, ap: 0, movement: 0, defense: 0.5, resistance: 0.3, speed: 0.4, accuracy: 0.4, evasion: 0.2, critRate: 0.2, }, equipment: { mainHand: "SWORD_BASIC", armor: "ARMOR_BASIC", }, skills: ["STRIKE", "GUARD"], }; const MOCK_ENEMY = { id: "ENEMY_DEFAULT", name: "Drone", baseStats: { health: 50, maxHealth: 50, ap: 3, maxAp: 3, movement: 4, defense: 2, resistance: 0, speed: 8, accuracy: 80, evasion: 10, critRate: 5, critDamage: 1.5, }, equipment: { mainHand: "BEAM_RIFLE", }, }; // Keep track of the stub to restore it let fetchStub = null; /** * Mocks window.fetch to return game assets. */ export function mockFetchAssets() { if (fetchStub) return; // Already mocked fetchStub = sinon.stub(window, "fetch"); fetchStub.callsFake(async (url) => { if (url.includes("manifest.json")) { return { ok: true, json: async () => MOCK_MANIFEST, }; } if (url.includes("classes/vanguard.json")) { return { ok: true, json: async () => MOCK_VANGUARD, }; } if (url.includes("enemies/default.json")) { return { ok: true, json: async () => MOCK_ENEMY, }; } // Return empty/default for others to prevent crashes return { ok: false, status: 404, json: async () => ({}), text: async () => "Not Found", }; }); } /** * Restores window.fetch. */ export function restoreFetchAssets() { if (fetchStub) { fetchStub.restore(); fetchStub = null; } } /** * Creates a basic GameLoop setup for tests. * @returns {{ gameLoop: GameLoop; container: HTMLElement }} */ export function createGameLoopSetup() { mockFetchAssets(); // Auto-mock assets const container = document.createElement("div"); document.body.appendChild(container); const gameLoop = new GameLoop(); return { gameLoop, container }; } /** * Cleans up GameLoop after tests. * @param {GameLoop} gameLoop * @param {HTMLElement} container */ export function cleanupGameLoop(gameLoop, container) { 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(); } restoreFetchAssets(); // Restore fetch } /** * Creates a mock game state manager for deployment phase. * @returns {Object} */ export function createMockGameStateManagerForDeployment() { return { currentState: "STATE_DEPLOYMENT", transitionTo: sinon.stub(), setCombatState: sinon.stub(), getCombatState: sinon.stub().returns(null), rosterManager: { roster: [], }, }; } /** * Creates a mock game state manager for combat phase. * @returns {Object} */ export function createMockGameStateManagerForCombat() { return { currentState: "STATE_COMBAT", transitionTo: sinon.stub(), setCombatState: sinon.stub(), getCombatState: sinon.stub(), rosterManager: { roster: [], }, }; } /** * Creates a mock mission manager with enemy spawns. * @param {Array} enemySpawns * @returns {Object} */ export function createMockMissionManager(enemySpawns = []) { const mockMissionDef = { id: "MISSION_TEST", config: { title: "Test Mission" }, enemy_spawns: enemySpawns, objectives: { primary: [] }, }; return { getActiveMission: sinon.stub().returns(mockMissionDef), setGridContext: sinon.stub(), populateZoneCoordinates: sinon.stub(), resolveMissionObjectPositions: sinon.stub().returns([]), }; } /** * Creates basic run data for tests. * @param {Object} overrides * @returns {Object} */ export function createRunData(overrides = {}) { return { seed: 12345, depth: 1, squad: [], ...overrides, }; } /** * Sets up combat test units. * @param {GameLoop} gameLoop * @returns {{ playerUnit: Object; enemyUnit: Object }} */ export function setupCombatUnits(gameLoop) { const playerUnit = gameLoop.unitManager.createUnit( "CLASS_VANGUARD", "PLAYER" ); playerUnit.baseStats.movement = 4; playerUnit.baseStats.speed = 10; playerUnit.currentAP = 10; playerUnit.chargeMeter = 100; playerUnit.position = { x: 5, y: 1, z: 5 }; gameLoop.grid.placeUnit(playerUnit, playerUnit.position); gameLoop.createUnitMesh(playerUnit, playerUnit.position); const enemyUnit = gameLoop.unitManager.createUnit("ENEMY_DEFAULT", "ENEMY"); enemyUnit.baseStats.speed = 8; enemyUnit.chargeMeter = 80; enemyUnit.position = { x: 15, y: 1, z: 15 }; gameLoop.grid.placeUnit(enemyUnit, enemyUnit.position); gameLoop.createUnitMesh(enemyUnit, enemyUnit.position); return { playerUnit, enemyUnit }; } /** * Cleans up turn system state. * @param {GameLoop} gameLoop */ export function cleanupTurnSystem(gameLoop) { if (gameLoop.turnSystem) { try { // First, try to end combat immediately to stop any ongoing turn advancement if ( gameLoop.turnSystem.phase !== "INIT" && gameLoop.turnSystem.phase !== "COMBAT_END" ) { // End combat first to stop any loops gameLoop.turnSystem.endCombat(); } // Then reset the turn system if (typeof gameLoop.turnSystem.reset === "function") { gameLoop.turnSystem.reset(); } else { // Fallback: manually reset state gameLoop.turnSystem.globalTick = 0; gameLoop.turnSystem.activeUnitId = null; gameLoop.turnSystem.phase = "INIT"; gameLoop.turnSystem.round = 1; gameLoop.turnSystem.turnQueue = []; } } catch (e) { // Ignore errors during cleanup console.warn("Error during turn system cleanup:", e); } } }