aether-shards/src/core/GameLoop.js

290 lines
8.5 KiB
JavaScript
Raw Normal View History

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();
}
}