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) andchunkRng(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
genHubsForPatternthat resolveshubRfromhubRMin/hubRMax/hubSizeand 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.8and a minimum hub-radius separation.
- The
RawHubinterface (local), theGenerateResultexported interface, and three public entry points:generateHubsAndSpokes,generateForViewport,precomputeWorld.
READS FROM
../../data/level-config— type-only imports forLevelConfig,Hub,Spoke.LevelConfig.pattern,seed,hubRMin,hubRMax,hubSize,blockSize,stepSize,ringCount,ringRadius,amplitude,densityare consumed../warp-puddles— usesspawnPuddlesForHubto spawn one to two puddles per hub (reusing the same chunk RNG for determinism) andgroupPuddlesto merge overlapping puddles into union-find groups; exposesWarpPuddleandWarpPuddleGrouptypes viaGenerateResult.
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 withx,y,r,id(formatted${cx},${cy}:${i}),chunkKey.spokes: Spoke[]— pairs of hub indices{ a, b }intohubs.chunkSize: number— derived side length used for the run.chunkHubIndices: Map<string, number[]>— chunk-key ("cx,cy") to indices intohubs.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
maxDistexcludes 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 fullGenerateResult.generateForViewport(config, centerX, centerY, viewW, viewH, padChunks)— convenience wrapper that converts a world-space viewport plus chunk padding into chunk-coord bounds before callinggenerateHubsAndSpokes.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 bychunk-manager.ts.mulberry32(seed)andchunkRng(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 seedingmulberry32. 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
hubRcap ofcSize * 0.35in grid and zigzag prevents the requested radius from overflowing a single chunk regardless ofhubSizeknob. - Rings always emits a centre hub (slightly larger at
hubR * 1.2) plus ring hubs; ring count and outer radius are derived fromcSizeproportions, not the absoluteringRadiusknob used incalcChunkSize. Ring hubs are rejected if they overlap any prior hub withinhubR * 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.8ceiling 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.4to 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
generateHubsAndSpokeswith a wider range.