damage.ts

PURPOSE

Single source of truth for combat damage resolution. Owns the two top-level damage entry points (damageEnemy and damagePlayer), the player-side shield-then-hull pipeline, enemy death bookkeeping (kill streaks, combo kills, elite/boss bonuses, archetype death audio, deferred death VFX), and the supporting helpers for warp-line damage, destructible damage, revive bookkeeping, and global player-damage scaling.

OWNS

  • PLAYER_DAMAGE_MULT — global player-output scalar (0.263) applied in computePlayerDamage.
  • ARCHETYPE_DEATH_AUDIO — behavior-string to death-sample map used at the kill-juice dispatch (charger, racer, shooter, gunner, mortar, sniper, field, brute, wisp, lurker, bombardier, sprinter, spitter, burner, suppressor).
  • Death VFX stagger queue: _deathVfxQueue, DEATH_VFX_CAP, updateDeathVfx(rawDt), internal _spawnDeathVfx(x, y, r).
  • Combo-kill camera/hit-stop state: _COMBO_KILL_WINDOW, _COMBO_SHAKE_COOLDOWN, _COMBO_PARAMS (scaled amp/dur/freeze for 1, 2, 3+ kills), _comboKillTimes, _comboShakeCooldownUntil, _trackComboKill(game).
  • Boss shared-health bookkeeping: getBossBarHpMax(world) and hasRemainingBossSharer(world, deadEnemy).
  • Public functions: pointToLineDistance, computePlayerDamage, damageEnemy, pushOffTerrain, spawnXPOrbs (with XP_NERF_MULT = 0.45), damageDestructible, warpLineDamage, damagePlayer, applyKnockback.
  • ReviveSystem object with _active, _timer, check(ship, game), update(dt), reset() — consumes bonuses.deathDefianceTokens and sets a 7s death-defiance grace.
  • Kill-reward helpers: _checkStreakMilestone, _awardEliteKillBonus, _rollEliteRewardDrops (rarity → drop count curve uncommon/rare/epic/legendary, 50/50 weapon-vs-artifact), _awardBossKillBonus.
  • Player-side stage constants: KB_BASE (5), KB_PER_DMG (0.19), KB_MAX (15); threshold-DR ramp _HP_DR_HI (0.40), _HP_DR_LO (0.05), _HP_DR_MAX (0.24).

