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; | |
| } | |
| } | |
| } | |