Spaces:
Running
Running
| 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); | |
| } | |
| } | |