What it is
The world is an infinite 2D plane populated with biome-specific terrain, hub-and-spoke navigation corridors, and procedurally generated nebula backdrops. Each run loads a planet whose biome drives terrain shape pools, hub topology, enemy set, fog density, and post-processing mastering. Player XP feeds both a per-run level curve (drives reward cards) and a per-planet meta-progression track (drives unlocks).
Stats / tables
Planets — gameplay-relevant parameters
| ID | Name | Biome | Enemy set | Boss | Fog alpha | Enemy mult | Spawn grace (s) | Shadow mult | Leaderboard |
|---|---|---|---|---|---|---|---|---|---|
| 12 | LANDING SITE | landing_site | bugs | pacemaker | 0.15 | 1.0 | 1 | 1.0 | no |
| 21 | SUNRISE CITY | sunrise_city | city | first_lady | 0.78 | 1.0 | 1 | 2.0 | no |
| 3 | THE VOIDSTAR | the_voidstar | bugs | cenotaph | 0.00 | 2.0 | 0 | 1.0 | yes |
| 30 | SOLARIS | landing_site | bugs_mortar | iron_throne | 0.20 | 1.0 | 1 | 1.0 | no |
| 31 | SPEEDWAY | landing_site | bugs_shooter | spire | 0.10 | 1.0 | 1 | 1.0 | yes |
| 32 | EDEN-5 | landing_site | bugs_charger | grand_procession | 0.20 | 1.0 | 1 | 1.0 | no |
| 33 | OLD EARTH | old_earth | bugs_sniper | ringmaster | 0.25 | 1.0 | 1 | 1.0 | no |
| 34 | NETWORK STATION | landing_site | bugs_field | foreman | 0.15 | 1.0 | 1 | 1.0 | no |
| 35 | DELPHI | delphi | bugs_racer | apex | 0.15 | 1.0 | 1 | 1.0 | no |
| 36 | DESOLATION | landing_site | bugs_heavy | — | 0.15 | 1.0 | 1 | 1.0 | no |
| 37 | OBELISK | landing_site | bugs_mixed | — | 0.10 | 1.0 | 1 | 1.0 | no |
| 99 | OBSIDIAN SPIRE | obsidian_spire | bugs | — | 0.05 | 1.5 | 1 | 1.5 | no |
Planet order in hub swiper: 12, 21, 3, 30, 31, 32, 33, 34, 35, 36, 37. Planet 99 is not in the swiper.
Biomes — terrain and hub topology
| Biome | Level radius | Density | Intensity | Terrain mix | Hub min dist | Hub max dist | Hubs/ring | Hub clear | Spoke width | Scatter only |
|---|---|---|---|---|---|---|---|---|---|---|
| landing_site | 2500 | 0.85 | 0.70 | asteroids | 400 | 1000 | 6 | 200 | 150 | no |
| sunrise_city | 2500 | 0.70 | 0.60 | buildings | 0 | 0 | 0 | 0 | 0 | yes |
| the_voidstar | 2500 | 0.60 | 0.55 | voidstar_mix | 500 | 1200 | 5 | 200 | 150 | no |
| obsidian_spire | 2500 | 0.55 | 0.50 | pillars | 500 | 1200 | 5 | 200 | 150 | no |
Terrain shapes — collision and visual radius
| Shape ID | Type | Verts | Base radius | Scale min | Scale max | Effective radius (px) |
|---|---|---|---|---|---|---|
| asteroid_sm | polygon | 5 | 75 | 1.0 | 1.5 | 75–112 |
| asteroid_md | polygon | 6 | 85 | 1.0 | 1.6 | 85–136 |
| asteroid_lg | polygon | 7 | 98 | 1.0 | 1.6 | 98–156 |
| asteroid_xl | polygon | 8 | 100 | 1.0 | 1.8 | 100–180 |
| rock_shard | polygon | 5 | 60 | 1.0 | 1.3 | 60–78 |
| building_1x1 | sprite | 4 | 277 | 1.0 | 1.0 | 277 |
| building_2x1 | sprite | 4 | 322 | 1.0 | 1.0 | 322 |
| building_1x2 | sprite | 4 | 284 | 1.0 | 1.0 | 284 |
| building_2x3 | sprite | 4 | 385 | 1.0 | 1.0 | 385 |
| pillar_sm | sprite | 4 | 576 | 1.0 | 1.0 | 576 |
| pillar_md | sprite | 4 | 768 | 1.0 | 1.0 | 768 |
| pillar_lg | sprite | 4 | 960 | 1.0 | 1.0 | 960 |
Terrain mix pools — cluster shape weights
| Mix | Tier | Shape | Weight |
|---|---|---|---|
| asteroids | center | asteroid_xl | 60 |
| asteroids | center | asteroid_lg | 40 |
| asteroids | medium | asteroid_lg | 40 |
| asteroids | medium | asteroid_md | 40 |
| asteroids | medium | rock_shard | 20 |
| asteroids | small | asteroid_sm | 40 |
| asteroids | small | asteroid_md | 30 |
| asteroids | small | rock_shard | 30 |
| buildings | center | building_2x3 | 50 |
| buildings | center | building_2x1 | 30 |
| buildings | center | building_1x2 | 20 |
| buildings | medium | building_1x1 | 35 |
| buildings | medium | building_2x1 | 30 |
| buildings | medium | building_1x2 | 25 |
| buildings | medium | building_2x3 | 10 |
| buildings | small | building_1x1 | 50 |
| buildings | small | building_2x1 | 25 |
| buildings | small | building_1x2 | 25 |
| voidstar_mix | center | pillar_md | 70 |
| voidstar_mix | center | asteroid_xl | 18 |
| voidstar_mix | center | asteroid_lg | 12 |
| voidstar_mix | medium | pillar_md | 40 |
| voidstar_mix | medium | pillar_sm | 28 |
| voidstar_mix | medium | asteroid_lg | 14 |
| voidstar_mix | medium | asteroid_md | 12 |
| voidstar_mix | medium | rock_shard | 6 |
| voidstar_mix | small | pillar_sm | 36 |
| voidstar_mix | small | pillar_md | 30 |
| voidstar_mix | small | asteroid_sm | 14 |
| voidstar_mix | small | asteroid_md | 10 |
| voidstar_mix | small | rock_shard | 10 |
| pillars | center | pillar_md | 60 |
| pillars | center | pillar_sm | 40 |
| pillars | medium | pillar_md | 55 |
| pillars | medium | pillar_sm | 45 |
| pillars | small | pillar_sm | 60 |
| pillars | small | pillar_md | 40 |
Hub-spoke pattern types
| Pattern | Hubs per chunk | Spoke algorithm | Chunk size range (px) |
|---|---|---|---|
| grid | 1, centered | orthogonal H/V only | 1500–3500 |
| zigzag | 1, alternating L/R by row | maximal planar | 1500–3000 |
| rings | center + concentric rings | maximal planar | (outer × 2 + 400) × 1.0–1.1 |
| chaotic | 1–4, Poisson-disc | maximal planar | 2000–4000 |
Hub-spoke generation constants
| Stat | Value |
|---|---|
| Max spoke distance | chunkSize × 1.8 |
| Grid spoke axis tolerance | chunkSize × 0.4 |
| Hub minimum separation | hubRadius × 2.5 |
| Chaotic placement attempts per chunk | 80 |
| Rings: min count | 1 |
| Rings: max count | 3 |
| Rings: outer radius range | 20%–45% of chunk size |
| Zigzag amplitude range | 15%–40% of chunk size |
| Grid hub jitter | ±8% of chunk size |
| Zigzag X jitter | ±6% of chunk size; Y ±8% |
| Chaotic margin | hubRadius + 5 px |
| Hub radius jitter | 90%–110% |
| Rings center-hub radius multiplier | 1.2 |
| Precompute grid radius (chunks each direction) | 10 |
Legacy ring-based hub placement (non-zone-aware)
| Stat | Value |
|---|---|
| Central START hub position | (0, 0) |
| Central START hub radius | 300 |
| Central START hub clearRadius | 600 |
| Hub radial jitter | ±30% |
| Hub angular jitter | ±40% of step |
| Ring outer cutoff | 95% of level radius |
| Spoke types | center→ring1; ringN→ringN+1 nearest; same-ring adjacency |
| Same-ring cross spoke width multiplier | 0.75 |
| Hub bounding radius | clearR × 0.7 |
| Location influence radius | max(clearR × 1.2, 2000) |
| Event slots per location | 2 |
Zone classification
| Zone | Definition | Lit variant trigger |
|---|---|---|
| hub_dark | Inside any hub circle (point distance ≤ hub.r) | hub illuminated |
| hub_lit | Inside any hub circle | yes |
| spoke_dark | Within spokeHalfWidth of any spoke center segment | either endpoint hub lit |
| spoke_lit | Within spokeHalfWidth of any spoke center segment | yes |
| wilds | Everywhere else | never illuminated |
Priority order: hub > spoke > wilds. Default zone-grid cell size: 16 px. Spoke bounding-box pad for live lookup: 50 px.
Terrain generation phases
| Phase | Action |
|---|---|
| A | Cluster-based fill on a grid (center + medium ring + small ring per cluster) |
| B | Carve hubs and spokes (delete overlapping terrain) — skipped in zone-aware mode |
| C | Removed (no boundary walls; infinite world) |
| D | Disabled (floaters disabled; terrain is static) |
Cluster fill — density-driven parameters
| Parameter | At intensity 0.0 | At intensity 1.0 |
|---|---|---|
| Cluster skip rate | 1.0 | 0.0 |
| Cluster grid step (px) | 900 | 120 |
| Medium pieces per cluster (intensity < 0.3) | 0 | — |
| Medium pieces per cluster (intensity ≥ 0.3) | — | floor((1 + rand×3) × intensity) |
| Small pieces per cluster (intensity < 0.4) | 0 | — |
| Small pieces per cluster (intensity ≥ 0.4) | — | floor((2 + rand×4) × intensity) |
| Overlap pad between asteroid edges (px) | 75 | 0 |
| Medium ring distance from center (px) | center.r + 40 to center.r + 90 | same |
| Small ring distance from center (px) | center.r + 100 to center.r + 180 | same |
| Cluster center jitter | ±50% of step | same |
Sunrise City grid layout
| Stat | Value |
|---|---|
| Block size (interior, px) | 1000 |
| Road width (px) | 200 |
| Cell pitch (block + road, px) | 1200 |
| Building sub-grid step (px) | 500 |
| Building sub-grid skip rate | 10% |
| Spawn clear radius (px) | 600 |
| Building center jitter | ±15 px |
| Overlap radius (rectangular footprint) | 30% of bounding radius |
| Roads registered as spokes (both axes) | yes |
| City start hub | (0, 0), radius 300, clearRadius 600 |
Dynamic terrain expansion
| Stat | Value |
|---|---|
| Super-chunk size (px) | 800 |
| Expansion radius around player (px) | 2200 |
| Skip rate (natural gaps) | 3% |
| Center cluster jitter | ±40% of super-chunk |
| Mediums per expansion cluster | 3–5 |
| Smalls per expansion cluster | 4–8 |
| Garbage-collect distance from player (px) | 3000 |
| Clear check vs hub clearRadius | + super-chunk × 0.7 |
| Clear check vs spoke width | spokeW × 0.5 + SC × 0.5 |
| Clear check vs event clearance | ev.radius + 80 + SC × 0.7 |
| Crate spawn chance per expansion super-chunk | 85% |
| Crates per firing super-chunk | 2–4 |
| Crate placement RNG seed term | seed + floor(playerX) × 33391 + floor(playerY) × 77317 |
| Chunk regen on GC | yes — generated-chunk key deleted |
| Spatial index chunk size (px) | 1024 |
Event placement on the playable area
| Stat | Value |
|---|---|
| Cell width (px) | 500 |
| Cell height (px) | 350 |
| Cell skip rate | 34% |
| Cell center jitter | ±40% of cell |
| Reject distance from origin (px, min) | 700 |
| Reject distance from origin (px, max) | 95% of level radius |
| Hub-clear test radius | clearRadius × 0.7 |
| Spoke-clear test radius | spokeW × 0.35 |
| Event-to-event edge minimum gap (px) | 400 |
| Event radius size jitter | ±20% |
| Terrain clear pad around event (px) | event.radius + 80 |
| Overlap-cleanup pad between events (px) | 150 |
| Sub-event spawn chance per non-start hub | 45% |
| Sub-event placement jitter (px) | ±100 |
| Crates per hub | 20–50 |
| Crate scatter radius around hub (px) | ±350 |
Per-run XP curve (level-up rewards)
Cumulative XP at each level, with per-level cost:
| Level | Cumulative XP | Cost |
|---|---|---|
| 0 | 0 | — |
| 1 | 50 | 50 |
| 2 | 150 | 100 |
| 3 | 300 | 150 |
| 4 | 500 | 200 |
| 5 | 750 | 250 |
| 6 | 1050 | 300 |
| 7 | 1400 | 350 |
| 8 | 1800 | 400 |
| 9 | 2250 | 450 |
| 10 | 2750 | 500 |
| 11 | 3325 | 575 |
| 12 | 4000 | 675 |
| 13 | 4800 | 800 |
| 14 | 5750 | 950 |
| 15 | 6900 | 1150 |
| 16 | 8300 | 1400 |
| 17 | 10000 | 1700 |
| 18 | 12100 | 2100 |
| 19 | 14700 | 2600 |
| 20 | 17900 | 3200 |
| 21+ | previous + previous_cost × 1.12 (rounded up) | compounding |
Per-planet meta-progression track
| Stat | Value |
|---|---|
| Levels per planet | 10 |
| Base XP thresholds (cumulative) | 100, 250, 500, 800, 1200, 1700, 2300, 3000, 4000, 5500 |
| Planet 12 XP multiplier | 1.0 |
| Planet 21 XP multiplier | 1.5 |
| Planet 3 XP multiplier | 2.0 |
| Planets 30–37 XP multiplier | 1.0 |
| Planet 12 base run XP | 10 |
| Planet 21 base run XP | 15 |
| Planet 3 base run XP | 20 |
| Planets 30–37 base run XP | 10 |
| Max level | 10 |
| Starting level | 1 |
Reward layout per planet:
| Level | Reward type | Rarity |
|---|---|---|
| 1 | custom_event | common |
| 2 | ship_common | common |
| 3 | weapon_1 | uncommon |
| 4 | ship_uncommon | uncommon |
| 5 | alt_boss | rare |
| 6 | ship_rare | rare |
| 7 | planet_master | epic |
| 8 | ship_epic | epic |
| 9 | weapon_2 | legendary |
| 10 | global_buff | legendary |
Level-up reward rarity roll (per card)
| Rarity | Weight | Effect multiplier |
|---|---|---|
| common | 50 | 1.00 |
| uncommon | 30 | 1.25 |
| rare | 15 | 1.50 |
| epic | 4 | 1.75 |
| legendary | 1 | 2.00 |
Luck bias coefficient per tier: 0, 0.01, 0.02, 0.03, 0.04. Each non-common weight is multiplied by 1 + effectiveLuck × bias. Effective luck = ship.luck × (1 + ship.luckMult).
Level-up card pool slot selection
| Stat | Value |
|---|---|
| Weapon-upgrade slot chance | 35% |
| Modifier slot chance | 65% |
| Merge cap (cards reserved when eligible) | min(merge candidates, ceil(count/2)) |
| Max weapon cards | unique upgradeable weapons |
| Max modifier cards | unique candidate modifiers |
| Fallback when chosen pool exhausted | switch to other pool |
Weapon-chest fractional upgrades
| Chest rarity | Level increment |
|---|---|
| common | 0.20 |
| uncommon | 0.40 |
| rare | 0.60 |
| epic | 0.80 |
| legendary | 1.00 |
Max weapon level: 20.
Nebula archetype catalog (background visuals)
Catalog: 100 baseline archetypes split into 11 thematic zones, plus per-planet overrides.
| Zone | Index range | Theme |
|---|---|---|
| I | 0–7 | Outer Acheron — entry/threshold |
| II | 8–15 | Tartarus Grid — industrial/ruin |
| III | 16–22 | Abyssal Styx — liquid/organic |
| IV | 23–30 | Phlegethon Wastes — radiation/chaos |
| V | 31–47 | Elysian Anomaly — surreal/divine |
| VI | 48–52 | Aquatic Layer — atmospheric haze |
| VII | 53–62 | Fog Realm — fog overlay |
| VIII | 63–70 | Molten Core — heat shimmer |
| IX | 71–79 | Crystal Wastes — prismatic refraction |
| X | 80–87 | Dark Sector — gravitational lensing |
| XI | 88+ | Hybrid Nexus — combination FX |
Smooth-archetype filter criteria (used for in-mission backgrounds): nt === 0, warp ≤ 0.7, 0.4 ≤ density ≤ 0.65, sat ≤ 1.0, SURF_MODE[i] === 0.
Per-planet nebula archetypes
| Planet ID | Archetype | Notes |
|---|---|---|
| 3 | Void Nebula | desaturated purple + bright orange |
| 12 | Abandoned Station (index 15) | luminosity × 1.5 |
| 21 | Dusk Haze (index 50) | luminosity × 1.95 |
| 30 | Solaris Nebula | amber/orange |
| 31 | Speedway Nebula | electric blue |
| 32 | Eden Nebula | deep green |
| 33 | Old Earth Nebula | grey-green mist |
| 34 | Network Station Nebula | cyan |
| 35 | Delphi Nebula | violet crystal |
| 36 | Desolation Nebula | rust-red |
| 37 | Obelisk Nebula | deep indigo |
Nebula archetype knobs
| Knob | Range | Effect |
|---|---|---|
| pa, pb, pc, pd | vec3 each | cosine palette parameters |
| warp | 0–3 | domain warp strength (0 = flat, 3 = chaotic) |
| density | 0–1 | nebula opacity/fill |
| speed | — | animation/palette drift speed |
| thresh | 0–1 | visibility threshold (higher = more void) |
| bg | vec3 0–1 | background base color |
| shape | sq, dot, dia, tri, hex, star5, spark, cross | near-layer star shape |
| nf | 0.3–1.5 | noise frequency multiplier |
| nt | 0, 1, 2 | classic FBM, ridged, voronoi |
| ec | vec3 0–255 | emission color for star glow |
| sat | default 1.0 | saturation multiplier |
| lum | default 1.0 | luminosity multiplier |
| fg | 0–1 | fog overlay intensity |
Boss-arena terrain patterns
| Pattern ID | Pillars | Hazard pads | Placement |
|---|---|---|---|
| open | 0 | 0 | — |
| pillar_ring | 6 | 0 | ring at 65% arena radius |
| pillar_cross | 4 | 0 | cardinal points at 55% arena radius |
| hazard_pads | 0 | 4 | ring at 50% arena radius |
| corridor | 8 | 0 | two walls of 4 along long axis at 55% short-axis offset |
| gauntlet | 6 | 0 | two staggered rows of 3 along long axis at 40% short-axis offset |
Pillar HP: 300. Pillar radius: 30 px. Hazard pad damage: 18 DPS. Hazard pad radius: 70 px.
How it works
World generation entry
WorldGenerator.generate(world, biomeId, levelData?) is the entry point. It clears world.terrain / structures / hubs / spokes, sets world.biomeId and world.levelRadius, then dispatches:
sunrise_citybiome →_fillCityGrid(grid of city blocks separated by 200 px roads).levelDatasupplied → zone-aware path. Hubs and spokes are copied fromlevelData.generation, then_fillTerrainruns with zone-grid filtering.- Otherwise → legacy ring-based path.
_generateHubAndSpokeproduces hubs in concentric rings, then_fillTerrainruns without zone-grid filtering.
If levelData is present, _validateGeneration warns when terrain bounding circles overlap hub zones.
Hub-spoke precompute (zone-aware)
precomputeWorld(config, spawnX, spawnY, gridRadius) calls generateHubsAndSpokes over a square chunk grid centered on spawn. Default grid radius: 10 chunks each direction (20×20 area). Steps:
- Compute chunk size from pattern + knobs via
calcChunkSize. - For each chunk, run pattern-specific hub generator (
grid/zigzag/rings/chaotic) with a per-chunk Mulberry32 RNG seeded fromglobalSeed × 73856093 ^ cx × 19349663 ^ cy × 83492791. - Append all hubs to a flat list, recording per-chunk indices.
- For each hub, spawn 1–2 warp puddles via
spawnPuddlesForHub. - Build spokes globally:
gridpattern → orthogonal H/V only;chaotic/rings/zigzag→ maximal planar graph (shortest-first, no edge crossings). - Merge overlapping warp puddles into groups via union-find.
Hub size: lerp(hubRMin, hubRMax, hubSize). Hub radius is then jittered ±10%.
Four-phase terrain fill
Phase A (cluster fill) iterates a grid over either the zone-grid bounds (zone-aware) or ±levelRadius. Per cell, with probability 1 − clusterSkipRate:
- Place a center piece (weighted-pick from
mix.centers) at the cell’s center plus 50%-of-step jitter. - Place a ring of mediums (count scales with intensity) at distance
centerR + 40..90. - Place a ring of smalls (count scales with intensity) at distance
centerR + 100..180.
Each placement runs tryPlace: scan terrainList, reject if the new piece’s bounding circle plus OVERLAP_PAD overlaps an existing piece. OVERLAP_PAD interpolates from 75 px at intensity 0 to 0 px at intensity ≥ 0.83.
Zone-aware mode rejects pieces whose centers fall outside wilds in the zone grid (per-cell check using estimated piece radii of 150 / 100 / 60 px for center/medium/small).
Phase B carves hubs and spokes in legacy mode. A piece is dropped if its center is within hub.clearRadius + boundingRadius × 0.5 + 30 of any hub, or within spoke.width × 0.5 + boundingRadius × 0.3 + 20 of any spoke segment.
Phase C (border wall) is removed.
Phase D (floaters) is disabled. world.floaters is always [].
After all phases, deduplicateTerrain runs: for each piece, delete any later piece whose center is within 2 × pieceRadius. Buildings and pillars use boundingRadius × 0.30 instead of full bounding for this test. Then buildTerrainChunks indexes all terrain into 1024 px spatial chunks.
Sunrise City layout
City uses a regular grid: 1000 px blocks of buildings separated by 200 px roads. Block centers are placed at (g + 500, g + 500) where g steps by 1200. Within each block, a 2×2 sub-grid of building slots is filled with random shapes from the buildings pool (40% from centers, 60% from smalls). Buildings with centers landing in a road corridor (localX > 1000 || localY > 1000) are rejected.
After buildings are placed and deduplicated, perpendicular roads are registered as full-length spokes (200 px wide) so the event placer treats them like normal corridors. A single start hub is placed at origin with radius 300 and clearRadius 600.
Zone classification
classifyZone(px, py, hubs, spokes, spokeHalfWidth, illuminationMap):
- Check every hub: if
(px − hub.x)² + (py − hub.y)² ≤ hub.r², returnhub_lit(if illuminated) orhub_dark. - Check every spoke: compute point-to-segment squared distance to the segment between the two endpoint hubs. If
≤ spokeHalfWidth², returnspoke_lit(if either endpoint hub is illuminated) orspoke_dark. - Otherwise return
wilds.
The zone grid (buildZoneGrid) is precomputed once at level load: structural zone (hub vs spoke vs wilds) is baked per 16 px cell; illumination is resolved live from the illumination map at runtime. Out-of-bounds cells return wilds.
Event placement
placeEvents(world, eventPool) walks a 500 × 350 px grid across the playable area. Per cell, with probability 66% (34% skipped):
- Jitter cell center by ±40%. Reject if distance from origin > 95% of level radius or < 700 px.
- Reject unless in clear area: inside
clearRadius × 0.7of a hub, or withinspokeWidth × 0.35of a spoke center segment. - Reject if edge distance to any existing event < 400 px.
- Pick event type uniformly from
eventPool. Jitter event radius by ±20%. - Delete every terrain piece within
event.radius + 80 + terrain.boundingRadiusof the event.
After grid placement, a cleanup pass removes any event whose circle plus 150 px pad intersects an earlier event.
Sub-events: for each non-start hub, with 45% probability spawn one additional event of a random type within ±100 px of the hub, then clear surrounding terrain. Each hub also seeds 20–50 crate slots in a 700 px ring (actual crate spawning is delegated to a pool system).
Dynamic expansion
Every frame, expandTerrain(world, playerX, playerY) runs:
- Compute the super-chunk bounds (800 px chunks) covering
±2200px around the player. - For each not-yet-generated super-chunk, mark generated, then test whether the chunk center falls within any hub clear zone, spoke corridor, or event clear zone (each with
super-chunk × 0.7or× 0.5pad). Skip if so. - With 97% probability, generate one cluster (center + 3–5 mediums + 4–8 smalls) at the chunk center with ±40% jitter. Reject pieces overlapping existing terrain (using AABB pre-filter then radial test with 20 px pad).
- If any terrain was added, run a separate per-chunk RNG to roll 2–4 crates with 85% chance per chunk.
After expansion, terrain garbage-collection deletes any piece beyond 3000 px from the player. Deleted pieces have their super-chunk key removed from _generatedChunks so re-entry regenerates them. Floaters and crates beyond the same distance are also culled (floaters always empty in current generation).
If terrain was added or removed, the world runs deduplicateTerrain and buildTerrainChunks again.
Run XP curve
LevelingSystem.update(game) loops checkLevelUp until current XP is below the next threshold. _ensureThreshold(level) returns the cumulative XP needed; the table extends on demand beyond level 20 by appending prev + lastCost × 1.12 rounded up. There is no level cap. Each level-up increments enemyDifficultyLevel, fires the level_up signal, and pushes a level_up entry onto rewardQueue.
Reward generation
generateRewardChoices(game, count, ship) builds three pools:
- New modifiers: modifier IDs not yet owned, gated by
game.runDef.context.upgradePoolif present.damage_*modifiers require an owned weapon with the matching damage tag (ordamage_allrequires at least one weapon). - Upgrade modifiers: owned modifiers below
maxLevel. - Weapon upgrades: owned weapons below level 20. Merge candidates: pairs of level-20 non-legendaries that can fuse into a legendary.
Merge cards are slotted first (cap ceil(count/2)). Remaining slots roll independently: 35% chance to draw from the weapon-upgrade pool, 65% from the modifier pool. If a pool is empty, the other is used. Every chosen card rolls its own rarity using the rarity roll table, and rarity drives rarityMult for effect scaling.
generateWeaponChoices(game, ship, count) filters all weapons (excluding disabled, legendary, owned, banished, and out-of-pool) and returns up to count non-duplicate picks at uniform weight.
Reward application
applyReward(choice, ship, game):
weapon— push a new weapon entry with cooldown derived fromdef.fireRate.base, random fire-timer offset, level 1.weapon_upgrade— addmax(1, round(rarityMult))levels, capped at 20. Ifsympathetic_resonanceartifact is owned, cascade the same number of bonus levels to all other weapons.weapon_merge— consume the two parent weapons and grant the resulting legendary viamergeWeapons.modifier— incrementgame.upgradeCounts[id], then register every effect as a permanentModifierwith sourcelevel_up:<modId>:<stat>. Recalc settles stats; HP/Shield current values bump by the new-max delta.artifact—grantArtifact(new or level up).shooting_star— dispatch by category: bump all weapons, all mods, all artifacts, lowest weapon +3, applystarpowerstate for 60 s, or grant +1 reroll/banish/refuel.
Per-planet track XP
getXpProgress(planetId, xp) returns {level, currentXp, nextThreshold, pct, totalXp}. Players always start at level 1; thresholds gate levels 2 through 10. At level 10 the return is {level: 10, currentXp: 0, nextThreshold: 0, pct: 1, totalXp: xp}. getOverallPct reports cumulative progress across all 10 levels.
Interactions
- Run loads with a
RunDefinitioncarryingcontext.planetId. ThePlanetDefresolves biome, enemy set, boss, fog alpha, post-processing mode, and level preset. levelPresetresolves to aLevelConfigconsumed bygenerateHubsAndSpokesand the zone-aware terrain fill.world.eventspopulates after generation; runs without events spawn only crates.- Reward cards push onto
game.rewardQueue. The HUD pops and presents cards viagenerateRewardChoices. - Modifier registration writes to
Modifiers.addkeyed bylevel_up:<modId>:<stat>.Modifiers.recalcresets stats toship._baseand re-applies all registered modifiers; level-up picks survive artifact-modifier expiry cycles. - Weapon-chest pickups call
resolveWeaponChestUpgrade(currentLevel, chestRarity)and bypass the level-up reward flow entirely. Weapon level-ups appear on level-up rewards only for owned weapons. - Background visuals call
PLANET_ARCHETYPE_IDX[planetId]for in-run rendering. Hub screen callsPLANET_ARCHETYPES[planetId]. - Boss arenas overlay one of six terrain patterns from
TERRAIN_PATTERNSindependent of biome terrain. - The XP track awards XP from challenge completions and run completions; XP gates 10 per-planet reward levels.
What it does NOT do
- Does not enforce a level cap on player XP — thresholds extend infinitely past level 20.
- Does not place border walls — the world is infinite; there is no level boundary.
- Does not spawn free-floating physics asteroids —
world.floatersis always empty. - Does not bleed damage from shield to hull — the world system does not handle damage routing.
- Does not award weapon level-ups from level-up cards for unowned weapons — weapon-chest pickups are the only source for new weapons.
- Does not handle interactive crates inside the world generator — crate lifecycle is delegated to a separate pool.
- Does not let the zone-aware fill carve hubs/spokes after placement — zone-aware terrain is rejected pre-placement against the zone grid.
- Does not render planet 99 (Obsidian Spire) in the hub swiper — it is data-only.
- Does not show legendary weapons in weapon-chest pulls — legendaries are merge-only.
- Does not banish charge-grant shooting-star cards (
grant_reroll,grant_banish,grant_refuel). - Does not place sub-events on the start hub or hubs flagged as objectives.