import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { SCENE_BACKGROUND, GROUND_SIZE, GRID_CELL_SIZE, } from "../config/gameConfig.js"; export class SceneSetup { constructor() { // Basic scene setup this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(SCENE_BACKGROUND); // Camera this.camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 2000 ); this.camera.position.set(20, 22, 24); // Renderer this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.shadowMap.enabled = true; document.body.appendChild(this.renderer.domElement); // Controls this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.target.set(0, 0, 0); this.controls.enableDamping = true; // Camera movement config this.CAMERA_MOVE_SPEED = 18; // units per second in world-space // Bounds relative to ground size, keep a small margin inside edges const half = GROUND_SIZE / 2; this.CAMERA_MIN_X = -half + 2; this.CAMERA_MAX_X = half - 2; this.CAMERA_MIN_Z = -half + 2; this.CAMERA_MAX_Z = half - 2; // Setup lighting this.setupLighting(); // Setup ground this.ground = this.setupGround(); // Setup grid this.grid = this.setupGrid(); // Handle window resize window.addEventListener("resize", () => this.onWindowResize()); } setupLighting() { const hemi = new THREE.HemisphereLight(0xffffff, 0x404040, 0.6); this.scene.add(hemi); const dirLight = new THREE.DirectionalLight(0xffffff, 1.0); dirLight.position.set(8, 20, 8); dirLight.castShadow = true; dirLight.shadow.mapSize.set(1024, 1024); this.scene.add(dirLight); } setupGround() { const groundGeo = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE); const groundMat = new THREE.MeshStandardMaterial({ color: 0x1d6e2f }); const ground = new THREE.Mesh(groundGeo, groundMat); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; ground.name = "ground"; this.scene.add(ground); return ground; } setupGrid() { const divisions = Math.floor(GROUND_SIZE / GRID_CELL_SIZE); const grid = new THREE.GridHelper( GROUND_SIZE, divisions, 0x8ab4f8, 0x3a97ff ); grid.position.y = 0.02; // avoid z-fighting with ground grid.material.transparent = true; grid.material.opacity = 0.35; grid.renderOrder = 0; this.scene.add(grid); return grid; } clampToBounds(vec3) { vec3.x = Math.min(this.CAMERA_MAX_X, Math.max(this.CAMERA_MIN_X, vec3.x)); vec3.z = Math.min(this.CAMERA_MAX_Z, Math.max(this.CAMERA_MIN_Z, vec3.z)); return vec3; } // Move camera and controls target horizontally in world space while keeping height moveCamera(direction, deltaTime) { // direction: {x: -1|0|1, z: -1|0|1} if (!direction || (direction.x === 0 && direction.z === 0)) return; // Compute normalized planar direction const move = new THREE.Vector3(direction.x, 0, direction.z); if (move.lengthSq() === 0) return; move.normalize().multiplyScalar(this.CAMERA_MOVE_SPEED * deltaTime); // Maintain current height const currentY = this.camera.position.y; // Move both camera and target so orbit feel is preserved const newCamPos = this.camera.position.clone().add(move); const newTarget = this.controls.target.clone().add(move); // Clamp within bounds this.clampToBounds(newCamPos); this.clampToBounds(newTarget); // Apply positions (preserve camera height) newCamPos.y = currentY; this.camera.position.copy(newCamPos); this.controls.target.copy(newTarget); // Let OrbitControls smoothing handle interpolation } onWindowResize() { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); } render(gameState) { // Optionally pass gameState so we can update transient visuals like electric arcs this.controls.update(); // Per-frame visual updates for towers (electric arcs fade/cleanup) if (gameState && Array.isArray(gameState.towers)) { const now = performance.now(); for (const t of gameState.towers) { if (t?.updateElectricArcs) { t.updateElectricArcs(now); } } } this.renderer.render(this.scene, this.camera); } }