Codex CLI
feat(powerups): add infinite ammo powerup mechanics and UI integration
a646668
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);
}