generation.ts
PURPOSE
World generation for Starship Survivors. Produces the static and dynamic terrain layout for a run: hub-and-spoke topology, four-phase asteroid fill, per-biome shape distributions, event placement, and infinite-world super-chunk expansion as the player moves. Also owns the canonical BIOMES registry and TerrainPiece / FloaterPiece data shapes.
OWNS
TerrainPieceandFloaterPieceinterfaces — position, shape id, scale, rotation, local and world-space verts, edge normals, bounding radius, spatial chunk key, color, and alpha.TerrainMixandTerrainMixEntryinterfaces — weighted shape pools split intocenters,mediums, andsmallsrings per biome.BiomeConfiginterface plus the four named mix presetsMIX_BUILDINGS,MIX_ASTEROIDS,MIX_VOIDSTAR, andMIX_PILLARS.- The
BIOMESregistry:landing_site,sunrise_city,the_voidstar, andobsidian_spire. Each biome ownslevelRadius,terrainDensity,terrainIntensity,terrainMix, hub topology knobs (hubMinDist,hubMaxDist,hubsPerRing,hubClearMin,hubClearMax,spokeWidthMin,spokeWidthMax), and pools for terrain, enemies, and events. CHUNK_SIZE = 1024spatial bucket size for the broad-phase collision index.- The
WorldGeneratorobject — entry points forgenerate,_generateHubAndSpoke,_fillTerrain,_fillCityGrid,_validateGeneration,placeEvents,getEnemySpawnPos,expandTerrain, andreset, plus the_SUPER_CHUNKsize and_generatedChunksset used by infinite-world expansion. buildTerrainChunks— exported helper that rebuildsworld.chunksafter any change toworld.terrain.- Vertex generation (
genVertsproduces a 12-vertex jittered polygon at +/- 4% radius), bounding-radius computation, terrain dedup (deduplicateTerrainculls later pieces within 2x the earlier piece’s radius, using a 0.30 footprint factor for buildings and pillars), andpointToSegDistfor spoke geometry.
READS FROM
../core/types—WorldStateshape.../core/utils—mulberry32seeded RNG.../../data/terrain-shapes—TERRAIN_SHAPEStable andTerrainShapeDefforbaseRadius,scaleMin,scaleMax, andtype(polygonorsprite).../../data/level-config—LevelConfigtype for the zone-aware path../chunk-manager—LevelData(zone grid plus precomputed hubs and spokes)../zone-classifier—lookupZoneis imported../events—createEvent,EventType, andGameEventfor event placement.
PUSHES TO
WorldState.terrain,WorldState.structures,WorldState.hubs,WorldState.spokes,WorldState.locations,WorldState.events,WorldState.chunks,WorldState.biomeId,WorldState.levelRadius, andworld.floaters(currently always cleared to[]).world.terrain[i].chunkKeyand per-piece_worldR,_origX,_origYprivate fields for downstream rendering and silhouette updates.- Console warnings via
console.warnfrom_validateGenerationwhen terrain edge-overlaps with hub zones (cosmetic, not blocking).
DOES NOT
- Does not bake or render terrain visuals. Final per-pixel collision polygons are computed later by
updateTerrainCollisionShapeindraw-3d-terrain.ts; this module only seeds an approximate bounding radius and emptyverts/worldVerts/normalsarrays. - Does not spawn enemies, the player, projectiles, drops, or crates.
getEnemySpawnPosonly returns a candidate point. Crate spawning calls are explicitly removed (the crate pool inworld/crates.tsowns lifecycle). - Does not build the zone grid or hub-and-spoke topology in the zone-aware path; those are pre-baked in
LevelData.generationandLevelData.zoneGridbychunk-manager. - Does not draw border walls. PHASE C is documented as removed for the infinite world.
- Does not animate floaters. PHASE D is disabled and
floatersis always set to[]. - Does not emit gameplay events, telemetry, or Supabase writes.
Signals
console.warnfrom_validateGenerationreporting the count of terrain pieces whose bounding radius overlaps a hub zone edge.- No other emitted events. Game events are constructed via
createEventand returned throughplaceEvents; consumers wire them in.
Entry points
WorldGenerator.generate(world, biomeId = 'landing_site', levelData?)— top-level call. Resets terrain, structures, hubs, and spokes. Forsunrise_cityit runs_fillCityGrid. WithlevelDatait copies hubs and spokes fromlevelData.generationand runs_fillTerrainin zone-aware mode, then_validateGeneration. Otherwise it runs_generateHubAndSpokefollowed by_fillTerrainin legacy mode.WorldGenerator.placeEvents(world, eventPool)— grid-walksCELL_W = 500byCELL_H = 350cells, skipping ~34% of cells, rejecting points outsidelevelRadius * 0.95or within 700px of spawn. Requires the candidate to sit inside a hub clear radius (scaled by 0.7) or withinspoke.width * 0.35of a spoke. Enforces a 400px edge-to-edge minimum between events, applies a +/- 20% radius jitter, and deletes terrain insideevent.radius + 80. A second sweep drops any event withinradius_i + radius_j + 150. Then per non-start hub: ~45% chance to add a sub-event (with the same terrain clear pass) and 20-50 candidate crate slots in a 700px ring, though crate spawning is currently a no-op.WorldGenerator.getEnemySpawnPos(world)— picks a random spoke, samples a point along its segment with random perpendicular offset withinwidth * 0.6. Falls back to a 2000x2000 random box centered on the origin when there are no spokes.WorldGenerator.expandTerrain(world, playerX, playerY)— per-frame infinite-world hook. Generates 800px super-chunks within 2200px of the player using a deterministic per-chunk seed derived fromworld.seed,scx * 73856093, andscy * 19349663. Skips chunks intersecting hubs (clearR + SC * 0.7), spokes (spoke.width * 0.5 + SC * 0.5), or events (event.radius + 80 + SC * 0.7). Drops 3% of remaining chunks. Each survivor spawns a center piece, 3-5 mediums, and 4-8 smalls. After expansion, GC removes any terrain or floater beyond 3000px (squared) of the player and unmarks the chunk so it can regenerate on return. CallsdeduplicateTerrainandbuildTerrainChunkswhenever the terrain set changes.WorldGenerator.reset(world)— clears terrain, structures, hubs, spokes, floaters, and_generatedChunks.buildTerrainChunks(world)— exported. Walksworld.terrain, computes the AABB of each piece’s bounding circle againstCHUNK_SIZE = 1024, and writes the resulting key-to-index map ontoworld.chunks._generateHubAndSpoke(world, biome)— legacy ring-based hub layout. Central START hub at origin withclearRadius600 andradius300, then rings at distances stepping byhubMinDist..hubMaxDistwith +/- 30% jitter and +/- 40% angular jitter, untillevelRadius * 0.95. Connects center to ring 1, each outer-ring hub to its nearest inner-ring hub, and adjacent same-ring hubs (cross-spoke width scaled by 0.75). Populatesworld.locationsfrom non-start hubs witheventSlots: 2andinfluence = max(clearRadius * 1.2, 2000)._fillTerrain(world, biome, levelData?)— four-phase fill. PHASE A: cluster grid usingclusterStep = 120 + (1 - intensity) * 780, density-driven skip1 - intensity,OVERLAP_PAD = 75 * max(0, 1 - intensity * 1.2), weighted picks fromactiveMix.centers, then 0-4 mediums in a ring atcR + 40..90, then 0-6 smalls atcR + 100..180. In zone-aware mode, every candidate goes throughisInWildsagainstlevelData.zoneGrid. PHASE B: carve hubs (withinclearR + tBR * 0.5 + 30) and spokes (withinspoke.width * 0.5 + tBR * 0.3 + 20) in legacy mode; skipped in zone-aware mode. PHASE C: removed (no border wall). PHASE D: floaters disabled; always assignsworld.floaters = []. Closes withdeduplicateTerrainand twobuildTerrainChunkscalls._fillCityGrid(world, biome)— city layout forsunrise_city.BLOCK = 1000interior,ROAD_W = 200,CELL = 1200. Phase 1 places buildings on a 500px sub-grid inside each block, 10% skip, with a 30% footprint overlap check and a road-corridor rejection. Phase 2 registers vertical and horizontal roads as full-length spokes of width 200. Phase 3 reserves a crate-seeding loop that is currently a no-op. Writes a single start hub at origin withclearRadius = 600._validateGeneration(world, levelData)— counts terrain pieces whose bounding radius intersects aLevelDatahub zone and warns; never blocks generation.
Pattern notes
- Deterministic seeding: every entry point derives its RNG from
mulberry32(world.seed + offset). Offsets are fixed per phase: 5555 for hub placement and main event placement, 7778 for terrain fill and city grid, 8888 for crate seed lines, 6666 for sub-events. Dynamic expansion uses a chunk-keyed hash for independent per-super-chunk seeding. - Two code paths through
generate: zone-aware (usesLevelData, skips PHASE B carving, drives terrain viaLevelConfig.terrainTypemapped throughTERRAIN_MIX_MAP) and legacy (usesBiomeConfigknobs and carves against generated hubs and spokes). Thesunrise_citybiome short-circuits both into_fillCityGrid. - Terrain shape is decoupled from collision shape.
makeTerrainPiecereturns empty vertex arrays and usesbaseRadius * scaleas a placeholder bounding radius and_worldR; the real pixel-traced polygon is filled in later by the renderer. - Building and pillar shapes are treated as rectangular for spacing math (30% footprint factor in
deduplicateTerrainand_fillCityGridoverlap checks) even though theirboundingRadiusis the inscribing circle. - The intensity knob is the single dial that scales every density-related quantity in PHASE A: cluster skip rate, cluster step, overlap padding, medium count, and small count. Intensity of 0 produces an empty world; near 1 packs the field into a near-solid wall. Zone-aware mode reads intensity from
LevelConfig.terrainDensityand overrides the biome default. - Infinite world:
_generatedChunksis a process-globalSet<string>keyedsc_<scx>_<scy>. GC removes terrain and floaters beyond 3000px (squared) of the player and removes the chunk key so the area regenerates if the player returns. This bounds frame cost without losing determinism. WorldGeneratoris exported as a plain object literal sothis._SUPER_CHUNKandthis._generatedChunkssurvive across calls; methods callWorldGenerator._validateGenerationdirectly to avoidthis-binding issues ingenerate.placeEventsrebuilds the chunk index after deleting overlapped terrain, both for the main event pass and for the sub-event pass per hub. Sub-events guarantee accessibility by clearing terrain withinradius + 80, matching the main-event behavior.- Crate creation has been removed throughout (city seed, sub-event seed, expansion seed). Loops remain in place but their spawn bodies are gone; the crate pool in
world/crates.tsowns spawning relative to the player.