import * as THREE from 'three'; import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; import { CFG } from './config.js'; import { G } from './globals.js'; import { makeRandom } from './utils.js'; import { setupLights } from './lighting.js'; import { setupGround, generateForest, generateGroundCover, getTerrainHeight, tickForest } from './world.js'; import { setupWeapon, updateWeaponAnchor, beginReload, updateWeapon } from './weapon.js'; import { setupEvents } from './events.js'; import { updatePlayer } from './player.js'; import { updateEnemies } from './enemies.js'; import { startNextWave, updateWaves } from './waves.js'; import { updateHUD, showOverlay, updateDamageEffect, updateHealEffect, updateCrosshair, updateHitMarker } from './hud.js'; import { updateFX } from './fx.js'; import { updateCasings } from './casings.js'; import { updateEnemyProjectiles } from './projectiles.js'; import { updateGrenades } from './grenades.js'; import { updateDayNight } from './daynight.js'; import { performShooting } from './combat.js'; import { updatePickups } from './pickups.js'; import { updateHelmets } from './helmets.js'; import { setupClouds, updateClouds } from './clouds.js'; import { startMusic, stopMusic } from './audio.js'; import { setupMountains, updateMountains } from './mountains.js'; init(); animate(); function init() { // Renderer G.renderer = new THREE.WebGLRenderer({ antialias: true }); G.renderer.setSize(window.innerWidth, window.innerHeight); // Lower pixel ratio cap for significant fill-rate savings G.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.0)); G.renderer.shadowMap.enabled = true; // Use the cheapest shadow filter for CPU savings G.renderer.shadowMap.type = THREE.BasicShadowMap; document.body.appendChild(G.renderer.domElement); // Scene G.scene = new THREE.Scene(); G.scene.background = new THREE.Color(0x0a1015); G.scene.fog = new THREE.FogExp2(0x05070a, CFG.fogDensity); // Camera G.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); // Controls G.controls = new PointerLockControls(G.camera, document.body); // Clock G.clock = new THREE.Clock(); // Seeded random G.random = makeRandom(CFG.seed); // Player const startY = getTerrainHeight(0, 0) + (CFG.player.eyeHeight || 1.8); G.player = { pos: new THREE.Vector3(0, startY, 0), vel: new THREE.Vector3(), speed: CFG.player.speed, radius: CFG.player.radius, health: CFG.player.health, alive: true, score: 0, yVel: 0, grounded: true, // Apex-like movement state sliding: false, jumpBuffer: 0, coyoteTimer: 0, lastWallNormal: new THREE.Vector3(), wallContactTimer: 0 , jumpHeld: false }; G.weapon.ammo = CFG.gun.magSize; G.weapon.reserve = Infinity; G.grenadeCount = 0; // Add camera to scene G.camera.position.copy(G.player.pos); // Lights setupLights(); // Ground and world setupGround(); // Ground cover before or after trees — independent generateGroundCover(); generateForest(); setupClouds(); setupMountains(); // Weapon setupWeapon(); updateWeaponAnchor(); // Events setupEvents({ startGame, restartGame, beginReload, updateWeaponAnchor }); // Pre-warm shaders to avoid first-frame hitches if (G.renderer && G.scene && G.camera) { try { G.renderer.compile(G.scene, G.camera); } catch (_) {} } } function startGame() { G.state = 'playing'; G.player.health = CFG.player.health; G.player.score = 0; G.player.alive = true; G.player.pos.set(0, getTerrainHeight(0, 0) + (CFG.player.eyeHeight || 1.8), 0); G.player.vel.set(0, 0, 0); G.player.yVel = 0; G.player.grounded = true; G.player.sliding = false; G.player.jumpBuffer = 0; G.player.coyoteTimer = 0; G.player.lastWallNormal.set(0, 0, 0); G.player.wallContactTimer = 0; G.camera.position.copy(G.player.pos); G.damageFlash = 0; G.healFlash = 0; G.hitFlash = 0; // Grenades reset G.grenades.length = 0; G.heldGrenade = null; G.grenadeCount = 0; // Clear enemies for (const enemy of G.enemies) { G.scene.remove(enemy.mesh); } G.enemies.length = 0; // Clear enemy projectiles for (const p of G.enemyProjectiles) { G.scene.remove(p.mesh); } G.enemyProjectiles.length = 0; // Clear pickups/orbs for (const o of G.orbs) { G.scene.remove(o.mesh); } G.orbs.length = 0; // Clear wave powerups for (const p of G.powerups || []) { if (p && p.mesh) G.scene.remove(p.mesh); } G.powerups.length = 0; // Clear any detached helmets for (const h of G.helmets) { G.scene.remove(h.mesh); } G.helmets.length = 0; // Clear ejected casings for (const c of G.casings) { G.scene.remove(c.mesh); } G.casings.length = 0; // Reset waves G.waves.current = 1; G.waves.aliveCount = 0; G.waves.spawnQueue = 0; G.waves.nextSpawnTimer = 0; G.waves.breakTimer = 0; G.waves.inBreak = false; G.waves.wolvesToSpawn = 0; G.waves.shamansToSpawn = 0; G.waves.golemsToSpawn = 0; // Reset weapon G.weapon.ammo = CFG.gun.magSize; G.weapon.reloading = false; G.weapon.reloadTimer = 0; G.weapon.recoil = 0; G.weapon.spread = CFG.gun.spreadMin ?? CFG.gun.bloom ?? 0; G.weapon.targetSpread = G.weapon.spread; G.weapon.viewPitch = 0; G.weapon.viewYaw = 0; G.weapon.appliedPitch = 0; G.weapon.appliedYaw = 0; G.weapon.rofMult = 1; G.weapon.rofBuffTimer = 0; G.weapon.rofBuffTotal = 0; G.movementMult = 1; G.movementBuffTimer = 0; G.weapon.infiniteAmmoTimer = 0; G.weapon.infiniteAmmoTotal = 0; G.weapon.ammoBeforeInf = null; G.weapon.reserveBeforeInf = null; const overlay = document.getElementById('overlay'); if (overlay) overlay.classList.add('hidden'); updateHUD(); // Initialize day/night visuals immediately updateDayNight(0); startNextWave(); // Start background music (once audio is unlocked) startMusic(); } function restartGame() { G.controls.lock(); } function gameOver() { G.state = 'gameover'; G.controls.unlock(); showOverlay('gameover'); // Gracefully fade out music stopMusic(0.6); } function animate() { requestAnimationFrame(animate); const delta = Math.min(G.clock.getDelta(), 0.1); if (G.state === 'playing') { updatePlayer(delta); updateEnemies(delta, gameOver); updateEnemyProjectiles(delta, gameOver); updateWaves(delta); performShooting(delta); updateWeapon(delta); updatePickups(delta); updateHUD(); updateCrosshair(delta); updateHitMarker(delta); updateDamageEffect(delta); updateHealEffect(delta); } updateDayNight(delta); updateFX(delta); updateHelmets(delta); updateCasings(delta); // Update grenades and previews last among gameplay if (G.state === 'playing') { updateGrenades(delta); if (!G.player.alive) { gameOver(); } } updateClouds(delta); updateMountains(delta); // Update subtle foliage wind sway using elapsedTime (avoid double-advancing clock) tickForest(G.clock.elapsedTime); G.renderer.render(G.scene, G.camera); // Lightweight FPS meter (updates ~2x/sec) if (!G._fpsAccum) { G._fpsAccum = 0; G._fpsFrames = 0; G._fpsNext = 0.5; } G._fpsAccum += delta; G._fpsFrames++; if (G._fpsAccum >= G._fpsNext) { const fps = Math.round(G._fpsFrames / G._fpsAccum); const el = document.getElementById('fps'); if (el) { el.textContent = String(fps); } G._fpsAccum = 0; G._fpsFrames = 0; } // Process a small budget of deferred disposals to avoid spikes if (G.disposeQueue && G.disposeQueue.length) { const budget = 24; // dispose up to N geometries per frame for (let i = 0; i < budget && G.disposeQueue.length; i++) { const geom = G.disposeQueue.pop(); if (geom && geom.dispose) { try { geom.dispose(); } catch (e) { /* noop */ } } } } }