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:
| Banner | Concern |
|---|---|
ARCADE RUN REWARDS | End-of-run Credits formula + bar breakdown |
MISSION REWARDS | Star-rated drop tables for mission rewards |
MISSION TIMER SKIP COSTS | Gem cost to instant-finish or refresh missions |
DEATH DEFIANCE | In-run revive: cost curve, HP/invuln, prompt timing |
PULL COSTS | Gacha single + 10-pull pricing |
SUPPORTER CLUB | Paid-tier USD price |
DAILY LIMITS | Ad-ticket cap |
GEM PACKS | Storefront pack catalog (fake IAP during beta) |
STARTER PACK & OFFERS | One-time + recurring USD bundles |
ROOKIE BOOST | First-week passive XP multiplier |
AD REWARD SLOTS | Watch-ad reward grid (chain / ship / material / resource) |
NEW PLAYER STARTER INVENTORY | Default ships + zeroed wallet on fresh account |
Public API
Arcade rewards
interface ArcadeCreditsBreakdown— full per-bar breakdown returned to the reveal screen. Carriestotal, four*BonusCreditschunks that sum exactly tototal, three0..1bar fills (killBonus,eventBonus,levelBonus), and threepar*integers for thecurrent/parlabels.computeArcadeCredits(result: MissionResult): number— convenience wrapper returning onlytotal.computeArcadeCreditsBreakdown(result: MissionResult): ArcadeCreditsBreakdown— full breakdown. Formula shape:baseAtTier(tier) × (1 + killBonus + eventBonus + levelBonus)where each bonus isclamp(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.shipChanceis a 3-tuple of probabilities indexed by star count (1/2/3 stars),shipMaxRaritycaps 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): number—BASE_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: truemeans 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' }.viewsRequiredis the number of ad views to unlock the slot’s payout;displayCategorypartitions 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 intolevelBonusCredits(clamped≥ 0). Reveal-screen tweens depend on this. - Bar fill bounds.
killBonus,eventBonus,levelBonusare always in[0, 1]afterclamp. Bars cap visually at 100% even when the player exceeds 2× par. - Defensive integer coercion.
result.progression.tierReached,totalKills,eventsCompleted,levelReachedare run through| 0andMath.max(0, …)— robust to NaN/negative/floating-point inputs from upstream. - Death-defiance real-time contract.
DEATH_DEFIANCE_TOTAL_REAL_DURATIONis the authoritative duration for both the bridge-side dismiss timer and the React-side bar animation. ChangingPROMPT_DURATION,CHEAT_THRESHOLD,CHEAT_STRETCH, orLINGER_DURATIONrecomputes it; both consumers must read the derived constant. - Tier-1 par curve.
parLevel(tier)is10 / 15 / 20for tiers 1–3 then+4per tier — non-linear at the low end on purpose.baseAtTier(tier)doubles per tier from~66.7at 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 dedicateddeath-defiance-timing.tsmodule 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 fordata/storefront.tsor a JSON/TOML content table once real IAP wiring lands andbetaTopUpgoes away. - Arcade reward formula (
ArcadeCreditsBreakdown+computeArcadeCreditsBreakdown+parLevel+baseAtTier) is the only block in this file with non-trivial math. Candidate fordata/arcade-rewards.tsif reward tuning starts churning independently of the rest of the economy. AD_REWARD_SLOTSlikely belongs alongside ad-system code rather than the economy file once the ad surface owns its own data tables.