74 lines
2.5 KiB
JavaScript
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 });
|
|
},
|
|
};
|
|
}
|