Spaces:
Running
Running
File size: 6,962 Bytes
1390db3 09a3a37 1390db3 a646668 1390db3 5cf582c a646668 1390db3 fd12cd0 1390db3 fd12cd0 1390db3 fd12cd0 1390db3 09a3a37 1390db3 09a3a37 1390db3 fe3647a 1390db3 1f43352 1390db3 09a3a37 1390db3 a646668 1390db3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
import * as THREE from 'three';
import { CFG } from './config.js';
import { G } from './globals.js';
import { updateHUD } from './hud.js';
import { spawnTracer, spawnImpact, spawnMuzzleFlash } from './fx.js';
import { getTreesInAABB, getTerrainHeight } from './world.js';
import { spawnShellCasing } from './casings.js';
import { popHelmet } from './helmets.js';
import { beginReload } from './weapon.js';
import { spawnHealthOrbs } from './pickups.js';
import { playGunshot, playHeadshot } from './audio.js';
// Reusable temps to reduce GC
const TMP2 = new THREE.Vector2();
const TMPv1 = new THREE.Vector3();
const TMPv2 = new THREE.Vector3();
const TMPn = new THREE.Vector3();
const HIT_OBJECTS = [];
const UP = new THREE.Vector3(0, 1, 0);
export function performShooting(delta) {
G.shootCooldown -= delta;
if (G.input.shoot && G.shootCooldown <= 0 && G.state === 'playing') {
if (G.weapon.reloading) return;
const infinite = G.weapon.infiniteAmmoTimer > 0;
if (!infinite && G.weapon.ammo <= 0) {
G.shootCooldown = 0.2;
G.weapon.recoil += CFG.gun.recoilKick * 0.25;
return;
}
G.shootCooldown = 1 / (CFG.gun.rof * (G.weapon.rofMult || 1));
if (!infinite) {
G.weapon.ammo--;
}
G.weapon.recoil += CFG.gun.recoilKick;
updateHUD();
// Increase dynamic spread per shot (clamped)
const inc = CFG.gun.spreadShotIncrease || 0;
const maxS = CFG.gun.spreadMax || 0.02;
G.weapon.spread = Math.min(maxS, G.weapon.spread + inc);
// View recoil: add a pitch up and small random yaw
const pitchKick = THREE.MathUtils.degToRad(CFG.gun.viewKickPitchDeg || 0);
const yawKick = THREE.MathUtils.degToRad((CFG.gun.viewKickYawDeg || 0) * (G.random() * 2 - 1));
G.weapon.viewPitch += pitchKick;
G.weapon.viewYaw += yawKick;
const spread = G.weapon.spread || (CFG.gun.bloom || 0);
const nx = (G.random() - 0.5) * spread * 2;
const ny = (G.random() - 0.5) * spread * 2;
TMP2.set(nx, ny);
G.raycaster.setFromCamera(TMP2, G.camera);
G.raycaster.far = CFG.gun.range;
// Build hit list using lightweight proxies and trunk-only blockers
HIT_OBJECTS.length = 0;
for (let i = 0; i < G.enemies.length; i++) {
const e = G.enemies[i];
if (!e.alive || !e.hitProxies) continue;
// push proxies directly; no recursion needed
for (let k = 0; k < e.hitProxies.length; k++) HIT_OBJECTS.push(e.hitProxies[k]);
}
for (let i = 0; i < G.blockers.length; i++) HIT_OBJECTS.push(G.blockers[i]);
const hits = G.raycaster.intersectObjects(HIT_OBJECTS, false);
G.weapon.muzzle.getWorldPosition(TMPv1);
G.camera.getWorldDirection(TMPv2);
let end = TMPv2.clone().multiplyScalar(CFG.gun.range).add(G.camera.position);
let firstHit = null;
// Choose nearest of raycast hit (enemy/ground) and tree trunk collision
const origin = G.camera.position;
const rayFirst = hits.length > 0 ? hits[0] : null;
const rayDist = rayFirst ? origin.distanceTo(rayFirst.point) : Infinity;
// Candidate tree hit along the segment (origin -> max range)
let treeHitU = Infinity;
{
const minX = Math.min(origin.x, end.x) - 2.0;
const maxX = Math.max(origin.x, end.x) + 2.0;
const minZ = Math.min(origin.z, end.z) - 2.0;
const maxZ = Math.max(origin.z, end.z) + 2.0;
const cands = getTreesInAABB(minX, minZ, maxX, maxZ);
const ox = origin.x, oz = origin.z;
const ex = end.x, ez = end.z;
const vx = ex - ox, vz = ez - oz;
const vv = vx * vx + vz * vz || 1;
for (let i = 0; i < cands.length; i++) {
const t = cands[i];
const wx = t.x - ox, wz = t.z - oz;
let u = (wx * vx + wz * vz) / vv;
if (u < 0) u = 0; else if (u > 1) u = 1;
const px = ox + u * vx, pz = oz + u * vz;
const dx = t.x - px, dz = t.z - pz;
const rr = (t.radius + 0.2) * (t.radius + 0.2);
if (dx * dx + dz * dz <= rr) {
const yAt = origin.y + (end.y - origin.y) * u;
if (yAt < 8 && u < treeHitU) treeHitU = u;
}
}
}
if (treeHitU !== Infinity && (treeHitU * origin.distanceTo(end)) < rayDist) {
// Trunk is the nearest hit
const hitPos = new THREE.Vector3(
origin.x + (end.x - origin.x) * treeHitU,
origin.y + (end.y - origin.y) * treeHitU,
origin.z + (end.z - origin.z) * treeHitU
);
const gy = getTerrainHeight(hitPos.x, hitPos.z) + 0.02;
if (hitPos.y < gy) hitPos.y = gy;
end.copy(hitPos);
firstHit = { point: hitPos, object: null, face: null };
} else if (rayFirst) {
firstHit = rayFirst;
end.copy(rayFirst.point);
}
if (firstHit && firstHit.object) {
// Find enemy and hit zone by traversing up the hierarchy
function findEnemyAndZone(obj) {
let cur = obj;
while (cur) {
if (cur.userData && cur.userData.enemy) {
return { enemy: cur.userData.enemy, zone: cur.userData.hitZone || 'body' };
}
cur = cur.parent;
}
return { enemy: null, zone: null };
}
const { enemy, zone } = findEnemyAndZone(firstHit.object);
if (enemy && enemy.alive) {
// Trigger hitmarker HUD flash
G.hitFlash = Math.min(1, (G.hitFlash || 0) + 1);
const isHead = zone === 'head';
const dmg = CFG.gun.damage * (isHead ? CFG.gun.headshotMult : 1);
enemy.hp -= dmg;
if (isHead) playHeadshot();
// If headshot, pop the helmet off with a kick in shot direction
if (isHead && enemy.helmetAttached) {
const shotDir = new THREE.Vector3();
G.camera.getWorldDirection(shotDir);
popHelmet(enemy, shotDir, firstHit.point);
}
if (enemy.hp <= 0) {
enemy.alive = false;
enemy.deathTimer = 0;
G.waves.aliveCount--;
G.player.score += isHead ? 15 : 10;
// Drop more orbs for special enemies
if (enemy.type === 'golem') {
const cnt = 15 + Math.floor(G.random() * 6); // 15..20
spawnHealthOrbs(enemy.pos, cnt);
} else {
const cnt = 1 + Math.floor(G.random() * 5);
spawnHealthOrbs(enemy.pos, cnt);
}
}
} else if (firstHit.object !== G.ground) {
const n = firstHit.face?.normal
? TMPn.copy(firstHit.face.normal).transformDirection(firstHit.object.matrixWorld)
: UP;
spawnImpact(firstHit.point, n);
}
} else if (firstHit && !firstHit.object) {
// Blocked by a tree trunk approximation
spawnImpact(firstHit.point, UP);
}
spawnTracer(TMPv1, end);
spawnMuzzleFlash();
spawnShellCasing();
playGunshot();
}
if (
!G.weapon.reloading &&
G.weapon.infiniteAmmoTimer <= 0 &&
G.weapon.ammo === 0 &&
(G.weapon.reserve > 0 || G.weapon.reserve === Infinity)
) {
beginReload();
}
}
|