Rarity Roll

Every level-up reward card independently rolls a rarity that scales its effect magnitude (rarityMult: common 1.0× → legendary 2.0×). The roll uses weighted random selection over a fixed 5-tier table, with ship.luck shifting probability mass toward higher tiers — without ever starving the common pool. Implemented in _rollRarity in engine/world/leveling.ts.

Base rarity table

The flat baseline (no luck) is hand-tuned so common dominates, uncommon is regular, and legendary stays rare-feeling.

RarityWeightrarityMultFlat probability
common501.00×50%
uncommon301.25×30%
rare151.50×15%
epic41.75×4%
legendary12.00×1%

Weights sum to 100 at flat baseline, so a row’s weight is its raw percent chance when effectiveLuck = 0.

Luck bias

Luck is computed once per roll as effectiveLuck = ship.luck * (1 + ship.luckMult). The bias coefficient table is tier-indexed:

IndexRarityluckBias
0common0.00
1uncommon0.01
2rare0.02
3epic0.03
4legendary0.04

Each non-common weight is scaled to weight * (1 + effectiveLuck * luckBias). Common’s bias is 0, so its weight is never multiplied — luck strictly redistributes mass toward higher tiers without removing commons from the pool. Legendary’s coefficient is the highest (0.04) so it scales fastest with luck, which is what gives the high-luck feel of “I actually see legendaries now.”

Roll algorithm

  1. Compute effectiveLuck = (ship.luck ?? 0) * (1 + (ship.luckMult ?? 0)).
  2. For each tier i, compute w_i = baseWeight_i * (1 + effectiveLuck * luckBias_i). Sum into total.
  3. Sample roll = Math.random() * total.
  4. Walk the weighted entries in fixed table order subtracting w_i from roll; the first entry where roll <= 0 after subtraction is the result.
  5. If the loop completes without returning (floating-point edge), fall back to { rarity: 'common', mult: 1.0 }.

Passing ship = undefined skips luck entirely and returns the flat baseline distribution — useful for deterministic baseline tests.

Where it fires

_rollRarity(_ship) is called per reward card in LevelingSystem.generateRewardChoices:

  • New modifier picks (sets rarity, rarityMult, scales currentValuenewValue).
  • Modifier upgrade picks (scales the delta added to modifierTotals).
  • Weapon upgrade picks (rarityMult is rounded to Math.max(1, Math.round(mult)) to produce bonus levels: common/uncommon → +1, rare → +2 (1.5 rounds to 2), epic/legendary → +2).

weapon_merge cards skip the roll and are always tagged legendary. shooting_star cards also skip the roll and are always tagged legendary. New-weapon (chest) cards use resolveWeaponRarity(def) from the weapon definition, not this rarity roll.

Why this shape

  • Common is never scaled. Prevents luck from ever making bad-feeling pulls feel worse — every commodity pull still has a real common floor.
  • Tier-weighted coefficients. A flat coefficient across all non-commons would make uncommon scale just as fast as legendary, washing out the “luck = legendary” feeling. The 0.01/0.02/0.03/0.04 ladder concentrates luck’s effect on the tiers players are chasing.
  • Per-card independent rolls. Each card in a 3-card pull rolls separately, so a single level-up can mix rarities (e.g. one common, one rare, one epic). This is what makes high-luck level-ups feel like “the whole hand lit up.”