import type { Vec3 } from "../math/Vec3" import { CollisionWorld, type CollisionWorld as World } from "./CollisionWorld" export type Character = { position: Vec3 yaw: number velocityY: number onGround: boolean } export type CharacterInput = { forward: number right: number jump: boolean run: boolean } export type CharacterConfig = { radius: number speed: number runMultiplier: number gravity: number jumpSpeed: number eyeHeight: number } export namespace CharacterController { export function update( character: Character, input: CharacterInput, dt: number, world: World, config: CharacterConfig, ): void { if (input.jump && character.onGround) { character.velocityY = config.jumpSpeed character.onGround = false } const steps = moveSubsteps(input, dt, config) for (let i = 0; i < steps; i++) { moveHorizontal(character, input, dt / steps, config) CollisionWorld.pushOut(world, character.position, config.radius) } character.velocityY -= config.gravity * dt character.position.y += character.velocityY * dt const ground = CollisionWorld.groundHeight(world, character.position) if (character.position.y <= ground) { character.position.y = ground character.velocityY = 0 character.onGround = true } else { character.onGround = false } } function moveSubsteps( input: CharacterInput, dt: number, config: CharacterConfig, ): number { const distance = config.speed * runFactor(input, config) * dt * Math.hypot(input.forward, input.right) return Math.max(1, Math.ceil(distance / config.radius)) } function moveHorizontal( character: Character, input: CharacterInput, dt: number, config: CharacterConfig, ): void { const speed = config.speed * runFactor(input, config) * dt const forwardX = Math.sin(character.yaw) const forwardZ = -Math.cos(character.yaw) const rightX = Math.cos(character.yaw) const rightZ = Math.sin(character.yaw) character.position.x += (forwardX * input.forward + rightX * input.right) * speed character.position.z += (forwardZ * input.forward + rightZ * input.right) * speed } function runFactor(input: CharacterInput, config: CharacterConfig): number { return input.run ? config.runMultiplier : 1 } }