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 incomputePlayerDamage.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)andhasRemainingBossSharer(world, deadEnemy). - Public functions:
pointToLineDistance,computePlayerDamage,damageEnemy,pushOffTerrain,spawnXPOrbs(withXP_NERF_MULT= 0.45),damageDestructible,warpLineDamage,damagePlayer,applyKnockback. ReviveSystemobject with_active,_timer,check(ship, game),update(dt),reset()— consumesbonuses.deathDefianceTokensand 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/types—WorldState,ShipState,GameState.../core/config(CFG) —CFG.INVULNfor shield-break grace.../enemies/spawner(GameMaster) — writes_recentDamagefor AI director pressure relief.../vfx/particles—Particles,DmgNumbers,ShieldDmgAccum,HpDmgAccum,XpAccum,Notifications.../vfx/juice(Juice) — firesenemy_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/artifacts—getWeaponKnockback,getWeaponStunDuration,notifyKnockback.../effects/enemy-status(getShredMult).@starship-survivors/data/kill-streaks—STREAK_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(PickupSystemimport retained).../affixes—runDamageFilterChain,applyAffixesOnDeath.../../data/bosses(BOSS_DEFS),../boss/encounter(getSharedBossVfxKit).../telemetry(telemetry.recordDamageChain).../player/states(hasExclusiveStateimport 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 intoworld.weaponBoxes/world.artifactBoxesfrom elite-drop rolls. - Accumulators:
DmgNumbers.add,ShieldDmgAccum.collect,HpDmgAccum.collect,XpAccum.collect. - Notifications:
Notifications.addfor 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 andtriggerTutorial(4)on first hull hit. - XP orbs through
xpOrbs.spawnfromspawnXPOrbs. - AI director: increments
GameMaster._recentDamageby post-multiplier damage. - Telemetry: every
damagePlayercall (invuln path and resolved path) emits atelemetry.recordDamageChainentry; the collector samples at ≤1 Hz. - Game counters/stats and kill-streak tracking; updates
bestStreak, resetskillStreak/lastStreakMilestoneon hull damage.
DOES NOT
- Does not set
_dying,_deathTimer,_deathPulseR, or spawn the post-death debris/sprite-transition VFX — bridge transitions_frozenForLag → _dyingonce_lagHpreaches 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 indamagePlayer(gate explicitly skips both). - Does not drop weapon chests from enemy kills — the inline drop chance is pinned to
Math.random() < 0and 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
damagePlayerfrom insidedamageEnemy; 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_isBossAnchordeaths — anchors only fireboss_anchor_destroyed. - Does not apply non-player damage to sharers of a shared-health boss (
sourceKind !== 'player'is rejected whensharesHealthWithBoss). - 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.checkonly setsphase = '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 withenemy_kill, payload carries_lastDmgTagfor 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 whenhullDmg > 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.hitAngledefaults to random when source position is unknown;hpDamageMultlets specific sources (e.g. shooter bullets) reduce hull damage while keeping full shield damage;sourceis a short label captured in the damage-chain trace.damageDestructible(destructible, rawDmg)— applies damage and floating numbers to inert destructibles; flipsaliveto 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 throughcomputePlayerDamageanddamageEnemy, then applies perpendicular 200-unit knockback and a purple spark burst.computePlayerDamage(baseDmg, weaponId, target, ship)— multiplies base damage byPLAYER_DAMAGE_MULT(0.263).weaponIdandtargetare currently unused.applyKnockback(entity, angle, force)— generic compatibility export that addscos(angle)*force/sin(angle)*forcetovx/vy.spawnXPOrbs(world, enemy, count, game?)— depth/tier-scaled orb amount, runs each spawn position throughpushOffTerrain.pushOffTerrain(x, y, world)— radial push out of any overlapping asteroid with a 40px padding; usesnearbyTerrainquery (radius 60+padding).pointToLineDistance(px, py, x1, y1, x2, y2)— utility used bywarpLineDamage.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 >= 100silently zeros damage), C_threshDRramp from 1.0 at 40% HP down to 0.76 at 5% HP, Ddmg = amount * _flatDR * _threshDR, E knockbackmin(KB_MAX, KB_BASE + dmg*KB_PER_DMG)applied unconditionally (still 5 even when dmg=0), F shield-vs-hull route, G shield-break graceship.invulnTimer = max(invulnTimer, CFG.INVULN). Every call ends withtelemetry.recordDamageChaincapturing 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, firesJuice.fire('shield_hit'), accumulates intoShieldDmgAccum, playsSampleSfx.playShieldHit, and shakes camera + hit-stops scaled toabsorbed/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), 6SonarRings.shockwaveArcfragments,_shieldBreakPulseflag for bridge,_shieldBrokenTimer = 2.5,_shieldRecovering = true,_shieldBackgroundRegen = 0, and theCFG.INVULNgrace window. - Hull route applies
hpDamageMultif provided, accumulates toHpDmgAccum, scalestriggerHpFlashandgame._hullFlashbyhullDmg/hpMax, playsSampleSfx.playHullHit, firesJuice.fire('player_hull_hit'), triggers tutorial step 4 on first occurrence, breaks the kill streak (killStreak = 0,lastStreakMilestone = -1), and flips_hullPulse = 1.0for 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) →getShredMultmultiplier → affix filter chain (runDamageFilterChainshort-circuits on<= 0) → boss damage cap: shared-health bodies cap at30 + getBossBarHpMax(world)*0.02, single boss/dmgCappedcap at30 + enemy.hpMax*0.02with_hitImmune = 12/60(≈200ms per-part). - Enemy hit bookkeeping:
enemy.hp -= rawDmg,flashAmount = 1.0,_dmgSquashT = 0.15,game.stats.damageDealt += rawDmg,game._pendingBarDamage += rawDmgfor shared-health bosses,Sig.fire('damage_dealt', ...),DmgNumbers.add, Personal Space knockbackgetWeaponKnockback()(skipped on_isPackLeader) optionally followed bygetWeaponStunDuration()setting_stunTimer, thenship.lifeStealheal. - 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, dispatchEnemyBehaviors.get(behavior).onDeathandapplyAffixesOnDeath. Branches: anchor →boss_anchor_destroyed; shared-health sharer →boss_body_kill, snapshot_lastBossDeathX/Yon final sharer, dispatchBOSS_DEFS[_activeBossDefId].onBodyDeathand.onDeathwith shared kit, thenboss_killon final; regular →enemy_kill+tagged_kill, defer_pendingXpCount = 1(5% in overtime), incrementtotalKills/killStreak, refreshkillStreakTimer = 2.0,_trackComboKill,_checkStreakMilestone, elite (dmgCapped) gets_awardEliteKillBonus+_rollEliteRewardDrops,isBossgets_awardBossKillBonus. Kill juice picks archetype audio fromARCHETYPE_DEATH_AUDIO[behavior]first, else size brackets atr >= 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_PARAMSby 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 settinggame.timeDilation = 0and_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/_vy400-500 px/s with_flightFric = 2.0(bridge handles the flight). - XP scaling:
XP_NERF_MULT = 0.45(history 0.75 → 0.45) multiplied againstenemy.xp * 0.2025 * 0.975 * depthMult * tierMultwheredepthMult = 1.3^(level-1)andtierMult = 1 + 0.5*(level-1); floor 1 viaMath.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). _shieldRecoveringis VISUAL-ONLY for the regen animation after a break; it no longer blocks damage. TheCFG.INVULNgrace window is the sole post-break protection. Hull takes hits while shield regenerates in the background; shield only restores when_shieldBackgroundRegenfills uninterrupted.ReviveSystemconsumesbonuses.deathDefianceTokens, setstracking.deathDefianceUsed, kicksdeathTimer = 7.0, flipsphase = '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.