READS FROM

  • ../core/typesWorldState, ShipState, GameState.
  • ../core/config (CFG) — CFG.INVULN for shield-break grace.
  • ../enemies/spawner (GameMaster) — writes _recentDamage for AI director pressure relief.
  • ../vfx/particlesParticles, DmgNumbers, ShieldDmgAccum, HpDmgAccum, XpAccum, Notifications.
  • ../vfx/juice (Juice) — fires enemy_kill_tiny/enemy_kill/enemy_kill_large, archetype kill cues, elite_kill, shield_hit, shield_broken, player_hull_hit.
  • ../vfx/explosion-fx (ExplosionFX.death), ../vfx/player-glow (PlayerGlow.flash), ../vfx/sonar-rings (SonarRings.shockwaveArc).
  • ../rendering/camera (Camera.shake), ../rendering/draw (triggerHpFlash, getShieldVisualRadius), ../rendering/hud (triggerTutorial, pushHitIndicator).
  • ../enemies/behaviors (EnemyBehaviors.get(...).onDeath).
  • ../core/signals (Sig.fire).
  • ../world/artifactsgetWeaponKnockback, getWeaponStunDuration, notifyKnockback.
  • ../effects/enemy-status (getShredMult).
  • @starship-survivors/data/kill-streaksSTREAK_MILESTONES, ELITE_KILL_BONUS, BOSS_KILL_BONUS.
  • ../audio/sample-sfx (SampleSfx.playShieldHit, SampleSfx.playHullHit).
  • ../physics/collision (nearbyTerrain), ../core/spatial-grid (enemyGrid.query).
  • ../world/xp-orbs (xpOrbs.spawn), ../world/pickups (PickupSystem import retained).
  • ../affixesrunDamageFilterChain, applyAffixesOnDeath.
  • ../../data/bosses (BOSS_DEFS), ../boss/encounter (getSharedBossVfxKit).
  • ../telemetry (telemetry.recordDamageChain).
  • ../player/states (hasExclusiveState import retained).
  • Per-enemy fields: alive, _frozenForLag, _dying, behavior, _chargerPhase, flashAmount, _immuneTextCooldown, isBoss, dmgCapped, _hitImmune, sharesHealthWithBoss, hasShield, shieldHp, affixes, hp, hpMax, radius, x, y, vx, vy, _isPackLeader, _stunTimer, _dmgSquashT, _lastDmgTag, _lagHp, _deathVfxT, _deathVfxMax, _explodeDebrisNow, _isBossAnchor, typeId, _rarityOverride, _pendingXpCount, _pendingXpGame, xp.
  • Ship fields: _gauntletPlayerDamageDisabled, lifeSteal, hp, hpMax, invulnerable, exclusiveState, invulnTimer, damageReduction, shield, shieldMax, shieldHitTimer, shieldHitIntensity, _shieldHitAngle, _hitFlash, _hitFlashColor, _dmgBlueFlash, _shieldBrokenTimer, _shieldRecovering, _shieldBackgroundRegen, _shieldBreakPulse, _hullPulse, _dmgWhiteFlash, vx, vy, x, y, radius, shieldRegenTimer, shieldRegenDelay, alive, weapons.
  • Game fields: time, timeDilation, _hitFreezeTimer, _pendingBarDamage, _invertScreenTimer, _hullFlash, _lastBossDeathX, _lastBossDeathY, _activeBossDefId, bossArena, overtime, runDef.sandbox, weaponSlotsMax, _wcSpawnedThisLevel, bonuses.deathDefianceTokens, tracking.deathDefianceUsed, tracking.damageTaken, tracking.lowestHpPercent, deathTimer, phase, killStreak, killStreakTimer, bestStreak, lastStreakMilestone, dmgFlash, stats.damageDealt, stats.damageTaken, stats.totalKills, stats.eliteKills, _currentLevel.
  • World fields: enemies, dmgNumbers, weaponBoxes, artifactBoxes.

PUSHES TO

  • Mutates enemy.hp, enemy.flashAmount, enemy.vx/vy, enemy._dmgSquashT, enemy._hitImmune, enemy._stunTimer, enemy._immuneTextCooldown, enemy._frozenForLag, enemy._lagHp, enemy._deathVfxT/_deathVfxMax, enemy._explodeDebrisNow, enemy._pendingXpCount/_pendingXpGame, enemy.shieldHp (bosses).
  • Mutates ship state: ship.hp, ship.shield, ship.vx/vy, ship.alive, shield-hit VFX timers/colors, _shieldBrokenTimer, _shieldRecovering, _shieldBackgroundRegen, _shieldBreakPulse, _hullPulse, _dmgWhiteFlash, _hitFlash, _hitFlashColor, _dmgBlueFlash, shieldRegenTimer, invulnerable, invulnTimer.
  • Pushes floating text into world.dmgNumbers (“Immune”), pushes loot into world.weaponBoxes / world.artifactBoxes from elite-drop rolls.
  • Accumulators: DmgNumbers.add, ShieldDmgAccum.collect, HpDmgAccum.collect, XpAccum.collect.
  • Notifications: Notifications.add for milestones, +ELITE BONUS, +BOSS BONUS.
  • Camera/juice/VFX side effects: Camera.shake, Juice.fire(...), Particles.burst/add/burstHex, ExplosionFX.death, PlayerGlow.flash, SonarRings.shockwaveArc, pushHitIndicator, triggerHpFlash, triggerTutorial(3) on first shield hit and triggerTutorial(4) on first hull hit.
  • XP orbs through xpOrbs.spawn from spawnXPOrbs.
  • AI director: increments GameMaster._recentDamage by post-multiplier damage.
  • Telemetry: every damagePlayer call (invuln path and resolved path) emits a telemetry.recordDamageChain entry; the collector samples at ≤1 Hz.
  • Game counters/stats and kill-streak tracking; updates bestStreak, resets killStreak/lastStreakMilestone on hull damage.

