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