destroyulator/web/hands.js

74 lines
2.5 KiB
JavaScript

// hands.js — first-person sci-fi hands (FAB "Sci Fi Hands" pack, merged to one GLB
// with clips: Idle, Idle_Fight, Attack_01-04, Energy_Attack_01, Get, Walk_01, Run_01).
// Camera-parented viewmodel: idle/walk bob from the pack's own clips, punch on smash.
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const TUNE = {
pos: new THREE.Vector3(0, -1.55, 0.25),
rotY: Math.PI, // pack forward is +Z; camera looks down -Z
scale: 1.0,
};
const ATTACKS = ['Attack_01', 'Attack_02', 'Attack_03', 'Attack_04'];
export async function createHands(camera) {
const gltf = await new GLTFLoader().loadAsync('assets/hands.glb');
const rig = gltf.scene;
rig.position.copy(TUNE.pos);
rig.rotation.y = TUNE.rotY;
rig.scale.setScalar(TUNE.scale);
// render on top-ish: keep depth test but bump renderOrder so close geometry
// rarely swallows the arms; frustum culling off (always in view).
rig.traverse(o => { if (o.isMesh) { o.frustumCulled = false; o.renderOrder = 5; } });
camera.add(rig);
const mixer = new THREE.AnimationMixer(rig);
const clips = {};
for (const c of gltf.animations) clips[c.name] = mixer.clipAction(c);
const has = n => Object.prototype.hasOwnProperty.call(clips, n);
let current = null;
let attacking = false;
function play(name, { once = false, fade = 0.12 } = {}) {
if (!has(name)) return null;
const a = clips[name];
if (current === a && !once) return a;
if (current) current.fadeOut(fade);
a.reset().fadeIn(fade);
if (once) { a.setLoop(THREE.LoopOnce); a.clampWhenFinished = true; }
a.play();
current = a;
return a;
}
mixer.addEventListener('finished', () => {
attacking = false;
play(lastLocomotion, { fade: 0.15 });
});
let lastLocomotion = 'Idle';
// draw the hands on boot if the pack has a Get clip
if (has('Get')) { attacking = true; play('Get', { once: true }); }
else play('Idle');
return {
rig,
tune: TUNE,
update(dt, moving = false, running = false) {
const want = running ? 'Run_01' : moving ? 'Walk_01' : 'Idle';
lastLocomotion = has(want) ? want : 'Idle';
if (!attacking) play(lastLocomotion, { fade: 0.18 });
mixer.update(dt);
},
attack() {
attacking = true;
play(ATTACKS[Math.floor(Math.random() * ATTACKS.length)], { once: true, fade: 0.05 });
},
special() {
attacking = true;
play('Energy_Attack_01', { once: true, fade: 0.05 });
},
};
}