orcs-in-the-forest / src /helmets.js
Codex CLI
Initial commit: Orcs In The Forest
1390db3
import * as THREE from 'three';
import { G } from './globals.js';
import { getTerrainHeight } from './world.js';
// Pop the enemy's helmet off and add simple physics so it drops to ground
export function popHelmet(enemy, impulseDir = new THREE.Vector3(0, 1, 0), hitPoint = null) {
if (!enemy || !enemy.helmet || !enemy.helmetAttached) return;
const h = enemy.helmet;
// Get world transform before detaching
const worldPos = new THREE.Vector3();
const worldQuat = new THREE.Quaternion();
const worldScale = new THREE.Vector3();
h.updateMatrixWorld();
h.getWorldPosition(worldPos);
h.getWorldQuaternion(worldQuat);
h.getWorldScale(worldScale);
// Detach from enemy and add to scene root
if (h.parent) h.parent.remove(h);
h.position.copy(worldPos);
h.quaternion.copy(worldQuat);
h.scale.copy(worldScale);
G.scene.add(h);
// It should no longer count as enemy geometry for ray hits
if (h.userData) {
h.userData.enemy = null;
h.userData.hitZone = null;
h.userData.isHelmet = true;
}
// Initial velocity: away from shot direction with a fun hop up
const dir = impulseDir.clone().normalize();
const upBoost = 3 + G.random() * 1.5;
const sideJitter = new THREE.Vector3((G.random() - 0.5) * 1.5, 0, (G.random() - 0.5) * 1.5);
const vel = dir.multiplyScalar(2.2).add(new THREE.Vector3(0, upBoost, 0)).add(sideJitter);
// Angular velocity for comedic spin
const angVel = new THREE.Vector3(
(G.random() - 0.5) * 6,
(G.random() - 0.5) * 8,
(G.random() - 0.5) * 6
);
G.helmets.push({
mesh: h,
pos: h.position,
vel,
angVel,
life: 12, // fade after a while
grounded: false
});
enemy.helmetAttached = false;
}
export function updateHelmets(delta) {
const gravity = 14; // stronger than arrows for punchy drop
const bounce = 0.35;
for (let i = G.helmets.length - 1; i >= 0; i--) {
const h = G.helmets[i];
// Integrate
h.vel.y -= gravity * delta;
h.pos.addScaledVector(h.vel, delta);
// Simple rotation integration
if (h.angVel) {
h.mesh.rotateX(h.angVel.x * delta);
h.mesh.rotateY(h.angVel.y * delta);
h.mesh.rotateZ(h.angVel.z * delta);
}
// Ground collision and bounce against terrain
const groundY = getTerrainHeight(h.pos.x, h.pos.z);
if (h.pos.y <= groundY) {
if (!h.grounded) {
// First impact gets a stronger bounce
h.grounded = true;
}
h.pos.y = groundY;
if (Math.abs(h.vel.y) > 0.4) {
h.vel.y = -h.vel.y * bounce;
} else {
h.vel.y = 0;
}
// Friction on ground
h.vel.x *= 0.7;
h.vel.z *= 0.7;
// Damp spin as it settles
if (h.angVel) h.angVel.multiplyScalar(0.8);
if (Math.hypot(h.vel.x, h.vel.z) < 0.2 && Math.abs(h.vel.y) < 0.2) {
h.vel.set(0, 0, 0);
if (h.angVel) h.angVel.set(0, 0, 0);
}
}
// Lifetime fade/cleanup
h.life -= delta;
if (h.life <= 2) {
const m = h.mesh.material;
if (m && m.opacity !== undefined) {
m.transparent = true;
m.opacity = Math.max(0, h.life / 2);
}
}
if (h.life <= 0) {
// Dispose per-helmet geometries
h.mesh.traverse((obj) => { if (obj.isMesh && obj.geometry?.dispose) obj.geometry.dispose(); });
G.scene.remove(h.mesh);
G.helmets.splice(i, 1);
}
}
}