extends RefCounted const LANDSCAPE := preload("res://scripts/planets/surface_landscape.gd") const GRID := 96 const CELL := 40.0 const WORLD_SIZE := GRID * CELL const EXTRA_STRUCTURE_CHANCE := 0.25 static func generate(seed_value: int, profile: Dictionary) -> Dictionary: var rng := RandomNumberGenerator.new() rng.seed = seed_value var landscape := LANDSCAPE.choose(seed_value,profile) var noise := FastNoiseLite.new() noise.seed = seed_value noise.frequency = 0.0008 + posmod(seed_value,8)*0.0001 noise.fractal_octaves = 4 var humidity := FastNoiseLite.new() humidity.seed = seed_value ^ 0x464f5245 humidity.frequency = 0.003 var heights := PackedFloat32Array() heights.resize(GRID * GRID) for y in GRID: for x in GRID: var point := Vector2(x, y) * CELL - Vector2.ONE * WORLD_SIZE * 0.5 var h := clampf(0.5 + noise.get_noise_2d(point.x, point.y) * 0.6, 0.0, 1.0) if int(landscape.index) == 2: h = 0.35 + noise.get_noise_2d(point.x,point.y)*0.45 elif int(landscape.index) == 3: h = 0.43 + absf(noise.get_noise_2d(point.x,point.y))*0.95 if point.length() < 200: h = lerpf(0.5, h, smoothstep(110, 200, point.length())) heights[y * GRID + x] = h var trees: Array[Dictionary] = [] var mountains: Array[Dictionary] = [] var rocks: Array[Dictionary] = [] for y in range(2, GRID - 2, 2): for x in range(2, GRID - 2, 2): var point := Vector2(x, y) * CELL - Vector2.ONE * WORLD_SIZE * 0.5 + Vector2(rng.randf_range(-25, 25), rng.randf_range(-25, 25)) var h: float = heights[y * GRID + x] if point.length() < 190: continue if h > 0.66 and rng.randf() < 0.65: mountains.append({"position": point, "size": rng.randf_range(110, 210), "rotation": rng.randf_range(-PI, PI)}) elif h > 0.28 and h < 0.65 and humidity.get_noise_2d(point.x, point.y) > -0.12 and rng.randf() < float(landscape.density): trees.append({"position": point, "size": rng.randf_range(52, 92), "rotation": rng.randf_range(-PI, PI), "shade": rng.randf_range(0.75, 1.1)}) for index in 180: var point := Vector2(rng.randf_range(-1750,1750),rng.randf_range(-1750,1750)) var gx := clampi(int((point.x+1920)/CELL),0,GRID-1) var gy := clampi(int((point.y+1920)/CELL),0,GRID-1) if point.length() > 200 and heights[gy*GRID+gx] > 0.30: rocks.append({"position":point,"size":rng.randf_range(14,45),"rotation":rng.randf_range(-PI,PI)}) var pois: Array[Dictionary] = [] var kinds: Array[String] = ["warehouse","ruins","deposit","deposit","village","cave"] if rng.randf() < EXTRA_STRUCTURE_CHANCE: kinds.append("ruins") for index in kinds.size(): var kind: String = kinds[index] var chosen := Vector2.ZERO var found := false for attempt in 1500: var x := rng.randi_range(5, GRID - 6) var y := rng.randi_range(5, GRID - 6) var h: float = heights[y * GRID + x] var point := Vector2(x, y) * CELL - Vector2.ONE * WORLD_SIZE * 0.5 if h < 0.29 or h > 0.71 or point.length() < 400: continue var clear := true for other in pois: if point.distance_to(other.position) < 420: clear = false if clear: chosen = point found = true break if not found: # A lander can cross every terrain; a remote fallback never traps it. chosen = Vector2.from_angle(index * TAU / kinds.size()) * 1250 pois.append({"id": index, "kind": kind, "position": chosen, "stage": 0, "complete": false, "discovered": false}) _attach_caves(pois,mountains) # Clear vegetation around explorable structures so entrances stay readable. for poi in pois: trees = trees.filter(func(prop): return prop.position.distance_to(poi.position) > 100) mountains = mountains.filter(func(prop): return bool(prop.get("cave_anchor",false)) or prop.position.distance_to(poi.position) > 120) rocks = rocks.filter(func(prop): return prop.position.distance_to(poi.position) > 65) var data := {"seed":seed_value,"heights":heights,"height_texture":LANDSCAPE.height_texture(heights),"landscape":landscape,"rocks":rocks,"trees":trees,"mountains":mountains,"pois":pois,"profile":profile.duplicate(true)} preload("res://scripts/planets/surface_scenery.gd").generate(data,rng) preload("res://scripts/planets/surface_landmarks.gd").generate(data) preload("res://scripts/planets/surface_details.gd").generate(data) return data static func _attach_caves(pois: Array[Dictionary], mountains: Array[Dictionary]) -> void: for poi in pois: if poi.kind != "cave": continue var best := -1 var distance := INF for index in mountains.size(): var entrance: Vector2 = mountains[index].position+Vector2(0,86) if absf(entrance.x) > 1700 or absf(entrance.y) > 1700 or entrance.length() < 400: continue var clear := true for other in pois: if other.id != poi.id and entrance.distance_to(other.position) < 420: clear = false if clear and entrance.distance_squared_to(poi.position) < distance: best = index distance = entrance.distance_squared_to(poi.position) if best < 0: mountains.append({"position":poi.position-Vector2(0,86),"size":285.0,"rotation":0.0,"cave_anchor":true}) best = mountains.size()-1 mountains[best].cave_anchor = true mountains[best].rotation = 0.0 mountains[best].size = 285.0 poi.position = mountains[best].position+Vector2(0,86) poi.mountain_position = mountains[best].position static func terrain_texture(data: Dictionary) -> ImageTexture: # Only the minimap is baked. The world uses full-resolution GPU terrain shading. var image := Image.create(256,256,false,Image.FORMAT_RGB8) for y in 256: for x in 256: var gx := clampi(int(float(x)/256*GRID),0,GRID-1) var gy := clampi(int(float(y)/256*GRID),0,GRID-1) image.set_pixel(x,y,LANDSCAPE.color_at(data.heights[gy*GRID+gx],0.0,data.landscape)) return ImageTexture.create_from_image(image)