import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { VoxelGrid } from "../grid/VoxelGrid.js"; import { VoxelManager } from "../grid/VoxelManager.js"; import { UnitManager } from "../managers/UnitManager.js"; import { CaveGenerator } from "../generation/CaveGenerator.js"; import { RuinGenerator } from "../generation/RuinGenerator.js"; export class GameLoop { constructor() { this.isRunning = false; this.phase = "INIT"; // INIT, DEPLOYMENT, ACTIVE, RESOLUTION // 1. Core Systems this.scene = new THREE.Scene(); this.camera = null; this.renderer = null; this.controls = null; this.grid = null; 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) { // Setup Three.js this.camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 1000 ); this.camera.position.set(20, 20, 20); this.camera.lookAt(0, 0, 0); this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setClearColor(0x111111); // Dark background container.appendChild(this.renderer.domElement); // Setup OrbitControls this.controls = new OrbitControls(this.camera, this.renderer.domElement); 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; // Lighting const ambient = new THREE.AmbientLight(0xffffff, 0.6); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(10, 20, 10); this.scene.add(ambient); this.scene.add(dirLight); // Handle Resize window.addEventListener("resize", () => { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); }); this.animate = this.animate.bind(this); } /** * 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: 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 // TODO: Switch generator based on runData.biome_id const generator = new RuinGenerator(this.grid, runData.seed); generator.generate(); // 3. Extract Spawn Zones if (generator.generatedAssets.spawnZones) { this.playerSpawnZone = generator.generatedAssets.spawnZones.player || []; this.enemySpawnZone = generator.generatedAssets.spawnZones.enemy || []; } // 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(); if (this.controls) { this.voxelManager.focusCamera(this.controls); } // 5. Initialize Unit Manager (Empty) const mockRegistry = { get: (id) => { if (id.startsWith("CLASS_")) return { type: "EXPLORER", name: id, stats: { hp: 100 } }; return { type: "ENEMY", name: "Enemy", stats: { hp: 50 }, ai_archetype: "BRUISER", }; }, }; this.unitManager = new UnitManager(mockRegistry); // 6. Highlight Spawn Zones (Visual Debug) this.highlightZones(); // Start Render Loop (Waiting for player input) this.animate(); } /** * 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; } // 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; } if (this.grid.isOccupied(targetTile)) { console.warn("Tile occupied."); return null; } // Create and Place const unit = this.unitManager.createUnit( unitDef.classId || unitDef.id, "PLAYER" ); if (unitDef.name) unit.name = unitDef.name; this.grid.placeUnit(unit, targetTile); this.createUnitMesh(unit, targetTile); console.log( `Deployed ${unit.name} at ${targetTile.x},${targetTile.y},${targetTile.z}` ); return unit; } /** * 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); } } 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); }); } animate() { if (!this.isRunning) return; requestAnimationFrame(this.animate); if (this.controls) { this.controls.update(); } 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; if (this.controls) this.controls.dispose(); } }