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