class_name WorldGen extends RefCounted const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") # Deterministic procedural world generation: the world is divided into # square chunks, and each chunk's biome is derived from a seeded # low-frequency noise field sampled at the chunk's own coordinates - so # revisiting the same chunk always yields the same biome, and biomes form # smooth multi-chunk regions instead of a per-chunk random checkerboard. enum Biome { SPACE, ASTEROID_FIELD } const CHUNK_SIZE: float = 1200.0 const NOISE_SEED: int = 1337 const ASTEROID_FIELD_THRESHOLD: float = 0.15 const PLANET_THRESHOLD: float = 0.33 const GAS_GIANT_THRESHOLD: float = 0.55 # A predictable early gas source for play-testing. The origin chunk is # deterministically normal space. Its explicit in-chunk position puts it # roughly 1,080 world units from a fresh fleet: easy to reach, but not # sitting on the spawn. const NEARBY_GAS_GIANT_CHUNK: Vector2i = Vector2i.ZERO const NEARBY_GAS_GIANT_LOCAL_POS: Vector2 = Vector2(900.0, 600.0) static var _noise: FastNoiseLite static var _planet_noise: FastNoiseLite static func _get_noise() -> FastNoiseLite: if _noise == null: _noise = FastNoiseLite.new() _noise.seed = NOISE_SEED _noise.frequency = 0.06 return _noise static func _get_planet_noise() -> FastNoiseLite: if _planet_noise == null: _planet_noise = FastNoiseLite.new() _planet_noise.seed = NOISE_SEED + 777 # Higher frequency than the biome noise so it spikes on individual # chunks rather than forming its own smooth multi-chunk regions - # "once or twice per asteroid biome [region]" needs a sparse, mostly # independent chance per chunk, not another smooth blob. _planet_noise.frequency = 0.35 return _planet_noise static func chunk_coords(world_pos: Vector2) -> Vector2i: return Vector2i(int(floor(world_pos.x / CHUNK_SIZE)), int(floor(world_pos.y / CHUNK_SIZE))) static func chunk_origin(chunk: Vector2i) -> Vector2: return Vector2(chunk.x * CHUNK_SIZE, chunk.y * CHUNK_SIZE) static func biome_at_chunk(chunk: Vector2i) -> int: var n: float = _get_noise().get_noise_2d(float(chunk.x), float(chunk.y)) if n > ASTEROID_FIELD_THRESHOLD: return Biome.ASTEROID_FIELD return Biome.SPACE static func has_planet_at_chunk(chunk: Vector2i) -> bool: if chunk == NEARBY_GAS_GIANT_CHUNK: return true var n: float = _get_planet_noise().get_noise_2d(float(chunk.x), float(chunk.y)) return (biome_at_chunk(chunk) == Biome.ASTEROID_FIELD and n > PLANET_THRESHOLD) or (biome_at_chunk(chunk) == Biome.SPACE and n > GAS_GIANT_THRESHOLD) static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: # Single source of truth for a planet's identity within its chunk - # seeded purely from the chunk's own coordinates (same pattern as the # biome/planet noise fields above) so it's reproducible from anywhere, # without needing an actual spawned Planet node. Both PlanetField (to # actually spawn the planet) and the world map (to draw a marker for # an explored-but-not-currently-loaded chunk) call this, so the two # can never drift out of sync with each other. var rng: RandomNumberGenerator = RandomNumberGenerator.new() rng.seed = hash(chunk) var guaranteed_nearby_giant: bool = chunk == NEARBY_GAS_GIANT_CHUNK var local_pos: Vector2 = NEARBY_GAS_GIANT_LOCAL_POS if guaranteed_nearby_giant \ else Vector2(rng.randf() * CHUNK_SIZE, rng.randf() * CHUNK_SIZE) var shape_seed: int = rng.randi() # A stable per-chunk roll makes gas giants persistent landmarks rather # than a different resource whenever the scene reloads. var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE var definition_id: String = PLANET_DEFINITIONS.GAS_GIANT_ID if gas_giant else PLANET_DEFINITIONS.ROCKY_ID var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) var names: PackedStringArray = definition.names var planet_name: String = names[rng.randi() % names.size()] var planet_id: String = "chunk:%d:%d" % [chunk.x, chunk.y] return {"planet_id": planet_id, "local_pos": local_pos, "name": planet_name, "shape_seed": shape_seed, "definition_id": definition_id} static func biome_color(biome: int) -> Color: match biome: Biome.ASTEROID_FIELD: return Color(0.55, 0.36, 0.18, 1.0) _: return Color(0.10, 0.13, 0.28, 1.0)