Spaces:
Running
Running
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; | |
} | |
} | |
} | |