141 lines
3.2 KiB
TypeScript
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();
|
|
}
|
|
}
|