Pull System

The pull system is the gacha layer that turns spent currency or tickets into hulls. Players pick a banner, choose a 1- or 10-pull, and the server returns rolled ships that are either unlocked for the first time or fed back into the existing hull as XP. Rolls are local; wallet, pity, and inventory persistence are server-authoritative.

Banners

There are exactly two banners, sun and moon, defined in data/pull-config.ts and data/pull-rates.ts (BANNERS map). Both banners share the same SHIP_RARITY_RATES table and the same hull pool — they differ only in framing (Sun is “ships of light and radiance”, Moon is “ships of shadow and fortune”). Universal pull tickets work on either banner, and pity is tracked independently per banner.

Single vs 10-pull

PullGem costTicket costConstant
Single100 Warp Crystals1 ticketSINGLE_PULL_GEM_COST / SINGLE_PULL_TICKET_COST
10-pull1,000 Warp Crystals10 ticketsTEN_PULL_GEM_COST / TEN_PULL_TICKET_COST

There is no bulk discount — 10 singles and one 10-pull cost the same. The 10-pull’s only mechanical edge is the uncommon+ floor: every 10-pull is guaranteed to contain at least one uncommon or higher result (TEN_PULL_GUARANTEE_RARITY / TEN_PULL_GUARANTEED_MIN_RARITY = 'uncommon'). See the pity-system concept for soft/hard pity gates that layer on top of this.

Payment

PaymentMethod in services/pullService.ts is 'tickets' | 'gems'. Tickets are universal and earned from daily ads (capped at DAILY_AD_TICKET_LIMIT = 5 per day) and missions; gems (Warp Crystals) are the premium currency. The server speaks credits and gems on the wire; the client mirrors the canonical wallet snapshot returned by the RPC.

Server authority — perform_pull

The flow in executePullPull (services/pullService.ts) is:

  1. Client rolls count results locally using weightedRoll(SHIP_RARITY_RATES) for rarity and pickHull() for a uniformly random hull class.
  2. Client calls the Supabase RPC perform_pull with p_banner_id, p_count, p_payment_type, and the rolled p_results array.
  3. Server validates wallet and pity, applies pity gates, persists player_pity, debits the wallet, and returns the canonical wallet, tickets, pity, and new_entities snapshot.
  4. Client replaces wallet state from the snapshot (wallet.replaceFromSnapshot) and updates the per-banner pity counter via playerStore.setPity.

If the RPC throws, the client flips syncStatus to error and surfaces the message — no local mutations are applied.

Duplicates and the isNew flag

V4 pulls do not roll per-rarity hull pools — every hull is equally likely. Duplicates are expected. After the RPC succeeds, the client walks v4Results and calls inventory.grantShipPull(shipId) for each one:

  • First pull of a hull: unlocked = true, xpGained = 0, the hull is added to the inventory, and V4_PULL_UNLOCK is tracked. The hull ID is pushed onto newShips so the reveal animation can flag it as new (the legacy PullResult type in data/pull-rates.ts carries this as the isNew boolean).
  • Subsequent pulls of the same hull: unlocked = false, xpGained = 1, and V4_PULL_XP_GAIN is tracked with the old/new XP and a starUp boolean derived from starFromXp. Stars increase as the hull accumulates duplicate XP.

The client snapshots pre-pull XP per shipId (localXp map) before applying grants, so a 10-pull that rolls the same hull multiple times still reports the correct per-result delta to the reveal engine.

Pity persistence

Pity is stored in the player_pity table, keyed by banner. The client reads it from playerStore.pity[bannerId] and writes it back from the RPC response — never locally. getBannerPity(bannerId) exposes the current counter for UI; loadPity is deprecated. See pity-system for the soft/hard thresholds and the 10-pull floor.

Rarity rates

Two rarity tables exist in the codebase and must stay aligned:

Raritypull-config.tspull-rates.ts
common0.550.60
uncommon0.270.25
rare0.120.10
epic0.060.05
legendary0.000.00

pullService.ts imports SHIP_RARITY_RATES from pull-config.ts, so the pull-config numbers are the live rates. Legendary is currently zero — no hulls roll legendary from banners. Rarity is preserved on the wire as a UI label only; v4 hull stats are not rarity-scaled.

Source

  • src/starship-survivors/services/pullService.ts — RPC orchestration, local rolling, inventory grant, telemetry
  • src/starship-survivors/data/pull-config.ts — costs, pity thresholds, banner definitions, live rate table
  • src/starship-survivors/data/pull-rates.ts — alternate rate table, pity thresholds, PullResult shape with isNew