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
| Region | Purpose | Contents |
|---|---|---|
| Top bar | Account access and currency display | ”My Account” pill that routes to profile, and a live warp-crystal balance with the crystal glyph |
| Bonus reward bar | Bonus progression visual | 0–100 progress fill with five evenly-spaced milestone nodes |
| Pull banners | Ship pulls | Two banner cards, each with ×1 and ×10 pull buttons |
| Warp crystal packs | Currency top-up | Three pack tiles, each with a crystal count and a buy button |
| Bottom tabs | Cross-screen navigation | Shared 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
| Currency | Where shown | Spent on |
|---|---|---|
| Warp crystals (gems) | Top-bar display next to the crystal glyph | Pulls 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 id | Name | Display emoji | Pull pool |
|---|---|---|---|
| sun | Sun | sun glyph | All hulls, uniform pick |
| moon | Moon | moon glyph | All 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 size | Warp crystal cost |
|---|---|
| ×1 | 100 |
| ×10 | 1,000 |
Pull rarity table
The same five-rarity table runs on every pull on every banner.
| Rarity | Roll weight | Card badge label | Badge color |
|---|---|---|---|
| Common | 0.55 | D | 9ca3af (grey) |
| Uncommon | 0.27 | C | 34d399 (green) |
| Rare | 0.12 | B | 60a5fa (blue) |
| Epic | 0.06 | A | c084fc (purple) |
| Legendary | 0.00 | S | fbbf24 (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
| Threshold | Value | Behavior |
|---|---|---|
| Soft pity start | 40 pulls | Defined as a constant; not currently applied client-side |
| Hard pity threshold | 50 pulls | Defined as a constant; the server enforces guaranteed high-rarity at this count |
| Ten-pull guarantee floor | uncommon or higher | Defined 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.
| Milestone | Pull count crossed | Bonus reward |
|---|---|---|
| 1 | 20 | One forced rarity-1 (common) bonus pull |
| 2 | 40 | One forced rarity-2 (uncommon) bonus pull |
| 3 | 60 | One forced rarity-3 (rare) bonus pull |
| 4 | 80 | One forced rarity-4 (epic) bonus pull |
| 5 | 100 | One 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 id | Crystal grant | Display price on the button |
|---|---|---|
| gems_100 | 100 | infinity glyph |
| gems_550 | 550 | infinity glyph |
| gems_1200 | 1,200 | infinity 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
- 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.
- Currency display reads warp crystals reactively from the wallet store; bonus bar fill width reads bonus points from the bonus store.
- 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.
- 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.
- 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.
- 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. - 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.
- 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.
- 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.
- When the queue is empty, the engine fires
onPullComplete. The bonus store’sresetIfFullresets the bar to 0 if it has reached 100. The main shop UI returns to its idle state. - 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
busyRefguard 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-syncclass 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
dangerouslySetInnerHTMLperforms 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: trueand 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.tsbut 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.