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