tower-defense / src /scene /SceneSetup.js
victor's picture
victor HF Staff
Initial commit
b29710c
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);
}
}