Level-Up Event Orchestrator
The level-up orchestrator is the flow that fires every time the player’s accumulated XP crosses the threshold for the next level. It is the single ceremony that turns raw XP gain into a hand-of-cards choice, pausing the simulation while the player decides which reward to apply.
Flow
The orchestrator runs as a sequenced ceremony each time LevelingSystem.checkLevelUp(game) returns true. The simulation update loop calls LevelingSystem.update(game) once per frame, which loops on checkLevelUp so multi-level jumps (e.g. boss-kill XP dumps) queue one card per level rather than collapsing into a single choice.
- Threshold cross.
game.xp >= XP_THRESHOLDS[game.level + 1]. The XP threshold table is_BASE_THRESHOLDS(levels 0-20, hand-tuned) and extends infinitely beyond level 20 by compounding the previous level’s cost at 12% per step via_ensureThreshold. - Increment level.
game.level++andgame.enemyDifficultyLevel++together — every level-up bumps enemy difficulty in lockstep with player power. - Recompute
xpToNext.XP_THRESHOLDS[game.level + 1] - game.xpso the HUD bar redraws against the new ceiling. - Fire
level_upsignal.Sig.fire('level_up', 0, 0, game.level, 0, '')— the effect system hooks (artifacts that trigger on level-up) listen here. - Pause sim.
game.rewardQueue.push({ type: 'level_up', level: game.level }). The reward queue is consumed by the UI layer, which setstimeDilation = 0while a reward card is being shown — the world freezes, projectiles hang in mid-air, the player picks at their leisure. - Generate reward choices.
generateRewardChoices(game, count, ship)builds the card pool (see below). - Display reward cards. UI renders the choices as tappable cards with rarity borders, before/after stat values, and NEW badges for first-time picks.
- Await tap. Player makes one selection. Reroll and banish charges may consume the current pool and reroll, or remove a specific card from the run’s pool forever.
- Apply reward.
applyReward(choice, ship, game)dispatches onchoice.type— modifier picks register as permanentModifierentries (sourcelevel_up:<modId>:<stat>), weapon upgrades bumpweapon.level, merges consume the two parents and grant the legendary, artifacts route throughgrantArtifact. - Unpause. Reward queue empty →
timeDilation = 1→ simulation resumes.
Reward pool composition
generateRewardChoices builds the card pool from up to five categories, all competing for the count slots (default 3). Categories:
- Modifiers (ship upgrades). Split into NEW (
upgradeCounts[id] === 0, not yet picked) and UPGRADE (already owned, current level belowmaxLevel).damage_*modifiers are filtered to the damage tags the player’s currently-owned weapons actually carry — picking up adamage_firemod when you own no fire weapons is impossible.damage_allis the exception: offered as long as the ship has at least one weapon. - Weapon upgrades. Owned weapons below
MAX_WEAPON_LEVEL(20). Each card bumpsweapon.levelbymax(1, round(rarityMult))— legendary rolls add 2 levels in one pick. - New weapons. Surfaced from weapon chests (
generateWeaponChoices) rather than level-up cards. Legendaries are excluded from chest pools — they’re merge-only. - Weapon merges.
findMergeCandidates(ship)returns any pair of level-20 non-legendary weapons that combine into a specific legendary. Merge cards are prepended to the pool — they take priority slots up toceil(count / 2), so a level-up that surfaces a merge will always show it as the first card. - Artifacts and shooting-star cards. Surfaced via separate paths (
grantArtifact,generateShootingStarChoices) and merged into the sameRewardChoice[]type with their owntypediscriminator.
Slot allocation
After prepending merges, each remaining slot independently rolls weapon (35%) vs. modifier (65%). This produces a natural distribution around ~35% weapon upgrades per pull. If the chosen pool is exhausted, the slot falls back to the other pool. Both pools are Fisher-Yates shuffled before drawing.
Hard caps: never offer the same weapon twice in one pull, never offer the same modifier twice in one pull. A padding pass at the end fills any unfilled slots from remaining unused entries. If a pull genuinely has fewer unique candidates than count, the player gets fewer cards rather than dupes.
Per-card rarity roll
Every card rolls its own rarity independently via _rollRarity(ship):
| Rarity | Weight | Multiplier |
|---|---|---|
| Common | 50 | 1.0 |
| Uncommon | 30 | 1.25 |
| Rare | 15 | 1.5 |
| Epic | 4 | 1.75 |
| Legendary | 1 | 2.0 |
Luck shifts the distribution via effectiveLuck = ship.luck * (1 + ship.luckMult). Each non-common tier’s weight is multiplied by (1 + effectiveLuck * _LUCK_BIAS[tier]) with bias coefficients [0, 0.01, 0.02, 0.03, 0.04] — legendary scales fastest, common stays unboosted so it’s never starved. The roll is uniform random against the post-luck weighted total.
The multiplier feeds back into the reward effect: modifier picks scale every effect value by mult, weapon-upgrade picks gain round(mult) extra levels (1 for common-rare, 2 for epic-legendary), and the card border colour matches the rolled rarity.
Banished keys
game.banishedKeys: Set<string> holds banish-charge entries that filter the pool. Keys are built by banishKeyForChoice:
weapon|<id>,weapon_upgrade|<id>,weapon_merge|<legendaryId>,modifier|<id>— direct id-based banishing.artifact_new|<id>vs.artifact_upgrade|<id>— separate keys so banishing a brand-new artifact doesn’t lock out future tier-ups of an already-owned one.shooting_star|<category>— banishes a shooting-star category from the pool for the rest of the run.grant_reroll/grant_banish/grant_refuelare not banishable — banishing the resource-grant cards would break the only refill path for the charge they grant.
The filter runs at the start of generateRewardChoices against modifier candidates, weapon upgrades, and merge candidates. Banished entries are dropped before the slot-allocation phase, so banished items never even reach the rarity roll.
Related
- XP orbs mechanics — how XP is awarded.
- Rarity roll — the shared rarity-distribution mechanism.
- Reroll and banish — per-run charge resources.
- Weapon merge rules — when merge cards surface.
- Reward batch — queue ordering when multiple reward types stack.
- Time dilation — the pause mechanism.