278 lines
6.2 KiB
JavaScript
278 lines
6.2 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|