199 lines
5.4 KiB
TypeScript
199 lines
5.4 KiB
TypeScript
import { Quaternion, Vector3 } from 'three';
|
|
import type Rapier from '@dimforge/rapier3d-compat';
|
|
import { PhysicsTicking } from 'src/physics';
|
|
import { GameObject } from './game-object';
|
|
import { ObjectProperty } from 'src/types/property.decorator';
|
|
import { clamp } from 'src/utils/clamp';
|
|
|
|
export class Humanoid extends GameObject implements PhysicsTicking {
|
|
public isTickingObject = true;
|
|
public objectType = 'Humanoid';
|
|
public name = 'Humanoid';
|
|
protected ready = false;
|
|
|
|
public characterHeight = 5.5;
|
|
public characterHalfHeight = this.characterHeight / 2;
|
|
|
|
protected shouldJump = false;
|
|
private _health = 100;
|
|
private _maxHealth = 100;
|
|
private _velocity = new Vector3(0, 0, 0);
|
|
private _appliedGravity = new Vector3(0, 0, 0);
|
|
private _grounded = true;
|
|
private _lookAt = new Vector3(0, 0, 1);
|
|
private _currentLookAt = new Vector3(0, 0, 1);
|
|
private _animState = 0;
|
|
|
|
protected collider?: Rapier.Collider;
|
|
protected rigidBody?: Rapier.RigidBody;
|
|
protected physicsWorldRef?: Rapier.World;
|
|
protected characterControllerRef?: Rapier.KinematicCharacterController;
|
|
|
|
@ObjectProperty()
|
|
public mass = -8;
|
|
|
|
@ObjectProperty()
|
|
public jumpPower = 8;
|
|
|
|
@ObjectProperty()
|
|
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;
|
|
}
|
|
|
|
@ObjectProperty()
|
|
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;
|
|
}
|
|
|
|
get grounded() {
|
|
return this._grounded;
|
|
}
|
|
set grounded(value: boolean) {
|
|
this._grounded = value;
|
|
}
|
|
|
|
private get weight() {
|
|
return (this.physicsWorldRef?.gravity.y || -9.81) + this.mass;
|
|
}
|
|
|
|
initialize(
|
|
physicsEngine: typeof Rapier,
|
|
physicsWorld: Rapier.World,
|
|
characterController: Rapier.KinematicCharacterController,
|
|
): void {
|
|
if (!this.parent)
|
|
throw new Error('Cannot initialize Humanoid to empty parent');
|
|
|
|
this.ready = true;
|
|
|
|
this.physicsWorldRef = physicsWorld;
|
|
this.characterControllerRef = characterController;
|
|
|
|
// Character Physics
|
|
const halfVec = new Vector3(0, this.characterHalfHeight, 0);
|
|
const colliderDesc = physicsEngine.ColliderDesc.cuboid(
|
|
1,
|
|
this.characterHalfHeight,
|
|
0.5,
|
|
);
|
|
const rigidBodyDesc = physicsEngine.RigidBodyDesc.kinematicPositionBased()
|
|
.setTranslation(...this.parent!.position.toArray())
|
|
.setRotation(this.parent!.quaternion);
|
|
const rigidBody = physicsWorld.createRigidBody(rigidBodyDesc);
|
|
const collider = physicsWorld.createCollider(colliderDesc, rigidBody);
|
|
collider.setTranslationWrtParent(halfVec);
|
|
|
|
this.rigidBody = rigidBody;
|
|
this.collider = collider;
|
|
}
|
|
|
|
setVelocity(velocity: Vector3) {
|
|
this._velocity.copy(velocity);
|
|
}
|
|
|
|
setLook(vector: Vector3) {
|
|
this._lookAt.lerp(vector, 0.15);
|
|
}
|
|
|
|
jump() {
|
|
if (!this.grounded || this.shouldJump) return;
|
|
this.shouldJump = true;
|
|
}
|
|
|
|
tick(dt: number): void {
|
|
if (!this.ready) return;
|
|
|
|
// Apply rigidbody transforms to object from last process tick
|
|
if (this.rigidBody) {
|
|
this.parent!.position.copy(this.rigidBody.translation() as any);
|
|
this.parent!.quaternion.copy(this.rigidBody.rotation() as any);
|
|
}
|
|
|
|
// Apply jumping
|
|
if (this.shouldJump) {
|
|
this._appliedGravity.y = Math.sqrt(-2 * this.weight * this.jumpPower);
|
|
this.grounded = false;
|
|
this.shouldJump = false;
|
|
}
|
|
|
|
// Apply gravity
|
|
this._appliedGravity.y += this.weight * dt;
|
|
|
|
// Apply velocity
|
|
this.applyVelocity(
|
|
this._velocity.clone().add(this._appliedGravity).multiplyScalar(dt),
|
|
);
|
|
|
|
// Apply look vector
|
|
this._currentLookAt.copy(this.parent!.position);
|
|
this._currentLookAt.add(this._lookAt);
|
|
this.parent?.lookAt(this._currentLookAt);
|
|
|
|
this.applyRotation(this.parent!.quaternion);
|
|
|
|
// Stick to ground
|
|
if (this.grounded) {
|
|
this._appliedGravity.y = 0;
|
|
}
|
|
}
|
|
|
|
die() {
|
|
if (!this.alive) return;
|
|
this.health = 0;
|
|
}
|
|
|
|
private applyVelocity(velocity: Vector3) {
|
|
if (!this.characterControllerRef || !this.parent || !this.rigidBody) return;
|
|
|
|
const vec3 = this.parent.position.clone();
|
|
this.characterControllerRef.computeColliderMovement(
|
|
this.collider!,
|
|
velocity,
|
|
);
|
|
const computed = this.characterControllerRef.computedMovement();
|
|
const grounded = this.characterControllerRef.computedGrounded();
|
|
vec3.copy(computed as Vector3);
|
|
this.rigidBody?.setNextKinematicTranslation(vec3.add(this.parent.position));
|
|
|
|
// After the collider movement calculation is done, we can read the
|
|
// collision events.
|
|
// for (let i = 0; i < this.controller.numComputedCollisions(); i++) {
|
|
// let collision = this.controller.computedCollision(i);
|
|
// // Do something with that collision information.
|
|
// console.log(collision);
|
|
// }
|
|
|
|
this._grounded = grounded;
|
|
}
|
|
|
|
private applyRotation(quat: Quaternion) {
|
|
this.rigidBody?.setRotation(quat, false);
|
|
}
|
|
|
|
dispose(): void {
|
|
if (this.collider && !this.rigidBody) {
|
|
this.physicsWorldRef?.removeCollider(this.collider, false);
|
|
}
|
|
|
|
if (this.rigidBody) {
|
|
this.physicsWorldRef?.removeRigidBody(this.rigidBody);
|
|
}
|
|
}
|
|
}
|