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

  • TerrainPiece and FloaterPiece interfaces — position, shape id, scale, rotation, local and world-space verts, edge normals, bounding radius, spatial chunk key, color, and alpha.
  • TerrainMix and TerrainMixEntry interfaces — weighted shape pools split into centers, mediums, and smalls rings per biome.
  • BiomeConfig interface plus the four named mix presets MIX_BUILDINGS, MIX_ASTEROIDS, MIX_VOIDSTAR, and MIX_PILLARS.
  • The BIOMES registry: landing_site, sunrise_city, the_voidstar, and obsidian_spire. Each biome owns levelRadius, terrainDensity, terrainIntensity, terrainMix, hub topology knobs (hubMinDist, hubMaxDist, hubsPerRing, hubClearMin, hubClearMax, spokeWidthMin, spokeWidthMax), and pools for terrain, enemies, and events.
  • CHUNK_SIZE = 1024 spatial bucket size for the broad-phase collision index.
  • The WorldGenerator object — entry points for generate, _generateHubAndSpoke, _fillTerrain, _fillCityGrid, _validateGeneration, placeEvents, getEnemySpawnPos, expandTerrain, and reset, plus the _SUPER_CHUNK size and _generatedChunks set used by infinite-world expansion.
  • buildTerrainChunks — exported helper that rebuilds world.chunks after any change to world.terrain.
  • Vertex generation (genVerts produces a 12-vertex jittered polygon at +/- 4% radius), bounding-radius computation, terrain dedup (deduplicateTerrain culls later pieces within 2x the earlier piece’s radius, using a 0.30 footprint factor for buildings and pillars), and pointToSegDist for spoke geometry.

READS FROM

  • ../core/typesWorldState shape.
  • ../core/utilsmulberry32 seeded RNG.
  • ../../data/terrain-shapesTERRAIN_SHAPES table and TerrainShapeDef for baseRadius, scaleMin, scaleMax, and type (polygon or sprite).
  • ../../data/level-configLevelConfig type for the zone-aware path.
  • ./chunk-managerLevelData (zone grid plus precomputed hubs and spokes).
  • ./zone-classifierlookupZone is imported.
  • ./eventscreateEvent, EventType, and GameEvent for event placement.

PUSHES TO

  • WorldState.terrain, WorldState.structures, WorldState.hubs, WorldState.spokes, WorldState.locations, WorldState.events, WorldState.chunks, WorldState.biomeId, WorldState.levelRadius, and world.floaters (currently always cleared to []).
  • world.terrain[i].chunkKey and per-piece _worldR, _origX, _origY private fields for downstream rendering and silhouette updates.
  • Console warnings via console.warn from _validateGeneration when 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 updateTerrainCollisionShape in draw-3d-terrain.ts; this module only seeds an approximate bounding radius and empty verts / worldVerts / normals arrays.
  • Does not spawn enemies, the player, projectiles, drops, or crates. getEnemySpawnPos only returns a candidate point. Crate spawning calls are explicitly removed (the crate pool in world/crates.ts owns lifecycle).
  • Does not build the zone grid or hub-and-spoke topology in the zone-aware path; those are pre-baked in LevelData.generation and LevelData.zoneGrid by chunk-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 floaters is always set to [].
  • Does not emit gameplay events, telemetry, or Supabase writes.

Signals

  • console.warn from _validateGeneration reporting the count of terrain pieces whose bounding radius overlaps a hub zone edge.
  • No other emitted events. Game events are constructed via createEvent and returned through placeEvents; consumers wire them in.

Entry points

  • WorldGenerator.generate(world, biomeId = 'landing_site', levelData?) — top-level call. Resets terrain, structures, hubs, and spokes. For sunrise_city it runs _fillCityGrid. With levelData it copies hubs and spokes from levelData.generation and runs _fillTerrain in zone-aware mode, then _validateGeneration. Otherwise it runs _generateHubAndSpoke followed by _fillTerrain in legacy mode.
  • WorldGenerator.placeEvents(world, eventPool) — grid-walks CELL_W = 500 by CELL_H = 350 cells, skipping ~34% of cells, rejecting points outside levelRadius * 0.95 or within 700px of spawn. Requires the candidate to sit inside a hub clear radius (scaled by 0.7) or within spoke.width * 0.35 of a spoke. Enforces a 400px edge-to-edge minimum between events, applies a +/- 20% radius jitter, and deletes terrain inside event.radius + 80. A second sweep drops any event within radius_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 within width * 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 from world.seed, scx * 73856093, and scy * 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. Calls deduplicateTerrain and buildTerrainChunks whenever the terrain set changes.
  • WorldGenerator.reset(world) — clears terrain, structures, hubs, spokes, floaters, and _generatedChunks.
  • buildTerrainChunks(world) — exported. Walks world.terrain, computes the AABB of each piece’s bounding circle against CHUNK_SIZE = 1024, and writes the resulting key-to-index map onto world.chunks.
  • _generateHubAndSpoke(world, biome) — legacy ring-based hub layout. Central START hub at origin with clearRadius 600 and radius 300, then rings at distances stepping by hubMinDist..hubMaxDist with +/- 30% jitter and +/- 40% angular jitter, until levelRadius * 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). Populates world.locations from non-start hubs with eventSlots: 2 and influence = max(clearRadius * 1.2, 2000).
  • _fillTerrain(world, biome, levelData?) — four-phase fill. PHASE A: cluster grid using clusterStep = 120 + (1 - intensity) * 780, density-driven skip 1 - intensity, OVERLAP_PAD = 75 * max(0, 1 - intensity * 1.2), weighted picks from activeMix.centers, then 0-4 mediums in a ring at cR + 40..90, then 0-6 smalls at cR + 100..180. In zone-aware mode, every candidate goes through isInWilds against levelData.zoneGrid. PHASE B: carve hubs (within clearR + tBR * 0.5 + 30) and spokes (within spoke.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 assigns world.floaters = []. Closes with deduplicateTerrain and two buildTerrainChunks calls.
  • _fillCityGrid(world, biome) — city layout for sunrise_city. BLOCK = 1000 interior, 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 with clearRadius = 600.
  • _validateGeneration(world, levelData) — counts terrain pieces whose bounding radius intersects a LevelData hub 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 (uses LevelData, skips PHASE B carving, drives terrain via LevelConfig.terrainType mapped through TERRAIN_MIX_MAP) and legacy (uses BiomeConfig knobs and carves against generated hubs and spokes). The sunrise_city biome short-circuits both into _fillCityGrid.
  • Terrain shape is decoupled from collision shape. makeTerrainPiece returns empty vertex arrays and uses baseRadius * scale as 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 deduplicateTerrain and _fillCityGrid overlap checks) even though their boundingRadius is 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.terrainDensity and overrides the biome default.
  • Infinite world: _generatedChunks is a process-global Set<string> keyed sc_<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.
  • WorldGenerator is exported as a plain object literal so this._SUPER_CHUNK and this._generatedChunks survive across calls; methods call WorldGenerator._validateGeneration directly to avoid this-binding issues in generate.
  • placeEvents rebuilds 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 within radius + 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.ts owns spawning relative to the player.