#74: RESEARCH_LAB_CHANCE raised from 0.08 to 0.16 - research points gate mid/late skill-tree nodes and vessel/lab research now (#63), so 0.08 was too scarce for how central they'd become to progression. #75: an extractor placed on a research_lab planet would sit on an empty resource id forever, since it yields nothing by design. Refused in BuildController's placement confirm, with the same "explain the refusal" idiom every other build gate uses. register_capture_points() gained an optional definition_id parameter so the refusal/reward logic can tell a research_lab planet apart without threading an extra argument through every existing caller. #76: conquering a research_lab awarded a research point with no on-screen acknowledgement beyond a HUD number the player has no reason to be watching at that exact moment. CaptureLabel's existing "CONQUERED" flash now names the reward directly on a research_lab. test_extractor_building.gd and test_planet_conquest_gate.gd both use a "first planet chunk found" scan with no regard for definition_id; in this world's fixed seed that chunk happens to be a research_lab, so #75's new gate started refusing their otherwise-unrelated extractor placement checks. Both now skip a research_lab chunk when picking their target, same as test_research_lab_extractor_gate.gd does on purpose for its own dedicated coverage of that refusal.
130 lines
5.8 KiB
GDScript
130 lines
5.8 KiB
GDScript
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 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
|
|
|
|
# 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()
|
|
# 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()
|
|
# 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
|
|
# 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
|
|
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)
|