What it is

The shop is the player’s spending surface. One fullscreen screen with three regions stacked vertically: a bonus reward progress bar at the top, a grid of pull banners in the middle, and a row of warp-crystal packs at the bottom. The player taps a banner button to pull one or ten ships, or taps a pack to top up their warp-crystal balance. Every pull tick also advances a 0-to-100 bonus bar that hands out free chest pulls at fixed milestones. There is no purchase confirmation modal, no preview pane, and no item detail page — the shop is a flat row of buttons that immediately fire their server RPCs on tap.

Layout regions

RegionPurposeContents
Top barAccount access and currency display”My Account” pill that routes to profile, and a live warp-crystal balance with the crystal glyph
Bonus reward barBonus progression visual0–100 progress fill with five evenly-spaced milestone nodes
Pull bannersShip pullsTwo banner cards, each with ×1 and ×10 pull buttons
Warp crystal packsCurrency top-upThree pack tiles, each with a crystal count and a buy button
Bottom tabsCross-screen navigationShared BottomNav component (same on every metagame screen)

The bonus bar is duplicated on the pull result overlay so the bar stays visible during the reveal animation.

Currencies

CurrencyWhere shownSpent on
Warp crystals (gems)Top-bar display next to the crystal glyphPulls on either banner; the only currency the shop reads or spends

The shop does not display credits, pull tickets, star XP, level XP, or any other in-run resource. Pull tickets were removed; the shop always pays with warp crystals.

Pull banners

Banner idNameDisplay emojiPull pool
sunSunsun glyphAll hulls, uniform pick
moonMoonmoon glyphAll hulls, uniform pick

Each banner card renders ×1 and ×10 pull buttons. The two banners are visually different (alternating “solar” and “freebooter” frame styles by index) but functionally identical — both share the same rarity table and the same hull pool. There is no rate-up, no featured ship, and no banner-specific exclusive.

Pull costs

Pull sizeWarp crystal cost
×1100
×101,000

Pull rarity table

The same five-rarity table runs on every pull on every banner.

RarityRoll weightCard badge labelBadge color
Common0.55D9ca3af (grey)
Uncommon0.27C34d399 (green)
Rare0.12B60a5fa (blue)
Epic0.06Ac084fc (purple)
Legendary0.00Sfbbf24 (gold)

Legendary has a roll weight of 0 — it is unreachable from the rarity table on a normal pull. The S/legendary visual treatment in the engine is reserved for future content; the legendary deep-dive reveal sequence is unreachable in the live build.

Pity and guarantee thresholds

ThresholdValueBehavior
Soft pity start40 pullsDefined as a constant; not currently applied client-side
Hard pity threshold50 pullsDefined as a constant; the server enforces guaranteed high-rarity at this count
Ten-pull guarantee flooruncommon or higherDefined as a constant; the server enforces this on each ten-pull

Pity counters are per-banner and persisted by the server (perform_pull RPC writes them to player_pity). The client rolls each card locally for the visual sequence; the server validates the wallet deduction, enforces pity, and persists the inventory write.

Bonus reward bar

A 0-to-100 progress bar that advances by 1 per card revealed during a pull. Five milestone nodes at 20, 40, 60, 80, and 100 trigger free bonus chest pulls when crossed.

MilestonePull count crossedBonus reward
120One forced rarity-1 (common) bonus pull
240One forced rarity-2 (uncommon) bonus pull
360One forced rarity-3 (rare) bonus pull
480One forced rarity-4 (epic) bonus pull
5100One forced rarity-5 (legendary) bonus pull

The bar resets to 0 only after the player has crossed 100 AND every queued reward has been claimed. Pending rewards drain one-at-a-time after the main pull sequence finishes via a chest-intro animation, then a single bonus card reveal at the queued rarity.

Warp crystal packs

Three pack tiles render in the packs grid. The display label, raw crystal grant, and price string are defined per pack.

Pack idCrystal grantDisplay price on the button
gems_100100infinity glyph
gems_550550infinity glyph
gems_12001,200infinity glyph

The shop renders three packs from a local PACKS_DATA constant. Two larger packs exist in the canonical GEM_PACKS table (gems_6500 at 6,500 and gems_15000 at 15,000) with “+50% BONUS” and “BEST VALUE” tags, but they are not rendered on the shop grid — only the smallest three appear in the UI. All three rendered packs display the infinity glyph as their price label.

Every pack in GEM_PACKS has betaTopUp: true, which means the server grants the crystals for free with no real payment flow.

