578 lines
21 KiB
JavaScript
578 lines
21 KiB
JavaScript
import { expect } from "@esm-bundle/chai";
|
|
import sinon from "sinon";
|
|
import * as THREE from "three";
|
|
import { GameLoop } from "../../src/core/GameLoop.js";
|
|
|
|
/**
|
|
* Tests for CombatState.spec.js Conditions of Acceptance
|
|
* This test suite verifies that the implementation matches the specification.
|
|
*/
|
|
describe("Combat State Specification - CoA Tests", function () {
|
|
this.timeout(30000);
|
|
|
|
let gameLoop;
|
|
let container;
|
|
let mockGameStateManager;
|
|
|
|
beforeEach(async () => {
|
|
// Stub fetch explicitly for this test suite
|
|
if (!window.fetch.restore) {
|
|
sinon.stub(window, "fetch");
|
|
}
|
|
|
|
const mockResponse = (body) =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve(body),
|
|
});
|
|
|
|
window.fetch.callsFake((url) => {
|
|
if (typeof url === "string" && url.includes("manifest.json")) {
|
|
return mockResponse({
|
|
classes: ["classes/vanguard.json"],
|
|
units: [],
|
|
});
|
|
}
|
|
if (typeof url === "string" && url.includes("vanguard.json")) {
|
|
return mockResponse({
|
|
id: "CLASS_VANGUARD",
|
|
name: "Vanguard",
|
|
type: "EXPLORER",
|
|
base_stats: {
|
|
health: 120,
|
|
attack: 12,
|
|
defense: 8,
|
|
magic: 0,
|
|
speed: 8,
|
|
willpower: 5,
|
|
movement: 3,
|
|
},
|
|
growth_rates: { health: 10, attack: 1, defense: 1 },
|
|
starting_equipment: ["ITEM_RUSTY_BLADE", "ITEM_SCRAP_PLATE"],
|
|
skillTreeData: { active_skills: [], passive_skills: [] },
|
|
model: "vanguard.glb",
|
|
});
|
|
}
|
|
return Promise.reject(new Error(`Not Found: ${url}`));
|
|
});
|
|
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
|
|
gameLoop = new GameLoop();
|
|
gameLoop.init(container);
|
|
|
|
// Setup mock game state manager with state tracking
|
|
let storedCombatState = null;
|
|
mockGameStateManager = {
|
|
currentState: "STATE_COMBAT",
|
|
transitionTo: sinon.stub(),
|
|
setCombatState: sinon.stub().callsFake((state) => {
|
|
storedCombatState = state;
|
|
}),
|
|
getCombatState: sinon.stub().callsFake(() => {
|
|
return storedCombatState;
|
|
}),
|
|
rosterManager: {
|
|
roster: [],
|
|
},
|
|
};
|
|
gameLoop.gameStateManager = mockGameStateManager;
|
|
|
|
// Initialize a level
|
|
const runData = {
|
|
seed: 12345,
|
|
depth: 1,
|
|
squad: [],
|
|
};
|
|
await gameLoop.startLevel(runData, { startAnimation: false });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (window.fetch.restore) {
|
|
window.fetch.restore();
|
|
}
|
|
gameLoop.stop();
|
|
if (container.parentNode) {
|
|
container.parentNode.removeChild(container);
|
|
}
|
|
if (gameLoop.renderer) {
|
|
gameLoop.renderer.dispose();
|
|
gameLoop.renderer.forceContextLoss();
|
|
}
|
|
});
|
|
|
|
describe("TurnSystem CoA Tests", () => {
|
|
let playerUnit1, playerUnit2, enemyUnit1;
|
|
|
|
beforeEach(() => {
|
|
// Create test units with different speeds
|
|
playerUnit1 = gameLoop.unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
|
|
playerUnit1.baseStats.speed = 15; // Fast
|
|
playerUnit1.position = { x: 5, y: 1, z: 5 };
|
|
gameLoop.grid.placeUnit(playerUnit1, playerUnit1.position);
|
|
|
|
playerUnit2 = gameLoop.unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
|
|
playerUnit2.baseStats.speed = 8; // Slow
|
|
playerUnit2.position = { x: 6, y: 1, z: 5 };
|
|
gameLoop.grid.placeUnit(playerUnit2, playerUnit2.position);
|
|
|
|
enemyUnit1 = gameLoop.unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
|
|
enemyUnit1.baseStats.speed = 12; // Medium
|
|
enemyUnit1.position = { x: 10, y: 1, z: 10 };
|
|
gameLoop.grid.placeUnit(enemyUnit1, enemyUnit1.position);
|
|
});
|
|
|
|
it("CoA 1: Initiative Roll - Units should be sorted by Speed (Highest First) on combat start", () => {
|
|
// Initialize combat
|
|
gameLoop.initializeCombatUnits();
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
const combatState = mockGameStateManager.getCombatState();
|
|
expect(combatState).to.exist;
|
|
expect(combatState.turnQueue).to.be.an("array");
|
|
|
|
// Check that turn queue is sorted by initiative (which should correlate with speed)
|
|
// Note: Current implementation uses chargeMeter, not direct speed sorting
|
|
// This test documents the current behavior vs spec
|
|
// turnQueue is string[] per spec, so we check that it exists and has entries
|
|
const queue = combatState.turnQueue;
|
|
expect(queue.length).to.be.greaterThan(0);
|
|
// Verify all entries are strings (unit IDs)
|
|
queue.forEach((unitId) => {
|
|
expect(unitId).to.be.a("string");
|
|
});
|
|
});
|
|
|
|
it("CoA 2: Turn Start Hygiene - AP should reset to baseAP when turn begins", () => {
|
|
// Set up a unit with low AP
|
|
playerUnit1.currentAP = 3;
|
|
playerUnit1.chargeMeter = 100; // Ready to act
|
|
|
|
// Initialize combat
|
|
gameLoop.initializeCombatUnits();
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
// When a unit's turn starts (they're at 100 charge), they should have full AP
|
|
// AP is calculated as: 3 + floor(speed/5)
|
|
// With speed 15: 3 + floor(15/5) = 3 + 3 = 6
|
|
const expectedAP = 3 + Math.floor(playerUnit1.baseStats.speed / 5);
|
|
expect(playerUnit1.currentAP).to.equal(expectedAP);
|
|
});
|
|
|
|
it("CoA 2: Turn Start Hygiene - Status effects should tick (placeholder test)", () => {
|
|
// TODO: Implement status effect ticking
|
|
// This is a placeholder to document the requirement
|
|
playerUnit1.statusEffects = [{ id: "poison", duration: 3, damage: 5 }];
|
|
|
|
// When turn starts, status effects should tick
|
|
// Currently not implemented - this test documents the gap
|
|
expect(playerUnit1.statusEffects.length).to.be.greaterThan(0);
|
|
});
|
|
|
|
it("CoA 2: Turn Start Hygiene - Cooldowns should decrement (placeholder test)", () => {
|
|
// TODO: Implement cooldown decrementing
|
|
// This is a placeholder to document the requirement
|
|
// Currently not implemented - this test documents the gap
|
|
expect(true).to.be.true; // Placeholder
|
|
});
|
|
|
|
it("CoA 3: Cycling - endTurn() should move to next unit in queue", () => {
|
|
gameLoop.initializeCombatUnits();
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
const initialCombatState = mockGameStateManager.getCombatState();
|
|
const initialActiveUnitId = initialCombatState.activeUnitId;
|
|
|
|
// End turn
|
|
gameLoop.endTurn();
|
|
|
|
// Verify updateCombatState was called (which recalculates queue)
|
|
expect(mockGameStateManager.setCombatState.called).to.be.true;
|
|
|
|
// Get the new combat state
|
|
const newCombatState = mockGameStateManager.getCombatState();
|
|
expect(newCombatState).to.exist;
|
|
// The active unit should have changed (unless it's the same unit's turn again)
|
|
// At minimum, the state should be updated
|
|
expect(newCombatState.activeUnitId).to.exist;
|
|
});
|
|
|
|
it("CoA 3: Cycling - Should increment round when queue is empty", () => {
|
|
// This test documents that round tracking should be implemented
|
|
// Currently roundNumber exists but doesn't increment
|
|
gameLoop.initializeCombatUnits();
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
const combatState = mockGameStateManager.getCombatState();
|
|
expect(combatState).to.exist;
|
|
// Check both round (spec) and roundNumber (UI alias)
|
|
expect(combatState.round).to.exist;
|
|
expect(combatState.roundNumber).to.exist;
|
|
// TODO: Verify round increments when queue cycles
|
|
});
|
|
});
|
|
|
|
describe("MovementSystem CoA Tests", () => {
|
|
let playerUnit;
|
|
|
|
beforeEach(() => {
|
|
playerUnit = gameLoop.unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
|
|
playerUnit.baseStats.movement = 4;
|
|
playerUnit.currentAP = 10;
|
|
playerUnit.position = { x: 5, y: 1, z: 5 };
|
|
gameLoop.grid.placeUnit(playerUnit, playerUnit.position);
|
|
gameLoop.createUnitMesh(playerUnit, playerUnit.position);
|
|
});
|
|
|
|
it("CoA 1: Validation - Move should fail if tile is blocked/occupied", async () => {
|
|
// Start combat with the player unit
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
// Ensure player unit is active
|
|
const activeUnit = gameLoop.turnSystem.getActiveUnit();
|
|
if (activeUnit && activeUnit.team !== "PLAYER") {
|
|
// If enemy is active, end turn until player is active
|
|
while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
|
|
gameLoop.endTurn();
|
|
}
|
|
}
|
|
|
|
// Place another unit on target tile
|
|
const enemyUnit = gameLoop.unitManager.createUnit(
|
|
"ENEMY_DEFAULT",
|
|
"ENEMY"
|
|
);
|
|
const occupiedPos = { x: 6, y: 1, z: 5 };
|
|
gameLoop.grid.placeUnit(enemyUnit, occupiedPos);
|
|
|
|
// Stub getCursorPosition without replacing the entire inputManager
|
|
const stub1 = sinon
|
|
.stub(gameLoop.inputManager, "getCursorPosition")
|
|
.returns(occupiedPos);
|
|
|
|
const initialPos = { ...playerUnit.position };
|
|
await gameLoop.handleCombatMovement(occupiedPos);
|
|
|
|
// Unit should not have moved
|
|
expect(playerUnit.position.x).to.equal(initialPos.x);
|
|
expect(playerUnit.position.z).to.equal(initialPos.z);
|
|
|
|
// Restore stub
|
|
stub1.restore();
|
|
});
|
|
|
|
it("CoA 1: Validation - Move should fail if no path exists", async () => {
|
|
// Start combat with the player unit
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
// Ensure player unit is active
|
|
const activeUnit = gameLoop.turnSystem.getActiveUnit();
|
|
if (activeUnit && activeUnit.team !== "PLAYER") {
|
|
// If enemy is active, end turn until player is active
|
|
while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
|
|
gameLoop.endTurn();
|
|
}
|
|
}
|
|
|
|
// Try to move to an unreachable position (far away)
|
|
const unreachablePos = { x: 20, y: 1, z: 20 };
|
|
|
|
// Stub getCursorPosition without replacing the entire inputManager
|
|
const stub2 = sinon
|
|
.stub(gameLoop.inputManager, "getCursorPosition")
|
|
.returns(unreachablePos);
|
|
|
|
const initialPos = { ...playerUnit.position };
|
|
await gameLoop.handleCombatMovement(unreachablePos);
|
|
|
|
// Unit should not have moved
|
|
expect(playerUnit.position.x).to.equal(initialPos.x);
|
|
|
|
// Restore stub
|
|
stub2.restore();
|
|
});
|
|
|
|
it("CoA 1: Validation - Move should fail if insufficient AP", async () => {
|
|
// Start combat with the player unit
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
// Ensure player unit is active
|
|
const activeUnit = gameLoop.turnSystem.getActiveUnit();
|
|
if (activeUnit && activeUnit.team !== "PLAYER") {
|
|
// If enemy is active, end turn until player is active
|
|
while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
|
|
gameLoop.endTurn();
|
|
}
|
|
}
|
|
|
|
playerUnit.currentAP = 0; // No AP
|
|
|
|
const targetPos = { x: 6, y: 1, z: 5 };
|
|
|
|
// Stub getCursorPosition without replacing the entire inputManager
|
|
const stub3 = sinon
|
|
.stub(gameLoop.inputManager, "getCursorPosition")
|
|
.returns(targetPos);
|
|
|
|
const initialPos = { ...playerUnit.position };
|
|
await gameLoop.handleCombatMovement(targetPos);
|
|
|
|
// Unit should not have moved
|
|
expect(playerUnit.position.x).to.equal(initialPos.x);
|
|
|
|
// Restore stub
|
|
stub3.restore();
|
|
});
|
|
|
|
it("CoA 2: Execution - Successful move should update Unit position", async () => {
|
|
// Start combat with the player unit
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
// Ensure player unit is active
|
|
const activeUnit = gameLoop.turnSystem.getActiveUnit();
|
|
if (activeUnit && activeUnit.team !== "PLAYER") {
|
|
// If enemy is active, end turn until player is active
|
|
while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
|
|
gameLoop.endTurn();
|
|
}
|
|
}
|
|
|
|
const targetPos = { x: 6, y: 1, z: 5 };
|
|
|
|
// Stub getCursorPosition without replacing the entire inputManager
|
|
sinon.stub(gameLoop.inputManager, "getCursorPosition").returns(targetPos);
|
|
|
|
await gameLoop.handleCombatMovement(targetPos);
|
|
|
|
// Restore stub
|
|
gameLoop.inputManager.getCursorPosition.restore();
|
|
|
|
// Unit position should be updated
|
|
expect(playerUnit.position.x).to.equal(targetPos.x);
|
|
expect(playerUnit.position.z).to.equal(targetPos.z);
|
|
});
|
|
|
|
it("CoA 2: Execution - Successful move should update VoxelGrid occupancy", async () => {
|
|
// Start combat with the player unit
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
// Ensure player unit is active
|
|
const activeUnit = gameLoop.turnSystem.getActiveUnit();
|
|
if (activeUnit && activeUnit.team !== "PLAYER") {
|
|
// If enemy is active, end turn until player is active
|
|
while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
|
|
gameLoop.endTurn();
|
|
}
|
|
}
|
|
|
|
const targetPos = { x: 6, y: 1, z: 5 };
|
|
const initialPos = { ...playerUnit.position };
|
|
|
|
// Stub getCursorPosition without replacing the entire inputManager
|
|
sinon.stub(gameLoop.inputManager, "getCursorPosition").returns(targetPos);
|
|
|
|
// Old position should have unit
|
|
expect(gameLoop.grid.getUnitAt(initialPos)).to.equal(playerUnit);
|
|
|
|
await gameLoop.handleCombatMovement(targetPos);
|
|
|
|
// Restore stub
|
|
gameLoop.inputManager.getCursorPosition.restore();
|
|
|
|
// New position should have unit
|
|
expect(gameLoop.grid.getUnitAt(targetPos)).to.equal(playerUnit);
|
|
// Old position should be empty
|
|
expect(gameLoop.grid.getUnitAt(initialPos)).to.be.undefined;
|
|
});
|
|
|
|
it("CoA 2: Execution - Successful move should deduct correct AP cost", async () => {
|
|
// Start combat with the player unit
|
|
const allUnits = gameLoop.unitManager.getAllUnits();
|
|
gameLoop.turnSystem.startCombat(allUnits);
|
|
gameLoop.updateCombatState();
|
|
|
|
// Ensure player unit is active
|
|
const activeUnit = gameLoop.turnSystem.getActiveUnit();
|
|
if (activeUnit && activeUnit.team !== "PLAYER") {
|
|
// If enemy is active, end turn until player is active
|
|
while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
|
|
gameLoop.endTurn();
|
|
}
|
|
}
|
|
|
|
const targetPos = { x: 6, y: 1, z: 5 };
|
|
const initialAP = playerUnit.currentAP;
|
|
|
|
// Stub getCursorPosition without replacing the entire inputManager
|
|
sinon.stub(gameLoop.inputManager, "getCursorPosition").returns(targetPos);
|
|
|
|
await gameLoop.handleCombatMovement(targetPos);
|
|
|
|
// Restore stub
|
|
gameLoop.inputManager.getCursorPosition.restore();
|
|
|
|
// AP should be deducted (at least 1 for adjacent move)
|
|
expect(playerUnit.currentAP).to.be.lessThan(initialAP);
|
|
expect(playerUnit.currentAP).to.equal(initialAP - 1); // 1 tile = 1 AP
|
|
});
|
|
|
|
it("CoA 3: Path Snapping - Should move to furthest reachable tile (optional QoL)", () => {
|
|
// This is an optional feature - document that it's not implemented
|
|
// If user clicks far away but only has AP for 2 tiles, should move 2 tiles
|
|
playerUnit.currentAP = 2; // Only enough for 2 tiles
|
|
const farTargetPos = { x: 10, y: 1, z: 5 }; // 5 tiles away
|
|
|
|
// Currently not implemented - this test documents the gap
|
|
// The current implementation just fails if unreachable
|
|
expect(true).to.be.true; // Placeholder
|
|
});
|
|
});
|
|
|
|
describe("CombatState Interface Compliance", () => {
|
|
let playerUnit;
|
|
|
|
beforeEach(() => {
|
|
// Create a unit so combat state can be generated
|
|
playerUnit = gameLoop.unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
|
|
playerUnit.position = { x: 5, y: 1, z: 5 };
|
|
gameLoop.grid.placeUnit(playerUnit, playerUnit.position);
|
|
});
|
|
|
|
it("Should track combat phase (now implemented per spec)", async () => {
|
|
// Spec defines: phase: CombatPhase
|
|
// Now implemented in TurnSystem
|
|
const runData = {
|
|
seed: 12345,
|
|
depth: 1,
|
|
squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
|
|
};
|
|
await gameLoop.startLevel(runData, { startAnimation: false });
|
|
// Set state to deployment so finalizeDeployment works
|
|
mockGameStateManager.currentState = "STATE_DEPLOYMENT";
|
|
const playerUnit = gameLoop.deployUnit(
|
|
runData.squad[0],
|
|
gameLoop.playerSpawnZone[0]
|
|
);
|
|
await gameLoop.finalizeDeployment();
|
|
// Wait a bit for updateCombatState to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
const combatState = mockGameStateManager.getCombatState();
|
|
expect(combatState).to.exist;
|
|
// Now has phase property per spec
|
|
expect(combatState.phase).to.be.oneOf([
|
|
"INIT",
|
|
"TURN_START",
|
|
"WAITING_FOR_INPUT",
|
|
"EXECUTING_ACTION",
|
|
"TURN_END",
|
|
"COMBAT_END",
|
|
]);
|
|
});
|
|
|
|
it("Should track isActive flag (now implemented per spec)", async () => {
|
|
// Spec defines isActive: boolean
|
|
// Now implemented in TurnSystem
|
|
const runData = {
|
|
seed: 12345,
|
|
depth: 1,
|
|
squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
|
|
};
|
|
await gameLoop.startLevel(runData, { startAnimation: false });
|
|
// Set state to deployment so finalizeDeployment works
|
|
mockGameStateManager.currentState = "STATE_DEPLOYMENT";
|
|
const playerUnit = gameLoop.deployUnit(
|
|
runData.squad[0],
|
|
gameLoop.playerSpawnZone[0]
|
|
);
|
|
await gameLoop.finalizeDeployment();
|
|
// Wait a bit for updateCombatState to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
const combatState = mockGameStateManager.getCombatState();
|
|
expect(combatState).to.exist;
|
|
// Now has isActive property per spec
|
|
expect(combatState.isActive).to.be.a("boolean");
|
|
expect(combatState.isActive).to.be.true; // Combat should be active
|
|
});
|
|
|
|
it("Should use activeUnitId string (now implemented per spec, also has activeUnit for UI)", async () => {
|
|
// Spec defines: activeUnitId: string | null
|
|
// Implementation now has both: activeUnitId (spec) and activeUnit (UI compatibility)
|
|
const runData = {
|
|
seed: 12345,
|
|
depth: 1,
|
|
squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
|
|
};
|
|
await gameLoop.startLevel(runData, { startAnimation: false });
|
|
// Set state to deployment so finalizeDeployment works
|
|
mockGameStateManager.currentState = "STATE_DEPLOYMENT";
|
|
const playerUnit = gameLoop.deployUnit(
|
|
runData.squad[0],
|
|
gameLoop.playerSpawnZone[0]
|
|
);
|
|
await gameLoop.finalizeDeployment();
|
|
// Wait a bit for updateCombatState to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
const combatState = mockGameStateManager.getCombatState();
|
|
expect(combatState).to.exist;
|
|
// Now has both: spec-compliant activeUnitId and UI-compatible activeUnit
|
|
expect(combatState.activeUnitId).to.be.a("string");
|
|
expect(combatState.activeUnit).to.exist; // Still available for UI
|
|
});
|
|
|
|
it("Should use turnQueue as string[] (now implemented per spec, also has enrichedQueue for UI)", async () => {
|
|
// Spec defines: turnQueue: string[]
|
|
// Implementation now has both: turnQueue (spec) and enrichedQueue (UI compatibility)
|
|
const runData = {
|
|
seed: 12345,
|
|
depth: 1,
|
|
squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
|
|
};
|
|
await gameLoop.startLevel(runData, { startAnimation: false });
|
|
// Set state to deployment so finalizeDeployment works
|
|
mockGameStateManager.currentState = "STATE_DEPLOYMENT";
|
|
const playerUnit = gameLoop.deployUnit(
|
|
runData.squad[0],
|
|
gameLoop.playerSpawnZone[0]
|
|
);
|
|
await gameLoop.finalizeDeployment();
|
|
// Wait a bit for updateCombatState to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
const combatState = mockGameStateManager.getCombatState();
|
|
expect(combatState).to.exist;
|
|
// Now has spec-compliant turnQueue as string[]
|
|
expect(combatState.turnQueue).to.be.an("array");
|
|
if (combatState.turnQueue.length > 0) {
|
|
// Spec requires just string IDs
|
|
expect(combatState.turnQueue[0]).to.be.a("string"); // Spec format
|
|
// Also has enrichedQueue for UI
|
|
expect(combatState.enrichedQueue).to.be.an("array");
|
|
if (combatState.enrichedQueue && combatState.enrichedQueue.length > 0) {
|
|
expect(combatState.enrichedQueue[0]).to.have.property("unitId"); // UI format
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|