Codex CLI
feat: update infinite ammo color to a brighter indigo for better visibility
578d40a
import * as THREE from 'three';
import { CFG } from './config.js';
import { G } from './globals.js';
import { updateHUD } from './hud.js';
import { playReloadStart, playReloadEnd } from './audio.js';
function computeWeaponBasePos() {
const d = G.weapon.anchor.depth;
const halfH = Math.tan(THREE.MathUtils.degToRad(G.camera.fov * 0.5)) * d;
const halfW = halfH * G.camera.aspect;
const x = halfW - G.weapon.anchor.right;
const y = -halfH + G.weapon.anchor.bottom;
return new THREE.Vector3(x, y, -d);
}
export function updateWeaponAnchor() {
G.weapon.basePos.copy(computeWeaponBasePos());
if (G.weapon.group) {
G.weapon.group.position.copy(G.weapon.basePos);
G.weapon.group.rotation.copy(G.weapon.baseRot);
}
}
export function setupWeapon() {
const makeVM = (color, metal = 0.4, rough = 0.6) => {
const m = new THREE.MeshStandardMaterial({ color, metalness: metal, roughness: rough });
m.fog = false;
m.depthTest = false;
return m;
};
const steel = makeVM(0x2a2d30, 0.8, 0.35);
const polymer = makeVM(0x1b1f23, 0.1, 0.8);
const tan = makeVM(0x7b6a4d, 0.2, 0.7);
const g = new THREE.Group();
g.renderOrder = 10;
g.castShadow = false;
g.receiveShadow = false;
const handguardL = 0.62;
const receiverL = 0.40;
const barrelL = 0.60;
const muzzleL = 0.10;
const stockL = 0.34;
const receiver = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.18, receiverL), steel);
receiver.position.set(0.00, 0.00, -0.42);
g.add(receiver);
const lower = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.10, 0.22), steel);
lower.position.set(0.00, -0.10, -0.36);
g.add(lower);
const grip = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.22, 0.08), polymer);
grip.position.set(-0.06, -0.19, -0.28);
grip.rotation.x = -0.6;
g.add(grip);
const mag = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.22, 0.14), polymer);
mag.position.set(0.02, -0.16, -0.44);
mag.rotation.x = 0.35;
g.add(mag);
const stock = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.15, stockL), tan);
stock.position.set(-0.02, 0.01, +0.02);
g.add(stock);
const butt = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.16, 0.06), polymer);
butt.position.set(-0.02, 0.01, +0.22);
g.add(butt);
const handguard = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.12, handguardL), tan);
handguard.position.set(0.00, 0.00, -0.90);
g.add(handguard);
const rail = new THREE.Group();
const lugW = 0.035, lugH = 0.01, lugD = 0.03;
const lugCount = 12;
for (let i = 0; i < lugCount; i++) {
const lug = new THREE.Mesh(new THREE.BoxGeometry(lugW, lugH, lugD), steel);
lug.position.set(0, 0.10, -0.55 - i * 0.03);
rail.add(lug);
}
g.add(rail);
const base = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.05, 0.08), steel);
base.position.set(0.00, 0.09, -0.62);
g.add(base);
const hood = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.08, 12), steel);
hood.rotation.x = Math.PI / 2;
hood.position.set(0.00, 0.09, -0.70);
g.add(hood);
const lens = new THREE.Mesh(
new THREE.CircleGeometry(0.028, 16),
new THREE.MeshStandardMaterial({ color: 0x66aaff, emissive: 0x112244, metalness: 0.2, roughness: 0.1 })
);
lens.position.set(0.00, 0.09, -0.66);
lens.rotation.x = Math.PI / 2;
lens.material.fog = false;
lens.material.depthTest = false;
g.add(lens);
const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.016, 0.016, barrelL, 12), steel);
barrel.rotation.x = Math.PI / 2;
barrel.position.set(0.00, 0.00, -1.25);
g.add(barrel);
const muzzle = new THREE.Mesh(new THREE.CylinderGeometry(0.028, 0.028, muzzleL, 10), steel);
muzzle.rotation.x = Math.PI / 2;
muzzle.position.set(0.00, 0.00, -1.58);
g.add(muzzle);
const frontPost = new THREE.Mesh(new THREE.BoxGeometry(0.01, 0.02, 0.02), steel);
frontPost.position.set(0.00, 0.06, -1.55);
g.add(frontPost);
const muzzleAnchor = new THREE.Object3D();
muzzleAnchor.position.set(0.00, 0.00, -1.63);
g.add(muzzleAnchor);
// Ejector anchor (right side of receiver)
const ejectorAnchor = new THREE.Object3D();
ejectorAnchor.position.set(0.09, 0.06, -0.42);
g.add(ejectorAnchor);
G.weapon.group = g;
G.weapon.muzzle = muzzleAnchor;
G.weapon.ejector = ejectorAnchor;
// Track materials we can tint when buffs are active
G.weapon.materials = [steel, polymer, tan];
G.camera.add(g);
g.scale.setScalar(1.55);
updateWeaponAnchor();
}
export function beginReload() {
if (G.weapon.reloading) return;
if (G.weapon.infiniteAmmoTimer > 0) return;
if (G.weapon.ammo >= CFG.gun.magSize) return;
G.weapon.reloading = true;
G.weapon.reloadTimer = CFG.gun.reloadTime;
if (CFG.audio.reloadStart) playReloadStart();
}
export function updateWeapon(delta) {
if (!G.weapon.group) return;
const moving = G.input.w || G.input.a || G.input.s || G.input.d;
const sprinting = moving && G.input.sprint;
const crouching = !!G.input.crouch;
// Reduce sway/bob intensity and slightly lower frequencies
G.weapon.swayT += delta * (sprinting ? 9 : (moving ? 7 : 2.5));
let bobAmp = sprinting ? 0.008 : (moving ? 0.006 : 0.0035);
let swayAmp = sprinting ? 0.0045 : (moving ? 0.0035 : 0.002);
if (crouching) { bobAmp *= 0.7; swayAmp *= 0.7; }
const bobX = Math.sin(G.weapon.swayT * 1.8) * bobAmp;
const bobY = Math.cos(G.weapon.swayT * 3.6) * bobAmp * 0.6;
const swayZRot = Math.sin(G.weapon.swayT * 1.4) * swayAmp;
G.weapon.recoil = Math.max(0, G.weapon.recoil - CFG.gun.recoilRecover * delta);
// ----- Dynamic spread update (CS-style) -----
const base = CFG.gun.spreadMin ?? CFG.gun.bloom ?? 0;
const moveMult = moving ? (sprinting ? (CFG.gun.spreadSprintMult || 1) : (CFG.gun.spreadMoveMult || 1)) : 1;
const airMult = G.player.grounded ? 1 : (CFG.gun.spreadAirMult || 1);
const crouchMult = crouching ? (CFG.gun.spreadCrouchMult || 1) : 1;
const target = Math.min(CFG.gun.spreadMax || 0.02, base * moveMult * airMult * crouchMult);
G.weapon.targetSpread = target;
const decay = CFG.gun.spreadDecay || 6.0;
// Exponential approach to target
const k = 1 - Math.exp(-decay * delta);
G.weapon.spread += (target - G.weapon.spread) * k;
let reloadTilt = 0;
if (G.weapon.reloading && G.weapon.infiniteAmmoTimer <= 0) {
G.weapon.reloadTimer -= delta;
reloadTilt = 0.4 * Math.sin(Math.min(1, 1 - G.weapon.reloadTimer / CFG.gun.reloadTime) * Math.PI);
if (G.weapon.reloadTimer <= 0) {
const needed = CFG.gun.magSize - G.weapon.ammo;
if (G.weapon.reserve === Infinity) {
G.weapon.ammo += needed;
} else {
const taken = Math.min(needed, G.weapon.reserve);
G.weapon.ammo += taken;
G.weapon.reserve -= taken;
}
G.weapon.reloading = false;
if (CFG.audio.reloadEnd) playReloadEnd();
updateHUD();
}
}
G.weapon.group.position.set(
G.weapon.basePos.x + bobX,
G.weapon.basePos.y + bobY,
G.weapon.basePos.z - G.weapon.recoil
);
// Aim barrel at crosshair
const muzzleWorld = G.tmpV1;
G.weapon.muzzle.getWorldPosition(muzzleWorld);
const muzzleCam = G.tmpV2.copy(muzzleWorld);
G.camera.worldToLocal(muzzleCam);
const aimPointCam = G.tmpV3.set(0, 0, -10);
const aimDirCam = aimPointCam.sub(muzzleCam).normalize();
// Reuse quaternions to reduce GC
const FWD = G.tmpFwd || (G.tmpFwd = new THREE.Vector3(0, 0, -1));
const QAIM = G.tmpQAim || (G.tmpQAim = new THREE.Quaternion());
const QROLL = G.tmpQRoll || (G.tmpQRoll = new THREE.Quaternion());
const QREL = G.tmpQRel || (G.tmpQRel = new THREE.Quaternion());
QAIM.setFromUnitVectors(FWD, aimDirCam);
const styleRoll = THREE.MathUtils.degToRad(-3);
QROLL.setFromAxisAngle(FWD, swayZRot + styleRoll + reloadTilt);
QREL.setFromAxisAngle(new THREE.Vector3(1, 0, 0), reloadTilt * 0.2);
G.weapon.group.quaternion.copy(QAIM).multiply(QROLL).multiply(QREL);
// ----- Apply view recoil to camera (non-destructive) -----
// Smoothly return view kick to zero
const ret = CFG.gun.viewReturn || 9.0;
const rk = 1 - Math.exp(-ret * delta);
G.weapon.viewPitch -= G.weapon.viewPitch * rk;
G.weapon.viewYaw -= G.weapon.viewYaw * rk;
// Apply the delta since last frame to the camera so it cancels on return
const dPitch = G.weapon.viewPitch - G.weapon.appliedPitch;
const dYaw = G.weapon.viewYaw - G.weapon.appliedYaw;
// Pitch up (negative X rotation) feels like CS, invert sign accordingly
G.camera.rotation.x -= dPitch;
G.camera.rotation.y += dYaw;
G.weapon.appliedPitch = G.weapon.viewPitch;
G.weapon.appliedYaw = G.weapon.viewYaw;
// ----- Temporary fire-rate buff -----
if (G.weapon.rofBuffTimer > 0) {
G.weapon.rofBuffTimer -= delta;
if (G.weapon.rofBuffTimer <= 0) {
G.weapon.rofBuffTimer = 0;
G.weapon.rofMult = 1;
}
}
// ----- Infinite ammo buff timer and restore -----
if (G.weapon.infiniteAmmoTimer > 0) {
G.weapon.infiniteAmmoTimer -= delta;
if (G.weapon.infiniteAmmoTimer <= 0) {
G.weapon.infiniteAmmoTimer = 0;
// Restore original ammo/reserve values if saved
if (G.weapon.ammoBeforeInf != null) {
G.weapon.ammo = G.weapon.ammoBeforeInf;
G.weapon.ammoBeforeInf = null;
}
if (G.weapon.reserveBeforeInf != null) {
G.weapon.reserve = G.weapon.reserveBeforeInf;
G.weapon.reserveBeforeInf = null;
}
updateHUD();
}
}
// ----- Weapon glow while buffs are active -----
const active = (G.weapon.rofBuffTimer > 0) || (G.weapon.infiniteAmmoTimer > 0);
// Pulse emissive when active (subtle), color depends on buff
const mats = G.weapon.materials || [];
if (active) {
G.weapon.glowT += delta * 3.0;
const pulse = 0.6 + Math.sin(G.weapon.glowT) * 0.4; // 0.2..1.0
for (let i = 0; i < mats.length; i++) {
const m = mats[i];
if (!m || !m.isMaterial) continue;
// Indigo for infinite ammo, yellow for accelerator
const color = (G.weapon.infiniteAmmoTimer > 0) ? 0x6366f1 : 0xffd84d;
if (m.emissive) m.emissive.setHex(color);
if ('emissiveIntensity' in m) m.emissiveIntensity = 0.8 + pulse * 0.6;
}
} else {
for (let i = 0; i < mats.length; i++) {
const m = mats[i];
if (!m || !m.isMaterial) continue;
if (m.emissive) m.emissive.setHex(0x000000);
if ('emissiveIntensity' in m) m.emissiveIntensity = 1.0;
}
}
}