server/src/game/humanoid.object.ts

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);
}
}
}