How to design a new biome

Step-by-step guide for adding a new themed biome (landing_site, sunrise_city, the_voidstar, etc.) to the world generator.

Before you start

A biome is a themed visual + terrain + spawn assembly. It bundles together a terrain shape mix, a hub-and-spoke topology, a parallax recipe (background art layers), a palette preference list, a nebula archetype pool, and a flag for which planets can reference it. Multiple planet configs can point at the same biome — for example landing_site is shared by LANDING SITE, SOLARIS, SPEEDWAY, EDEN-5, NETWORK STATION, OBELISK, and DESOLATION. Only sunrise_city, the_voidstar, obsidian_spire, delphi, and old_earth are bespoke single-planet biomes today.

A biome does NOT pick the boss — the planet config does (PlanetDef.boss). The boss’s own arena definition picks the terrain pattern that fills the sealed arena. Biome terrain only governs the wilds outside the arena footprint.

Adding a new biome = one BiomeId entry + one BIOMES entry + one parallax recipe + one palette-preference row + one nebula-pool row + one or more planet references.

Step 1: Identity

Pick the load-bearing identity values up front.

FieldWhat it controlsReference values
BiomeIdStable string id used by every subsystemlanding_site, sunrise_city, the_voidstar, obsidian_spire, delphi, old_earth
nameDisplay label in dev HUDs and tab pickersLanding Site, Sunrise City, The Voidstar
levelRadiusOuter ring of the playable disc, world units2500 (every shipped biome)
postProcessing modeMastering preset on the planet, not the biomedark, sunlit
fogAlphaPer-planet atmospheric fog overlay opacity0.00 (Voidstar) → 0.78 (Sunrise City)
shadowOffsetMultDrop-shadow Y multiplier on the planet1.0 default; 2.0 for tall city buildings

The BiomeId type union lives in data/planet-config.ts. Add your id there first — it forces the rest of the system to compile-error until every keyed table is filled.

Step 2: Terrain mix

A biome’s terrainMix is three weighted shape pools: centers (large cluster anchors), mediums (the ring around each center), smalls (the outer scatter). The cluster generator picks one shape from each pool per cluster.

Available shape primitives from data/terrain-shapes.ts:

Shape idTypeBase radius (world units)Scale rangeNotes
asteroid_smpolygon751.0 — 1.5Small rock, 5 verts, jitter 0.20
asteroid_mdpolygon851.0 — 1.6Mid rock, 6 verts, jitter 0.18
asteroid_lgpolygon981.0 — 1.6Large rock, 7 verts, jitter 0.15
asteroid_xlpolygon1001.0 — 1.8Boulder, 8 verts, jitter 0.12
rock_shardpolygon601.0 — 1.3Spiky shard, 5 verts, jitter 0.25
building_1x1sprite2771.0City block, small footprint
building_2x1sprite3221.0City block, wide footprint
building_1x2sprite2841.0City block, tall footprint
building_2x3sprite3851.0City block, large footprint
pillar_smsprite5761.0Obsidian pillar, small
pillar_mdsprite7681.0Obsidian pillar, medium
pillar_lgsprite9601.0Obsidian pillar, large

Reference mixes shipped today:

BiomeCenter pool weightsNotes
landing_siteasteroid_xl 60, asteroid_lg 40Pure asteroid field
sunrise_citybuilding_2x3 50, building_2x1 30, building_1x2 20Buildings only, scatterOnly: true
the_voidstarasteroid_xl 18, asteroid_lg 12, pillar_md 70Asteroids reduced to 30 % weight, pillars dominate
obsidian_spirepillar_md 60, pillar_sm 40Pure pillar field

Each biome also carries density knobs:

FieldRangeReference values
terrainDensityCluster-fill coverage target0.55 (Voidstar) → 0.85 (Landing Site)
terrainIntensityAsteroid density multiplier0.50 (Obsidian) → 0.70 (Landing Site)

Step 3: Nebula archetype

Background nebula is rendered by the WebGL nebula shader behind every parallax recipe. Biomes do not own a single archetype — they own a pool of archetype indexes, and the engine picks one per run from the pool.

BIOME_BACKGROUNDS in data/nebula-archetypes.ts maps biomeId → archetype index[]. Every shipped biome currently points at the shared SMOOTH_NEBULA_POOL, which is the subset of the 100 archetypes that pass isSmoothArchetype:

FilterValue
nt (noise type)0 (classic FBM)
warp≤ 0.7
density0.4 — 0.65
sat (saturation)≤ 1.0
SURF_MODE0 (space, not boss-arena surface)

For planet-locked looks, register a one-off archetype in _PLANET_<ID>_ARCH and push it into the ARCHETYPES array, then add the index to PLANET_ARCHETYPE_IDX. The renderer prefers this per-planet index over the biome pool.

Archetype knobs available (see Archetype interface):

FieldWhat it controls
pa, pb, pc, pdCosine palette parameters, vec3 each
warpDomain-warp strength, 0 flat → 3 chaotic
densityNebula fill, 0 sparse → 1 opaque
speedAnimation drift speed
threshVisibility threshold, higher = more void
bgBackground base color [r,g,b] 0 — 1
shapeNear-layer star shape — sq, dot, dia, tri, hex, star5, spark, cross
nfNoise frequency, 0.3 smooth → 1.5 detailed
ntNoise type — 0 FBM, 1 ridged veins, 2 voronoi
ecEmission tint [r,g,b] 0 — 255 (star glow)
sat, lumSaturation and luminosity multipliers
fgFog overlay intensity, 0 — 1
modeAdvanced render mode (0 classic, 1 enriched, 2 spiral, 3 blackhole, 4 horizon, 5 aurora, 6 warp tunnel, 7 cavern)
spkSparkle / twinkle intensity, 0 — 1
ssaShooting-star base angle in radians (default ≈ 0.6)
ssvShooting-star angle variance, 0 parallel → 1 chaotic

