// Neon Void: Orbital Survivors - main game script let scene, camera, renderer; let player; let enemies = []; let projectiles = []; let xpOrbs = []; let areaZones = []; let deathParticles = []; let pillarGroup; let teleportPadGroup; let slashEffects = []; let highScores = loadHighScores(); let lastTime = 0; let running = false; let pausedForUpgrade = false; let elapsedTime = 0; const worldSize = 80; const halfSize = worldSize / 2; let enemySpawnTimer = 0; let enemySpawnInterval = 1.0; let kills = 0; const keys = { w: false, a: false, s: false, d: false, ArrowUp: false, ArrowLeft: false, ArrowDown: false, ArrowRight: false }; // Weapons const weapons = { melee: { level: 1, damage: 18, cooldown: 1.4, timer: 0, range: 2.2, enabled: false }, ranged: { level: 1, damage: 20, cooldown: 0.9, timer: 0, projectiles: 1, enabled: true // start with ranged only }, area: { level: 1, damagePerSecond: 14, cooldown: 4.0, timer: 0, radius: 4.0, duration: 0.8, enabled: false }, missile: { unlocked: false, level: 0, damage: 40, cooldown: 3.0, timer: 0, speed: 18, turnRate: 5.0, lifetime: 4.0 }, drones: { unlocked: false, level: 0, damage: 10, cooldown: 1.1, timer: 0, projectileSpeed: 26 } }; // UI elements const hpText = document.getElementById("hpText"); const levelText = document.getElementById("levelText"); const xpText = document.getElementById("xpText"); const timeText = document.getElementById("timeText"); const waveText = document.getElementById("waveText"); const killsText = document.getElementById("killsText"); const enemiesText = document.getElementById("enemiesText"); const hpBar = document.getElementById("hpBar"); const xpBar = document.getElementById("xpBar"); const meleeText = document.getElementById("meleeText"); const rangedText = document.getElementById("rangedText"); const areaText = document.getElementById("areaText"); const missileText = document.getElementById("missileText"); const dronesText = document.getElementById("dronesText"); const centerMessage = document.getElementById("centerMessage"); const startBtn = document.getElementById("startBtn"); const upgradeOverlay = document.getElementById("upgradeOverlay"); const upgradeOptionsDiv = document.getElementById("upgradeOptions"); const UPGRADE_DEFS = { meleeDamage: { name: "Plasma Edge", desc: "+35% melee damage (unlocks melee)", apply() { weapons.melee.damage *= 1.35; weapons.melee.level++; weapons.melee.enabled = true; } }, meleeRange: { name: "Arc Slash", desc: "+20% melee range (unlocks melee)", apply() { weapons.melee.range *= 1.2; weapons.melee.level++; weapons.melee.enabled = true; } }, meleeHaste: { name: "Servo Overdrive", desc: "Melee cooldown -15% (unlocks melee)", apply() { weapons.melee.cooldown *= 0.85; weapons.melee.level++; weapons.melee.enabled = true; } }, rangedDamage: { name: "Ion Amplifier", desc: "+25% ranged damage", apply() { weapons.ranged.damage *= 1.25; weapons.ranged.level++; } }, rangedProjectiles: { name: "Multi-Cast Core", desc: "+1 ranged projectile", apply() { weapons.ranged.projectiles += 1; weapons.ranged.level++; } }, rangedHaste: { name: "Rapid Channel", desc: "Ranged cooldown -12%", apply() { weapons.ranged.cooldown *= 0.88; weapons.ranged.level++; } }, areaPower: { name: "Flux Field", desc: "+30% area damage (unlocks area)", apply() { weapons.area.damagePerSecond *= 1.3; weapons.area.level++; weapons.area.enabled = true; } }, areaRadius: { name: "Expanding Pulse", desc: "+20% area radius (unlocks area)", apply() { weapons.area.radius *= 1.2; weapons.area.level++; weapons.area.enabled = true; } }, areaHaste: { name: "Pulse Loop", desc: "Area cooldown -15% (unlocks area)", apply() { weapons.area.cooldown *= 0.85; weapons.area.level++; weapons.area.enabled = true; } }, missileUnlock: { name: "Seeker Battery", desc: "Unlock heat-seeking missiles / level up missiles", apply() { if (!weapons.missile.unlocked) { weapons.missile.unlocked = true; weapons.missile.level = 1; } else { weapons.missile.level++; weapons.missile.damage *= 1.2; weapons.missile.cooldown *= 0.9; } } }, dronesUnlock: { name: "Orbital Cannons", desc: "Unlock drone auto-guns / level up drones", apply() { if (!weapons.drones.unlocked) { weapons.drones.unlocked = true; weapons.drones.level = 1; } else { weapons.drones.level++; weapons.drones.damage *= 1.15; weapons.drones.cooldown *= 0.93; } } }, hpMax: { name: "Regen Matrix", desc: "+20 max HP and fully heal", apply() { player.maxHp += 20; player.hp = player.maxHp; } }, moveSpeed: { name: "Grav Boosters", desc: "+15% movement speed", apply() { player.speed *= 1.15; } }, pickupRadius: { name: "Mag Core", desc: "+25% XP pickup range", apply() { player.pickupRadius *= 1.25; } } }; let pendingLevelUps = 0; function initThree() { scene = new THREE.Scene(); scene.background = new THREE.Color(0x050509); scene.fog = new THREE.FogExp2(0x02030a, 0.02); const aspect = window.innerWidth / window.innerHeight; camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 300); camera.position.set(0, 40, 40); camera.lookAt(0, 0, 0); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; document.body.appendChild(renderer.domElement); window.addEventListener("resize", onWindowResize); const ambient = new THREE.AmbientLight(0xffffff, 0.45); scene.add(ambient); const dir = new THREE.DirectionalLight(0x66ccff, 1.1); dir.position.set(30, 60, 20); dir.castShadow = true; scene.add(dir); const floorGeo = new THREE.PlaneGeometry(worldSize, worldSize, 40, 40); const floorMat = new THREE.MeshStandardMaterial({ color: 0x050813, roughness: 0.4, metalness: 0.6, wireframe: false }); const floor = new THREE.Mesh(floorGeo, floorMat); floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor); addArenaDetail(); player = createPlayer(); scene.add(player.mesh); requestAnimationFrame(animate); } function addArenaDetail() { pillarGroup = new THREE.Group(); const pillarGeo = new THREE.CylinderGeometry(0.2, 0.3, 1.5, 12); for (let i = 0; i < 20; i++) { const glowColor = new THREE.Color().setHSL(0.65 + Math.random() * 0.15, 0.9, 0.5); const pillarMat = new THREE.MeshStandardMaterial({ color: 0x111423, emissive: glowColor, emissiveIntensity: 0.7, metalness: 0.6, roughness: 0.3 }); const mesh = new THREE.Mesh(pillarGeo, pillarMat); const angle = (i / 20) * Math.PI * 2; const radius = halfSize * 0.9; mesh.position.set(Math.cos(angle) * radius, 0.75, Math.sin(angle) * radius); mesh.castShadow = true; mesh.receiveShadow = true; pillarGroup.add(mesh); } scene.add(pillarGroup); // Teleport/base pad in the center teleportPadGroup = new THREE.Group(); const padBaseGeo = new THREE.CylinderGeometry(1.8, 1.8, 0.2, 32); const padBaseMat = new THREE.MeshStandardMaterial({ color: 0x050813, metalness: 0.9, roughness: 0.2, emissive: 0x00ffff, emissiveIntensity: 0.3 }); const padBase = new THREE.Mesh(padBaseGeo, padBaseMat); padBase.position.y = 0.11; padBase.receiveShadow = true; teleportPadGroup.add(padBase); const padRingGeo = new THREE.TorusGeometry(2.1, 0.06, 12, 48); const padRingMat = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 0.8, metalness: 1.0, roughness: 0.1 }); const padRing = new THREE.Mesh(padRingGeo, padRingMat); padRing.rotation.x = Math.PI / 2; padRing.position.y = 0.18; teleportPadGroup.add(padRing); scene.add(teleportPadGroup); } function createPlayer() { const bodyGeo = new THREE.CapsuleGeometry(0.5, 0.8, 8, 16); const bodyMat = new THREE.MeshStandardMaterial({ color: 0x00e5ff, metalness: 0.7, roughness: 0.15, emissive: 0x00b4ff, emissiveIntensity: 0.9 }); const body = new THREE.Mesh(bodyGeo, bodyMat); body.castShadow = true; body.receiveShadow = true; const glowGeo = new THREE.RingGeometry(0.7, 0.9, 32); const glowMat = new THREE.MeshBasicMaterial({ color: 0x44ccff, side: THREE.DoubleSide, transparent: true, opacity: 0.4 }); const glow = new THREE.Mesh(glowGeo, glowMat); glow.rotation.x = -Math.PI / 2; glow.position.y = -0.4; body.add(glow); body.position.set(0, 0.6, 0); // Orbiting drones around the player const orbiters = []; const orbGeo = new THREE.TetrahedronGeometry(0.25, 0); const orbMat = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00b4ff, emissiveIntensity: 1.5, metalness: 0.9, roughness: 0.1 }); for (let i = 0; i < 2; i++) { const orb = new THREE.Mesh(orbGeo, orbMat); orb.position.set(i === 0 ? 1.2 : -1.2, 0.4, 0); body.add(orb); orbiters.push(orb); } return { mesh: body, speed: 11, radius: 0.7, hp: 100, maxHp: 100, xp: 0, level: 1, xpToNext: 15, pickupRadius: 6, orbiters, orbitAngle: 0 }; } function spawnEnemy() { const wave = getWave(); const roll = Math.random(); let type = "shardling"; if (wave >= 5 && roll < 0.25) type = "warpEye"; else if (wave >= 3 && roll < 0.6) type = "razorDisk"; let container = new THREE.Group(); let speedMult = 1; let hpMult = 1; let radius = 0.7; if (type === "shardling") { const coreGeo = new THREE.DodecahedronGeometry(0.6); const coreMat = new THREE.MeshStandardMaterial({ color: 0x2bffb3, emissive: 0x13cfa3, emissiveIntensity: 1.1, metalness: 0.9, roughness: 0.3 }); const core = new THREE.Mesh(coreGeo, coreMat); core.castShadow = true; container.add(core); for (let i = 0; i < 3; i++) { const shardGeo = new THREE.TetrahedronGeometry(0.35); const shardMat = new THREE.MeshStandardMaterial({ color: 0x25ffd9, emissive: 0x25ffd9, emissiveIntensity: 0.8, metalness: 1.0, roughness: 0.15 }); const shard = new THREE.Mesh(shardGeo, shardMat); const angle = (i / 3) * Math.PI * 2; shard.position.set(Math.cos(angle) * 0.9, 0.3, Math.sin(angle) * 0.9); shard.castShadow = true; container.add(shard); } speedMult = 1.3 + wave * 0.03; hpMult = 1.0 + wave * 0.05; radius = 0.7; } else if (type === "razorDisk") { const diskGeo = new THREE.CylinderGeometry(0.1, 0.8, 0.1, 24, 1, true); const diskMat = new THREE.MeshStandardMaterial({ color: 0xff4f4f, emissive: 0xff4f4f, emissiveIntensity: 1.2, metalness: 1.0, roughness: 0.2 }); const disk = new THREE.Mesh(diskGeo, diskMat); disk.rotation.x = Math.PI / 2; disk.castShadow = true; container.add(disk); const coreGeo = new THREE.SphereGeometry(0.3, 16, 16); const coreMat = new THREE.MeshStandardMaterial({ color: 0x222831, emissive: 0xffaa00, emissiveIntensity: 1.0, metalness: 0.9, roughness: 0.2 }); const core = new THREE.Mesh(coreGeo, coreMat); core.position.y = 0.15; container.add(core); speedMult = 1.1 + wave * 0.04; hpMult = 1.4 + wave * 0.08; radius = 0.8; } else if (type === "warpEye") { const eyeGeo = new THREE.SphereGeometry(0.9, 24, 24); const eyeMat = new THREE.MeshStandardMaterial({ color: 0x101118, emissive: 0x8f3dff, emissiveIntensity: 1.2, metalness: 0.95, roughness: 0.25 }); const eye = new THREE.Mesh(eyeGeo, eyeMat); eye.castShadow = true; container.add(eye); const irisGeo = new THREE.CircleGeometry(0.5, 24); const irisMat = new THREE.MeshBasicMaterial({ color: 0x00ffe0 }); const iris = new THREE.Mesh(irisGeo, irisMat); iris.position.z = 0.85; container.add(iris); const ringGeo = new THREE.TorusGeometry(1.4, 0.08, 12, 48); const ringMat = new THREE.MeshStandardMaterial({ color: 0x8f3dff, emissive: 0x8f3dff, emissiveIntensity: 1.5, metalness: 1.0, roughness: 0.1 }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = Math.PI / 2; container.add(ring); speedMult = 0.85 + wave * 0.03; hpMult = 2.3 + wave * 0.12; radius = 1.0; } const angle = Math.random() * Math.PI * 2; const spawnRadius = halfSize * 1.1; const x = Math.cos(angle) * spawnRadius; const z = Math.sin(angle) * spawnRadius; container.position.set(x, 0.7, z); const baseSpeed = 2.5 + wave * 0.3; const baseHp = 12 + wave * 7; const speed = baseSpeed * speedMult; const hp = baseHp * hpMult; scene.add(container); enemies.push({ mesh: container, speed, radius, hp, type }); } // --- Weapons --- function triggerMelee() { const pPos = player.mesh.position.clone(); const range = weapons.melee.range; const damage = weapons.melee.damage + player.level * 3; const createSlash = (inner, outer, maxOpacity, life) => { const geo = new THREE.RingGeometry(inner, outer, 32); const mat = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: maxOpacity, side: THREE.DoubleSide }); const slash = new THREE.Mesh(geo, mat); slash.rotation.x = -Math.PI / 2; slash.position.set(pPos.x, 0.05, pPos.z); scene.add(slash); slashEffects.push({ mesh: slash, life, maxLife: life, maxOpacity }); }; createSlash(range * 0.6, range, 0.8, 0.18); if (weapons.melee.level >= 3) { createSlash(range * 0.9, range * 1.25, 0.5, 0.22); } const effectiveRange = range * (weapons.melee.level >= 3 ? 1.25 : 1.0); const rangeSq = effectiveRange * effectiveRange; for (let i = enemies.length - 1; i >= 0; i--) { const e = enemies[i]; const ePos = e.mesh.position; const dx = ePos.x - pPos.x; const dz = ePos.z - pPos.z; const distSq = dx * dx + dz * dz; if (distSq <= rangeSq) { e.hp -= damage; if (e.hp <= 0) { const deathPos = e.mesh.position.clone(); kills++; spawnXpOrb(deathPos, Math.random() < 0.15 ? 6 : 3); spawnDeathBurst(deathPos); scene.remove(e.mesh); enemies.splice(i, 1); } } } } function triggerRanged() { if (enemies.length === 0) return; const pPos = player.mesh.position; let closest = null; let bestDistSq = Infinity; for (const e of enemies) { const m = e.mesh.position; const dx = m.x - pPos.x; const dz = m.z - pPos.z; const d2 = dx * dx + dz * dz; if (d2 < bestDistSq) { bestDistSq = d2; closest = e; } } if (!closest) return; const from = new THREE.Vector3(pPos.x, 0.8, pPos.z); const to = closest.mesh.position.clone(); to.y = 0.8; const baseDir = to.clone().sub(from); baseDir.y = 0; if (baseDir.lengthSq() === 0) return; baseDir.normalize(); const projCount = weapons.ranged.projectiles; const baseSpread = 0.28; // Always fire one shot dead-center, additional shots fan around it const angleOffsets = [0]; if (projCount > 1) { const sideCount = projCount - 1; const step = sideCount === 1 ? 1 : 2 / (sideCount - 1); for (let i = 0; i < sideCount; i++) { const t = -1 + i * step; // from -1..1 angleOffsets.push(t * baseSpread); } } for (const angleOffset of angleOffsets) { const dir = baseDir.clone(); if (angleOffset !== 0) { const cos = Math.cos(angleOffset); const sin = Math.sin(angleOffset); const rx = dir.x * cos - dir.z * sin; const rz = dir.x * sin + dir.z * cos; dir.set(rx, 0, rz).normalize(); } const size = 0.22 + 0.06 * (weapons.ranged.level - 1); const projGeo = new THREE.SphereGeometry(size, 16, 16); const projMat = new THREE.MeshStandardMaterial({ color: 0x66fffe, emissive: 0x33ccff, emissiveIntensity: 1.6, metalness: 0.85, roughness: 0.08 }); const projMesh = new THREE.Mesh(projGeo, projMat); projMesh.position.copy(from); projMesh.castShadow = true; const speed = 25 * (1 + 0.05 * (weapons.ranged.level - 1)); const velocity = dir.multiplyScalar(speed); const lifetime = 2.5; const damage = weapons.ranged.damage + player.level * 4; scene.add(projMesh); projectiles.push({ mesh: projMesh, velocity, lifetime, damage, homing: false }); } } function triggerArea() { const pPos = player.mesh.position.clone(); const radius = weapons.area.radius; const baseDuration = weapons.area.duration; const duration = baseDuration + 0.15 * (weapons.area.level - 1); const geo = new THREE.CylinderGeometry(radius * 0.95, radius * 0.95, 0.2, 48, 1, true); const mat = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1.4, transparent: true, opacity: 0.4, metalness: 0.9, roughness: 0.15, side: THREE.DoubleSide }); const ring = new THREE.Mesh(geo, mat); ring.position.set(pPos.x, 0.12, pPos.z); ring.rotation.y = Math.random() * Math.PI; scene.add(ring); areaZones.push({ mesh: ring, center: pPos, radius, lifetime: duration }); } function triggerMissile() { if (!weapons.missile.unlocked || enemies.length === 0) return; const pPos = player.mesh.position; const spawnPos = new THREE.Vector3(pPos.x, 0.9, pPos.z); let closest = null; let bestDistSq = Infinity; for (const e of enemies) { const m = e.mesh.position; const dx = m.x - spawnPos.x; const dz = m.z - spawnPos.z; const d2 = dx * dx + dz * dz; if (d2 < bestDistSq) { bestDistSq = d2; closest = e; } } let dir = new THREE.Vector3(0, 0, 1); if (closest) { dir = closest.mesh.position.clone().setY(spawnPos.y).sub(spawnPos); dir.y = 0; if (dir.lengthSq() > 0) dir.normalize(); else dir.set(0, 0, 1); } const missileGeo = new THREE.CylinderGeometry(0.12, 0.2, 0.9, 12); const missileMat = new THREE.MeshStandardMaterial({ color: 0xfff06a, emissive: 0xffa800, emissiveIntensity: 1.4, metalness: 1.0, roughness: 0.2 }); const missileMesh = new THREE.Mesh(missileGeo, missileMat); missileMesh.castShadow = true; alignMeshToDirection(missileMesh, dir); missileMesh.position.copy(spawnPos); scene.add(missileMesh); const speed = weapons.missile.speed * (1 + 0.04 * weapons.missile.level); const velocity = dir.clone().multiplyScalar(speed); const lifetime = weapons.missile.lifetime; const damage = weapons.missile.damage + player.level * 5; projectiles.push({ mesh: missileMesh, velocity, lifetime, damage, homing: true, turnRate: weapons.missile.turnRate * (1 + 0.1 * weapons.missile.level), target: closest }); } function triggerDrones() { if (!player.orbiters || enemies.length === 0 || !weapons.drones.unlocked) return; const target = enemies[0]; const tPos = target.mesh.position.clone(); tPos.y = 0.8; for (const orb of player.orbiters) { const origin = orb.getWorldPosition(new THREE.Vector3()); const dir = tPos.clone().sub(origin); dir.y = 0; if (dir.lengthSq() === 0) continue; dir.normalize(); const projGeo = new THREE.SphereGeometry(0.18, 12, 12); const projMat = new THREE.MeshStandardMaterial({ color: 0x9bff6a, emissive: 0x6bff3a, emissiveIntensity: 1.4, metalness: 0.9, roughness: 0.15 }); const projMesh = new THREE.Mesh(projGeo, projMat); projMesh.position.copy(origin); projMesh.castShadow = true; const speed = weapons.drones.projectileSpeed * (1 + 0.04 * weapons.drones.level); const velocity = dir.multiplyScalar(speed); const lifetime = 1.6; const damage = weapons.drones.damage + player.level * 2; scene.add(projMesh); projectiles.push({ mesh: projMesh, velocity, lifetime, damage, homing: false }); } } function spawnXpOrb(position, value) { const orbGeo = new THREE.IcosahedronGeometry(0.25, 1); const orbMat = new THREE.MeshStandardMaterial({ color: value >= 6 ? 0xffe066 : 0x7bff9f, emissive: value >= 6 ? 0xffc733 : 0x3bffcf, emissiveIntensity: value >= 6 ? 1.7 : 1.4, metalness: 0.9, roughness: 0.05 }); const orb = new THREE.Mesh(orbGeo, orbMat); orb.castShadow = true; orb.position.copy(position); orb.position.y = 0.5; scene.add(orb); xpOrbs.push({ mesh: orb, value, radius: 0.4, bobOffset: Math.random() * Math.PI * 2 }); } function levelUp() { const wave = getWave(); player.level++; player.maxHp += 5 + Math.floor(wave * 0.5); player.hp = player.maxHp; player.xpToNext = Math.floor(player.xpToNext * 1.4 + 10 + wave * 2); pendingLevelUps++; flashScreen(); showUpgradeChoices(); } function flashScreen() { const flash = document.createElement("div"); flash.style.position = "fixed"; flash.style.left = "0"; flash.style.top = "0"; flash.style.width = "100%"; flash.style.height = "100%"; flash.style.background = "radial-gradient(circle at center, rgba(0,255,200,0.35), rgba(0,0,0,0.0))"; flash.style.pointerEvents = "none"; flash.style.zIndex = "5"; document.body.appendChild(flash); flash.animate( [{ opacity: 0 }, { opacity: 1 }, { opacity: 0 }], { duration: 350, easing: "ease-out" } ).onfinish = () => flash.remove(); } function showUpgradeChoices() { if (!running || pausedForUpgrade === true) return; pausedForUpgrade = true; const keys = Object.keys(UPGRADE_DEFS); const chosen = []; while (chosen.length < 3 && chosen.length < keys.length) { const k = keys[Math.floor(Math.random() * keys.length)]; if (!chosen.includes(k)) chosen.push(k); } upgradeOptionsDiv.innerHTML = ""; chosen.forEach(id => { const def = UPGRADE_DEFS[id]; const btn = document.createElement("button"); btn.className = "upgrade-btn"; btn.innerHTML = `${def.name}${def.desc}`; btn.addEventListener("click", () => { def.apply(); pendingLevelUps = Math.max(0, pendingLevelUps - 1); if (pendingLevelUps > 0) { showUpgradeChoices(); } else { upgradeOverlay.style.display = "none"; pausedForUpgrade = false; } }); upgradeOptionsDiv.appendChild(btn); }); upgradeOverlay.style.display = "flex"; } function animate(time) { requestAnimationFrame(animate); if (!lastTime) lastTime = time; const dt = Math.min((time - lastTime) / 1000, 0.05); lastTime = time; if (running && !pausedForUpgrade) { updateGame(dt); } const t = time / 1000; if (pillarGroup) { pillarGroup.rotation.y = t * 0.08; } if (teleportPadGroup) { teleportPadGroup.rotation.y = t * 0.6; if (teleportPadGroup.children[1]) { const ring = teleportPadGroup.children[1]; const pulse = 1 + Math.sin(t * 3) * 0.07; ring.scale.set(pulse, pulse, pulse); } } renderer.render(scene, camera); } function updateGame(dt) { elapsedTime += dt; enemySpawnTimer += dt; const wave = getWave(); const targetInterval = Math.max(0.18, 1.0 / (0.4 + wave * 0.25)); enemySpawnInterval += (targetInterval - enemySpawnInterval) * 0.08; if (enemySpawnTimer >= enemySpawnInterval) { enemySpawnTimer = 0; spawnEnemy(); } const moveVec = new THREE.Vector3(0, 0, 0); if (keys.w || keys.ArrowUp) moveVec.z -= 1; if (keys.s || keys.ArrowDown) moveVec.z += 1; if (keys.a || keys.ArrowLeft) moveVec.x -= 1; if (keys.d || keys.ArrowRight) moveVec.x += 1; if (moveVec.lengthSq() > 0) { moveVec.normalize(); moveVec.multiplyScalar(player.speed * dt); player.mesh.position.add(moveVec); player.mesh.position.x = THREE.MathUtils.clamp(player.mesh.position.x, -halfSize * 0.9, halfSize * 0.9); player.mesh.position.z = THREE.MathUtils.clamp(player.mesh.position.z, -halfSize * 0.9, halfSize * 0.9); } // Weapon timers if (weapons.melee.enabled) weapons.melee.timer += dt; if (weapons.ranged.enabled) weapons.ranged.timer += dt; if (weapons.area.enabled) weapons.area.timer += dt; weapons.missile.timer += dt; weapons.drones.timer += dt; if (weapons.melee.enabled && weapons.melee.timer >= weapons.melee.cooldown) { weapons.melee.timer = 0; triggerMelee(); } if (weapons.ranged.enabled && weapons.ranged.timer >= weapons.ranged.cooldown) { weapons.ranged.timer = 0; triggerRanged(); } if (weapons.area.enabled && weapons.area.timer >= weapons.area.cooldown) { weapons.area.timer = 0; triggerArea(); } if (weapons.missile.unlocked && weapons.missile.timer >= weapons.missile.cooldown) { weapons.missile.timer = 0; triggerMissile(); } if (weapons.drones.unlocked && weapons.drones.timer >= weapons.drones.cooldown) { weapons.drones.timer = 0; triggerDrones(); } const pPos = player.mesh.position; for (const e of enemies) { const ePos = e.mesh.position; const dir = new THREE.Vector3(pPos.x - ePos.x, 0, pPos.z - ePos.z); const dist = dir.length(); if (dist > 0.0001) { dir.normalize(); const speed = e.speed; ePos.x += dir.x * speed * dt; ePos.z += dir.z * speed * dt; } } for (let i = projectiles.length - 1; i >= 0; i--) { const pr = projectiles[i]; pr.lifetime -= dt; if (pr.homing) { updateHomingProjectile(pr, dt); } pr.mesh.position.addScaledVector(pr.velocity, dt); if (pr.lifetime <= 0) { scene.remove(pr.mesh); projectiles.splice(i, 1); continue; } const pPos3 = pr.mesh.position; for (let j = enemies.length - 1; j >= 0; j--) { const e = enemies[j]; const ePos = e.mesh.position; const dx = pPos3.x - ePos.x; const dz = pPos3.z - ePos.z; const distSq = dx * dx + dz * dz; const rad = 0.35 + e.radius; if (distSq < rad * rad) { e.hp -= pr.damage; scene.remove(pr.mesh); projectiles.splice(i, 1); if (e.hp <= 0) { const deathPos = e.mesh.position.clone(); kills++; spawnXpOrb(deathPos, Math.random() < 0.18 ? 6 : 3); spawnDeathBurst(deathPos); scene.remove(e.mesh); enemies.splice(j, 1); } break; } } } for (let i = areaZones.length - 1; i >= 0; i--) { const zone = areaZones[i]; zone.lifetime -= dt; const alpha = Math.max(0, zone.lifetime / (weapons.area.duration + 0.15 * (weapons.area.level - 1))); zone.mesh.material.opacity = 0.45 * alpha; if (zone.lifetime <= 0) { scene.remove(zone.mesh); areaZones.splice(i, 1); continue; } const center = zone.center; center.copy(player.mesh.position); zone.mesh.position.set(center.x, 0.12, center.z); const radiusSq = zone.radius * zone.radius; for (const e of enemies) { const ePos = e.mesh.position; const dx = ePos.x - center.x; const dz = ePos.z - center.z; const distSq = dx * dx + dz * dz; if (distSq <= radiusSq) { e.hp -= weapons.area.damagePerSecond * dt; } } } for (let i = enemies.length - 1; i >= 0; i--) { const e = enemies[i]; if (e.hp <= 0) { const pos = e.mesh.position.clone(); kills++; spawnXpOrb(pos, Math.random() < 0.18 ? 6 : 3); spawnDeathBurst(pos); scene.remove(e.mesh); enemies.splice(i, 1); } } for (let i = xpOrbs.length - 1; i >= 0; i--) { const orb = xpOrbs[i]; const pos = orb.mesh.position; orb.bobOffset += dt * 3; pos.y = 0.4 + Math.sin(orb.bobOffset) * 0.1; const dx = pos.x - pPos.x; const dz = pos.z - pPos.z; const dist = Math.sqrt(dx * dx + dz * dz); const magnetRadius = player.pickupRadius; if (dist < magnetRadius && dist > 0.0001) { const dir = new THREE.Vector3(-dx / dist, 0, -dz / dist); const pullSpeed = 8 + player.level * 0.5; pos.addScaledVector(dir, pullSpeed * dt); } if (dist < player.radius + orb.radius) { player.xp += orb.value; scene.remove(orb.mesh); xpOrbs.splice(i, 1); while (player.xp >= player.xpToNext) { player.xp -= player.xpToNext; levelUp(); } } } for (let i = deathParticles.length - 1; i >= 0; i--) { const p = deathParticles[i]; p.lifetime -= dt; if (p.lifetime <= 0) { scene.remove(p.mesh); deathParticles.splice(i, 1); continue; } p.mesh.position.addScaledVector(p.velocity, dt); p.mesh.position.y -= dt * 3; } for (let i = slashEffects.length - 1; i >= 0; i--) { const s = slashEffects[i]; s.life -= dt; if (s.life <= 0) { scene.remove(s.mesh); slashEffects.splice(i, 1); continue; } const t = 1 - s.life / s.maxLife; s.mesh.material.opacity = s.maxOpacity * (1 - t); const scale = 1 + 0.15 * t; s.mesh.scale.set(scale, scale, scale); } if (player.orbiters) { player.orbitAngle += dt * 1.5; const radius = 1.2; const y = 0.4; for (let i = 0; i < player.orbiters.length; i++) { const angle = player.orbitAngle + i * Math.PI; const orb = player.orbiters[i]; orb.position.x = Math.cos(angle) * radius; orb.position.z = Math.sin(angle) * radius; orb.position.y = y; } } // Collision damage from enemies for (const e of enemies) { const ePos = e.mesh.position; const dx = ePos.x - pPos.x; const dz = ePos.z - pPos.z; const distSq = dx * dx + dz * dz; const rad = player.radius + e.radius * 0.7; if (distSq < rad * rad) { const baseDps = 13 + getWave() * 2; player.hp -= baseDps * dt; } } // Base healing when standing on the central pad const baseRadius = 2.3; const baseDistSq = pPos.x * pPos.x + pPos.z * pPos.z; if (baseDistSq < baseRadius * baseRadius && player.hp > 0) { const healRate = 6 + getWave(); player.hp = Math.min(player.maxHp, player.hp + healRate * dt); } if (player.hp <= 0) { player.hp = 0; gameOver(); } updateUI(); } function updateHomingProjectile(pr, dt) { if (!pr.target || !enemies.includes(pr.target)) { // reacquire closest enemy let closest = null; let bestDistSq = Infinity; const pos = pr.mesh.position; for (const e of enemies) { const m = e.mesh.position; const dx = m.x - pos.x; const dz = m.z - pos.z; const d2 = dx * dx + dz * dz; if (d2 < bestDistSq) { bestDistSq = d2; closest = e; } } pr.target = closest || null; } if (!pr.target) return; const pos = pr.mesh.position; const targetPos = pr.target.mesh.position.clone(); targetPos.y = pos.y; const desiredDir = targetPos.clone().sub(pos); desiredDir.y = 0; if (desiredDir.lengthSq() === 0) return; desiredDir.normalize(); const vel = pr.velocity.clone(); const speed = vel.length() || weapons.missile.speed; if (speed === 0) return; vel.y = 0; vel.normalize(); const maxTurn = pr.turnRate * dt; let dot = THREE.MathUtils.clamp(vel.dot(desiredDir), -1, 1); let angle = Math.acos(dot); if (angle < 1e-3) { pr.velocity.copy(desiredDir.multiplyScalar(speed)); } else { const t = Math.min(1, maxTurn / angle); const newDir = vel.lerp(desiredDir, t).normalize(); pr.velocity.copy(newDir.multiplyScalar(speed)); } alignMeshToDirection(pr.mesh, pr.velocity.clone().normalize()); } function alignMeshToDirection(mesh, dir) { const up = new THREE.Vector3(0, 1, 0); const forward = dir.clone().normalize(); const right = new THREE.Vector3().crossVectors(up, forward).normalize(); const adjustedUp = new THREE.Vector3().crossVectors(forward, right).normalize(); const m = new THREE.Matrix4().makeBasis(right, adjustedUp, forward); mesh.quaternion.setFromRotationMatrix(m); } function spawnDeathBurst(position) { const count = 18; for (let i = 0; i < count; i++) { const geo = new THREE.SphereGeometry(0.06 + Math.random() * 0.05, 8, 8); const mat = new THREE.MeshStandardMaterial({ color: 0xff6a6a, emissive: i % 3 === 0 ? 0xfff066 : 0xff3a3a, emissiveIntensity: 1.3, metalness: 0.7, roughness: 0.25 }); const mesh = new THREE.Mesh(geo, mat); mesh.position.copy(position); mesh.position.y = 0.7 + Math.random() * 0.3; const dir = new THREE.Vector3( (Math.random() - 0.5) * 2, (Math.random() * 0.6) + 0.1, (Math.random() - 0.5) * 2 ).normalize().multiplyScalar(4 + Math.random() * 3); const life = 0.35 + Math.random() * 0.25; scene.add(mesh); deathParticles.push({ mesh, velocity: dir, lifetime: life }); } } function updateUI() { hpText.textContent = `${Math.round(player.hp)} / ${player.maxHp}`; levelText.textContent = player.level; xpText.textContent = `${player.xp} / ${player.xpToNext}`; const hpRatio = THREE.MathUtils.clamp(player.hp / player.maxHp, 0, 1); const xpRatio = THREE.MathUtils.clamp(player.xp / player.xpToNext, 0, 1); hpBar.style.width = (hpRatio * 100) + "%"; xpBar.style.width = (xpRatio * 100) + "%"; timeText.textContent = formatTime(elapsedTime); waveText.textContent = getWave(); killsText.textContent = kills; enemiesText.textContent = enemies.length; meleeText.textContent = weapons.melee.enabled ? "Lv " + weapons.melee.level : "Off"; rangedText.textContent = weapons.ranged.enabled ? "Lv " + weapons.ranged.level : "Off"; areaText.textContent = weapons.area.enabled ? "Lv " + weapons.area.level : "Off"; missileText.textContent = weapons.missile.unlocked ? "Lv " + weapons.missile.level : "Off"; dronesText.textContent = weapons.drones.unlocked ? "Lv " + weapons.drones.level : "Off"; } function formatTime(t) { const total = Math.floor(t); const m = Math.floor(total / 60); const s = total % 60; return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; } function getWave() { return Math.max(1, Math.floor(elapsedTime / 30) + 1); } function loadHighScores() { try { const raw = localStorage.getItem("neon_void_highscores_v1"); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function saveHighScores() { try { localStorage.setItem("neon_void_highscores_v1", JSON.stringify(highScores)); } catch { // ignore } } function recordHighScore() { const entry = { score: kills, time: elapsedTime, level: player.level, wave: getWave(), timestamp: Date.now() }; highScores.push(entry); highScores.sort((a, b) => { if (b.score !== a.score) return b.score - a.score; return b.time - a.time; }); highScores = highScores.slice(0, 10); saveHighScores(); return highScores; } function gameOver() { running = false; pausedForUpgrade = false; upgradeOverlay.style.display = "none"; const scores = recordHighScore(); let tableHtml = ""; if (scores.length > 0) { tableHtml = '
| # | Kills | Time | Wave | Lv |
|---|---|---|---|---|
| ${i + 1} | ${s.score} | ${formatTime( s.time )} | ${s.wave} | ${s.level} |
You survived for ${formatTime(elapsedTime)} and defeated ${kills} enemies.
Final Wave: ${getWave()} • Final Level: ${player.level}
${tableHtml} `; document.getElementById("restartBtn").addEventListener("click", resetGame); } function resetGame() { for (const e of enemies) scene.remove(e.mesh); enemies = []; for (const p of projectiles) scene.remove(p.mesh); projectiles = []; for (const orb of xpOrbs) scene.remove(orb.mesh); xpOrbs = []; for (const z of areaZones) scene.remove(z.mesh); areaZones = []; for (const p of deathParticles) scene.remove(p.mesh); deathParticles = []; for (const s of slashEffects) scene.remove(s.mesh); slashEffects = []; player.mesh.position.set(0, 0.6, 0); player.hp = 100; player.maxHp = 100; player.xp = 0; player.level = 1; player.xpToNext = 15; player.speed = 11; player.pickupRadius = 6; weapons.melee.level = 1; weapons.melee.damage = 18; weapons.melee.cooldown = 1.4; weapons.melee.range = 2.2; weapons.melee.timer = 0; weapons.melee.enabled = false; weapons.ranged.level = 1; weapons.ranged.damage = 20; weapons.ranged.cooldown = 0.9; weapons.ranged.projectiles = 1; weapons.ranged.timer = 0; weapons.ranged.enabled = true; weapons.area.level = 1; weapons.area.damagePerSecond = 14; weapons.area.cooldown = 4.0; weapons.area.radius = 4.0; weapons.area.duration = 0.8; weapons.area.timer = 0; weapons.area.enabled = false; weapons.missile.unlocked = false; weapons.missile.level = 0; weapons.missile.damage = 40; weapons.missile.cooldown = 3.0; weapons.missile.timer = 0; weapons.missile.speed = 18; weapons.missile.turnRate = 5.0; weapons.missile.lifetime = 4.0; weapons.drones.unlocked = false; weapons.drones.level = 0; weapons.drones.damage = 10; weapons.drones.cooldown = 1.1; weapons.drones.timer = 0; weapons.drones.projectileSpeed = 26; kills = 0; elapsedTime = 0; enemySpawnTimer = 0; enemySpawnInterval = 1.0; pendingLevelUps = 0; lastTime = 0; centerMessage.style.display = "none"; running = true; pausedForUpgrade = false; updateUI(); } function onWindowResize() { const width = window.innerWidth; const height = window.innerHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); } // Input window.addEventListener("keydown", (e) => { if (e.key in keys) { keys[e.key] = true; } }); window.addEventListener("keyup", (e) => { if (e.key in keys) { keys[e.key] = false; } }); startBtn.addEventListener("click", () => { if (!running) { resetGame(); } }); // Kickoff initThree(); updateUI();