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"; import { InputManager } from "./InputManager.js"; import { MissionManager } from "../managers/MissionManager.js"; export class GameLoop { constructor() { this.isRunning = false; this.phase = "INIT"; // 1. Core Systems this.scene = new THREE.Scene(); this.camera = null; this.renderer = null; this.controls = null; this.inputManager = null; this.grid = null; this.voxelManager = null; this.unitManager = null; this.unitMeshes = new Map(); this.runData = null; this.playerSpawnZone = []; this.enemySpawnZone = []; // Input Logic State this.lastMoveTime = 0; this.moveCooldown = 120; // ms between cursor moves this.selectionMode = "MOVEMENT"; // MOVEMENT, TARGETING this.missionManager = new MissionManager(this); // Init Mission Manager // Deployment State this.deploymentState = { selectedUnitIndex: -1, deployedUnits: new Map(), // Map }; } 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); container.appendChild(this.renderer.domElement); this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping = true; this.controls.dampingFactor = 0.05; // --- SETUP INPUT MANAGER --- this.inputManager = new InputManager( this.camera, this.scene, this.renderer.domElement ); // Bind Buttons (Events) this.inputManager.addEventListener("gamepadbuttondown", (e) => this.handleButtonInput(e.detail) ); this.inputManager.addEventListener("keydown", (e) => this.handleKeyInput(e.detail) ); // Default Validator: Movement Logic (Will be overridden in startLevel) this.inputManager.setValidator(this.validateCursorMove.bind(this)); 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); 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); } /** * Validation Logic for Standard Movement. */ validateCursorMove(x, y, z) { if (!this.grid) return true; // Allow if grid not ready // 1. Basic Bounds Check if (!this.grid.isValidBounds({ x, y: 0, z })) return false; // 2. Scan Column for Surface (Climb/Drop Logic) let bestY = null; if (this.isWalkable(x, y, z)) bestY = y; else if (this.isWalkable(x, y + 1, z)) bestY = y + 1; else if (this.isWalkable(x, y - 1, z)) bestY = y - 1; else if (this.isWalkable(x, y - 2, z)) bestY = y - 2; if (bestY !== null) { return { x, y: bestY, z }; } return false; } /** * Validation Logic for Deployment Phase. */ validateDeploymentCursor(x, y, z) { if (!this.grid || this.playerSpawnZone.length === 0) return false; // Check if the target X,Z is inside the spawn zone list const validSpot = this.playerSpawnZone.find((t) => t.x === x && t.z === z); if (validSpot) { // Snap Y to the valid floor height defined in the zone return { x: validSpot.x, y: validSpot.y, z: validSpot.z }; } return false; // Cursor cannot leave the spawn zone } isWalkable(x, y, z) { if (this.grid.getCell(x, y, z) !== 0) return false; if (this.grid.getCell(x, y - 1, z) === 0) return false; if (this.grid.getCell(x, y + 1, z) !== 0) return false; return true; } validateInteractionTarget(x, y, z) { if (!this.grid) return true; return this.grid.isValidBounds({ x, y, z }); } handleButtonInput(detail) { if (detail.buttonIndex === 0) { // A / Cross this.triggerSelection(); } } handleKeyInput(code) { if (code === "Space" || code === "Enter") { this.triggerSelection(); } if (code === "Tab") { this.selectionMode = this.selectionMode === "MOVEMENT" ? "TARGETING" : "MOVEMENT"; const validator = this.selectionMode === "MOVEMENT" ? this.validateCursorMove.bind(this) : this.validateInteractionTarget.bind(this); this.inputManager.setValidator(validator); } } /** * Called by UI when a unit is clicked in the Roster. * @param {number} index - The index of the unit in the squad to select. */ selectDeploymentUnit(index) { this.deploymentState.selectedUnitIndex = index; console.log(`Deployment: Selected Unit Index ${index}`); } triggerSelection() { const cursor = this.inputManager.getCursorPosition(); console.log("Action at:", cursor); if (this.phase === "DEPLOYMENT") { const selIndex = this.deploymentState.selectedUnitIndex; if (selIndex !== -1) { // Attempt to deploy OR move the selected unit const unitDef = this.runData.squad[selIndex]; const existingUnit = this.deploymentState.deployedUnits.get(selIndex); const resultUnit = this.deployUnit(unitDef, cursor, existingUnit); if (resultUnit) { // Track it this.deploymentState.deployedUnits.set(selIndex, resultUnit); // Notify UI window.dispatchEvent( new CustomEvent("deployment-update", { detail: { deployedIndices: Array.from( this.deploymentState.deployedUnits.keys() ), }, }) ); } } else { console.log("No unit selected."); } } } async startMission(missionId) { const mission = await fetch( `assets/data/missions/${missionId.toLowerCase()}.json` ); const missionData = await mission.json(); this.missionManager.startMission(missionData); } async startLevel(runData) { console.log("GameLoop: Generating Level..."); this.runData = runData; this.isRunning = true; this.phase = "DEPLOYMENT"; this.clearUnitMeshes(); // Reset Deployment State this.deploymentState = { selectedUnitIndex: -1, deployedUnits: new Map(), // Map }; this.grid = new VoxelGrid(20, 10, 20); const generator = new RuinGenerator(this.grid, runData.seed); generator.generate(); if (generator.generatedAssets.spawnZones) { this.playerSpawnZone = generator.generatedAssets.spawnZones.player || []; this.enemySpawnZone = generator.generatedAssets.spawnZones.enemy || []; } if (this.playerSpawnZone.length === 0) this.playerSpawnZone.push({ x: 2, y: 1, z: 2 }); if (this.enemySpawnZone.length === 0) this.enemySpawnZone.push({ x: 18, y: 1, z: 18 }); this.voxelManager = new VoxelManager(this.grid, this.scene); this.voxelManager.updateMaterials(generator.generatedAssets); this.voxelManager.update(); if (this.controls) this.voxelManager.focusCamera(this.controls); 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); this.highlightZones(); if (this.playerSpawnZone.length > 0) { let sumX = 0, sumY = 0, sumZ = 0; for (const spot of this.playerSpawnZone) { sumX += spot.x; sumY += spot.y; sumZ += spot.z; } const centerX = sumX / this.playerSpawnZone.length; const centerY = sumY / this.playerSpawnZone.length; const centerZ = sumZ / this.playerSpawnZone.length; const start = this.playerSpawnZone[0]; this.inputManager.setCursor(start.x, start.y, start.z); if (this.controls) { this.controls.target.set(centerX, centerY, centerZ); this.controls.update(); } } this.inputManager.setValidator(this.validateDeploymentCursor.bind(this)); this.animate(); } deployUnit(unitDef, targetTile, existingUnit = null) { if (this.phase !== "DEPLOYMENT") return null; const isValid = this.validateDeploymentCursor( targetTile.x, targetTile.y, targetTile.z ); // Check collision if (!isValid) { console.warn("Invalid spawn zone"); return null; } // If tile occupied... if (this.grid.isOccupied(targetTile)) { // If occupied by SELF (clicking same spot), that's valid, just do nothing if ( existingUnit && existingUnit.position.x === targetTile.x && existingUnit.position.z === targetTile.z ) { return existingUnit; } console.warn("Tile occupied"); return null; } if (existingUnit) { // MOVE logic this.grid.moveUnit(existingUnit, targetTile, { force: true }); // Force to bypass standard move checks if any // Update Mesh const mesh = this.unitMeshes.get(existingUnit.id); if (mesh) { mesh.position.set(targetTile.x, targetTile.y + 0.6, targetTile.z); } console.log( `Moved ${existingUnit.name} to ${targetTile.x},${targetTile.y},${targetTile.z}` ); return existingUnit; } else { // CREATE logic 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; } } finalizeDeployment() { if (this.phase !== "DEPLOYMENT") return; const enemyCount = 2; for (let i = 0; i < enemyCount; i++) { const spotIndex = Math.floor(Math.random() * this.enemySpawnZone.length); const spot = this.enemySpawnZone[spotIndex]; if (spot && !this.grid.isOccupied(spot)) { const enemy = this.unitManager.createUnit("ENEMY_DEFAULT", "ENEMY"); this.grid.placeUnit(enemy, spot); this.createUnitMesh(enemy, spot); this.enemySpawnZone.splice(spotIndex, 1); } } this.phase = "ACTIVE"; // Switch to standard movement validator for the game this.inputManager.setValidator(this.validateCursorMove.bind(this)); console.log("Combat Started!"); } 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.team === "ENEMY") color = 0x550000; 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() { 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); this.scene.add(mesh); }); 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.inputManager) this.inputManager.update(); if (this.controls) this.controls.update(); const now = Date.now(); if (now - this.lastMoveTime > this.moveCooldown) { let dx = 0; let dz = 0; if ( this.inputManager.isKeyPressed("KeyW") || this.inputManager.isKeyPressed("ArrowUp") ) dz = -1; if ( this.inputManager.isKeyPressed("KeyS") || this.inputManager.isKeyPressed("ArrowDown") ) dz = 1; if ( this.inputManager.isKeyPressed("KeyA") || this.inputManager.isKeyPressed("ArrowLeft") ) dx = -1; if ( this.inputManager.isKeyPressed("KeyD") || this.inputManager.isKeyPressed("ArrowRight") ) dx = 1; if (dx !== 0 || dz !== 0) { const currentPos = this.inputManager.getCursorPosition(); const newX = currentPos.x + dx; const newZ = currentPos.z + dz; this.inputManager.setCursor(newX, currentPos.y, newZ); this.lastMoveTime = now; } } 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.inputManager) this.inputManager.detach(); if (this.controls) this.controls.dispose(); } }