spaceship/WorldGen.gd

130 lines
5.8 KiB
GDScript3
Raw Normal View History

class_name WorldGen
extends RefCounted
2026-08-14 16:13:24 +02:00
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
2026-08-13 22:04:21 +02:00
const GAS_GIANT_THRESHOLD: float = 0.55
# A flat per-planet-chunk probability rather than another noise field: gas
# giants already cover every SPACE-biome planet chunk (see GAS_GIANT_THRESHOLD
# above), so making black holes noticeably rarer just needs a small, uniform
# chance independent of biome - not a second smooth region to tune against
# the first.
const BLACK_HOLE_CHANCE: float = 0.05
# #63/#74: findable without excessive exploration - research points gate
# mid/late skill-tree nodes and vessel research, so a player needs to run
# into one of these reasonably often, not just as a rare curiosity. Raised
# from 0.08 after playtesting found that rate too scarce for how central
# research points had become to progression.
const RESEARCH_LAB_CHANCE: float = 0.16
2026-08-14 12:07:50 +02:00
# 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:
2026-08-14 12:07:50 +02:00
if chunk == NEARBY_GAS_GIANT_CHUNK:
return true
var n: float = _get_planet_noise().get_noise_2d(float(chunk.x), float(chunk.y))
2026-08-13 22:04:21 +02:00
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)
2026-08-14 12:07:50 +02:00
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()
# Drawn unconditionally, in the same fixed position every call,
# regardless of biome or the gas-giant roll below - so adding this
# roll doesn't shift which random values any LATER draw (planet_name,
# below) consumes for chunks that would have existed before it.
var black_hole_roll: float = rng.randf()
# Same principle, its own independent roll (#63).
var research_lab_roll: float = rng.randf()
2026-08-13 21:07:35 +02:00
# A stable per-chunk roll makes gas giants persistent landmarks rather
# than a different resource whenever the scene reloads.
2026-08-14 12:07:50 +02:00
var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE
2026-08-14 16:13:24 +02:00
var definition_id: String = PLANET_DEFINITIONS.GAS_GIANT_ID if gas_giant else PLANET_DEFINITIONS.ROCKY_ID
# A rare third outcome, seeded the same deterministic way as everything
# else here. Never overrides the guaranteed early gas giant landmark -
# that chunk's whole purpose is to always be the same reachable source.
if not guaranteed_nearby_giant and black_hole_roll < BLACK_HOLE_CHANCE:
definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID
elif not guaranteed_nearby_giant and research_lab_roll < RESEARCH_LAB_CHANCE:
definition_id = PLANET_DEFINITIONS.RESEARCH_LAB_ID
2026-08-14 16:13:24 +02:00
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,
2026-08-14 16:13:24 +02:00
"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)