economy.ts

Canonical constants and formulas for all currency rates, costs, rewards, and IAP shape. Two currencies: Gems (hard/premium) and Credits (soft).

Pure data + small pure functions — no side effects, no I/O. Imports clamp from engine utils and the MissionResult type. Consumed by run-end reward screens, the shop, the mission board, the death-defiance bridge, and starter-inventory bootstrap.

Sections

The file is partitioned into commented banners. Each banner owns one concern:

BannerConcern
ARCADE RUN REWARDSEnd-of-run Credits formula + bar breakdown
MISSION REWARDSStar-rated drop tables for mission rewards
MISSION TIMER SKIP COSTSGem cost to instant-finish or refresh missions
DEATH DEFIANCEIn-run revive: cost curve, HP/invuln, prompt timing
PULL COSTSGacha single + 10-pull pricing
SUPPORTER CLUBPaid-tier USD price
DAILY LIMITSAd-ticket cap
GEM PACKSStorefront pack catalog (fake IAP during beta)
STARTER PACK & OFFERSOne-time + recurring USD bundles
ROOKIE BOOSTFirst-week passive XP multiplier
AD REWARD SLOTSWatch-ad reward grid (chain / ship / material / resource)
NEW PLAYER STARTER INVENTORYDefault ships + zeroed wallet on fresh account

Public API

Arcade rewards

  • interface ArcadeCreditsBreakdown — full per-bar breakdown returned to the reveal screen. Carries total, four *BonusCredits chunks that sum exactly to total, three 0..1 bar fills (killBonus, eventBonus, levelBonus), and three par* integers for the current/par labels.
  • computeArcadeCredits(result: MissionResult): number — convenience wrapper returning only total.
  • computeArcadeCreditsBreakdown(result: MissionResult): ArcadeCreditsBreakdown — full breakdown. Formula shape: baseAtTier(tier) × (1 + killBonus + eventBonus + levelBonus) where each bonus is clamp(current / (2 × par), 0, 1). Reaching par yields a 50% bonus chunk; 2× par caps at 100%.

Internal helpers (not exported): parLevel(tier) (step-then-linear curve) and baseAtTier(tier) (exponential in tier). The levelBonusCredits field absorbs rounding so the four chunks sum exactly to total — clamped to ≥ 0 so the credits-tween never animates backwards.

Mission rewards

  • type MissionRewardRarity'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'.
  • MISSION_REWARD_TABLES: Record<MissionRewardRarity, MissionRewardTable> — per-rarity drop config. shipChance is a 3-tuple of probabilities indexed by star count (1/2/3 stars), shipMaxRarity caps the rarity of any ship drop from that band.

Mission economy

  • missionSkipCost(remainingHours: number): number — gem cost to instantly complete a mission, minimum 5.
  • MISSION_BOARD_REFRESH_COST: number — gem cost to refresh the board.

Death defiance (in-run revive)

Cost curve, behavior, and the prompt-timing contract:

  • DEATH_DEFIANCE_BASE_COST, DEATH_DEFIANCE_COST_ESCALATION, DEATH_DEFIANCE_MAX_USES — cost scaling per use within a single run.
  • deathDefianceCost(useNumber: number): numberBASE_COST × useNumber (1-indexed).
  • DEATH_DEFIANCE_HP_RESTORE, DEATH_DEFIANCE_INVULN_SECONDS — revive gameplay effect.
  • DEATH_DEFIANCE_FREE_DAILY — free uses per day.
  • DEATH_DEFIANCE_PROMPT_DURATION, DEATH_DEFIANCE_CINEMATIC_DURATION — display-time durations.
  • DEATH_DEFIANCE_CHEAT_THRESHOLD, DEATH_DEFIANCE_CHEAT_STRETCH, DEATH_DEFIANCE_LINGER_DURATION — “cheater timer” that stretches the final fraction of the prompt to feel longer, plus a near-empty linger at the end.
  • DEATH_DEFIANCE_TOTAL_REAL_DURATION — computed IIFE: total real-world seconds the prompt is on screen. Bridge timer must match this so it doesn’t dismiss before the React bar reaches zero.

Pulls

  • PULL_COST_TICKETS, PULL_COST_GEMS — single-pull cost.
  • PULL_10_COST_TICKETS, PULL_10_COST_GEMS — 10-pull cost (gem path is discounted vs. 10× single).

