engine/vfx/thruster.ts
PURPOSE
Renders the player ship’s engine fire jet — a single tapered flame drawn behind the ship in the world, under it in the draw order. Color and intensity are driven by the ship’s heat relative to its ideal heat rating: low heat reads yellow/white, ideal heat reads red-orange at full size, overheat slides through deep red into an “angry” dark red as the flame slightly chokes. Cold-flame color is also tinted by the active ship’s rarity. The page covers four stacked additive cones (outer glow, main flame, hot core, second offset flame layer) plus a fixed ring buffer of tiny fire-spark particles drifting along the flame body. No smoke is emitted — the older regular-and-overheat smoke streams were removed so the field reads as a clean fire jet at all heat levels.
OWNS
MAX_FIRE_SPARKSfixed-size ring bufferfireSparks: FireSpark[]of{alive, x, y, vx, vy, life, maxLife, size, r, g, b}slots, all pre-allocated at module load to avoid per-frame allocation.- Module-level spark-spawn accumulator
_sparkAccum(carries fractional spawn budget across frames, hard-clamped to3). - Module-level
_rarityRGB: [number, number, number]holding the cold-flame tint for the currently active ship rarity (default deep yellow[255,200,60]). - Tuning constants for flame geometry (
FLAME_REACH = 0.85,FLAME_OVERSHOOT = 0.15,FLAME_WIDTH_BASE = 0.24), glow shell (GLOW_WIDTH_MULT = 1.5,GLOW_ALPHA = 0.18), hot core (CORE_LEN_FRAC = 0.45,CORE_WIDTH_FRAC = 0.5), flicker (FLICKER_SPEED = 12Hz,FLICKER_AMP = 0.12), and spark spawn rate (FIRE_SPARK_RATE_THRUST = 35per second). - Heat-to-color palette stops: outer cones (
COL_COLD,COL_WARM,COL_IDEAL,COL_OVERHEAT,COL_MAX_HEAT) and inner cores (CORE_COLD,CORE_IDEAL,CORE_OVERHEAT). - Per-rarity flame-color table (
common,uncommon,rare,epic) used bysetThrusterRarityColor. - An unused vestigial constant
FLAME_REACH_IDLEkept for future idle-flame work (the function currently returns immediately when not thrusting). - Private helpers
lerpColor,rgbStr, and_spawnFireSpark(free-slot scan with oldest-spark recycle fallback).
READS FROM
../rendering/camera—Camera.toS(worldX, worldY)to project the flame base, tip, core tip, and each spark from world space to screen space.../core/state—camera.zoomto scale flame widths and spark sizes;game.timeto drive the second flame layer’s independent flicker oscillator.- Caller-provided arguments only for ship state: world position (
sx,sy),angle, collisionradius,heat,idealHeat,thrustingflag,time,dt, ship velocity (shipVx,shipVy), and visualshieldRadius.
PUSHES TO
- The 2D canvas context
ctxpassed in by the caller. All four flame cones and every spark are drawn withglobalCompositeOperation = 'lighter'(additive blending) inside a singlesave()/restore()pair so the composite mode never leaks. - The internal
fireSparksring buffer (mutatesalive, position, velocity, life, size, and RGB on spawn; mutates position/velocity/life on update; clearsalivewhen life expires or onresetThruster). - The internal
_rarityRGBstate whensetThrusterRarityColoris invoked at run start.
DOES NOT
- Does not mutate the ship, the camera, game state, or any other game system. Pure draw + private particle state.
- Does not emit smoke, soot, exhaust trails, or overheat black-smoke streams — those layers were removed and the legacy
updateThrusterSmokename is retained only for caller-side import stability. - Does not render anything when
thrustingis false; alive sparks continue to fade naturally via the nextupdateThrusterSmoketick, but no new geometry or sparks are drawn or spawned that frame. - Does not allocate per frame — sparks live in a pre-sized ring buffer and the spawn helper either claims a dead slot or recycles the spark with the lowest remaining life.
- Does not adapt the flame to the ship sprite, hull shape, or weapon loadout — geometry is derived purely from
radiusandshieldRadius. - Does not handle multiple thrusters, side jets, or boost effects; only one rear jet per call.
- Does not own its own
requestAnimationFrame/update loop, sound, screen-shake, or post-processing.
Signals
setThrusterRarityColor(rarity: string): void— Sets the cold-flame tint for the run from a fixed{common, uncommon, rare, epic} → RGBtable; unknown rarities fall back to the default deep yellow.updateThrusterSmoke(dt: number): void— Advances all alive sparks: decrementslife, kills expired sparks, integratesx/ybyvx/vy, and applies an exponential-decay drag of0.5^dtper axis so sparks slow naturally.drawThruster(ctx, sx, sy, angle, radius, heat, idealHeat, thrusting, time, dt, shipVx, shipVy, shieldRadius): void— Renders the flame stack for one ship and spawns fire sparks for this frame. Early-returns whenthrustingis false.resetThruster(): void— Marks every spark slot dead and zeroes the spawn accumulator; called at the start of each run.
Entry points
- Imported by
engine/bridge.tsasimport { drawThruster, updateThrusterSmoke, resetThruster, setThrusterRarityColor } from './vfx/thruster'. resetThruster()is called from the run-start path inbridge.tsalongside the planet/context initialisation.setThrusterRarityColor(shipRarity)is called once at run start inbridge.tsafter the active ship hull and rarity are resolved.updateThrusterSmoke(rawDt)is called every frame from the bridge update loop alongside other VFX systems (ExplosionFX.update,AoeExplosion.update,PlayerGlow.update).drawThruster(...)is called from the bridge ship-render path before the sticker layer is blitted, so the flame appears under the ship. The angle passed in is the ship’s visual angle, falling back totargetAnglefor hulls withrotates === falseso the flame still tracks the aim direction.
Pattern notes
- Single thrusting gate.
thrusting === falseshort-circuits the entire draw and spawn pipeline. Alive sparks still fade out via the nextupdateThrusterSmokecall. The unusedFLAME_REACH_IDLEconstant is referenced asvoid FLAME_REACH_IDLEto suppress the unused-symbol warning while preserving it for future idle-flame work. - Heat normalisation.
h = clamp(heat/100, 0, 1)andidealH = clamp(idealHeat/100, 0.1, 1)— the0.1floor onidealHkeeps the cold/warm/ideal split well-defined when callers pass small or zero ideal-heat targets. - Heat-driven length curve. Below the ideal heat band the multiplier ramps
0.85 → 1.0linearly over[0, idealH]. Above ideal it eases back from1.0 → 0.9over[idealH, 1], modelling the flame choking slightly at overheat rather than continuing to grow. - Two-sine flicker. Length flicker is
1 + sin(time*12)*0.12 + sin(time*12*1.7 + 2)*0.06, producing a non-periodic shimmer at ~12 Hz. The second flame cone runs an independentsin(game.time*18 + 2.0)*0.10oscillator so the two layers beat against each other rather than pulsing in lockstep. - Width discipline. Heat width scales as
1 + h²(quadratic), but the final flame half-width is hard-clamped to the ship’s collisionradiusso the jet never visually exceeds the ship footprint. - Heat-banded color blend. The heat range is split into four bands at
[0, 0.5·idealH], [0.5·idealH, idealH], [idealH, idealH + 0.5·(1−idealH)], [idealH + 0.5·(1−idealH), 1]and each band lerps its own outer/core color pair. The cold-flame band additionally lerps the outer color toward the rarity tint with weight(0.3 − h)/0.3forh < 0.3, so the rarity color is dominant at idle and dissolves into the normal fire palette by the time the ship is mildly warm. - Geometry. Each cone is drawn as a triangle: two base vertices at
±halfWidthalong the perpendicular at the ship centre, and one tip vertex alongangle + πatflameLen. The hot core uses the same base point but a shorter tip atflameLen * CORE_LEN_FRACwithcoreW = halfW * CORE_WIDTH_FRAC. All four cones use linear gradients from base to tip with the alpha taper baked into the gradient stops, so no per-pixel alpha math runs in script. - Additive composition. A single
ctx.save()setsglobalCompositeOperation = 'lighter'and is mirrored byctx.restore()after the spark pass, guaranteeing the additive mode does not leak into subsequent draws even if cones or sparks throw. - Ring-buffer particles, no allocations.
MAX_FIRE_SPARKS = 30slots are allocated once at module load;_spawnFireSparkfirst walks the array for a dead slot, then falls back to recycling the slot with the lowest remaininglife. The spawn accumulator carries the fractional spark budget across frames and is hard-clamped to3so a long pause cannot release a burst. - Spark placement. Sparks spawn at a random
t ∈ [0.3, 0.9]along the flame, offset laterally by±(1 − t) · 6 / 2so the cloud tapers with the flame. Initial velocity combines sideways jitter (±20per axis on the perpendicular), a small backward drift (+10along the back axis), and 20% of the ship velocity, so sparks lag the ship realistically. Lifetime is0.1–0.3 sand the spark color is the current outer-flame RGB brightened by+30per channel and clamped to 255. - Spark draw. Each alive spark is drawn as a filled circle of screen radius
max(0.5, size · zoom · (1 − 0.5·progress))with alpha1 − progressfor a linear fade. Because the spark pass runs inside the additivesave/restore, sparks composite over the flame as bright hot dots. - Legacy name retained. The per-frame update is still exported as
updateThrusterSmoketo keep the import site inbridge.tsstable across the v5.164.3 smoke removal; the function body now only ticks fire sparks.