DOES NOT

  • Does not set _dying, _deathTimer, _deathPulseR, or spawn the post-death debris/sprite-transition VFX — bridge transitions _frozenForLag → _dying once _lagHp reaches 0.
  • Does not let shield damage bleed into HP; if any shield remains, the entire hit routes to shield and excess is discarded.
  • Does not apply enemy-side knockback to pack leaders (_isPackLeader), and does not apply knockback or damage when the invuln gate fires in damagePlayer (gate explicitly skips both).
  • Does not drop weapon chests from enemy kills — the inline drop chance is pinned to Math.random() < 0 and weapons only come from weapon-type events.
  • Does not drop mod capsules — mods unlock via captain level and are bought with credits.
  • Does not call damagePlayer from inside damageEnemy; the two entry points are independent.
  • Does not advance the death VFX queue itself — bridge calls updateDeathVfx(rawDt) with raw delta so VFX play during time dilation.
  • Does not award enemy_kill/tagged_kill/streak/elite/XP on _isBossAnchor deaths — anchors only fire boss_anchor_destroyed.
  • Does not apply non-player damage to sharers of a shared-health boss (sourceKind !== 'player' is rejected when sharesHealthWithBoss).
  • Does not let the boss shield bleed through on a single hit — when boss shield absorbs everything, the function returns after flashing the body.
  • Does not reset shield, hp, or any cooldowns on revive — ReviveSystem.check only sets phase = 'dead', ship.alive = false, and starts a 7s timer.

Signals

Fired via Sig.fire(name, 0, 0, ...):

  • damage_dealt — every enemy hit that survives gating, payload (rawDmg, enemy.x, _lastDmgTag || '').
  • enemy_kill — non-anchor, non-sharer kill, payload (enemy.x, enemy.y, typeId || '').
  • tagged_kill — paired with enemy_kill, payload carries _lastDmgTag for damage-type-conditional effects.
  • boss_anchor_destroyed — anchor body died, payload (enemy.x, enemy.y, typeId || '').
  • boss_body_kill — shared-health body died (any sharer), payload (enemy.x, enemy.y, typeId || '').
  • boss_kill — last sharer died (final-sharer condition), payload (enemy.x, enemy.y, typeId || '').
  • kill_streak_milestone — newly-reached highest milestone, payload (m.killCount, 0, '').
  • shield_hit — shield absorbed any portion of a player hit, payload (ship.x, ship.y, '').
  • shield_break — shield dropped to 0 in this call, payload (ship.x, ship.y, '').
  • player_damage — fired once per resolved player hit, payload (amount, 0, 'shield' | 'hull').
  • hull_damage — only when hullDmg > 0, payload (hullDmg, 0, '').