Storefront / IAP

  • SUPPORTER_CLUB_PAID_COST_USD: number — paid supporter tier USD price.
  • DAILY_AD_TICKET_LIMIT: number — daily cap on ad-rewarded pull tickets.
  • interface GemPack{ id, gems, displayLabel, betaTopUp, bonusLabel? }. betaTopUp: true means the pack grants gems for free during beta (no real payment).
  • GEM_PACKS: GemPack[] — ordered catalog rendered in the storefront.
  • interface SpecialOffer{ id, name, contents: { gems?, tickets? }, priceUsd, oneTime }.
  • SPECIAL_OFFERS: SpecialOffer[] — starter pack (oneTime: true) plus recurring bundles.

Progression boosts

  • ROOKIE_BOOST_XP_MULT: number — passive XP multiplier active during the first-week boost window.

Ad rewards

  • interface AdRewardSlot{ id, rewardType, amount, viewsRequired, entityTemplateId?, displayCategory: 'chain' | 'ship' | 'material' | 'resource' }. viewsRequired is the number of ad views to unlock the slot’s payout; displayCategory partitions the grid into rows.
  • AD_REWARD_SLOTS: AdRewardSlot[] — current slate of watch-ad rewards.

Starter inventory

  • STARTER_INVENTORY{ ships: readonly tuple, gems, credits, pullTickets }. Ships are template IDs from the ship catalog. Wallet values are zeroed on a fresh account — everything earned in-run.

Contracts and invariants

  • Sum-to-total invariant. tierBase + killBonusCredits + eventBonusCredits + levelBonusCredits === total. Enforced by absorbing rounding error into levelBonusCredits (clamped ≥ 0). Reveal-screen tweens depend on this.
  • Bar fill bounds. killBonus, eventBonus, levelBonus are always in [0, 1] after clamp. Bars cap visually at 100% even when the player exceeds 2× par.
  • Defensive integer coercion. result.progression.tierReached, totalKills, eventsCompleted, levelReached are run through | 0 and Math.max(0, …) — robust to NaN/negative/floating-point inputs from upstream.
  • Death-defiance real-time contract. DEATH_DEFIANCE_TOTAL_REAL_DURATION is the authoritative duration for both the bridge-side dismiss timer and the React-side bar animation. Changing PROMPT_DURATION, CHEAT_THRESHOLD, CHEAT_STRETCH, or LINGER_DURATION recomputes it; both consumers must read the derived constant.
  • Tier-1 par curve. parLevel(tier) is 10 / 15 / 20 for tiers 1–3 then +4 per tier — non-linear at the low end on purpose. baseAtTier(tier) doubles per tier from ~66.7 at tier 1.

Cross-references

  • MissionResult — input shape for the arcade reward formula. See mission-result.
  • clamp — engine utility. See utils.
  • Consumer screens: reveal screen, shop, mission board, death-defiance prompt — see the metagame screen pages.
  • Balance values (actual numeric tuning, drop rates, gem-pack pricing rationale) are documented on the gameplay-side economy and progression pages.

EXTRACT-CANDIDATE

  • Death-defiance prompt-timing block (PROMPT_DURATION / CINEMATIC_DURATION / CHEAT_THRESHOLD / CHEAT_STRETCH / LINGER_DURATION / TOTAL_REAL_DURATION) is a self-contained UX timing primitive — candidate for a dedicated death-defiance-timing.ts module so the bridge + React component import a single source of truth without dragging in pull costs, gem packs, etc.
  • Storefront catalog (GEM_PACKS, SPECIAL_OFFERS, SUPPORTER_CLUB_PAID_COST_USD) is content-shaped, not formula-shaped — candidate for data/storefront.ts or a JSON/TOML content table once real IAP wiring lands and betaTopUp goes away.
  • Arcade reward formula (ArcadeCreditsBreakdown + computeArcadeCreditsBreakdown + parLevel + baseAtTier) is the only block in this file with non-trivial math. Candidate for data/arcade-rewards.ts if reward tuning starts churning independently of the rest of the economy.
  • AD_REWARD_SLOTS likely belongs alongside ad-system code rather than the economy file once the ad surface owns its own data tables.