PURPOSE
Imperative DOM-manipulation engine that drives the S.I.G. Storefront V32 card-pull reveal sequence. Owns the 3D card spawn animation, per-card reveal cadence, the legendary “supernova” deep-dive stage, the rainbow auto-continue bar, and the z-arc outro that flicks pulled cards into the collection tab. React manages the surrounding shop layout; this engine is the overlay layer. The engine is a direct port of vanilla JS from test.html and intentionally avoids React because the sequence is imperative and DOM-heavy.
OWNS
- Module-level engine state:
currentPullResults,currentRevealIdx,totalCollected,isBonusPullActive,engineRoot,callbacks,allRevealed,activePopover,pausedBarRemaining,barStartedAt. - All reveal timers/intervals:
autoRevealTimer,solarInterval,legendaryTimeoutId,autoContinueTimerId,preBarTimerId. - Card grid DOM construction inside
#pull-grid— wrappers, faces, fronts, backs, badges, glass-style CSS custom properties, NEW tag. - Bloom-cell layer (
#pull-bloom) mirroring the card grid for blurred glow merging. - Perimeter projectile VFX (
spawnPerimeterProjectiles), solar inflow particles (startSolarInflow), chest burst particles (spawnChestBurstParticles), and bonus-bar flare particles (spawnFlareParticle). - Legendary deep-dive overlay swap into
#pull-solo-stage/#solo-container, plus the whiteout flash, screen shake, and persona-timer fill. - Bonus chest intro phases (hover → flash → crack → whiteout) on
#bonus-chest-intro/#bonus-chest-sprite. - Auto-continue rainbow bar (
#pull-auto-continue/#auto-continue-fill) with pause/resume against popover state. - Stat popover overlays appended to revealed cards (
.card-stat-popover). - The XP-gain card variant keyframes injected once into
document.headas#xpcard-keyframes.
READS FROM
@starship-survivors/data/ships—ShipRaritystring type.@starship-survivors/engine/rendering/ships-v4-loader—getShipV4SpritePath(hullClass)for card-front ship images.@starship-survivors/data/ship-progression—STAR_XP_THRESHOLDSfor the duplicate-pull XP bar math.@starship-survivors/engine/rendering/medical-canvas-palette—MedicalPalette.magentaandMedicalPalette.accentBrightfor legendary supernova, perimeter projectiles, and chest burst tints.- DOM elements queried by ID inside the scoped
engineRoot:pull-grid,pull-bloom,pull-result-stage,pull-solo-stage,solo-container,overlay-spree,shop-spree,whiteout,main-nav,nav-home,nav-shop,nav-collection,collection-badge,persona-ui-container,persona-timer-fill,bonus-chest-intro,bonus-chest-sprite,ui-blocker,pull-auto-continue,auto-continue-fill, plus dynamic idspull-card-${i},bloom-cell-${i}, and overlay-spree milestone nodes.m-${tier}. - The
EngineCallbacksinterface —getBonusPoints,shiftPendingBonus,getPendingBonusCount(read-only queries into the caller).
PUSHES TO
EngineCallbacks.onCollectionTick()once per card during the z-arc outro, after the card lands on the collection tab.EngineCallbacks.onPullComplete()after the full sequence (including any chained bonus pulls) finishes, or immediately for mission-reward mode when no collection tab exists.EngineCallbacks.onBonusAdvance()before every non-bonus card reveal (both standard grid reveals and legendary triggers) to advance the bonus bar by one point.document.bodyfor transient particles, in-flight outro cards, and the whiteout overlay (cards are temporarily reparented tobodyso the z-arc animation isn’t clipped).- The collection tab DOM —
#collection-badgetext and#nav-collectioncounter-pulseclass — directly during the outro. - Inline
stylemutations onoverlay-spree,main-nav,nav-home,nav-shop,ui-blocker,pull-solo-stage,pull-result-stage,persona-ui-container,persona-timer-fill,whiteout.
DOES NOT
- Does not import or read any Zustand store, save state, or inventory. The caller (
ShopScreen.tsx/pullService.ts) computes results and passes them in viaPullCard[], including the v4 XP-gain snapshot (oldXp,newXp,oldStar,newStar) captured before inventory mutation. - Does not use
CollectibleCard.tsx— card HTML is hand-generated imperatively to match the V32 liquid-glass style. - Does not handle navigation, route changes, or post-pull modals beyond firing
onPullComplete. - Does not produce React elements. No JSX, no hooks, no virtual DOM.
- Does not validate
PullCardpayloads — assumes the caller supplied a legal rarity (1–5) and matching XP fields whenunlocked === false. - Does not own bonus-tier logic —
shiftPendingBonus/getPendingBonusCount/getBonusPointsare queried; the caller decides what rewards to enqueue. - Does not progress the bonus bar during bonus pulls —
isBonusPullActivesuppressesonBonusAdvancecalls. - Does not play audio; sound is left to the caller / other systems.
- Does not draw via Canvas/WebGL. Everything is DOM + CSS animations + Web Animations API.
Signals
EngineCallbacks.onCollectionTick: () => void— one per card collected.EngineCallbacks.onPullComplete: () => void— sequence finished, control returns to React.EngineCallbacks.onBonusAdvance: () => void— advance bonus meter by 1.EngineCallbacks.getBonusPoints: () => number— engine reads current value for flare placement.EngineCallbacks.shiftPendingBonus: () => number | null— engine asks for the next queued bonus rarity after a main pull completes.EngineCallbacks.getPendingBonusCount: () => number— engine reads pending count.- DOM click event on
#pull-result-stage— overlay tap → dismiss popover or skip to outro. - DOM click events on each
#pull-card-${i}— toggle stat popover.
Entry points
Public exports:
initPullEngine(root: HTMLElement, cbs: EngineCallbacks): void— mount the engine against a scoped.sig-shoproot and wire callbacks. ResetstotalCollectedand injects XP-card keyframes.startPull(cards: PullCard[]): void— kick off the main reveal sequence with the supplied card list (1–15 cards expected; layout assumes a 5-column grid up to 3 rows).closeLegendary(): void— manually close the legendary deep-dive stage (for an explicit CONTINUE button).spawnBonusFlare(bonusPoints: number): void— spawn a flare particle on the bonus bar at the given progress percentage.flashNode(tier: number): void— flash the milestone node for the given tier on#overlay-spree .m-${tier}.destroyPullEngine(): void— clear all timers/intervals, drop the root and callbacks. Called on shop unmount.
Public type:
PullCard—{ rarity: 1..5; hullClass?: string; customFrontHtml?: string; isNew?: boolean; unlocked?: boolean; oldXp?: number; newXp?: number; oldStar?: number; newStar?: number }.customFrontHtmlis used by mission rewards.unlocked === falseselects the duplicate-pull XP-gain card variant.
Helper export:
rarityToNum(rarity: string): number— string → numeric rarity lookup.
Pattern notes
- Scoped queries via
engineRoot. All DOM lookups go throughq(selector)andqId(id)which scope to the caller-provided root, so the engine cannot accidentally collide with other shop instances or stray globals. Transient particles, in-flight outro cards, and the whiteout overlay are the only exceptions — they attach todocument.bodyto escape clipping. - Front-HTML selection priority in
startPullSequence:customFrontHtml(mission rewards) >unlocked === false(XP-gain variant) > default ship-unlock card. The engine has no store dependency because the caller already derived the snapshot. - Rarity is numeric 1..5 internally. String rarities arrive on the
ShipRarityboundary and are mapped viaRARITY_STRING_TO_NUM/NUM_TO_RARITY. Five parallel constant maps drive visuals:RARITY_GLOW_COLORS,RARITY_EDGE_COLORS,RARITY_INNER_GLOW,RARITY_OUTER_GLOW,RARITY_BORDER_COLOR,RARITY_BADGE_COLOR,RARITY_BADGE_LABEL(D/C/B/A/S),RARITY_SHIP_OUTLINE. Adding a rarity tier means updating every map. - CSS-custom-property handoff.
cardFrontStyle(rarityNum)emits a single inlinestylestring with--card-edge-color,--card-glow-inner,--card-glow-outer,--card-border-color. Card CSS in the shop stylesheet consumes those tokens — JS only sets vars. - Card cap at ★5. The XP-gain variant uses
MAX_STAR = 5; at cap it renders a full bar with “MAX ★5” instead of advancing toward a non-existent next threshold. - Reveal pacing.
REVEAL_DELAY_MS = 400between non-legendary card reveals. After the last card, aPRE_BAR_PAUSE_MS = 2500admire window then theAUTO_CONTINUE_MS = 2500rainbow bar. Popovers pause both timers; resume restarts the pre-bar phase or finishes the remainder of the bar. - Legendary path is asymmetric.
processNextCardshort-circuits totriggerLegendarySequencefor rarity 5: shrinks the spree bar, raises the wrapper toz-index: 9999, flashes white over 450ms, swaps content into#pull-solo-stage, fires two perimeter bursts 300ms apart, starts the solar inflow particle stream, runs a 2.5s persona timer, thencloseLegendaryStageafter 2500ms continues the queue. - Bonus chain. When
triggerAutoOutrofinishes,checkPendingBonusRewardscallscallbacks.shiftPendingBonus(). A non-null rarity triggersplayBonusChestIntro(1.0s hover → 0.3s flash → 0.6s crack with burst → whiteout →startBonusPullfor a 1-card forced pull).isBonusPullActive = truesuppressesonBonusAdvanceduring bonus pulls. - Mission-reward fallback. When
#nav-collection/#collection-badgeare absent (RevealScreen mode),triggerAutoOutroskips the z-arc flight, fades the overlays, and firesonPullCompleteimmediately. - Bloom layer separation. A parallel
#pull-bloomgrid sits behind the real cards. Activating abloom-cell-${i}background to the rarity glow color and applying CSSfilter: blur()to the layer produces merged whole-layer bloom without flattening the real grid’stransform-style: preserve-3d. - Animation strategy. Most motion uses the Web Animations API (
element.animate(...)) with explicit cleanup timeouts. CSS transitions handle simple opacity/transform fades (whiteout, persona fill, auto-continue bar) so they can be paused mid-flight by re-reading and re-applyingcomputed.transform. - Idempotent keyframe injection.
ensureXpCardKeyframesguards against double-insertion by checking for#xpcard-keyframesbefore appending the<style>block. - Cleanup is centralized.
destroyPullEnginecancels every named timer/interval, dropsengineRootandcallbacks, and resetsactivePopover/allRevealed. The caller must invoke it on shop unmount to avoid leaks. - Legendary fixed to hullClass
'Bulwark'.triggerLegendarySequencecallscardFrontHtml(5)without a hull class, so the giant legendary deep-dive card always shows the Bulwark sprite. (Caller passes the actual hull only in the small grid card.)