PURPOSE

Pure-math, framework-agnostic deterministic generator for the hub-and-spoke terrain topology used by world biomes. Divides the infinite 2D plane into square chunks; given the same seed and chunk coordinates, the same hubs (anchor zones) and spokes (connecting paths) are produced. Implements the Voidstar Algorithm Spec v1.0: four pattern types (grid, zigzag, rings, chaotic) with two spoke algorithms (orthogonal for grid, maximal planar for the rest).

OWNS

  • Seeded PRNG primitives: mulberry32(seed) (fast 32-bit hashing PRNG returning 0-1 floats) and chunkRng(globalSeed, cx, cy) (per-chunk RNG derived from global seed XOR chunk coords with large prime multipliers).
  • Chunk-size derivation via calcChunkSize(config) — maps pattern knobs (blockSize, stepSize, ringCount, ringRadius, density) to a chunk side length between roughly 1500 and 4000 world pixels.
  • Four pattern-specific raw-hub generators:
    • genHubs_grid — one centered hub per chunk with slight jitter.
    • genHubs_zigzag — one hub per chunk, side alternates by chunk row parity, amplitude controls offset.
    • genHubs_rings — center hub plus one to three concentric rings; ring hubs spaced by minimum-distance check.
    • genHubs_chaotic — Poisson-disc-like rejection sampling, one to four hubs per chunk, falls back to one centered hub if all attempts fail.
  • Pattern router genHubsForPattern that resolves hubR from hubRMin/hubRMax/hubSize and dispatches.
  • Spoke construction:
    • buildSpokes_planar — maximal planar graph; sorts candidate pairs by distance, accepts shortest non-crossing edges using a CCW orientation test and proper segment-intersection check. Skips pairs that share a hub when testing crossings. Used by zigzag, rings, chaotic.
    • buildSpokes_grid — only accepts near-horizontal or near-vertical pairs within a tolerance band.
    • Both gated by maxDist = cSize * 1.8 and a minimum hub-radius separation.
  • The RawHub interface (local), the GenerateResult exported interface, and three public entry points: generateHubsAndSpokes, generateForViewport, precomputeWorld.

READS FROM

  • ../../data/level-config — type-only imports for LevelConfig, Hub, Spoke. LevelConfig.pattern, seed, hubRMin, hubRMax, hubSize, blockSize, stepSize, ringCount, ringRadius, amplitude, density are consumed.
  • ./warp-puddles — uses spawnPuddlesForHub to spawn one to two puddles per hub (reusing the same chunk RNG for determinism) and groupPuddles to merge overlapping puddles into union-find groups; exposes WarpPuddle and WarpPuddleGroup types via GenerateResult.

PUSHES TO

Nothing directly — module is a pure function library. The returned GenerateResult is consumed by callers (see Entry points). The shape contains:

  • hubs: Hub[] — flat list with x, y, r, id (formatted ${cx},${cy}:${i}), chunkKey.
  • spokes: Spoke[] — pairs of hub indices { a, b } into hubs.
  • chunkSize: number — derived side length used for the run.
  • chunkHubIndices: Map<string, number[]> — chunk-key ("cx,cy") to indices into hubs.
  • warpPuddles: WarpPuddle[] — flat list across the level (sparse; many hubs spawn none).
  • warpPuddleGroups: WarpPuddleGroup[] — overlap-merged groups for rendering and collision.

DOES NOT

  • Does not perform rendering, collision, spatial queries, or any per-frame update.
  • Does not touch DOM, canvas, Pixi, audio, telemetry, Supabase, or any framework.
  • Does not maintain global state across calls; results are returned, not cached internally.
  • Does not stream chunks at runtime — all chunks in the requested range are generated in one pass.
  • Does not validate LevelConfig; bad input produces NaN or empty results without error.
  • Does not place enemies, props, crates, XP orbs, or biome decorations; only abstract hub circles, spoke edges, and warp puddles.
  • Does not enforce reachability between hubs beyond what the spoke algorithms naturally produce; isolated hubs are possible if maxDist excludes all neighbours.

Signals

This module emits no events and exposes no observers. All output is the synchronous return value of the three entry points. Determinism is the public contract: identical (config.seed, cx, cy) produces an identical RawHub[] and an identical puddle list for that hub.

Entry points

  • generateHubsAndSpokes(config, minCX, maxCX, minCY, maxCY) — core function. Iterates the inclusive chunk-coord rectangle, runs the pattern’s hub generator with a per-chunk RNG, builds spokes across the combined hub list, merges warp puddles into groups, returns the full GenerateResult.
  • generateForViewport(config, centerX, centerY, viewW, viewH, padChunks) — convenience wrapper that converts a world-space viewport plus chunk padding into chunk-coord bounds before calling generateHubsAndSpokes.
  • precomputeWorld(config, spawnX, spawnY, gridRadius = 10) — primary entry used at level load. Computes the centre chunk from spawn coordinates and generates a symmetric (2 * gridRadius + 1) square grid of chunks (default 21 by 21). Imported and called by chunk-manager.ts.
  • mulberry32(seed) and chunkRng(globalSeed, cx, cy) — exported for reuse by other deterministic systems that need to align with this generator’s seeding scheme.
  • calcChunkSize(config) — exported for callers that need the derived chunk side without running full generation.

Pattern notes

  • Determinism is anchored at the chunk level by chunkRng, which mixes the global seed with chunk coords via three large primes (73856093, 19349663, 83492791) before seeding mulberry32. Hub placement, hub radius jitter, ring rotation offset, chaotic placement attempts, and the warp-puddle spawn for each hub all share the same chunk RNG, so changing one early read shifts everything downstream within that chunk — but only that chunk.
  • The hubR cap of cSize * 0.35 in grid and zigzag prevents the requested radius from overflowing a single chunk regardless of hubSize knob.
  • Rings always emits a centre hub (slightly larger at hubR * 1.2) plus ring hubs; ring count and outer radius are derived from cSize proportions, not the absolute ringRadius knob used in calcChunkSize. Ring hubs are rejected if they overlap any prior hub within hubR * 2.5.
  • Chaotic gives up after 80 placement attempts per chunk and guarantees at least one fallback hub at chunk centre.
  • Spoke generation runs across the global hub list, not per-chunk, so spokes can span chunk boundaries naturally. The maxDist = cSize * 1.8 ceiling keeps spokes reasonably local.
  • The planar spoke builder is greedy (shortest first) rather than computing a true Delaunay triangulation; the same-hub skip in the crossing check is a correctness optimisation since edges sharing an endpoint cannot properly cross.
  • Grid spokes use tolerance = cSize * 0.4 to allow modest jitter while still classifying a pair as horizontal or vertical; pairs that are neither are dropped, producing a sparse axis-aligned graph.
  • The CCW test uses strict inequality, so collinear or touching segments are treated as non-crossing — this lets shared-endpoint edges always coexist but may admit rare degenerate overlaps if hubs are exactly aligned.
  • Hub IDs (${cx},${cy}:${i}) are stable across runs with the same seed and are reused as the keying basis for warp-puddle spawn, so puddles persist their identity across regenerations of the same chunk range.
  • All three entry points re-run the full generator; there is no incremental update path. Callers wanting streamed expansion must call generateHubsAndSpokes with a wider range.