freeblox/packages/engine/src/gameobjects/humanoid.object.ts

141 lines
3.2 KiB
TypeScript

import {
AnimationAction,
AnimationClip,
AnimationMixer,
Object3D,
Skeleton,
SkinnedMesh,
} from 'three';
import { GameObject } from '../types/game-object';
import { Ticking } from '../types/ticking';
import { EditorProperty } from '../decorators/property';
import { MeshPart } from './mesh.object';
import { clamp } from 'three/src/math/MathUtils.js';
export type HumanoidBodyPart =
| 'Head'
| 'Torso'
| 'ArmLeft'
| 'ArmRight'
| 'LegRight'
| 'LegLeft';
export class Humanoid extends GameObject implements Ticking {
public isTickingObject = true;
public objectType = Humanoid.name;
public name = Humanoid.name;
private ready = false;
private skeleton!: Skeleton;
private _health = 100;
private _maxHealth = 100;
public static bodyPartNames = [
'Head',
'Torso',
'ArmLeft',
'ArmRight',
'LegRight',
'LegLeft',
] as HumanoidBodyPart[];
public static bodyBoneNames = [
'BHead',
'BTorso',
'BArmL',
'BArmR',
'BLegR',
'BLegL',
] as string[];
public static bodyAnimationNames = ['Idle', 'Walk'];
private mixer!: AnimationMixer;
@EditorProperty({ type: Number })
get health() {
return this._health;
}
set health(value: number) {
const health = clamp(Math.floor(value), 0, this.maxHealth);
if (health === 0) this.die();
this.health = health;
}
@EditorProperty({ type: Number })
get maxHealth() {
return this._maxHealth;
}
set maxHealth(value: number) {
const maxHealth = Math.floor(value);
if (this.health > maxHealth) {
this.health = maxHealth;
}
this._maxHealth = maxHealth;
}
get alive() {
return this.health > 0;
}
private get bodyParts() {
return Humanoid.bodyPartNames.map((key) => this.getBodyPartByName(key));
}
getBodyPartByName(name: string) {
return this.parent?.getObjectByName(name) as MeshPart;
}
getBoneForBodyPart(part: HumanoidBodyPart) {
return this.skeleton.getBoneByName(
Humanoid.bodyBoneNames[Humanoid.bodyPartNames.indexOf(part)]
);
}
initialize(): void {
if (!this.parent)
throw new Error('Cannot initialize Humanoid to empty parent');
this.skeleton = (this.bodyParts[0]?.getMesh() as SkinnedMesh).skeleton;
if (!this.skeleton) throw new Error('Could not find Skeleton for Humanoid');
this.skeleton.pose();
this.mixer = new AnimationMixer(this.parent);
this.ready = true;
const clip = AnimationClip.findByName(this.parent.animations, 'Idle');
const action = this.mixer.clipAction(clip);
action.play();
}
tick(dt: number): void {
if (!this.ready) return;
this.mixer.update(dt);
}
detach(bodyPart?: HumanoidBodyPart) {
if (!bodyPart) {
Humanoid.bodyPartNames.forEach((part) => this.detach(part));
return;
}
const part = this.getBodyPartByName(bodyPart);
if (!part) return;
const partMesh = part.getMesh() as SkinnedMesh;
if (partMesh.bindMode === 'detached') return;
partMesh.bindMode = 'detached';
partMesh.updateMatrixWorld(true);
const bone = this.getBoneForBodyPart(bodyPart);
if (!bone) return;
bone.removeFromParent();
this.skeleton.update();
}
die() {
if (!this.alive) return;
this.health = 0;
this.detach();
}
}