How it works

  1. Screen mounts and React initializes the imperative pull engine over the shop’s DOM root, registering callbacks for collection ticks, pull completion, bonus advance, bonus point reads, and pending reward drains.
  2. Currency display reads warp crystals reactively from the wallet store; bonus bar fill width reads bonus points from the bonus store.
  3. The player taps a ×1 or ×10 button on a banner card. The handler checks the warp-crystal balance against the pull cost; if insufficient, the warp display shakes with an error class and the handler returns without contacting the server.
  4. If the balance covers the cost, the handler calls the pull RPC with the banner id, count, and payment type. The client rolls each card locally for animation purposes; the server runs its own roll, validates the deduction, enforces pity and the ten-pull floor, deducts the wallet, and persists the inventory.
  5. The server response includes the canonical wallet snapshot, the updated pity counters, and per-pull XP deltas (oldXp, newXp, oldStar, newStar, unlocked). The client wallet store replaces from the server snapshot; pity replaces from the server snapshot; inventory is updated locally to match.
  6. The handler hands the per-pull results array to the pull engine’s startPull. The engine takes over the screen with a full-overlay reveal sequence.
  7. As each card reveals, the engine fires the bonus advance callback, which adds one point to the bonus bar. Crossing a 20/40/60/80/100 milestone pushes a rarity tier (1-5 matching the milestone) onto a pending-rewards queue.
  8. After all cards reveal, an auto-continue bar fills over 2.5 seconds. Tapping anywhere skips ahead. When it completes, every card animates along a curved path into the collection tab.
  9. The engine then drains the pending bonus queue: for each queued rarity, it plays the chest-intro animation (hover, flash, crack, particle burst, whiteout), then a single forced-rarity card reveal at the queued tier.
  10. When the queue is empty, the engine fires onPullComplete. The bonus store’s resetIfFull resets the bar to 0 if it has reached 100. The main shop UI returns to its idle state.
  11. Tapping a pack tile calls the gem-grant RPC. The server validates the pack id, credits the wallet, and writes an audit row. The wallet store replaces from the server snapshot, and the top-bar crystal display animates with a counter pulse.

Interactions

  • The shop is the only metagame surface that mounts the pull engine; the same engine code is reused by the mission reward reveal, but with custom front-html cards and no banner / wallet wiring.
  • The bonus reward queue persists in-memory only inside the bonus store. It is drained sequentially regardless of how many pulls advanced the bar past how many milestones; a single ten-pull can queue more than one bonus chest.
  • Pull cards animate in a fixed 5-column grid; a ten-pull fills two rows. A single pull renders one card centered with a “BONUS CHEST” banner above it during bonus pulls.
  • The legendary reveal sequence (whiteout, solar inflow particles, supernova card, persona timer) is fully wired in the engine but unreachable on the live banners because the rarity-5 weight is 0. Bonus pulls at milestone 5 are the only path to a rarity-5 card reveal.
  • The pull RPC is the canonical authority for wallet deduction, pity, and inventory. The client never deducts the wallet locally — even insufficient-funds checks are advisory; a duplicate tap is blocked by a busyRef guard during the RPC.
  • Pack purchases run the same server-authority pattern: the server grants gems, the client wallet store replaces from the response snapshot, and the audit row goes to fake_store_grants.
  • The bonus bar is mirrored on the pull result overlay; both fill elements share a spree-progress-sync class and are width-synced on every UI update.
  • All pull buttons fire through a single delegated click handler bound to the shop root. This survives the imperative DOM replacement that dangerouslySetInnerHTML performs on the packs grid.
  • The “My Account” pill on the top bar routes to the profile screen; the bottom tabs route to the other metagame screens via the shared BottomNav.

What it does NOT do

  • The shop does NOT show pull tickets, credits, star XP, level XP, mods, artifacts, or any other resource — only warp crystals.
  • The shop does NOT display per-banner rate tables, featured ships, or which hull the banner can drop. Both banners share the same uniform hull pool.
  • The shop does NOT preview a ship before pulling, gate any ship behind ownership, or show duplicate-protection rules. Every roll runs from the same flat table.
  • The shop does NOT spend or display real money. Every pack is flagged betaTopUp: true and the server grants gems for free.
  • The shop does NOT render the two larger packs from the canonical pack table (gems_6500, gems_15000); only the three smallest are rendered.
  • The shop does NOT roll a legendary card from the rarity table — the legendary weight is 0 and the only path to a legendary reveal is the rarity-5 bonus chest at milestone 100.
  • The shop does NOT block on a confirmation modal for any action. Pulls and pack purchases fire immediately on tap, guarded only by an in-flight RPC busy ref.
  • The shop does NOT consume pull tickets even if the player has any in their wallet — the ticket path was removed and the handler always pays with gems.
  • The shop does NOT carry a “missions”, “events”, “starter pack”, or “weekly bundle” tab; the special-offer definitions exist in economy.ts but are not surfaced on the shop screen.
  • The shop does NOT persist bonus reward progress across sessions; the bonus store is a pure in-memory zustand store with no rehydration.
  • The shop does NOT have a pity counter readout for the player; pity is tracked server-side but never displayed.