Spaces:
Running
Running
Codex CLI
feat(fx, pickups, projectiles): remove dynamic lights to optimize performance and shader stability
ed36a74
import * as THREE from 'three'; | |
import { CFG } from './config.js'; | |
import { G } from './globals.js'; | |
export function spawnTracer(from, to) { | |
const geo = new THREE.BufferGeometry().setFromPoints([from.clone(), to.clone()]); | |
const mat = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.85, depthTest: false }); | |
const line = new THREE.Line(geo, mat); | |
line.renderOrder = 9; | |
G.scene.add(line); | |
G.fx.tracers.push({ mesh: line, life: CFG.fx.tracerLife }); | |
} | |
export function spawnTracerColored(from, to, color = 0xff4444, opacity = 0.85) { | |
const geo = new THREE.BufferGeometry().setFromPoints([from.clone(), to.clone()]); | |
const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity, depthTest: false }); | |
const line = new THREE.Line(geo, mat); | |
line.renderOrder = 9; | |
G.scene.add(line); | |
G.fx.tracers.push({ mesh: line, life: CFG.fx.tracerLife }); | |
} | |
export function spawnImpact(point, normal) { | |
const plane = new THREE.Mesh( | |
new THREE.PlaneGeometry(0.15, 0.15), | |
new THREE.MeshBasicMaterial({ color: 0xffbb55, transparent: true, opacity: 0.9, depthTest: true }) | |
); | |
plane.position.copy(point); | |
plane.lookAt(G.camera.position); | |
G.scene.add(plane); | |
G.fx.impacts.push({ mesh: plane, life: CFG.fx.impactLife }); | |
} | |
export function spawnMuzzleFlash() { | |
if (!G.weapon.muzzle) return; | |
const quad = new THREE.Mesh( | |
new THREE.PlaneGeometry(0.2, 0.2), | |
new THREE.MeshBasicMaterial({ color: 0xffe070, transparent: true, opacity: 1.0, depthTest: false }) | |
); | |
G.weapon.muzzle.getWorldPosition(quad.position); | |
quad.lookAt(G.camera.position); | |
quad.renderOrder = 11; | |
G.scene.add(quad); | |
// Avoid dynamic lights to prevent shader recompiles | |
G.fx.flashes.push({ mesh: quad, light: null, life: CFG.fx.muzzleLife }); | |
} | |
export function spawnMuzzleFlashAt(worldPos, color = 0xffc060) { | |
const quad = new THREE.Mesh( | |
new THREE.PlaneGeometry(0.18, 0.18), | |
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 1.0, depthTest: false }) | |
); | |
quad.position.copy(worldPos); | |
quad.lookAt(G.camera.position); | |
quad.renderOrder = 11; | |
G.scene.add(quad); | |
G.fx.flashes.push({ mesh: quad, light: null, life: CFG.fx.muzzleLife }); | |
} | |
// Quick, subtle dust puff at a world position | |
export function spawnDustAt(worldPos, color = 0xcdbf9e, size = 0.55, life = 0.14) { | |
const quad = new THREE.Mesh( | |
new THREE.PlaneGeometry(size, size), | |
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.85, depthTest: true }) | |
); | |
quad.position.copy(worldPos); | |
quad.position.y += 0.15; // lift slightly off the ground | |
quad.lookAt(G.camera.position); | |
quad.renderOrder = 8; | |
G.scene.add(quad); | |
G.fx.dusts.push({ mesh: quad, life, maxLife: life }); | |
} | |
// Portal effect: additive ring that grows and fades, plus soft light | |
export function spawnPortalAt(worldPos, color = 0xff5522, size = 1.1, life = 0.35) { | |
const ring = new THREE.Mesh( | |
new THREE.TorusGeometry(size * 0.35, size * 0.08, 12, 28), | |
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.95, blending: THREE.AdditiveBlending, depthWrite: false }) | |
); | |
ring.position.copy(worldPos); | |
ring.rotation.x = Math.PI / 2; | |
ring.renderOrder = 12; | |
const flare = new THREE.Mesh( | |
new THREE.PlaneGeometry(size * 0.9, size * 0.9), | |
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending, depthWrite: false }) | |
); | |
flare.position.copy(worldPos); | |
flare.lookAt(G.camera.position); | |
flare.renderOrder = 12; | |
G.scene.add(ring); | |
G.scene.add(flare); | |
G.fx.portals.push({ ring, flare, light: null, life, maxLife: life, rot: Math.random() * Math.PI * 2 }); | |
} | |
// Grenade explosion: additive glow sphere + shock ring + light | |
export function spawnExplosionAt(worldPos, radius = 6) { | |
const glow = new THREE.Mesh( | |
new THREE.SphereGeometry(radius * 0.4, 18, 14), | |
new THREE.MeshBasicMaterial({ color: 0xffaa55, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false }) | |
); | |
glow.position.copy(worldPos); | |
const ring = new THREE.Mesh( | |
new THREE.TorusGeometry(radius * 0.25, radius * 0.08, 12, 28), | |
new THREE.MeshBasicMaterial({ color: 0xffdd99, transparent: true, opacity: 0.85, blending: THREE.AdditiveBlending, depthWrite: false }) | |
); | |
ring.position.copy(worldPos); | |
ring.rotation.x = Math.PI / 2; | |
G.scene.add(glow); | |
G.scene.add(ring); | |
G.explosions.push({ glow, ring, light: null, life: 0.5, maxLife: 0.5 }); | |
} | |
export function updateFX(delta) { | |
for (let i = G.fx.tracers.length - 1; i >= 0; i--) { | |
const t = G.fx.tracers[i]; | |
t.life -= delta; | |
t.mesh.material.opacity = Math.max(0, t.life / CFG.fx.tracerLife); | |
if (t.life <= 0) { | |
G.scene.remove(t.mesh); | |
t.mesh.geometry.dispose(); | |
if (t.mesh.material && t.mesh.material.dispose) t.mesh.material.dispose(); | |
G.fx.tracers.splice(i, 1); | |
} | |
} | |
for (let i = G.fx.impacts.length - 1; i >= 0; i--) { | |
const s = G.fx.impacts[i]; | |
s.life -= delta; | |
s.mesh.material.opacity = Math.max(0, s.life / CFG.fx.impactLife); | |
s.mesh.scale.setScalar(1 + (1 - s.life / CFG.fx.impactLife) * 0.5); | |
if (s.life <= 0) { | |
G.scene.remove(s.mesh); | |
s.mesh.geometry.dispose(); | |
if (s.mesh.material && s.mesh.material.dispose) s.mesh.material.dispose(); | |
G.fx.impacts.splice(i, 1); | |
} | |
} | |
for (let i = G.fx.flashes.length - 1; i >= 0; i--) { | |
const m = G.fx.flashes[i]; | |
m.life -= delta; | |
m.mesh.material.opacity = Math.max(0, m.life / CFG.fx.muzzleLife); | |
m.mesh.scale.setScalar(1 + (1 - m.life / CFG.fx.muzzleLife) * 0.6); | |
if (m.life <= 0) { | |
G.scene.remove(m.mesh); | |
m.mesh.geometry.dispose(); | |
if (m.mesh.material && m.mesh.material.dispose) m.mesh.material.dispose(); | |
G.fx.flashes.splice(i, 1); | |
} | |
} | |
// Dust puffs: very short-lived billboards that expand and fade | |
for (let i = G.fx.dusts.length - 1; i >= 0; i--) { | |
const d = G.fx.dusts[i]; | |
d.life -= delta; | |
const t = Math.max(0, d.life / d.maxLife); | |
d.mesh.material.opacity = t; | |
d.mesh.scale.setScalar(1 + (1 - t) * 0.8); | |
d.mesh.lookAt(G.camera.position); | |
if (d.life <= 0) { | |
G.scene.remove(d.mesh); | |
d.mesh.geometry.dispose(); | |
if (d.mesh.material && d.mesh.material.dispose) d.mesh.material.dispose(); | |
G.fx.dusts.splice(i, 1); | |
} | |
} | |
// Portals | |
for (let i = G.fx.portals.length - 1; i >= 0; i--) { | |
const p = G.fx.portals[i]; | |
p.life -= delta; | |
const t = Math.max(0, p.life / p.maxLife); | |
const s = 1 + (1 - t) * 1.6; | |
p.rot += delta * 4; | |
p.ring.rotation.z = p.rot; | |
p.ring.material.opacity = 0.25 + 0.7 * t; | |
p.ring.scale.setScalar(s); | |
p.flare.material.opacity = 0.15 + 0.45 * t; | |
p.flare.scale.setScalar(s * 1.2); | |
p.flare.lookAt(G.camera.position); | |
if (p.life <= 0) { | |
G.scene.remove(p.ring); | |
G.scene.remove(p.flare); | |
p.ring.geometry.dispose(); | |
p.flare.geometry.dispose(); | |
if (p.ring.material && p.ring.material.dispose) p.ring.material.dispose(); | |
if (p.flare.material && p.flare.material.dispose) p.flare.material.dispose(); | |
G.fx.portals.splice(i, 1); | |
} | |
} | |
// Explosions | |
for (let i = G.explosions.length - 1; i >= 0; i--) { | |
const e = G.explosions[i]; | |
e.life -= delta; | |
const t = Math.max(0, e.life / e.maxLife); | |
const s = 1 + (1 - t) * 2.2; | |
if (e.glow) { | |
e.glow.material.opacity = 0.3 + 0.7 * t; | |
e.glow.scale.setScalar(s); | |
} | |
if (e.ring) { | |
e.ring.material.opacity = 0.2 + 0.7 * t; | |
e.ring.scale.setScalar(0.9 + (1 - t) * 2.6); | |
e.ring.rotation.z += delta * 2.5; | |
} | |
if (e.life <= 0) { | |
if (e.glow) { G.scene.remove(e.glow); e.glow.geometry.dispose(); if (e.glow.material?.dispose) e.glow.material.dispose(); } | |
if (e.ring) { G.scene.remove(e.ring); e.ring.geometry.dispose(); if (e.ring.material?.dispose) e.ring.material.dispose(); } | |
G.explosions.splice(i, 1); | |
} | |
} | |
} | |