Spaces:
Running
Running
import * as THREE from 'three'; | |
import { G } from './globals.js'; | |
// Shared casing geometry/materials to avoid per-shot allocations | |
const CASING = (() => { | |
const bodyGeo = new THREE.CylinderGeometry(0.018, 0.018, 0.12, 10); | |
const capGeo = new THREE.CylinderGeometry(0.018, 0.018, 0.01, 10); | |
const brass = new THREE.MeshStandardMaterial({ color: 0xb48a3a, metalness: 0.7, roughness: 0.35 }); | |
const capMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.2, roughness: 0.7 }); | |
return { bodyGeo, capGeo, brass, capMat }; | |
})(); | |
const MAX_CASINGS = 40; | |
// Spawn a brass shell casing at the weapon's ejector anchor | |
export function spawnShellCasing() { | |
if (!G.weapon || !G.weapon.ejector) return; | |
const anchor = G.weapon.ejector; | |
const pos = new THREE.Vector3(); | |
const q = new THREE.Quaternion(); | |
anchor.getWorldPosition(pos); | |
anchor.getWorldQuaternion(q); | |
// Visual: small brass cylinder + dark cap | |
const group = new THREE.Group(); | |
// Cylinder aligned along X: use rotation | |
const body = new THREE.Mesh(CASING.bodyGeo, CASING.brass); | |
body.rotation.z = Math.PI / 2; | |
body.castShadow = false; body.receiveShadow = false; | |
group.add(body); | |
const cap = new THREE.Mesh(CASING.capGeo, CASING.capMat); | |
cap.position.x = 0.06; | |
cap.rotation.z = Math.PI / 2; | |
cap.castShadow = false; cap.receiveShadow = false; | |
group.add(cap); | |
group.position.copy(pos); | |
G.scene.add(group); | |
// Orientation basis from weapon | |
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(q).normalize(); | |
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(q).normalize(); | |
const fwd = new THREE.Vector3(0, 0, -1).applyQuaternion(q).normalize(); | |
// Ejection velocity: mostly right, a bit up, slightly backward | |
const baseRight = 2.0 + G.random() * 1.2; | |
const baseUp = 1.6 + G.random() * 0.8; | |
const back = 0.6 + G.random() * 0.6; | |
const jitter = new THREE.Vector3((G.random() - 0.5) * 0.4, (G.random() - 0.5) * 0.4, (G.random() - 0.5) * 0.4); | |
const vel = new THREE.Vector3() | |
.addScaledVector(right, baseRight) | |
.addScaledVector(up, baseUp) | |
.addScaledVector(fwd, -back) | |
.add(jitter); | |
// Random angular velocity for spin | |
const angVel = new THREE.Vector3( | |
(G.random() - 0.5) * 20, | |
(G.random() - 0.5) * 30, | |
(G.random() - 0.5) * 20 | |
); | |
// Keep list bounded to avoid unbounded accumulation | |
if (G.casings.length >= MAX_CASINGS) { | |
const old = G.casings.shift(); | |
if (old) G.scene.remove(old.mesh); | |
} | |
G.casings.push({ | |
mesh: group, | |
pos: group.position, | |
vel, | |
angVel, | |
life: 6, | |
grounded: false | |
}); | |
} | |
export function updateCasings(delta) { | |
const gravity = 20; | |
const bounce = 0.25; | |
for (let i = G.casings.length - 1; i >= 0; i--) { | |
const c = G.casings[i]; | |
// Integrate | |
c.vel.y -= gravity * delta; | |
c.pos.addScaledVector(c.vel, delta); | |
// Spin | |
if (c.angVel) { | |
c.mesh.rotateX(c.angVel.x * delta); | |
c.mesh.rotateY(c.angVel.y * delta); | |
c.mesh.rotateZ(c.angVel.z * delta); | |
} | |
// Ground collision (approximate at y ~ body radius) | |
const floor = 0.02; | |
if (c.pos.y <= floor) { | |
c.pos.y = floor; | |
if (Math.abs(c.vel.y) > 0.3) { | |
c.vel.y = -c.vel.y * bounce; | |
} else { | |
c.vel.y = 0; | |
} | |
// Horizontal friction | |
c.vel.x *= 0.75; | |
c.vel.z *= 0.75; | |
// Dampen spin | |
if (c.angVel) c.angVel.multiplyScalar(0.88); | |
} | |
// Lifetime fade and cleanup | |
c.life -= delta; | |
if (c.life <= 1.5) { | |
for (const child of c.mesh.children) { | |
const m = child.material; | |
if (m && m.opacity !== undefined) { | |
m.transparent = true; | |
m.opacity = Math.max(0, c.life / 1.5); | |
} | |
} | |
} | |
if (c.life <= 0) { | |
G.scene.remove(c.mesh); | |
G.casings.splice(i, 1); | |
} | |
} | |
} | |