Spaces:
Running
Running
Codex CLI
commited on
Commit
·
d6dfcf1
1
Parent(s):
bf6239d
feat(player): enhance movement mechanics with sliding, jump buffering, and wall bounce features
Browse files- src/config.js +22 -1
- src/events.js +12 -6
- src/hud.js +1 -1
- src/main.js +15 -1
- src/player.js +152 -43
src/config.js
CHANGED
|
@@ -26,7 +26,28 @@ export const CFG = {
|
|
| 26 |
health: 100,
|
| 27 |
jumpVel: 5,
|
| 28 |
eyeHeight: 1.8,
|
| 29 |
-
crouchEyeHeight: 1.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
},
|
| 31 |
flashlight: {
|
| 32 |
on: true,
|
|
|
|
| 26 |
health: 100,
|
| 27 |
jumpVel: 5,
|
| 28 |
eyeHeight: 1.8,
|
| 29 |
+
crouchEyeHeight: 1.2,
|
| 30 |
+
// Apex-like movement tuning
|
| 31 |
+
move: {
|
| 32 |
+
friction: 5.5, // ground friction (~4-6)
|
| 33 |
+
stopSpeed: 6.0, // speed where friction clamps (m/s)
|
| 34 |
+
groundAccel: 11.0, // ground acceleration
|
| 35 |
+
airAccel: 18.0, // air acceleration (allow redirection)
|
| 36 |
+
gravity: 15.0, // matches prior gravity
|
| 37 |
+
jumpSpeed: 5.0, // vertical impulse (mirrors jumpVel)
|
| 38 |
+
airSpeedCap: 10.0, // optional cap on sideways air accel
|
| 39 |
+
// Sliding
|
| 40 |
+
slideFriction: 1.2, // lower than ground friction
|
| 41 |
+
slideAccel: 10.0,
|
| 42 |
+
slideMinSpeed: 6.0, // enter slide when crouch and >= this speed
|
| 43 |
+
slideJumpBoost: 1.0, // forward boost on slide-jump (m/s)
|
| 44 |
+
// Helpers
|
| 45 |
+
jumpBuffer: 0.12, // seconds to buffer jump presses
|
| 46 |
+
coyoteTime: 0.12, // late jump after leaving ground
|
| 47 |
+
// Wall bounce
|
| 48 |
+
wallBounceWindow: 0.10, // seconds after wall contact to accept jump
|
| 49 |
+
wallBounceImpulse: 1.2 // outward push on wall bounce (m/s)
|
| 50 |
+
}
|
| 51 |
},
|
| 52 |
flashlight: {
|
| 53 |
on: true,
|
src/events.js
CHANGED
|
@@ -15,6 +15,9 @@ export function setupEvents({ startGame, restartGame, beginReload, updateWeaponA
|
|
| 15 |
G.controls.lock();
|
| 16 |
} else if (G.state === 'paused') {
|
| 17 |
G.controls.lock();
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
});
|
| 20 |
|
|
@@ -24,6 +27,9 @@ export function setupEvents({ startGame, restartGame, beginReload, updateWeaponA
|
|
| 24 |
} else if (G.state === 'paused') {
|
| 25 |
G.state = 'playing';
|
| 26 |
overlay.classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
| 27 |
}
|
| 28 |
});
|
| 29 |
|
|
@@ -49,10 +55,9 @@ export function setupEvents({ startGame, restartGame, beginReload, updateWeaponA
|
|
| 49 |
}
|
| 50 |
break;
|
| 51 |
case 'Space':
|
| 52 |
-
if (G.state === 'playing'
|
| 53 |
-
//
|
| 54 |
-
G.
|
| 55 |
-
G.player.grounded = false;
|
| 56 |
}
|
| 57 |
break;
|
| 58 |
case 'KeyF':
|
|
@@ -69,8 +74,6 @@ export function setupEvents({ startGame, restartGame, beginReload, updateWeaponA
|
|
| 69 |
case 'KeyR':
|
| 70 |
if (G.state === 'playing') {
|
| 71 |
beginReload();
|
| 72 |
-
} else if (G.state === 'gameover') {
|
| 73 |
-
restartGame();
|
| 74 |
}
|
| 75 |
break;
|
| 76 |
}
|
|
@@ -90,6 +93,9 @@ export function setupEvents({ startGame, restartGame, beginReload, updateWeaponA
|
|
| 90 |
releaseGrenade();
|
| 91 |
}
|
| 92 |
break;
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
});
|
| 95 |
|
|
|
|
| 15 |
G.controls.lock();
|
| 16 |
} else if (G.state === 'paused') {
|
| 17 |
G.controls.lock();
|
| 18 |
+
} else if (G.state === 'gameover') {
|
| 19 |
+
// Restart on click after game over
|
| 20 |
+
restartGame();
|
| 21 |
}
|
| 22 |
});
|
| 23 |
|
|
|
|
| 27 |
} else if (G.state === 'paused') {
|
| 28 |
G.state = 'playing';
|
| 29 |
overlay.classList.add('hidden');
|
| 30 |
+
} else if (G.state === 'gameover') {
|
| 31 |
+
// Treat lock like a fresh start after game over
|
| 32 |
+
startGame();
|
| 33 |
}
|
| 34 |
});
|
| 35 |
|
|
|
|
| 55 |
}
|
| 56 |
break;
|
| 57 |
case 'Space':
|
| 58 |
+
if (G.state === 'playing') {
|
| 59 |
+
// Use buffered jump handled in player physics
|
| 60 |
+
G.input.jump = true;
|
|
|
|
| 61 |
}
|
| 62 |
break;
|
| 63 |
case 'KeyF':
|
|
|
|
| 74 |
case 'KeyR':
|
| 75 |
if (G.state === 'playing') {
|
| 76 |
beginReload();
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
break;
|
| 79 |
}
|
|
|
|
| 93 |
releaseGrenade();
|
| 94 |
}
|
| 95 |
break;
|
| 96 |
+
case 'Space':
|
| 97 |
+
G.input.jump = false;
|
| 98 |
+
break;
|
| 99 |
}
|
| 100 |
});
|
| 101 |
|
src/hud.js
CHANGED
|
@@ -92,7 +92,7 @@ export function showOverlay(type) {
|
|
| 92 |
<h1>You Died</h1>
|
| 93 |
<p>Score: ${G.player.score}</p>
|
| 94 |
<p>Wave: ${G.waves.current}</p>
|
| 95 |
-
<p>
|
| 96 |
`;
|
| 97 |
}
|
| 98 |
}
|
|
|
|
| 92 |
<h1>You Died</h1>
|
| 93 |
<p>Score: ${G.player.score}</p>
|
| 94 |
<p>Wave: ${G.waves.current}</p>
|
| 95 |
+
<p>Click to Restart</p>
|
| 96 |
`;
|
| 97 |
}
|
| 98 |
}
|
src/main.js
CHANGED
|
@@ -65,7 +65,14 @@ function init() {
|
|
| 65 |
alive: true,
|
| 66 |
score: 0,
|
| 67 |
yVel: 0,
|
| 68 |
-
grounded: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
};
|
| 70 |
G.weapon.ammo = CFG.gun.magSize;
|
| 71 |
G.weapon.reserve = Infinity;
|
|
@@ -105,6 +112,13 @@ function startGame() {
|
|
| 105 |
G.player.alive = true;
|
| 106 |
G.player.pos.set(0, getTerrainHeight(0, 0) + (CFG.player.eyeHeight || 1.8), 0);
|
| 107 |
G.player.vel.set(0, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
G.camera.position.copy(G.player.pos);
|
| 109 |
G.damageFlash = 0;
|
| 110 |
G.healFlash = 0;
|
|
|
|
| 65 |
alive: true,
|
| 66 |
score: 0,
|
| 67 |
yVel: 0,
|
| 68 |
+
grounded: true,
|
| 69 |
+
// Apex-like movement state
|
| 70 |
+
sliding: false,
|
| 71 |
+
jumpBuffer: 0,
|
| 72 |
+
coyoteTimer: 0,
|
| 73 |
+
lastWallNormal: new THREE.Vector3(),
|
| 74 |
+
wallContactTimer: 0
|
| 75 |
+
, jumpHeld: false
|
| 76 |
};
|
| 77 |
G.weapon.ammo = CFG.gun.magSize;
|
| 78 |
G.weapon.reserve = Infinity;
|
|
|
|
| 112 |
G.player.alive = true;
|
| 113 |
G.player.pos.set(0, getTerrainHeight(0, 0) + (CFG.player.eyeHeight || 1.8), 0);
|
| 114 |
G.player.vel.set(0, 0, 0);
|
| 115 |
+
G.player.yVel = 0;
|
| 116 |
+
G.player.grounded = true;
|
| 117 |
+
G.player.sliding = false;
|
| 118 |
+
G.player.jumpBuffer = 0;
|
| 119 |
+
G.player.coyoteTimer = 0;
|
| 120 |
+
G.player.lastWallNormal.set(0, 0, 0);
|
| 121 |
+
G.player.wallContactTimer = 0;
|
| 122 |
G.camera.position.copy(G.player.pos);
|
| 123 |
G.damageFlash = 0;
|
| 124 |
G.healFlash = 0;
|
src/player.js
CHANGED
|
@@ -3,82 +3,191 @@ import { CFG } from './config.js';
|
|
| 3 |
import { G } from './globals.js';
|
| 4 |
import { getTerrainHeight, getNearbyTrees } from './world.js';
|
| 5 |
|
| 6 |
-
|
| 7 |
const FWD = new THREE.Vector3();
|
| 8 |
const RIGHT = new THREE.Vector3();
|
| 9 |
const NEXT = new THREE.Vector3();
|
| 10 |
const PREV = new THREE.Vector3();
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
export function updatePlayer(delta) {
|
| 13 |
if (!G.player.alive) return;
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
|
|
|
|
| 18 |
G.camera.getWorldDirection(FWD);
|
| 19 |
-
FWD.y = 0;
|
| 20 |
-
FWD.normalize();
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
}
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
const nearTrees = getNearbyTrees(NEXT.x, NEXT.z, 3.5);
|
| 41 |
for (let i = 0; i < nearTrees.length; i++) {
|
| 42 |
const tree = nearTrees[i];
|
| 43 |
const dx = NEXT.x - tree.x;
|
| 44 |
const dz = NEXT.z - tree.z;
|
| 45 |
-
const dist = Math.
|
| 46 |
-
const minDist =
|
| 47 |
if (dist < minDist && dist > 0) {
|
| 48 |
-
const
|
| 49 |
-
const
|
| 50 |
-
|
| 51 |
-
NEXT.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
}
|
| 54 |
|
| 55 |
-
// Bounds
|
| 56 |
-
const halfSize = CFG.forestSize / 2 -
|
| 57 |
NEXT.x = Math.max(-halfSize, Math.min(halfSize, NEXT.x));
|
| 58 |
NEXT.z = Math.max(-halfSize, Math.min(halfSize, NEXT.z));
|
| 59 |
|
| 60 |
-
//
|
| 61 |
-
G.player.yVel -= 15 * delta; // gravity
|
| 62 |
-
NEXT.y += G.player.yVel * delta;
|
| 63 |
-
|
| 64 |
-
// Grounded against terrain height
|
| 65 |
const eye = G.input.crouch ? (CFG.player.crouchEyeHeight || 1.8) : (CFG.player.eyeHeight || 1.8);
|
| 66 |
const groundEye = getTerrainHeight(NEXT.x, NEXT.z) + eye;
|
| 67 |
if (NEXT.y <= groundEye) {
|
| 68 |
NEXT.y = groundEye;
|
| 69 |
-
|
| 70 |
-
|
| 71 |
} else {
|
| 72 |
-
|
| 73 |
}
|
| 74 |
|
| 75 |
-
//
|
| 76 |
-
PREV.copy(
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
G.player.vel.copy(G.player.pos).sub(PREV).multiplyScalar(1 / delta);
|
| 80 |
-
// Ignore vertical component for prediction stability
|
| 81 |
-
G.player.vel.y = 0;
|
| 82 |
-
}
|
| 83 |
-
G.camera.position.copy(G.player.pos);
|
| 84 |
}
|
|
|
|
| 3 |
import { G } from './globals.js';
|
| 4 |
import { getTerrainHeight, getNearbyTrees } from './world.js';
|
| 5 |
|
| 6 |
+
// Working vectors
|
| 7 |
const FWD = new THREE.Vector3();
|
| 8 |
const RIGHT = new THREE.Vector3();
|
| 9 |
const NEXT = new THREE.Vector3();
|
| 10 |
const PREV = new THREE.Vector3();
|
| 11 |
|
| 12 |
+
// Helpers implementing Quake/Source-like movement in XZ plane
|
| 13 |
+
function applyFriction(vel, friction, stopSpeed, dt) {
|
| 14 |
+
const vx = vel.x, vz = vel.z;
|
| 15 |
+
const speed = Math.hypot(vx, vz);
|
| 16 |
+
if (speed <= 0.0001) return;
|
| 17 |
+
const control = Math.max(speed, stopSpeed);
|
| 18 |
+
const drop = control * friction * dt;
|
| 19 |
+
const newSpeed = Math.max(0, speed - drop);
|
| 20 |
+
if (newSpeed !== speed) {
|
| 21 |
+
const k = newSpeed / speed;
|
| 22 |
+
vel.x *= k;
|
| 23 |
+
vel.z *= k;
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function accelerate(vel, wishDir, wishSpeed, accel, dt) {
|
| 28 |
+
const current = vel.x * wishDir.x + vel.z * wishDir.z;
|
| 29 |
+
let add = wishSpeed - current;
|
| 30 |
+
if (add <= 0) return;
|
| 31 |
+
const push = Math.min(accel * wishSpeed * dt, add);
|
| 32 |
+
vel.x += wishDir.x * push;
|
| 33 |
+
vel.z += wishDir.z * push;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
function airAccelerate(vel, wishDir, wishSpeedCap, airAccel, dt) {
|
| 37 |
+
const current = vel.x * wishDir.x + vel.z * wishDir.z;
|
| 38 |
+
const wishSpeed = Math.min(wishSpeedCap, Math.hypot(wishDir.x, wishDir.z) > 0 ? wishSpeedCap : 0);
|
| 39 |
+
let add = wishSpeed - current;
|
| 40 |
+
if (add <= 0) return;
|
| 41 |
+
const push = Math.min(airAccel * wishSpeed * dt, add);
|
| 42 |
+
vel.x += wishDir.x * push;
|
| 43 |
+
vel.z += wishDir.z * push;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
export function updatePlayer(delta) {
|
| 47 |
if (!G.player.alive) return;
|
| 48 |
|
| 49 |
+
const P = G.player;
|
| 50 |
+
const M = CFG.player.move;
|
| 51 |
|
| 52 |
+
// Forward/right in the horizontal plane
|
| 53 |
G.camera.getWorldDirection(FWD);
|
| 54 |
+
FWD.y = 0; FWD.normalize();
|
| 55 |
+
RIGHT.crossVectors(FWD, G.camera.up).normalize();
|
| 56 |
|
| 57 |
+
// Build wish direction from inputs
|
| 58 |
+
let wishX = 0, wishZ = 0;
|
| 59 |
+
if (G.input.w) { wishX += FWD.x; wishZ += FWD.z; }
|
| 60 |
+
if (G.input.s) { wishX -= FWD.x; wishZ -= FWD.z; }
|
| 61 |
+
if (G.input.d) { wishX += RIGHT.x; wishZ += RIGHT.z; }
|
| 62 |
+
if (G.input.a) { wishX -= RIGHT.x; wishZ -= RIGHT.z; }
|
| 63 |
+
const wishLen = Math.hypot(wishX, wishZ);
|
| 64 |
+
if (wishLen > 0.0001) { wishX /= wishLen; wishZ /= wishLen; }
|
| 65 |
|
| 66 |
+
// Desired speeds
|
| 67 |
+
let baseSpeed = P.speed * (G.input.sprint ? CFG.player.sprintMult : 1);
|
| 68 |
+
const crouchMult = (CFG.player.crouchMult || 1);
|
| 69 |
+
// If not sliding, crouch reduces speed
|
| 70 |
+
if (G.input.crouch && !P.sliding) baseSpeed *= crouchMult;
|
| 71 |
|
| 72 |
+
// Timers: jump buffer and coyote time
|
| 73 |
+
// Buffer jump on key press (edge), not hold
|
| 74 |
+
if (G.input.jump && !P.jumpHeld) {
|
| 75 |
+
P.jumpBuffer = M.jumpBuffer;
|
| 76 |
+
} else {
|
| 77 |
+
P.jumpBuffer = Math.max(0, P.jumpBuffer - delta);
|
| 78 |
}
|
| 79 |
+
P.jumpHeld = !!G.input.jump;
|
| 80 |
|
| 81 |
+
if (P.grounded) P.coyoteTimer = M.coyoteTime; else P.coyoteTimer = Math.max(0, P.coyoteTimer - delta);
|
| 82 |
+
P.wallContactTimer = Math.max(0, P.wallContactTimer - delta);
|
| 83 |
|
| 84 |
+
// Sliding enter/exit
|
| 85 |
+
const horizSpeed = Math.hypot(P.vel.x, P.vel.z);
|
| 86 |
+
if (P.grounded && G.input.crouch && (horizSpeed >= M.slideMinSpeed)) {
|
| 87 |
+
P.sliding = true;
|
| 88 |
+
} else if (!G.input.crouch || !P.grounded) {
|
| 89 |
+
P.sliding = false;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Jump handling (ground, coyote, or wall bounce)
|
| 93 |
+
let skippedFriction = false;
|
| 94 |
+
if (P.jumpBuffer > 0) {
|
| 95 |
+
if (P.coyoteTimer > 0) {
|
| 96 |
+
// Ground/coyote jump
|
| 97 |
+
P.yVel = M.jumpSpeed;
|
| 98 |
+
// Slide-jump: preserve momentum and add small boost
|
| 99 |
+
if (P.sliding) {
|
| 100 |
+
const sp = Math.hypot(P.vel.x, P.vel.z);
|
| 101 |
+
if (sp > 0.0001) {
|
| 102 |
+
const nx = P.vel.x / sp, nz = P.vel.z / sp;
|
| 103 |
+
P.vel.x += nx * M.slideJumpBoost;
|
| 104 |
+
P.vel.z += nz * M.slideJumpBoost;
|
| 105 |
+
}
|
| 106 |
+
skippedFriction = true;
|
| 107 |
+
}
|
| 108 |
+
P.grounded = false;
|
| 109 |
+
P.jumpBuffer = 0; // consume
|
| 110 |
+
P.coyoteTimer = 0;
|
| 111 |
+
} else if (!P.grounded && P.wallContactTimer > 0) {
|
| 112 |
+
// Wall bounce: reflect into-wall component and add outward pop
|
| 113 |
+
const n = P.lastWallNormal;
|
| 114 |
+
const dot = P.vel.x * n.x + P.vel.z * n.z;
|
| 115 |
+
// Remove into-wall component
|
| 116 |
+
P.vel.x -= n.x * dot;
|
| 117 |
+
P.vel.z -= n.z * dot;
|
| 118 |
+
// Add outward impulse
|
| 119 |
+
P.vel.x += n.x * M.wallBounceImpulse;
|
| 120 |
+
P.vel.z += n.z * M.wallBounceImpulse;
|
| 121 |
+
// Give a small jump
|
| 122 |
+
P.yVel = M.jumpSpeed;
|
| 123 |
+
P.jumpBuffer = 0;
|
| 124 |
+
P.wallContactTimer = 0;
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// State-based friction and acceleration
|
| 129 |
+
if (P.grounded) {
|
| 130 |
+
// Friction (skip on slide-jump frame)
|
| 131 |
+
if (!skippedFriction) {
|
| 132 |
+
const fric = P.sliding ? M.slideFriction : M.friction;
|
| 133 |
+
applyFriction(P.vel, fric, M.stopSpeed, delta);
|
| 134 |
+
}
|
| 135 |
+
// Accelerate toward wishdir
|
| 136 |
+
accelerate(P.vel, { x: wishX, z: wishZ }, baseSpeed, P.sliding ? M.slideAccel : M.groundAccel, delta);
|
| 137 |
+
} else {
|
| 138 |
+
// Air movement
|
| 139 |
+
airAccelerate(P.vel, { x: wishX, z: wishZ }, M.airSpeedCap, M.airAccel, delta);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// Gravity (vertical only)
|
| 143 |
+
P.yVel -= M.gravity * delta;
|
| 144 |
+
|
| 145 |
+
// Integrate position
|
| 146 |
+
NEXT.copy(P.pos);
|
| 147 |
+
NEXT.x += P.vel.x * delta;
|
| 148 |
+
NEXT.z += P.vel.z * delta;
|
| 149 |
+
NEXT.y += P.yVel * delta;
|
| 150 |
+
|
| 151 |
+
// Collide with tree trunks (cylinders in XZ), push out
|
| 152 |
const nearTrees = getNearbyTrees(NEXT.x, NEXT.z, 3.5);
|
| 153 |
for (let i = 0; i < nearTrees.length; i++) {
|
| 154 |
const tree = nearTrees[i];
|
| 155 |
const dx = NEXT.x - tree.x;
|
| 156 |
const dz = NEXT.z - tree.z;
|
| 157 |
+
const dist = Math.hypot(dx, dz);
|
| 158 |
+
const minDist = P.radius + tree.radius;
|
| 159 |
if (dist < minDist && dist > 0) {
|
| 160 |
+
const nx = dx / dist;
|
| 161 |
+
const nz = dz / dist;
|
| 162 |
+
const push = (minDist - dist);
|
| 163 |
+
NEXT.x += nx * push;
|
| 164 |
+
NEXT.z += nz * push;
|
| 165 |
+
// Record wall contact for potential wall-bounce when airborne
|
| 166 |
+
if (!P.grounded) {
|
| 167 |
+
P.lastWallNormal.set(nx, 0, nz);
|
| 168 |
+
P.wallContactTimer = Math.max(P.wallContactTimer, CFG.player.move.wallBounceWindow);
|
| 169 |
+
}
|
| 170 |
}
|
| 171 |
}
|
| 172 |
|
| 173 |
+
// Bounds clamp
|
| 174 |
+
const halfSize = CFG.forestSize / 2 - P.radius;
|
| 175 |
NEXT.x = Math.max(-halfSize, Math.min(halfSize, NEXT.x));
|
| 176 |
NEXT.z = Math.max(-halfSize, Math.min(halfSize, NEXT.z));
|
| 177 |
|
| 178 |
+
// Ground resolve against terrain (using eye height)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
const eye = G.input.crouch ? (CFG.player.crouchEyeHeight || 1.8) : (CFG.player.eyeHeight || 1.8);
|
| 180 |
const groundEye = getTerrainHeight(NEXT.x, NEXT.z) + eye;
|
| 181 |
if (NEXT.y <= groundEye) {
|
| 182 |
NEXT.y = groundEye;
|
| 183 |
+
P.yVel = 0;
|
| 184 |
+
P.grounded = true;
|
| 185 |
} else {
|
| 186 |
+
P.grounded = false;
|
| 187 |
}
|
| 188 |
|
| 189 |
+
// Commit position and keep camera in sync
|
| 190 |
+
PREV.copy(P.pos);
|
| 191 |
+
P.pos.copy(NEXT);
|
| 192 |
+
G.camera.position.copy(P.pos);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
}
|