Randoloot/scripts/planets/surface_details.gd
2026-10-18 08:43:54 +02:00

75 lines
3.5 KiB
GDScript

extends RefCounted
## Decorative only: its own random stream keeps POI, loot and events unchanged.
const ATLAS_PATH := "res://assets/planets/nature/nature_atlas.png"
const TREE_CELLS := [[0, 1, 2], [1, 2], [1, 2], [0, 2, 3], [3], [0, 3]]
const DETAIL_CELLS := [[7, 8, 9, 10, 13, 14], [7, 8, 9, 10, 13, 14], [6, 8, 9, 14, 15], [4, 5, 7, 11], [4, 5, 7, 11], [4, 5, 6, 7, 11]]
static func generate(data: Dictionary) -> void:
var rng := RandomNumberGenerator.new()
rng.seed = int(data.seed) ^ 0x4e415455
var biome := int(data.landscape.index)
var trees: Array = TREE_CELLS[biome]
for tree in data.trees:
# Retain some of the original canopy alongside the new species.
tree.variant = -1 if rng.randf() < 0.35 else int(trees[rng.randi_range(0, trees.size()-1)])
for rock in data.rocks:
rock.variant = rng.randi_range(4, 6)
data.details = []
var kinds: Array = DETAIL_CELLS[biome]
# Local clusters read as vegetation patches, rather than a uniform scatter.
for index in 150:
var anchor := Vector2(rng.randf_range(-1760, 1760), rng.randf_range(-1760, 1760))
var cell := int(kinds[rng.randi_range(0, kinds.size()-1)])
for member in rng.randi_range(2, 5):
var point := anchor + Vector2.from_angle(rng.randf()*TAU)*rng.randf_range(0, 65)
var width := rng.randf_range(16, 32)
if cell == 4 or cell == 5 or cell == 6:
width = rng.randf_range(24, 43)
elif cell == 8 or cell == 9 or cell == 15:
width = rng.randf_range(26, 44)
if not _clear_site(data, point, width*0.5):
continue
if cell == 15 and _height(data, point) > 0.39:
continue
data.details.append({"position":point,"size":width,"rotation":rng.randf_range(-PI, PI),"variant":cell,"shade":rng.randf_range(0.88, 1.08)})
# At most one fallen tree, only on 8% of forested landing maps.
if biome < 4 and rng.randf() < 0.08:
for attempt in 80:
if data.trees.is_empty():
break
var tree: Dictionary = data.trees[rng.randi_range(0, data.trees.size()-1)]
var point: Vector2 = tree.position + Vector2.from_angle(rng.randf()*TAU)*rng.randf_range(70, 110)
if _clear_site(data, point, 65):
data.details.append({"position":point,"size":92.0,"rotation":rng.randf_range(-PI, PI),"variant":12,"shade":1.0})
break
static func _height(data: Dictionary, point: Vector2) -> float:
var x := clampi(int((point.x+1920)/40), 0, 95)
var y := clampi(int((point.y+1920)/40), 0, 95)
return float(data.heights[y*96+x])
static func _clear_site(data: Dictionary, point: Vector2, radius: float) -> bool:
if point.length() < 210+radius or absf(point.x)+radius > 1860 or absf(point.y)+radius > 1860:
return false
# Sample the full footprint, keeping props away from water and steep peaks.
for offset in [Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN, Vector2(-0.7,-0.7), Vector2(0.7,-0.7), Vector2(-0.7,0.7), Vector2(0.7,0.7)]:
var h := _height(data, point+offset*(radius+25))
if h < 0.315 or h > 0.68:
return false
for poi in data.pois:
if point.distance_to(poi.position) < 135+radius:
return false
for event in data.events:
if point.distance_to(event.position) < 115+radius:
return false
for structure in data.structures:
if point.distance_to(structure.position) < float(structure.size)*0.7+radius:
return false
for landmark in data.get("landmarks", []):
if point.distance_to(landmark.position) < float(landmark.size)*0.58+radius+15:
return false
for mountain in data.mountains:
if point.distance_to(mountain.position) < float(mountain.size)*0.48+radius:
return false
return true