Files
forma-engine/engine/character.ts
T

55 lines
2.7 KiB
TypeScript

/** Velocity-based kinematic motion. Rapier remains the collision authority. */
export class CharacterMotor {
velocity = { x: 0, y: 0, z: 0 };
actualVelocity = { x: 0, y: 0, z: 0 };
grounded = false;
gravityScale = 1;
contacts: { handle: number; normal: number[] }[] = [];
constructor(public body: any, public collider: any, public controller: any,
public config: any, private rapier: any) {}
set(value: any) {
for (const axis of ["x", "y", "z"] as const)
if (value[axis] !== undefined) {
if (!Number.isFinite(value[axis])) throw Error("Invalid character velocity");
this.velocity[axis] = Math.max(-100, Math.min(100, value[axis]));
}
if (value.gravityScale !== undefined) {
if (!Number.isFinite(value.gravityScale)) throw Error("Invalid gravity scale");
this.gravityScale = Math.max(0, Math.min(5, value.gravityScale));
}
}
teleport(position: number[]) {
if (position.length !== 3 || !position.every(Number.isFinite)) throw Error("Invalid teleport");
const p = { x: position[0], y: position[1], z: position[2] };
this.body.setTranslation(p, true);
this.body.setNextKinematicTranslation(p);
this.velocity = { x: 0, y: 0, z: 0 };
this.actualVelocity = { x: 0, y: 0, z: 0 };
this.gravityScale = 1;
this.grounded = false;
this.contacts = [];
}
step(dt: number, extra = [0, 0, 0]) {
this.velocity.y = Math.max(-45, this.velocity.y - (this.config.gravity ?? 24) * this.gravityScale * dt);
// Fixed-step gravity already maintains ground contact. Rapier 0.20 snap-down
// plus small vertical gravity steps can accumulate penetration on flat floors.
this.controller.disableSnapToGround();
this.controller.computeColliderMovement(this.collider, {
x: this.velocity.x * dt + extra[0],
y: this.velocity.y * dt + extra[1],
z: this.velocity.z * dt + extra[2],
}, this.rapier.QueryFilterFlags.EXCLUDE_SENSORS);
const movement = this.controller.computedMovement(), p = this.body.translation();
this.actualVelocity = { x: movement.x / dt, y: movement.y / dt, z: movement.z / dt };
this.body.setNextKinematicTranslation({ x: p.x + movement.x, y: p.y + movement.y, z: p.z + movement.z });
this.grounded = this.controller.computedGrounded();
this.contacts = [];
for (let i = 0; i < this.controller.numComputedCollisions(); i++) {
const hit = this.controller.computedCollision(i);
if (hit?.collider) this.contacts.push({ handle: hit.collider.handle, normal: [hit.normal1.x, hit.normal1.y, hit.normal1.z] });
}
if (this.grounded && this.velocity.y < 0) this.velocity.y = 0;
if (this.velocity.y > 0 && this.contacts.some(c => c.normal[1] < -0.6)) this.velocity.y = 0;
}
}