orcs-in-the-forest / src /casings.js
Codex CLI
Initial commit: Orcs In The Forest
1390db3
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);
}
}
}