1import * as THREE from 'three'; 2import { GAME_CONFIG, getActiveMapSize, BOX_POSITION, getPackAPunchCostForWeapon } from './config.js'; 3import { state } from './state.js'; 4import { dialogue } from './dialogue.js'; 5import { scene, playerGroup, camera, worldPropsGroup, resetGameplayCameraPresentation, boxGroup } from './scene.js'; 6import { FirstPersonCamera } from './input.js'; 7import { 8 createHitParticles, 9 createZombieDeathParticles, 10 spawnBloodSplat, 11 spawnBloodOnZombieHit, 12 spawnBloodOnZombieKill, 13 SPLAT_BLOOD_PALETTE, 14 updateBloodSplats 15} from './entities.js'; 16import { 17 spawnZombieGoreChunks, 18 updateGoreChunks 19} from './zombie-gore-chunks.js'; 20import { playSound, playHitSound } from './audio.js'; 21import { die, tryEnterDownedOrDie } from './game.js'; 22import { updateWeaponFeel, updateMuzzleFlash } from './weapon-feel.js'; 23import { updateMuzzleBarrelFx } from './muzzle-fx.js'; 24import { spawnDamageNumber, updateDamageNumbers } from './damage-numbers.js'; 25import { spawnChainBoomFanfare, shotBoomThreshold, updateShotBooms } from './chain-boom.js'; 26import { updateZombieAnimations, destroyZombieVisuals, separateActiveZombies } from './zombie-manager.js'; 27import * as net from './network.js'; 28import { getClipBrushBoxes, resolveGlbColliders, resolveClipBrushesXZ } from './map-file.js'; 29import { getNearestDownedPlayerForRevive } from './remote-players.js'; 30import { applyZombieTerrainColliders, getZombiePathSubsteps } from './zombie-manager.js'; 31import { 32 getPointsMultiplier, 33 isInstaKillActive, 34 trySpawnSoloPowerupAt, 35 getMysteryBoxCostDisplay 36} from './powerup-drops.js'; 37import { mapUsesPowerMechanic } from './power-system.js'; 38 39const zombieLookTarget = new THREE.Vector3(); 40const particleWorldPos = new THREE.Vector3(); 41// Reused every frame by the PAP interaction loop to avoid per-frame allocation. 42const _papWorldPos = new THREE.Vector3(); 43const _powerSwitchWorldPos = new THREE.Vector3(); 44 45function buysNeedMapPowerLocked() { 46 const ps = GAME_CONFIG.powerSystem ?? {}; 47 return ps.enabled !== false && mapUsesPowerMechanic() && !state.powerOn; 48} 49 50function isZombieHeadshot(projMesh, zombieMesh, zCfg) { 51 const relY = projMesh.position.y - zombieMesh.position.y; 52 const y0 = zCfg.headshotMinY ?? 1.38; 53 const y1 = zCfg.headshotMaxY ?? 2.02; 54 return relY >= y0 && relY <= y1; 55} 56 57export function updatePhysics(dt) { 58 if (!state.fpsCamera) { 59 // Previously this traversed the entire scene to build a Box3 per mesh for 60 // FirstPersonCamera. The resulting array was never read â collision runs 61 // through resolveGlbColliders / _resolveClipBrushes / getFloorYAt instead. 62 // The traversal was a ~5-15 ms one-off hitch on the first game frame. 63 state.fpsCamera = new FirstPersonCamera(camera); 64 state.fpsCamera.translation_.set(0, GAME_CONFIG.player.height, 0); 65 // Re-apply saved sensitivity now that the camera instance exists. 66 const _savedSens = parseFloat(localStorage.getItem('zbonk_mouseSensitivity') || '5'); 67 const _factor = _savedSens / 5; 68 state.fpsCamera.phiSpeed_ = 8 * _factor; 69 state.fpsCamera.thetaSpeed_ = 5 * _factor; 70 // `gamePhase` can be `menu` during the map intro â update() is skipped, so the 71 // previous session's camera aim would otherwise show through the composer. 72 if (state.gamePhase !== 'playing' && state.gamePhase !== 'upgrade') { 73 resetGameplayCameraPresentation(); 74 } 75 } 76 77 const pausedPlaying = 78 state.isPaused && 79 (state.gamePhase === 'playing' || state.gamePhase === 'upgrade'); 80 81 // Camera first so viewmodel walk sway uses the same-frame `bobPhase_` / speed as the body bob. 82 if (!pausedPlaying) { 83 if (state.gamePhase === 'playing' || state.gamePhase === 'upgrade') { 84 if (state.gamePhase === 'playing' && state.spectating && GAME_CONFIG.player.multiplayer) { 85 state.fpsCamera.update(dt); 86 state.wallRunning = false; 87 88 // Clip brush collision (Radiant-style volumes placed in the editor) 89 const clipBoxesSpec = getClipBrushBoxes(); 90 if (clipBoxesSpec.length > 0) { 91 resolveClipBrushesXZ(state.fpsCamera.translation_, GAME_CONFIG.player.hitboxRadius, clipBoxesSpec); 92 } 93 // GLB prop collision 94 resolveGlbColliders(state.fpsCamera.translation_, GAME_CONFIG.player.hitboxRadius); 95 96 playerGroup.position.copy(state.fpsCamera.translation_); 97 playerGroup.position.y = 0; 98 } else { 99 state.fpsCamera.update(dt); 100 state.wallRunning = !!(state.fpsCamera && state.fpsCamera.wallRunSide_ > 0); 101 102 // When downed the player has zero velocity â skip collision resolution 103 // entirely. The prone Y (0.46) would cause the 3D BVH probe to intersect 104 // building-floor geometry and fire a spurious XZ push that ejects the 105 // player from the map. 106 if (!state.downed) { 107 const clipBoxes = getClipBrushBoxes(); 108 if (clipBoxes.length > 0) { 109 resolveClipBrushesXZ(state.fpsCamera.translation_, GAME_CONFIG.player.hitboxRadius, clipBoxes); 110 } 111 resolveGlbColliders(state.fpsCamera.translation_, GAME_CONFIG.player.hitboxRadius); 112 } 113 114 // Hard map-bounds clamp â prevents corner-fighting between sequential 115 // resolvers from slowly walking the player outside the world. 116 const mapHalfPlayer = getActiveMapSize() / 2 - GAME_CONFIG.player.hitboxRadius; 117 state.fpsCamera.translation_.x = Math.max(-mapHalfPlayer, Math.min(mapHalfPlayer, state.fpsCamera.translation_.x)); 118 state.fpsCamera.translation_.z = Math.max(-mapHalfPlayer, Math.min(mapHalfPlayer, state.fpsCamera.translation_.z)); 119 120 playerGroup.position.copy(state.fpsCamera.translation_); 121 playerGroup.position.y = 0; 122 } 123 } 124 } 125 126 updateWeaponFeel(dt); 127 updateMuzzleFlash(dt); 128 updateMuzzleBarrelFx(dt); 129 130 if (pausedPlaying) { 131 if (state.gamePhase === 'playing' || state.gamePhase === 'upgrade') { 132 state.interactTarget = null; 133 state.inBoxRange = false;
134 } 135 return; 136 } 137 138 const worldInteract = 139 state.gamePhase === 'playing' || state.gamePhase === 'upgrade'; 140 141 // Ally revive wins over buyables when a downed teammate is in range (multiplayer). 142 const rng = GAME_CONFIG.interactRange ?? 3; 143 const px = playerGroup.position.x; 144 const pz = playerGroup.position.z; 145 let best = null; 146 let bestD = Infinity; 147 148 if (GAME_CONFIG.player.multiplayer && worldInteract && !state.downed && !state.spectating) { 149 const rr = GAME_CONFIG.player.mpReviveRange ?? 2.8; 150 const near = getNearestDownedPlayerForRevive(px, pz, rr); 151 if (near) { 152 best = { kind: 'revive', sessionId: near.sessionId }; 153 bestD = near.dist; 154 } 155 } 156 157 if (!best && worldInteract) { 158 const considerXZ = (wx, wz, payload) => { 159 const d = Math.hypot(px - wx, pz - wz); 160 if (d < rng && d < bestD) { 161 bestD = d; 162 best = payload; 163 } 164 }; 165 166 const powerLocksBuys = buysNeedMapPowerLocked(); 167 168 // Mystery box is always accessible regardless of power state 169 { 170 const bx = boxGroup.position.x; 171 const bz = boxGroup.position.z; 172 if (state.boxPhase === 'waitingPickup') { 173 considerXZ(bx, bz, { 174 kind: 'box_pickup', 175 weaponName: (GAME_CONFIG.weapons[state.boxLandedWeapon]?.name ?? 'Weapon'), 176 cost: 0, 177 }); 178 } else if (state.boxPhase === 'idle') { 179 considerXZ(bx, bz, { 180 kind: 'box', 181 cost: getMysteryBoxCostDisplay() 182 }); 183 } 184 } 185 186 if (!powerLocksBuys) { 187 for (const ch of worldPropsGroup.children) { 188 if (ch.userData.entityType !== 'perkMachine') continue; 189 const cfg = GAME_CONFIG.worldPerkMachines.find((p) => p.id === ch.userData.perkId); 190 if (!cfg) continue; 191 considerXZ(ch.position.x, ch.position.z, { 192 kind: 'perk', 193 id: cfg.id, 194 title: cfg.title, 195 desc: cfg.desc ?? '', 196 cost: cfg.cost, 197 }); 198 } 199 for (const ch of worldPropsGroup.children) { 200 if (ch.userData?.entityType !== 'packAPunch') continue; 201 ch.getWorldPosition(_papWorldPos); 202 considerXZ(_papWorldPos.x, _papWorldPos.z, { 203 kind: 'pap', 204 cost: getPackAPunchCostForWeapon(state.currentWeapon, state) 205 }); 206 } 207 } 208 209 if (powerLocksBuys) { 210 for (const ch of worldPropsGroup.children) { 211 if (ch.userData?.entityType !== 'powerSwitch') continue; 212 ch.getWorldPosition(_powerSwitchWorldPos); 213 considerXZ(_powerSwitchWorldPos.x, _powerSwitchWorldPos.z, { kind: 'powerSwitch' }); 214 } 215 } 216 } 217 state.interactTarget = best; 218 state.inBoxRange = !state.downed && (best?.kind === 'box' || best?.kind === 'box_pickup'); 219 if (state.downed || state.spectating) { 220 state.interactTarget = null; 221 state.inBoxRange = false; 222 } 223 224 // Projectiles 225 for (let i = state.projectiles.length - 1; i >= 0; i--) { 226 const p = state.projectiles[i]; 227 p.mesh.position.addScaledVector(p.dir, p.speed * dt); 228 p.life -= dt; 229 230 let hit = false; 231 const mapHalf = getActiveMapSize() / 2; 232 if (p.mesh.position.y < 0 || Math.abs(p.mesh.position.x) > mapHalf || Math.abs(p.mesh.position.z) > mapHalf) { 233 hit = true; 234 } 235 236 if (!hit) { 237 const py = p.mesh.position.y; 238 let bestZ = null; 239 let bestJ = -1; 240 let bestDistSq = Infinity; 241 for (let j = state.activeZombies.length - 1; j >= 0; j--) { 242 const z = state.activeZombies[j]; 243 if (p.hitZombies?.has(z)) continue; 244 const dx = p.mesh.position.x - z.mesh.position.x; 245 const dz = p.mesh.position.z - z.mesh.position.z; 246 const distSq = dx * dx + dz * dz; 247 const rad = z.radius + 0.2; 248 if (distSq < rad * rad && py > 0 && py < 2) { 249 if (distSq < bestDistSq) { 250 bestDistSq = distSq; 251 bestZ = z; 252 bestJ = j; 253 } 254 } 255 } 256 257 if (bestZ) { 258 const z = bestZ; 259 const j = bestJ; 260 if (p.hitZombies) p.hitZombies.add(z); 261 262 const zCfg = GAME_CONFIG.zombie; 263 const plCfg = GAME_CONFIG.player;
264 const headshot = isZombieHeadshot(p.mesh, z.mesh, zCfg); 265 const criticalSodaBase = state.worldPerks?.criticalSoda 266 ? (plCfg.critStrikeChance ?? 0.25) 267 : 0; 268 const critBonus = state.upgradeCritChanceAdd ?? 0; 269 const critChance = Math.min(0.95, criticalSodaBase + critBonus); 270 const crit = critChance > 0 && Math.random() < critChance; 271 let dmg = p.damageBase ?? p.damage ?? 0; 272 if (crit) dmg *= plCfg.critDamageMult ?? 1.85; 273 if (headshot) { 274 const hs = 275 (zCfg.headshotDamageMult ?? 2.35) + 276 (state.upgradeHeadshotMultAdd || 0); 277 dmg *= Math.max(1, hs); 278 } 279 dmg = Math.max(1, Math.floor(dmg)); 280 if (!GAME_CONFIG.player.multiplayer && isInstaKillActive()) { 281 dmg = z.health; 282 } 283 284 if (GAME_CONFIG.player.multiplayer && z.networkId) { 285 // ââ Multiplayer: server is authoritative âââââââââââââââââââ 286 // Visual feedback is immediate (optimistic); the zombie mesh 287 // disappears only when the server removes it from state. 288 // Hits are coalesced per-frame and flushed once via 289 // net.flushZombieHits() in main.js's gameLoop â drops late- 290 // round message rate by ~10Ã when multiple players + pierce 291 // are involved. 292 net.queueZombieHit(z.networkId, dmg, headshot, crit); 293 294 const bloodDropletColor = 295 SPLAT_BLOOD_PALETTE[Math.floor(Math.random() * SPLAT_BLOOD_PALETTE.length)]; 296 createHitParticles(p.mesh.position, bloodDropletColor); 297 spawnZombieGoreChunks(p.mesh.position.clone(), p.dir, { variant: 'hit' }); 298 spawnBloodOnZombieHit(z.mesh.position.x, z.mesh.position.z); 299 playHitSound(headshot); 300 301 const lo = new THREE.Vector3( 302 (Math.random() - 0.5) * 0.45, 303 headshot ? 1.52 + Math.random() * 0.28 : 1.05 + Math.random() * 0.35, 304 (Math.random() - 0.5) * 0.45 305 ); 306 // Can't know if it's a kill client-side; show hit number only 307 spawnDamageNumber(dmg, { 308 kill: false, 309 followMesh: z.mesh, 310 localOffset: lo, 311 headshot, 312 crit 313 }); 314 315 if (p.shotTally && !p.shotTally.zombiesHit.has(z)) { 316 p.shotTally.zombiesHit.add(z); 317 const boomTh = shotBoomThreshold(); 318 if (p.shotTally.zombiesHit.size >= boomTh && !p.shotTally.boomShown) { 319 p.shotTally.boomShown = true; 320 spawnChainBoomFanfare({ 321 followMesh: z.mesh, 322 localOffset: new THREE.Vector3(0, 1.58, 0) 323 }); 324 } 325 } 326 } else { 327 // ââ Solo: local authority ââââââââââââââââââââââââââââââââââ 328 z.health -= dmg; 329 const ptMul = getPointsMultiplier(); 330 state.points += Math.floor(GAME_CONFIG.economy.pointsPerHit * ptMul); 331 const bloodDropletColor = 332 SPLAT_BLOOD_PALETTE[ 333 Math.floor(Math.random() * SPLAT_BLOOD_PALETTE.length) 334 ]; 335 createHitParticles(p.mesh.position, bloodDropletColor); 336 spawnZombieGoreChunks(p.mesh.position.clone(), p.dir, { variant: 'hit' }); 337 spawnBloodOnZombieHit(z.mesh.position.x, z.mesh.position.z);
338 playHitSound(headshot); 339 const willKill = z.health <= 0; 340 const lo = new THREE.Vector3( 341 (Math.random() - 0.5) * 0.45, 342 headshot ? 1.52 + Math.random() * 0.28 : 1.05 + Math.random() * 0.35, 343 (Math.random() - 0.5) * 0.45 344 ); 345 const frozen = willKill 346 ? new THREE.Vector3( 347 z.mesh.position.x + lo.x, 348 Math.min(1.95, z.mesh.position.y + lo.y), 349 z.mesh.position.z + lo.z 350 ) 351 : null; 352 spawnDamageNumber(dmg, { 353 kill: willKill, 354 followMesh: willKill ? null : z.mesh, 355 localOffset: lo, 356 frozenWorld: frozen, 357 headshot, 358 crit 359 }); 360 361 if (p.shotTally && !p.shotTally.zombiesHit.has(z)) { 362 p.shotTally.zombiesHit.add(z); 363 const boomTh = shotBoomThreshold(); 364 if (p.shotTally.zombiesHit.size >= boomTh && !p.shotTally.boomShown) { 365 p.shotTally.boomShown = true; 366 const boomY = z.mesh.position.y + 1.58; 367 const boomAnchor = willKill 368 ? { 369 frozenWorld: new THREE.Vector3( 370 z.mesh.position.x, 371 boomY, 372 z.mesh.position.z 373 ) 374 } 375 : { 376 followMesh: z.mesh, 377 localOffset: new THREE.Vector3(0, 1.58, 0) 378 }; 379 spawnChainBoomFanfare(boomAnchor); 380 } 381 } 382 383 if (z.health <= 0) { 384 dialogue.onKill(); 385 state.soloScoreboardKills++; 386 state.points += Math.floor(GAME_CONFIG.economy.pointsPerKill * ptMul); 387 state.health = Math.min(state.maxHealth, state.health + state.lifeSteal); 388 spawnBloodOnZombieKill(z.mesh.position.x, z.mesh.position.z); 389 createZombieDeathParticles(z.mesh.position); 390 const burst = z.mesh.position.clone(); 391 burst.y += 0.82 + Math.random() * 0.32; 392 const ang = Math.random() * Math.PI * 2; 393 const killDir = new THREE.Vector3( 394 Math.cos(ang) * 0.75, 395 0.28 + Math.random() * 0.22, 396 Math.sin(ang) * 0.75 397 ).normalize(); 398 spawnZombieGoreChunks(burst, killDir, { variant: 'kill' }); 399 trySpawnSoloPowerupAt(z.mesh.position.x, z.mesh.position.z); 400 destroyZombieVisuals(z); 401 scene.remove(z.mesh); 402 state.activeZombies.splice(j, 1); 403 } 404 } 405 406 p.hitsOnZombies = (p.hitsOnZombies ?? 0) + 1; 407 const maxHits = p.maxPierceHits ?? 1; 408 hit = p.hitsOnZombies >= maxHits; 409 } 410 } 411 412 if (hit || p.life <= 0) { 413 scene.remove(p.mesh); 414 // Tracer materials are cached in createWeaponTracerMaterial â do not dispose. 415 state.projectiles.splice(i, 1); 416 } 417 } 418 419 // Particles (blood droplets leave a floor splat when they land) 420 const splatHalf = getActiveMapSize() / 2 - 0.35; 421 for (let i = state.particles.length - 1; i >= 0; i--) { 422 const p = state.particles[i]; 423 p.mesh.position.addScaledVector(p.vel, dt);
424 p.vel.y -= 20 * dt; 425 p.life -= dt; 426 427 p.mesh.getWorldPosition(particleWorldPos); 428 const wx = particleWorldPos.x; 429 const wy = particleWorldPos.y; 430 const wz = particleWorldPos.z; 431 432 const detach = () => { 433 if (p.mesh.parent) p.mesh.parent.remove(p.mesh); 434 state.particles.splice(i, 1); 435 }; 436 437 if (p.life <= 0) { 438 detach(); 439 continue; 440 } 441 442 if (wy < 0.11) { 443 if ( 444 p.leavesBloodSplat && 445 Math.abs(wx) < splatHalf && 446 Math.abs(wz) < splatHalf 447 ) { 448 spawnBloodSplat(wx, wz, p.splatColor); 449 } 450 detach(); 451 continue; 452 } 453 } 454 455 updateGoreChunks(dt); 456 457 updateBloodSplats(dt); 458 459 // Update zombie animations (runs in both solo and multiplayer for smooth visuals) 460 updateZombieAnimations(dt); 461 462 // ââ Solo-only: zombie movement, player collision, zombie damage âââââââââ 463 if (!GAME_CONFIG.player.multiplayer) { 464 const pHitR = GAME_CONFIG.player.hitboxRadius; 465 const attackPad = 0.2; 466 const pathSub = getZombiePathSubsteps(); 467 const subInv = 1 / pathSub; 468 469 state.activeZombies.forEach(z => { 470 const px = playerGroup.position.x; 471 const pz = playerGroup.position.z; 472 const zx = z.mesh.position.x; 473 const zz = z.mesh.position.z; 474 475 let dirx = state.downed ? zx - px : px - zx; 476 let dirz = state.downed ? zz - pz : pz - zz; 477 let dist = Math.hypot(dirx, dirz); 478 479 if (dist > 0.1) { 480 dirx /= dist; 481 dirz /= dist; 482 zombieLookTarget.set(z.mesh.position.x + dirx, z.mesh.position.y, z.mesh.position.z + dirz); 483 z.mesh.lookAt(zombieLookTarget); 484 // Shorter sub-steps: re-resolve floor + clip + GLB per slice so 485 // zombies donât tunnel past steep mesh or get stuck in one frame. 486 for (let s = 0; s < pathSub; s++) { 487 z.mesh.position.x += dirx * z.speed * dt * subInv; 488 z.mesh.position.z += dirz * z.speed * dt * subInv; 489 applyZombieTerrainColliders(z); 490 } 491 } else if (state.downed) { 492 const ang = Math.random() * Math.PI * 2; 493 const c = Math.cos(ang); 494 const s = Math.sin(ang); 495 for (let k = 0; k < pathSub; k++) { 496 z.mesh.position.x += c * z.speed * dt * subInv; 497 z.mesh.position.z += s * z.speed * dt * subInv; 498 applyZombieTerrainColliders(z); 499 } 500 } 501 502 dirx = px - z.mesh.position.x; 503 dirz = pz - z.mesh.position.z; 504 dist = Math.hypot(dirx, dirz); 505 const minSep = pHitR + z.radius + 0.12; 506 if (dist < minSep) { 507 if (dist < 1e-5) { 508 const ang = Math.random() * Math.PI * 2; 509 dirx = Math.cos(ang); 510 dirz = Math.sin(ang); 511 dist = 1e-5; 512 } else { 513 dirx /= dist; 514 dirz /= dist; 515 } 516 const push = minSep - dist; 517 z.mesh.position.x -= dirx * push; 518 z.mesh.position.z -= dirz * push; 519 dist = minSep; 520 } 521 522 if (dist < pHitR + z.radius + attackPad && !state.downed) { 523 const now = Date.now(); 524 const cooldown = GAME_CONFIG.zombie.attackCooldownMs ?? 1500; 525 if (now - (z.lastAttackTime || 0) >= cooldown) { 526 z.lastAttackTime = now; 527 state.lastDamageTime = now; 528 state.health -= GAME_CONFIG.zombie.baseDamage; 529 dialogue.onDamage(); 530 document.getElementById('damageOverlay').style.boxShadow = 'inset 0 0 100px rgba(255,0,0,0.5)'; 531 setTimeout(() => document.getElementById('damageOverlay').style.boxShadow = 'inset 0 0 100px rgba(255,0,0,0)', 200); 532 if (state.health <= 0) { 533 state.health = 0; 534 tryEnterDownedOrDie(); 535 } 536 } 537 } 538 }); 539 540 separateActiveZombies(); 541 } 542 543 const trailCfg = !GAME_CONFIG.player.multiplayer && GAME_CONFIG.zombie.bloodTrail; 544 if (trailCfg) { 545 const ratio = trailCfg.healthRatio; 546 const step = trailCfg.stepDistance; 547 const jitter = trailCfg.jitter ?? 0; 548 for (const z of state.activeZombies) { 549 const maxHp = z.maxHealth ?? z.health; 550 if (maxHp <= 0 || z.health <= 0) continue; 551 if (z.health > maxHp * ratio) { 552 z._bloodTrailAnchor = null; 553 continue; 554 } 555 const zx = z.mesh.position.x; 556 const zz = z.mesh.position.z; 557 if (!z._bloodTrailAnchor) { 558 z._bloodTrailAnchor = { x: zx, z: zz }; 559 continue; 560 } 561 const adx = zx - z._bloodTrailAnchor.x; 562 const adz = zz - z._bloodTrailAnchor.z; 563 if (adx * adx + adz * adz < step * step) continue; 564 565 const jx = (Math.random() - 0.5) * 2 * jitter; 566 const jz = (Math.random() - 0.5) * 2 * jitter; 567 const c = SPLAT_BLOOD_PALETTE[Math.floor(Math.random() * SPLAT_BLOOD_PALETTE.length)]; 568 spawnBloodSplat(zx + jx, zz + jz, c, { 569 scaleMin: trailCfg.scaleMin, 570 scaleMax: trailCfg.scaleMax, 571 lifeMin: trailCfg.lifeMin, 572 lifeMax: trailCfg.lifeMax, 573 peakOpacity: trailCfg.peakOpacity 574 }); 575 z._bloodTrailAnchor.x = zx; 576 z._bloodTrailAnchor.z = zz; 577 } 578 } 579 580 updateDamageNumbers(); 581 updateShotBooms(); 582}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.