PURPOSE
Defines the shooting star — a rare collectible entity with a rainbow trail and golden ring that spawns during the second half of each level. If the player touches the ring, the star is caught: it triggers a multi-phase gold/white particle explosion, freezes the game, flies to screen center, and queues a shooting_star reward (pick a category to level up all items in that category). While uncaught, the star also acts as a movement buff source — flying through its rainbow trail grants the ship a temporary +30% speed boost.
OWNS
ShootingStarinterface — runtime fields for a single star (position, velocity, lifetime, trail history, spin, speed-modulation state, collection animation state). The trail is a position-history ring buffer of up to 120 points sampled every 30ms (~3.6s of history).spawnShootingStar(ship)— constructs a new star 600px from the player at a random angle, base speed ~154 px/s with ±20 jitter, aimed back toward the player.updateShootingStars(stars, ship, world, game, dt)— per-frame update for all live stars. Drives the player-reactive speed system, trail sampling, ambient particles, trail-speed-buff write, off-screen despawn, gem/weaponBox bump physics, ring-collision pickup detection, multi-phase particle explosion, freeze trigger, and fly-to-center animation. Returns an array of collected star world positions.ringPop— module-level world-space pop VFX state ({ active, x, y, timer }) andupdateRingPop(dt)to advance its timer; auto-deactivates at 0.4s.ShootingStarSpawner— module-level spawn controller withinit()andtick(game, ship, world, dt). Holds private_spawnTimerand_activestate._nextInterval(tierLevel)— log-normal interval sampler. Median scales as120 / max(1, tierLevel)seconds; sigma is 0.5; clamped to[5s, 120s]. Targets ~1 star per tier during the 120s spawn window.- Tuning constants:
STAR_RING_COLLISION_RADIUS = 100,STAR_FLY_DURATION = 0.6s,STAR_DESPAWN_DISTANCE = 3000(~3 screens),TRAIL_SAMPLE_INTERVAL = 0.03s,TRAIL_MAX_LENGTH = 120,TRAIL_BUFF_RADIUS = 60,TRAIL_BUFF_REFRESH_SECONDS = 0.12.
READS FROM
ShipStatefrom../core/types— readsship.x,ship.y,ship.outerRadius/ship.radius(collision radius for the ring touch; falls back to 12).WorldStatefrom../core/types— readsworld.gemsandworld.weaponBoxesfor bump physics; writes spawned stars intoworld.comets(the legacy field name is reused for shooting stars).GameStatefrom../core/types— readsgame.uiTime(drives the fly-to-center animation during freeze),game._rawDt(frame delta during freeze; falls back to 0.016),game.missionTimerMax(default 240s),game.missionElapsed, andgame._currentLevel(treated as the tier; default 1).Camera.toS(x, y)from../rendering/camera— converts world position to screen space at the moment of collection.W,Hfrom../core/state— imported (screen extents); not directly referenced inside live logic in this module.
PUSHES TO
world.comets—ShootingStarSpawner.tickpushes a newShootingStarwhen the spawn timer expires inside the second-half spawn window.ship._shootingStarBoostTimer— set toTRAIL_BUFF_REFRESH_SECONDS(0.12s) every frame the ship is within 60px of any point in the star’s rainbow trail. Decremented and read byengine/physics/movement.tsto apply a 1.30x speed/thrust multiplier.game._weaponChestFreeze = trueandgame.timeDilation = 0— on collection, the game freezes for the reward cinematic.ringPop— on collection,ringPop.active,ringPop.x,ringPop.yare set andringPop.timeris reset to 0. Read bybridge.tsand the renderer to draw the expanding golden ring.Particles.add/Particles.burstfrom../vfx/particles— sparse ambient smoke during flight, the 14-piece golden ring shatter on catch, and four phases of catch particles: 80 gold sparks (Phase 1), 50 bright-white sparks (Phase 2), 40 slow long-lived gold confetti (Phase 3), 30 upward gold geyser sparks (Phase 4), plus a 30-spark white-hot core burst. Phases 1–4 are centered on the player; the ring shatter is centered on the star.Juice.fire('comet_catch')from../vfx/juice— fires the catch juice cue on collection.- Bump impulses to nearby
world.gems(withinc.rad + 12 + g.rad) — adds outward velocity (80) plus 30% of the star’s velocity, and emits a 6-particle spark burst. Skips gems withcollectedor active_spawnTimer. - Bump VFX on nearby
world.weaponBoxes(withinc.rad + 12 + 18) — 10-particle spark burst only; no impulse. Skips_flyingor spawn-timer-active boxes. collectedreturn array — caller (bridge.ts) uses this to push{ type: 'shooting_star' }ontogame.rewardQueue.
DOES NOT
- Does not despawn stars by lifetime —
land_maxLare retained on the struct for type compatibility but set to 9999 and never decremented. Stars only despawn when distance from the player exceedsSTAR_DESPAWN_DISTANCE(3000px) or after the post-catch fly-to-center animation completes. Comment in source notes Nate observed a star vanish mid-flight, which the distance-only rule prevents. - Does not draw the star or its trail. Rendering lives in
engine/rendering/draw-shooting-star.ts, invoked frombridge.ts. - Does not pick or apply the reward. It only queues a
shooting_starreward token (via the caller inbridge.ts); choice generation isgenerateShootingStarChoicesinengine/world/leveling.ts, and category effects (weapons,ship_upgrades,artifacts,lowest_weapon,player_buff,grant_reroll,grant_banish,grant_refuel) are applied by leveling/reward code. - Does not apply the +30% trail speed buff itself — it only refreshes
ship._shootingStarBoostTimer. The buff multiplier (1.30) is applied insideengine/physics/movement.ts. - Does not spawn outside the second half of the level.
ShootingStarSpawner.tickearly-returns and clears_activewhenmissionElapsed < halfTimeor>= timerSeconds. The very first star inside the window is rolled at 0.5× the normal interval so the first one arrives sooner. - Does not bump weapon boxes physically — only emits a spark burst. Gems get both impulse and spark burst.
- Does not handle the ring-pop expansion math — it only flips
ringPop.activeand resets its timer;updateRingPoponly advances the timer and auto-deactivates past 0.4s. The actual ring rendering is owned bybridge.ts/ the renderer.
Signals
- Return value of
updateShootingStars—Array<{ x, y }>of world positions for stars whose fly-to-center animation finished this frame. Caller pushes one{ type: 'shooting_star' }reward per entry intogame.rewardQueue. ship._shootingStarBoostTimer(write) — refreshed to 0.12s whenever the ship is in the trail; consumed by movement physics for the +30% boost.ringPopmodule export — public mutable VFX state (active,x,y,timer) read bybridge.ts.game._weaponChestFreeze+game.timeDilation = 0— freeze handshake with the global reward / time-dilation system; the same freeze flag is reused across weapon-chest, artifact-box, and shooting-star catches.
Entry points
spawnShootingStar(ship)— factory; called byShootingStarSpawner.tickto produce a star and push it intoworld.comets.updateShootingStars(stars, ship, world, game, dt)— per-frame update; called frombridge.tsafterShootingStarSpawner.tick.ShootingStarSpawner.init()— called at level start frombridge.ts(also called at level reset paths).ShootingStarSpawner.tick(game, ship, world, dt)— called every frame frombridge.tsinside the simulation step.updateRingPop(dt)— called frombridge.tswith raw delta so the pop animates during the post-catch freeze.ringPop(export) — read bybridge.tsfor ring-pop rendering.ShootingStartype (export) — used bybridge.tsfor typed access toworld.comets.
Pattern notes
- Iterates backwards with
swapRemove. The loop runsfor (i = stars.length - 1; i >= 0; i--)so removals viaswapRemove(stars, i)are safe within the same pass. - Reuses
world.comets. The shooting star is a rename/repurposing of the archive’s comet entity. The world array, theShootingStarstruct fields (e.g._baseVx,_prevDist,_approachT,_exiting), and thecomet_catchjuice cue all keep the comet vocabulary for archive compatibility. - Player-reactive speed system. Per-frame distance and approach-vs-recede detection (
_prevDist) drives a speed multiplier that shapes a small chase mini-game: approach → decelerate to 0.5× over 4.5s, then exit; recede while nearby → drop to 0.8× then exit; far away → drift to 0.9× then exit. Once_exitingis set, the multiplier ramps back to 1.0×. - Trail is both visual and gameplay surface. The 120-point/3.6s rainbow trail is what the renderer draws AND the active buff zone. Trail-buff detection uses a cheap broad-phase check against the trail head (radius
STAR_RING_COLLISION_RADIUS + TRAIL_BUFF_RADIUS + 400) before walking the trail array, and breaks at the first hit. - Crash-on-bad-data avoided at boundary. Defensive fallbacks exist only at the runtime boundary:
game._rawDt ?? 0.016,ship.outerRadius || ship.radius || 12,game.missionTimerMax || 240,game.missionElapsed ?? 0,game._currentLevel ?? 1. Internal state is initialized at spawn and not re-defaulted. - Cast through
anyfor cross-module fields.(ship as any)._shootingStarBoostTimerand(game as any)._currentLevelare written/read throughanyrather than adding cross-cutting type bridges; the field exists on the canonicalShipState/GameStatetype definitions inengine/core/types.ts. - Log-normal spawn interval.
_nextIntervaluses Box-Muller for a standard normal thenexp(mu + sigma*z)withsigma = 0.5(tighter than the archive’s 1.15). Median scales inversely with tier so deeper tiers see more frequent stars; clamped to[5s, 120s]. - Freeze-aware animation. The fly-to-center animation reads
game.uiTimeandgame._rawDtso it advances even thoughdt(the sim delta) is 0 during the post-catch freeze. The rewardcollectedsignal is intentionally deferred until the animation finishes, not fired at the moment of touch. - Particle catch sequence is fixed-count, not data-driven. The four phases (80 gold + 50 white + 40 confetti + 30 geyser) plus the 14-piece ring shatter and 30-spark core burst are all inlined as numeric loops; values live as locals in the catch block rather than in a data table.