Enhance GameLoop with deployment phase management and spawn zone logic. Introduce unit deployment functionality and finalize deployment to spawn enemies. Update RuinGenerator to define valid spawn zones for player and enemy units. Add integration tests for GameLoop to verify initialization, deployment, and enemy spawning behaviors.
This commit is contained in:
parent
781aee81a7
commit
2d72fb9170
3 changed files with 346 additions and 80 deletions
|
|
@ -5,11 +5,11 @@ import { VoxelManager } from "../grid/VoxelManager.js";
|
|||
import { UnitManager } from "../managers/UnitManager.js";
|
||||
import { CaveGenerator } from "../generation/CaveGenerator.js";
|
||||
import { RuinGenerator } from "../generation/RuinGenerator.js";
|
||||
// import { TurnSystem } from '../systems/TurnSystem.js';
|
||||
|
||||
export class GameLoop {
|
||||
constructor() {
|
||||
this.isRunning = false;
|
||||
this.phase = "INIT"; // INIT, DEPLOYMENT, ACTIVE, RESOLUTION
|
||||
|
||||
// 1. Core Systems
|
||||
this.scene = new THREE.Scene();
|
||||
|
|
@ -21,8 +21,13 @@ export class GameLoop {
|
|||
this.voxelManager = null;
|
||||
this.unitManager = null;
|
||||
|
||||
// Store visual meshes for units [unitId -> THREE.Mesh]
|
||||
this.unitMeshes = new Map();
|
||||
|
||||
// 2. State
|
||||
this.runData = null;
|
||||
this.playerSpawnZone = [];
|
||||
this.enemySpawnZone = [];
|
||||
}
|
||||
|
||||
init(container) {
|
||||
|
|
@ -43,12 +48,12 @@ export class GameLoop {
|
|||
|
||||
// Setup OrbitControls
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true; // Smooth camera movement
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.05;
|
||||
this.controls.screenSpacePanning = false;
|
||||
this.controls.minDistance = 5;
|
||||
this.controls.maxDistance = 100;
|
||||
this.controls.maxPolarAngle = Math.PI / 2; // Prevent going below ground
|
||||
this.controls.maxPolarAngle = Math.PI / 2;
|
||||
|
||||
// Lighting
|
||||
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
|
||||
|
|
@ -69,91 +74,195 @@ export class GameLoop {
|
|||
|
||||
/**
|
||||
* Starts a Level based on Run Data (New or Loaded).
|
||||
* Generates the map but does NOT spawn units immediately.
|
||||
*/
|
||||
async startLevel(runData) {
|
||||
console.log("GameLoop: Starting Level...");
|
||||
console.log("GameLoop: Generating Level...");
|
||||
this.runData = runData;
|
||||
this.isRunning = true;
|
||||
this.phase = "DEPLOYMENT";
|
||||
|
||||
// Cleanup previous level
|
||||
this.clearUnitMeshes();
|
||||
|
||||
// 1. Initialize Grid (20x10x20 for prototype)
|
||||
this.grid = new VoxelGrid(20, 10, 20);
|
||||
|
||||
// 2. Generate World (Using saved seed)
|
||||
// 2. Generate World
|
||||
// TODO: Switch generator based on runData.biome_id
|
||||
const generator = new RuinGenerator(this.grid, runData.seed);
|
||||
generator.generate();
|
||||
|
||||
// 3. Initialize Visuals
|
||||
this.voxelManager = new VoxelManager(this.grid, this.scene);
|
||||
// 3. Extract Spawn Zones
|
||||
if (generator.generatedAssets.spawnZones) {
|
||||
this.playerSpawnZone = generator.generatedAssets.spawnZones.player || [];
|
||||
this.enemySpawnZone = generator.generatedAssets.spawnZones.enemy || [];
|
||||
}
|
||||
|
||||
// Apply textures generated by the biome logic
|
||||
// Safety Fallback if generator provided no zones
|
||||
if (this.playerSpawnZone.length === 0) {
|
||||
console.warn("No Player Spawn Zone generated. Using default.");
|
||||
this.playerSpawnZone.push({ x: 2, y: 1, z: 2 });
|
||||
}
|
||||
if (this.enemySpawnZone.length === 0) {
|
||||
console.warn("No Enemy Spawn Zone generated. Using default.");
|
||||
this.enemySpawnZone.push({ x: 18, y: 1, z: 18 });
|
||||
}
|
||||
|
||||
// 4. Initialize Visuals
|
||||
this.voxelManager = new VoxelManager(this.grid, this.scene);
|
||||
this.voxelManager.updateMaterials(generator.generatedAssets);
|
||||
this.voxelManager.update();
|
||||
|
||||
// Center camera using the focus target
|
||||
if (this.controls) {
|
||||
this.voxelManager.focusCamera(this.controls);
|
||||
}
|
||||
|
||||
// 4. Initialize Units
|
||||
// Mock Registry for Prototype so UnitManager doesn't crash on createUnit
|
||||
// 5. Initialize Unit Manager (Empty)
|
||||
const mockRegistry = {
|
||||
get: (id) => {
|
||||
if (id.startsWith("CLASS_"))
|
||||
return { type: "EXPLORER", name: id, stats: { hp: 100 } };
|
||||
return {
|
||||
type: "EXPLORER",
|
||||
name: id, // Fallback name
|
||||
stats: { hp: 100, attack: 10, speed: 10 },
|
||||
type: "ENEMY",
|
||||
name: "Enemy",
|
||||
stats: { hp: 50 },
|
||||
ai_archetype: "BRUISER",
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
this.unitManager = new UnitManager(mockRegistry);
|
||||
this.spawnSquad(runData.squad);
|
||||
|
||||
// Start Loop
|
||||
// 6. Highlight Spawn Zones (Visual Debug)
|
||||
this.highlightZones();
|
||||
|
||||
// Start Render Loop (Waiting for player input)
|
||||
this.animate();
|
||||
}
|
||||
|
||||
spawnSquad(squadManifest) {
|
||||
if (!squadManifest || !this.unitManager) return;
|
||||
/**
|
||||
* Called by UI to place a unit during Deployment Phase.
|
||||
*/
|
||||
deployUnit(unitDef, targetTile) {
|
||||
if (this.phase !== "DEPLOYMENT") {
|
||||
console.warn("Cannot deploy unit outside Deployment phase.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simple spawn logic: line them up starting at (2, 1, 2)
|
||||
// In a full implementation, the Generator would provide 'StartPoints'
|
||||
let spawnX = 2;
|
||||
let spawnZ = 2;
|
||||
const spawnY = 1; // Assuming flat floor at y=1 for Ruins
|
||||
// Validate Tile
|
||||
const isValid = this.playerSpawnZone.some(
|
||||
(t) => t.x === targetTile.x && t.z === targetTile.z
|
||||
);
|
||||
if (!isValid) {
|
||||
console.warn("Invalid spawn location.");
|
||||
return null;
|
||||
}
|
||||
|
||||
squadManifest.forEach((member) => {
|
||||
if (!member) return;
|
||||
if (this.grid.isOccupied(targetTile)) {
|
||||
console.warn("Tile occupied.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create Unit (this uses the registry to look up stats)
|
||||
const unit = this.unitManager.createUnit(member.classId, "PLAYER");
|
||||
// Create and Place
|
||||
const unit = this.unitManager.createUnit(
|
||||
unitDef.classId || unitDef.id,
|
||||
"PLAYER"
|
||||
);
|
||||
if (unitDef.name) unit.name = unitDef.name;
|
||||
|
||||
// Override name if provided in manifest
|
||||
if (member.name) unit.name = member.name;
|
||||
this.grid.placeUnit(unit, targetTile);
|
||||
this.createUnitMesh(unit, targetTile);
|
||||
|
||||
// Find a valid spot (basic collision check)
|
||||
let placed = false;
|
||||
// Try a few spots if the first is taken
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const pos = { x: spawnX + i, y: spawnY, z: spawnZ };
|
||||
console.log(
|
||||
`Deployed ${unit.name} at ${targetTile.x},${targetTile.y},${targetTile.z}`
|
||||
);
|
||||
return unit;
|
||||
}
|
||||
|
||||
// Ensure we don't spawn inside a wall or off the map
|
||||
if (this.grid.isValidBounds(pos) && !this.grid.isSolid(pos)) {
|
||||
this.grid.placeUnit(unit, pos);
|
||||
console.log(
|
||||
`Spawned ${unit.name} (${unit.id}) at ${pos.x},${pos.y},${pos.z}`
|
||||
);
|
||||
placed = true;
|
||||
// Update X for next unit to be next to this one
|
||||
spawnX = pos.x + 1;
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* Called when player clicks "Start Battle".
|
||||
* Spawns enemies and switches phase.
|
||||
*/
|
||||
finalizeDeployment() {
|
||||
if (this.phase !== "DEPLOYMENT") return;
|
||||
|
||||
console.log("Finalizing Deployment. Spawning Enemies...");
|
||||
|
||||
// Simple Enemy Spawning Logic
|
||||
// In a real game, this would read from a Level Design configuration
|
||||
const enemyCount = 2;
|
||||
|
||||
for (let i = 0; i < enemyCount; i++) {
|
||||
// Pick a random spot in enemy zone
|
||||
const spotIndex = Math.floor(Math.random() * this.enemySpawnZone.length);
|
||||
const spot = this.enemySpawnZone[spotIndex];
|
||||
|
||||
// Ensure spot is valid/empty
|
||||
if (spot && !this.grid.isOccupied(spot)) {
|
||||
const enemy = this.unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
|
||||
this.grid.placeUnit(enemy, spot);
|
||||
this.createUnitMesh(enemy, spot);
|
||||
// Remove spot from pool to avoid double spawn
|
||||
this.enemySpawnZone.splice(spotIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!placed) {
|
||||
console.warn(`Could not find spawn point for ${unit.name}`);
|
||||
}
|
||||
this.phase = "ACTIVE";
|
||||
// TODO: Start Turn System here
|
||||
}
|
||||
|
||||
clearUnitMeshes() {
|
||||
this.unitMeshes.forEach((mesh) => this.scene.remove(mesh));
|
||||
this.unitMeshes.clear();
|
||||
}
|
||||
|
||||
createUnitMesh(unit, pos) {
|
||||
const geometry = new THREE.BoxGeometry(0.6, 1.2, 0.6);
|
||||
let color = 0xcccccc;
|
||||
|
||||
if (unit.id.includes("VANGUARD")) color = 0xff3333;
|
||||
else if (unit.id.includes("WEAVER")) color = 0x3333ff;
|
||||
else if (unit.id.includes("SCAVENGER")) color = 0xffff33;
|
||||
else if (unit.id.includes("TINKER")) color = 0xff9933;
|
||||
else if (unit.id.includes("CUSTODIAN")) color = 0x33ff33;
|
||||
else if (unit.team === "ENEMY") color = 0x550000; // Dark Red for enemies
|
||||
|
||||
const material = new THREE.MeshStandardMaterial({ color: color });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
mesh.position.set(pos.x, pos.y + 0.6, pos.z);
|
||||
|
||||
this.scene.add(mesh);
|
||||
this.unitMeshes.set(unit.id, mesh);
|
||||
}
|
||||
|
||||
highlightZones() {
|
||||
// Visual debug for spawn zones (Green for Player, Red for Enemy)
|
||||
// In a full implementation, this would use the VoxelManager's highlight system
|
||||
const highlightMatPlayer = new THREE.MeshBasicMaterial({
|
||||
color: 0x00ff00,
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
});
|
||||
const highlightMatEnemy = new THREE.MeshBasicMaterial({
|
||||
color: 0xff0000,
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
});
|
||||
const geo = new THREE.PlaneGeometry(1, 1);
|
||||
geo.rotateX(-Math.PI / 2);
|
||||
|
||||
this.playerSpawnZone.forEach((pos) => {
|
||||
const mesh = new THREE.Mesh(geo, highlightMatPlayer);
|
||||
mesh.position.set(pos.x, pos.y + 0.05, pos.z); // Slightly above floor
|
||||
this.scene.add(mesh);
|
||||
// Note: Should track these to clean up later
|
||||
});
|
||||
|
||||
this.enemySpawnZone.forEach((pos) => {
|
||||
const mesh = new THREE.Mesh(geo, highlightMatEnemy);
|
||||
mesh.position.set(pos.x, pos.y + 0.05, pos.z);
|
||||
this.scene.add(mesh);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -161,20 +270,20 @@ export class GameLoop {
|
|||
if (!this.isRunning) return;
|
||||
requestAnimationFrame(this.animate);
|
||||
|
||||
// Update Logic
|
||||
// TWEEN.update();
|
||||
|
||||
if (this.controls) {
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
// Render
|
||||
const time = Date.now() * 0.002;
|
||||
this.unitMeshes.forEach((mesh) => {
|
||||
mesh.position.y += Math.sin(time) * 0.002;
|
||||
});
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
// Cleanup Three.js resources if needed
|
||||
if (this.controls) this.controls.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { BaseGenerator } from "./BaseGenerator.js";
|
||||
import { RustedFloorTextureGenerator } from "./textures/RustedFloorTextureGenerator.js";
|
||||
// We can reuse the texture generators or create specific Ruin ones.
|
||||
import { RustedWallTextureGenerator } from "./textures/RustedWallTextureGenerator.js";
|
||||
import { RustedFloorTextureGenerator } from "./textures/RustedFloorTextureGenerator.js";
|
||||
|
||||
/**
|
||||
* Generates structured rooms and corridors.
|
||||
|
|
@ -11,25 +12,22 @@ export class RuinGenerator extends BaseGenerator {
|
|||
constructor(grid, seed) {
|
||||
super(grid, seed);
|
||||
|
||||
// Initialize Texture Generators
|
||||
// (In a full game, we would replace these with Ruin-specific aesthetic generators)
|
||||
// Use Rusted Floor for the Industrial aesthetic
|
||||
this.floorGen = new RustedFloorTextureGenerator(seed);
|
||||
this.wallGen = new RustedWallTextureGenerator(seed);
|
||||
|
||||
// Container for assets generated by this biome logic.
|
||||
// We preload 10 variations of floors and walls.
|
||||
// The VoxelManager will need to read this 'palette' to create materials.
|
||||
this.generatedAssets = {
|
||||
palette: {}, // Maps Voxel ID -> Texture Asset Config
|
||||
palette: {},
|
||||
// New: Explicitly track valid spawn locations for teams
|
||||
spawnZones: {
|
||||
player: [],
|
||||
enemy: [],
|
||||
},
|
||||
};
|
||||
|
||||
this.preloadTextures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-generates texture variations so we don't regenerate them per voxel.
|
||||
* Assigns them to specific Voxel IDs.
|
||||
*/
|
||||
preloadTextures() {
|
||||
const VARIATIONS = 10;
|
||||
const TEXTURE_SIZE = 128;
|
||||
|
|
@ -37,7 +35,6 @@ export class RuinGenerator extends BaseGenerator {
|
|||
// 1. Preload Wall Variations (IDs 100 - 109)
|
||||
for (let i = 0; i < VARIATIONS; i++) {
|
||||
const wallSeed = this.rng.next() * 5000 + i;
|
||||
// Generate the complex map (Diffuse + Emissive + Normal etc.)
|
||||
const tempWallGen = new RustedWallTextureGenerator(wallSeed);
|
||||
this.generatedAssets.palette[100 + i] =
|
||||
tempWallGen.generateWall(TEXTURE_SIZE);
|
||||
|
|
@ -84,6 +81,13 @@ export class RuinGenerator extends BaseGenerator {
|
|||
this.buildCorridor(prev, curr); // Additive building
|
||||
}
|
||||
|
||||
// 3. Define Spawn Zones
|
||||
// Player gets the first room, Enemy gets the last room
|
||||
if (rooms.length > 0) {
|
||||
this.markSpawnZone(rooms[0], "player");
|
||||
this.markSpawnZone(rooms[rooms.length - 1], "enemy");
|
||||
}
|
||||
|
||||
// 3. Apply Texture/Material Logic
|
||||
this.applyTextures();
|
||||
|
||||
|
|
@ -93,6 +97,26 @@ export class RuinGenerator extends BaseGenerator {
|
|||
this.scatterCover(10, 0.1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects valid floor tiles from a room and adds them to a spawn zone.
|
||||
*/
|
||||
markSpawnZone(room, type) {
|
||||
// Scan the room's interior (excluding walls)
|
||||
// We assume y=1 is the walking surface
|
||||
for (let x = room.x + 1; x < room.x + room.w - 1; x++) {
|
||||
for (let z = room.z + 1; z < room.z + room.d - 1; z++) {
|
||||
// Double check it's a valid floor (Solid at 0, Air at 1)
|
||||
// Note: IDs haven't been textured yet, so floor is still ID 1
|
||||
if (
|
||||
this.grid.getCell(x, 0, z) !== 0 &&
|
||||
this.grid.getCell(x, 1, z) === 0
|
||||
) {
|
||||
this.generatedAssets.spawnZones[type].push({ x, y: 1, z });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
roomsOverlap(room, rooms) {
|
||||
for (const r of rooms) {
|
||||
if (
|
||||
|
|
@ -126,11 +150,9 @@ export class RuinGenerator extends BaseGenerator {
|
|||
x === r.x || x === r.x + r.w - 1 || z === r.z || z === r.z + r.d - 1;
|
||||
|
||||
if (isWall) {
|
||||
// Build Wall stack - Placeholder ID 1
|
||||
this.grid.setCell(x, r.y, z, 1); // Wall Base
|
||||
this.grid.setCell(x, r.y + 1, z, 1); // Wall Top
|
||||
} else {
|
||||
// Ensure Interior is Air
|
||||
this.grid.setCell(x, r.y, z, 0);
|
||||
this.grid.setCell(x, r.y + 1, z, 0);
|
||||
}
|
||||
|
|
@ -155,17 +177,11 @@ export class RuinGenerator extends BaseGenerator {
|
|||
}
|
||||
|
||||
buildPathPoint(x, y, z) {
|
||||
// Build Floor - Placeholder ID 1
|
||||
this.grid.setCell(x, y - 1, z, 1);
|
||||
// Clear Path (Air)
|
||||
this.grid.setCell(x, y, z, 0);
|
||||
this.grid.setCell(x, y + 1, z, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through the grid to identify Floor and Wall voxels,
|
||||
* assigning randomized IDs from our preloaded palette.
|
||||
*/
|
||||
applyTextures() {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
for (let z = 0; z < this.depth; z++) {
|
||||
|
|
@ -173,17 +189,11 @@ export class RuinGenerator extends BaseGenerator {
|
|||
const current = this.grid.getCell(x, y, z);
|
||||
const above = this.grid.getCell(x, y + 1, z);
|
||||
|
||||
// If it's currently a placeholder solid block (ID 1)
|
||||
// Note: We check if it is NOT Air (0)
|
||||
if (current !== 0) {
|
||||
if (above === 0) {
|
||||
// This is a Floor Surface
|
||||
// Pick random ID from 200 to 209
|
||||
const variant = this.rng.rangeInt(0, 9);
|
||||
this.grid.setCell(x, y, z, 200 + variant);
|
||||
} else {
|
||||
// This is a Wall or internal block
|
||||
// Pick random ID from 100 to 109
|
||||
const variant = this.rng.rangeInt(0, 9);
|
||||
this.grid.setCell(x, y, z, 100 + variant);
|
||||
}
|
||||
|
|
|
|||
147
test/core/GameLoop.test.js
Normal file
147
test/core/GameLoop.test.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue