Spaces:
Running
Running
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); | |
} | |
} | |
} | |