Step 4: Zone classifier rules

Every world point is classified into one of five zone types by classifyZone() in engine/world/zone-classifier.ts. The biome does NOT pick the rules — it picks the topology knobs that the classifier uses.

Zone typeWhen
hub_darkInside a hub circle, illumination map says hub is dark
hub_litInside a hub circle, illumination map says hub is lit
spoke_darkWithin spokeHalfWidth of a spoke center line, both end hubs dark
spoke_litWithin spokeHalfWidth of a spoke center line, at least one end hub lit
wildsEverywhere else (never illuminated)

Hub topology knobs the biome supplies:

FieldWhat it controlsReference values
hubMinDistRing spacing floor, world units400 — 500 (none for scatter-only)
hubMaxDistRing spacing ceiling1000 — 1200
hubsPerRingHubs placed on each ring5 — 6
hubClearMin, hubClearMaxHub clear-radius RNG range200 / 200 today
spokeWidthMin, spokeWidthMaxSpoke corridor width RNG range150 / 150 today
scatterOnlyDisables hub-and-spoke entirelytrue for sunrise_city

Priority: hubs win over spokes win over wilds — the classifier checks hubs first since they are smaller and more specific.

Step 5: Boss-arena terrain pattern default

Boss arenas are sealed squares that despawn all biome terrain inside the arena footprint and drop in a static terrain pattern from data/terrain-patterns.ts. The pattern is keyed PER-BOSS in the boss def (def.arena.terrain), not per-biome — so a biome cannot force a pattern, it can only inform which boss the planet picks. Patterns shipped today:

Pattern idLayoutUses
openNo terrain at allDefault fallback
pillar_ring6 destructible pillars in a ring at 65 % arena radiusHive Queen
pillar_cross4 pillars in a + cross at 55 % radiusDoomsayer
hazard_pads4 damaging floor pads in a ring at 50 % radius, 70 unit radius, 18 dpsAwakened Mech phase 3
corridorTwo walls of 4 pillars along the long axis at 55 % short offsetReactor Core (rect 800×400)
gauntletTwo staggered rows of 3 pillars at 40 % short offsetJunkrat Captains

Pillar specs are shared across patterns:

ConstantValue
PILLAR_HP300
PILLAR_RADIUS30 world units
HAZARD_DPS18 damage / sec
HAZARD_RADIUS70 world units

If a new biome’s bosses need a new pattern (e.g. a maze, a spiral, asymmetric cover), add a TerrainPatternDef to TERRAIN_PATTERNS with a positionFn(arena) that emits world coordinates, then point a boss def at it.

Step 6: Surface FX

The Archetype interface reserves ten surface-FX flags — hm, wv, gl, ca, pr, ln, sm, sb, sp, ss — for heat shimmer, waves, lensing, chromatic aberration, prisms, lens flares, smoke, sub-bands, sparkle, shooting stars. As of the last verified commit, none of the 100 shipped archetypes set these flags — the comment in nebula-archetypes.ts reads “Surface FX flags … are preserved for future boss-arena rendering but not used in normal gameplay.”

For a new biome, leave these unset. Use spk, ssa, ssv and the mode knob (sparkle, shooting-star angle, advanced render mode) for sparkle / streak effects today.

Step 7: Register

A new biome touches six tables. Every one is keyed by the same BiomeId string.

FileEdit
data/planet-config.tsAdd to BiomeId union
engine/world/generation.tsAdd BIOMES[<id>] BiomeConfig entry with terrainPool, terrainMix, density knobs, hub topology knobs
engine/rendering/parallax/biome-recipes.tsAdd a recipe factory and register it in _recipeFactories
engine/rendering/palette/palette-presets.tsAdd a BIOME_PREFERRED_PALETTES[<id>] entry (array of preset ids in preference order)
data/nebula-archetypes.tsAdd a BIOME_BACKGROUNDS[<id>] entry (array of archetype indexes)
data/planet-config.tsReference the biome from one or more PLANETS[<id>].biome slots

Available palette preset ids (from palette-presets.ts): pure_grey, noir, ice_seed, voidmilk, deep_frost, cold_ember, amethyst_drift, rose_quartz, dusty_rose, mint_jade, seafoam, cotton_sky, periwinkle, peach_cream, lavender_haze.

A planet also needs an enemySet, levelPreset, boss, destructibles density, and a planet image — those are planet-level, not biome-level.

Step 8: Validate

Boot the playground (LevelTab or PlanetsTab), pick a planet that references the new biome, and confirm:

  1. Terrain pieces match the mix — centers are the large shapes, mediums and smalls fall off correctly.
  2. Hub-and-spoke topology emerges (unless scatterOnly is set) with the configured ring spacing and spoke width.
  3. Parallax backdrop draws — back / mid / near / fg slots all render, nebula reads through at low back-slot opacity.
  4. Nebula archetype draws — color, density, shape, and animation match expectations.
  5. Palette preset matches the biome’s mood — preferred list is honored.
  6. Ambient drop-shadow Y-offset is correct (shadowOffsetMult on the planet).
  7. Fog overlay opacity is correct (fogAlpha on the planet).
  8. Boss arena replaces biome terrain cleanly when an encounter starts.

Custom-element rule

New biomes are data-only — every change lives in the six tables above. New terrain shape primitives, however, are engine work: they need a TerrainShapeDef entry in data/terrain-shapes.ts and (for sprite types) an asset registered with the renderer. New parallax layer kinds beyond starfield / silhouette stamp / atmosphere FBM / ground texture need new layer factories alongside createStarfieldLayer, createSilhouetteStampLayer, createAtmosphereFbmLayer, createGroundTextureLayer.