Entry points

  • damageEnemy(enemy, rawDmg, game, ship, world, sourceKind = 'player') — call site for every enemy hit, including weapon damage, contact damage, warp-line, DoT through filter chain, and boss-encounter-scripted damage. Returns early on dead/lag-frozen/charger-phase-2/boss-hit-immune/gauntlet-disabled/shared-with-boss-non-player.
  • damagePlayer(ship, amount, game, hitAngle?, hpDamageMult?, source = 'unknown') — call site for every player hit. hitAngle defaults to random when source position is unknown; hpDamageMult lets specific sources (e.g. shooter bullets) reduce hull damage while keeping full shield damage; source is a short label captured in the damage-chain trace.
  • damageDestructible(destructible, rawDmg) — applies damage and floating numbers to inert destructibles; flips alive to false when HP drops, no follow-up callback wired.
  • warpLineDamage(x1, y1, x2, y2, world, game, ship) — corridor query (half-width 25, base damage 120) against enemy spatial grid; routes each hit through computePlayerDamage and damageEnemy, then applies perpendicular 200-unit knockback and a purple spark burst.
  • computePlayerDamage(baseDmg, weaponId, target, ship) — multiplies base damage by PLAYER_DAMAGE_MULT (0.263). weaponId and target are currently unused.
  • applyKnockback(entity, angle, force) — generic compatibility export that adds cos(angle)*force / sin(angle)*force to vx/vy.
  • spawnXPOrbs(world, enemy, count, game?) — depth/tier-scaled orb amount, runs each spawn position through pushOffTerrain.
  • pushOffTerrain(x, y, world) — radial push out of any overlapping asteroid with a 40px padding; uses nearbyTerrain query (radius 60+padding).
  • pointToLineDistance(px, py, x1, y1, x2, y2) — utility used by warpLineDamage.
  • updateDeathVfx(rawDt) — bridge-driven tick over the death VFX queue; spawns at most 2 deferred death VFX per frame.
  • ReviveSystem.check / update / reset — death-defiance gating and its 7s timer.

