Spaces:
Running
Running
import * as THREE from 'three'; | |
import { CFG } from './config.js'; | |
import { G } from './globals.js'; | |
import { getTerrainHeight, getNearbyTrees } from './world.js'; | |
// Working vectors | |
const FWD = new THREE.Vector3(); | |
const RIGHT = new THREE.Vector3(); | |
const NEXT = new THREE.Vector3(); | |
const PREV = new THREE.Vector3(); | |
// Helpers implementing Quake/Source-like movement in XZ plane | |
function applyFriction(vel, friction, stopSpeed, dt) { | |
const vx = vel.x, vz = vel.z; | |
const speed = Math.hypot(vx, vz); | |
if (speed <= 0.0001) return; | |
const control = Math.max(speed, stopSpeed); | |
const drop = control * friction * dt; | |
const newSpeed = Math.max(0, speed - drop); | |
if (newSpeed !== speed) { | |
const k = newSpeed / speed; | |
vel.x *= k; | |
vel.z *= k; | |
} | |
} | |
function accelerate(vel, wishDir, wishSpeed, accel, dt) { | |
const current = vel.x * wishDir.x + vel.z * wishDir.z; | |
let add = wishSpeed - current; | |
if (add <= 0) return; | |
const push = Math.min(accel * wishSpeed * dt, add); | |
vel.x += wishDir.x * push; | |
vel.z += wishDir.z * push; | |
} | |
function airAccelerate(vel, wishDir, wishSpeedCap, airAccel, dt) { | |
const current = vel.x * wishDir.x + vel.z * wishDir.z; | |
const wishSpeed = Math.min(wishSpeedCap, Math.hypot(wishDir.x, wishDir.z) > 0 ? wishSpeedCap : 0); | |
let add = wishSpeed - current; | |
if (add <= 0) return; | |
const push = Math.min(airAccel * wishSpeed * dt, add); | |
vel.x += wishDir.x * push; | |
vel.z += wishDir.z * push; | |
} | |
export function updatePlayer(delta) { | |
if (!G.player.alive) return; | |
const P = G.player; | |
const M = CFG.player.move; | |
// Handle timed movement buff | |
if (G.movementBuffTimer > 0) { | |
G.movementBuffTimer -= delta; | |
if (G.movementBuffTimer <= 0) { | |
G.movementBuffTimer = 0; | |
G.movementMult = 1; | |
} | |
} | |
// Forward/right in the horizontal plane | |
G.camera.getWorldDirection(FWD); | |
FWD.y = 0; FWD.normalize(); | |
RIGHT.crossVectors(FWD, G.camera.up).normalize(); | |
// Build wish direction from inputs | |
let wishX = 0, wishZ = 0; | |
if (G.input.w) { wishX += FWD.x; wishZ += FWD.z; } | |
if (G.input.s) { wishX -= FWD.x; wishZ -= FWD.z; } | |
if (G.input.d) { wishX += RIGHT.x; wishZ += RIGHT.z; } | |
if (G.input.a) { wishX -= RIGHT.x; wishZ -= RIGHT.z; } | |
const wishLen = Math.hypot(wishX, wishZ); | |
if (wishLen > 0.0001) { wishX /= wishLen; wishZ /= wishLen; } | |
// Desired speeds | |
let baseSpeed = P.speed * (G.movementMult || 1) * (G.input.sprint ? CFG.player.sprintMult : 1); | |
const crouchMult = (CFG.player.crouchMult || 1); | |
// If not sliding, crouch reduces speed | |
if (G.input.crouch && !P.sliding) baseSpeed *= crouchMult; | |
// Timers: jump buffer and coyote time | |
// Buffer jump on key press (edge), not hold | |
if (G.input.jump && !P.jumpHeld) { | |
P.jumpBuffer = M.jumpBuffer; | |
} else { | |
P.jumpBuffer = Math.max(0, P.jumpBuffer - delta); | |
} | |
P.jumpHeld = !!G.input.jump; | |
if (P.grounded) P.coyoteTimer = M.coyoteTime; else P.coyoteTimer = Math.max(0, P.coyoteTimer - delta); | |
P.wallContactTimer = Math.max(0, P.wallContactTimer - delta); | |
// Sliding enter/exit | |
const horizSpeed = Math.hypot(P.vel.x, P.vel.z); | |
if (P.grounded && G.input.crouch && (horizSpeed >= M.slideMinSpeed)) { | |
P.sliding = true; | |
} else if (!G.input.crouch || !P.grounded) { | |
P.sliding = false; | |
} | |
// Jump handling (ground, coyote, or wall bounce) | |
let skippedFriction = false; | |
if (P.jumpBuffer > 0) { | |
if (P.coyoteTimer > 0) { | |
// Ground/coyote jump | |
P.yVel = M.jumpSpeed; | |
// Slide-jump: preserve momentum and add small boost | |
if (P.sliding) { | |
const sp = Math.hypot(P.vel.x, P.vel.z); | |
if (sp > 0.0001) { | |
const nx = P.vel.x / sp, nz = P.vel.z / sp; | |
P.vel.x += nx * M.slideJumpBoost; | |
P.vel.z += nz * M.slideJumpBoost; | |
} | |
skippedFriction = true; | |
} | |
P.grounded = false; | |
P.jumpBuffer = 0; // consume | |
P.coyoteTimer = 0; | |
} else if (!P.grounded && P.wallContactTimer > 0) { | |
// Wall bounce: reflect into-wall component and add outward pop | |
const n = P.lastWallNormal; | |
const dot = P.vel.x * n.x + P.vel.z * n.z; | |
// Remove into-wall component | |
P.vel.x -= n.x * dot; | |
P.vel.z -= n.z * dot; | |
// Add outward impulse | |
P.vel.x += n.x * M.wallBounceImpulse; | |
P.vel.z += n.z * M.wallBounceImpulse; | |
// Give a small jump | |
P.yVel = M.jumpSpeed; | |
P.jumpBuffer = 0; | |
P.wallContactTimer = 0; | |
} | |
} | |
// State-based friction and acceleration | |
if (P.grounded) { | |
// Friction (skip on slide-jump frame) | |
if (!skippedFriction) { | |
const fric = P.sliding ? M.slideFriction : M.friction; | |
applyFriction(P.vel, fric, M.stopSpeed, delta); | |
} | |
// Accelerate toward wishdir | |
accelerate(P.vel, { x: wishX, z: wishZ }, baseSpeed, P.sliding ? M.slideAccel : M.groundAccel, delta); | |
} else { | |
// Air movement | |
airAccelerate(P.vel, { x: wishX, z: wishZ }, M.airSpeedCap, M.airAccel, delta); | |
} | |
// Gravity (vertical only) | |
P.yVel -= M.gravity * delta; | |
// Integrate position | |
NEXT.copy(P.pos); | |
NEXT.x += P.vel.x * delta; | |
NEXT.z += P.vel.z * delta; | |
NEXT.y += P.yVel * delta; | |
// Collide with tree trunks (cylinders in XZ), push out | |
const nearTrees = getNearbyTrees(NEXT.x, NEXT.z, 3.5); | |
for (let i = 0; i < nearTrees.length; i++) { | |
const tree = nearTrees[i]; | |
const dx = NEXT.x - tree.x; | |
const dz = NEXT.z - tree.z; | |
const dist = Math.hypot(dx, dz); | |
const minDist = P.radius + tree.radius; | |
if (dist < minDist && dist > 0) { | |
const nx = dx / dist; | |
const nz = dz / dist; | |
const push = (minDist - dist); | |
NEXT.x += nx * push; | |
NEXT.z += nz * push; | |
// Record wall contact for potential wall-bounce when airborne | |
if (!P.grounded) { | |
P.lastWallNormal.set(nx, 0, nz); | |
P.wallContactTimer = Math.max(P.wallContactTimer, CFG.player.move.wallBounceWindow); | |
} | |
} | |
} | |
// Bounds clamp | |
const halfSize = CFG.forestSize / 2 - P.radius; | |
NEXT.x = Math.max(-halfSize, Math.min(halfSize, NEXT.x)); | |
NEXT.z = Math.max(-halfSize, Math.min(halfSize, NEXT.z)); | |
// Ground resolve against terrain (using eye height) | |
const eye = G.input.crouch ? (CFG.player.crouchEyeHeight || 1.8) : (CFG.player.eyeHeight || 1.8); | |
const groundEye = getTerrainHeight(NEXT.x, NEXT.z) + eye; | |
if (NEXT.y <= groundEye) { | |
NEXT.y = groundEye; | |
P.yVel = 0; | |
P.grounded = true; | |
} else { | |
P.grounded = false; | |
} | |
// Commit position and keep camera in sync | |
PREV.copy(P.pos); | |
P.pos.copy(NEXT); | |
G.camera.position.copy(P.pos); | |
} | |