Pattern notes

  • Player damage pipeline (damagePlayer) stages: A invuln gate (early return, knockback skipped), B _flatDR = max(0, 1 - damageReduction/100) (clamped — damageReduction >= 100 silently zeros damage), C _threshDR ramp from 1.0 at 40% HP down to 0.76 at 5% HP, D dmg = amount * _flatDR * _threshDR, E knockback min(KB_MAX, KB_BASE + dmg*KB_PER_DMG) applied unconditionally (still 5 even when dmg=0), F shield-vs-hull route, G shield-break grace ship.invulnTimer = max(invulnTimer, CFG.INVULN). Every call ends with telemetry.recordDamageChain capturing the full per-stage trace; the invuln-gate path emits its own record before returning.
  • Shield route raises shieldHitTimer = 0.3, drives blue spark cone (~120° centered on hit angle) and white core sparks, fires Juice.fire('shield_hit'), accumulates into ShieldDmgAccum, plays SampleSfx.playShieldHit, and shakes camera + hit-stops scaled to absorbed/shieldMax. On break: 20 blue glass shards + 16 white-blue dust, Juice.fire('shield_broken'), _invertScreenTimer = 0.10, _hitFreezeTimer ≥ 0.10, Camera.shake(11, 0.45), 6 SonarRings.shockwaveArc fragments, _shieldBreakPulse flag for bridge, _shieldBrokenTimer = 2.5, _shieldRecovering = true, _shieldBackgroundRegen = 0, and the CFG.INVULN grace window.
  • Hull route applies hpDamageMult if provided, accumulates to HpDmgAccum, scales triggerHpFlash and game._hullFlash by hullDmg/hpMax, plays SampleSfx.playHullHit, fires Juice.fire('player_hull_hit'), triggers tutorial step 4 on first occurrence, breaks the kill streak (killStreak = 0, lastStreakMilestone = -1), and flips _hullPulse = 1.0 for the persistent red sin-wave tint. Shield damage explicitly does not break the streak.
  • Enemy damage pipeline ordering: charger-phase-2 immunity (with “Immune” floating text on a 5s per-enemy cooldown) → boss/dmgCapped hit-immune gate → shared-health-with-boss requires sourceKind === 'player' → boss shield absorbs first (no bleed) → getShredMult multiplier → affix filter chain (runDamageFilterChain short-circuits on <= 0) → boss damage cap: shared-health bodies cap at 30 + getBossBarHpMax(world)*0.02, single boss/dmgCapped cap at 30 + enemy.hpMax*0.02 with _hitImmune = 12/60 (≈200ms per-part).
  • Enemy hit bookkeeping: enemy.hp -= rawDmg, flashAmount = 1.0, _dmgSquashT = 0.15, game.stats.damageDealt += rawDmg, game._pendingBarDamage += rawDmg for shared-health bosses, Sig.fire('damage_dealt', ...), DmgNumbers.add, Personal Space knockback getWeaponKnockback() (skipped on _isPackLeader) optionally followed by getWeaponStunDuration() setting _stunTimer, then ship.lifeSteal heal.
  • Enemy death sequence (HP 0, not already dying): clamp HP to 0, flip _frozenForLag = true, force _lagHp = 0, white-dust burst at HP-bar world position, set _deathVfxT = _deathVfxMax = 0.075, latch _explodeDebrisNow = true, dispatch EnemyBehaviors.get(behavior).onDeath and applyAffixesOnDeath. Branches: anchor → boss_anchor_destroyed; shared-health sharer → boss_body_kill, snapshot _lastBossDeathX/Y on final sharer, dispatch BOSS_DEFS[_activeBossDefId].onBodyDeath and .onDeath with shared kit, then boss_kill on final; regular → enemy_kill + tagged_kill, defer _pendingXpCount = 1 (5% in overtime), increment totalKills/killStreak, refresh killStreakTimer = 2.0, _trackComboKill, _checkStreakMilestone, elite (dmgCapped) gets _awardEliteKillBonus + _rollEliteRewardDrops, isBoss gets _awardBossKillBonus. Kill juice picks archetype audio from ARCHETYPE_DEATH_AUDIO[behavior] first, else size brackets at r >= 40 (enemy_kill_large), r <= 8 (enemy_kill_tiny), default (enemy_kill).
  • Combo-kill hit-stop tracks recent kill times within _COMBO_KILL_WINDOW = 0.1s, gates triggers behind a 0.6s cooldown, and indexes into _COMBO_PARAMS by kills-in-window (clamped at index 2): 1 kill {amp:2.2, dur:0.09, freeze:0.014}, 2 kills {amp:4.0, dur:0.15, freeze:0.03}, 3+ kills {amp:7.0, dur:0.22, freeze:0.07}. Freeze pauses by setting game.timeDilation = 0 and _hitFreezeTimer.
  • Death VFX stagger queue caps at 8 entries (silent drop beyond), spawns at most 2 deferred death VFX per updateDeathVfx(rawDt) call using swap-remove. Only cosmetic VFX defer; damage/XP/loot/signals all fire on the killing-blow frame.
  • Elite reward drop curve: uncommon 25% of 1, rare 50% of 1, epic 20% of 2 else 90% of 1, legendary guaranteed 2. Each drop is independently 50/50 weapon-vs-artifact; drops carry _vx/_vy 400-500 px/s with _flightFric = 2.0 (bridge handles the flight).
  • XP scaling: XP_NERF_MULT = 0.45 (history 0.75 → 0.45) multiplied against enemy.xp * 0.2025 * 0.975 * depthMult * tierMult where depthMult = 1.3^(level-1) and tierMult = 1 + 0.5*(level-1); floor 1 via Math.max(1, Math.ceil(...)).
  • Boss damage capping intentionally combines an absolute floor (30) with a 2% maxHP scaling so adding HP genuinely extends TTK; shared-health bodies cap against the live bar pool (getBossBarHpMax) rather than the single body being hit, preventing sub-body chunking. Per-part immunity is 200ms (12/60).
  • _shieldRecovering is VISUAL-ONLY for the regen animation after a break; it no longer blocks damage. The CFG.INVULN grace window is the sole post-break protection. Hull takes hits while shield regenerates in the background; shield only restores when _shieldBackgroundRegen fills uninterrupted.
  • ReviveSystem consumes bonuses.deathDefianceTokens, sets tracking.deathDefianceUsed, kicks deathTimer = 7.0, flips phase = 'dead', ship.alive = false, and starts an internal 7s timer. update(dt) deactivates when the timer expires; reset() zeroes both.
  • Compatibility shims: applyKnockback(entity, angle, force) is exported separately from the inline player/enemy knockback math so external systems can drive generic shoves without going through the damage pipeline.