feat(generation): ✨ Add advanced procedural generation algorithms for hydrology, rivers, terrain, and balanced starting positions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
f6495a989c
commit
e1070b6467
7 changed files with 42 additions and 54 deletions
|
|
@ -9,7 +9,6 @@ extends RefCounted
|
|||
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
|
||||
const HydrologyRiversScript: GDScript = preload("res://engine/src/generation/hydrology_rivers.gd")
|
||||
const OPPOSITE_DIR: Array[int] = [3, 4, 5, 0, 1, 2]
|
||||
const WATER_TERRAINS: Array[String] = ["ocean", "coast", "lake", "inland_sea"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -47,7 +46,7 @@ static func update_rivers(game_map: RefCounted, params: Dictionary) -> void:
|
|||
var t: Variant = game_map.tiles[axial]
|
||||
temperature[axial] = t.temperature
|
||||
flow_dir[axial] = t.river_flow.get("_flow_dir", -1)
|
||||
rainfall[axial] = 0.0 if _is_water(t) else \
|
||||
rainfall[axial] = 0.0 if BiomeRegistry.has_tag(t.biome_id, "is_water") else \
|
||||
maxf(0.0, t.moisture * _climate_mult(t.temperature, params))
|
||||
var topo: Array = _kahn_sort(game_map, flow_dir)
|
||||
var acc: Dictionary = _accumulate_flow(game_map, rainfall, flow_dir, topo)
|
||||
|
|
@ -91,7 +90,7 @@ static func recompute_flow_directions(
|
|||
var t: Variant = game_map.tiles[axial]
|
||||
full_flow[axial] = t.river_flow.get("_flow_dir", -1)
|
||||
temperature[axial] = t.temperature
|
||||
rainfall[axial] = 0.0 if _is_water(t) else \
|
||||
rainfall[axial] = 0.0 if BiomeRegistry.has_tag(t.biome_id, "is_water") else \
|
||||
maxf(0.0, t.moisture * _climate_mult(t.temperature, mini_params))
|
||||
var topo: Array = _kahn_sort(game_map, full_flow)
|
||||
var acc: Dictionary = _accumulate_flow(game_map, rainfall, full_flow, topo)
|
||||
|
|
@ -118,7 +117,7 @@ static func _compute_rainfall(
|
|||
var out: Dictionary = {}
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var t: Variant = game_map.tiles[axial]
|
||||
if _is_water(t):
|
||||
if BiomeRegistry.has_tag(t.biome_id, "is_water"):
|
||||
out[axial] = 0.0
|
||||
continue
|
||||
var base: float = moisture.get(axial, t.moisture) * _climate_mult(
|
||||
|
|
@ -201,7 +200,7 @@ static func _depression_fill_subset(
|
|||
var t: Variant = game_map.tiles.get(axial)
|
||||
if t == null:
|
||||
continue
|
||||
if _is_water(t):
|
||||
if BiomeRegistry.has_tag(t.biome_id, "is_water"):
|
||||
filled[axial] = 0.0
|
||||
flow_dir[axial] = -1
|
||||
_hpush(heap, [0.0, axial.x, axial.y])
|
||||
|
|
@ -326,9 +325,6 @@ static func _kahn_sort(game_map: RefCounted, flow_dir: Dictionary) -> Array:
|
|||
return order
|
||||
|
||||
|
||||
static func _is_water(tile: Variant) -> bool:
|
||||
return tile.biome_id in WATER_TERRAINS
|
||||
|
||||
|
||||
static func _adj_terrain(axial: Vector2i, terrains: Array, game_map: RefCounted) -> bool:
|
||||
for dir: Vector2i in HexUtilsScript.AXIAL_DIRECTIONS:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ extends RefCounted
|
|||
|
||||
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
|
||||
const OPPOSITE_DIR: Array[int] = [3, 4, 5, 0, 1, 2]
|
||||
const WATER_TERRAINS: Array[String] = ["ocean", "coast", "lake", "inland_sea"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -23,7 +22,7 @@ static func detect_lakes(
|
|||
var lake_tiles: Dictionary = {}
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var t: Variant = game_map.tiles[axial]
|
||||
if not _is_water(t):
|
||||
if not BiomeRegistry.has_tag(t.biome_id, "is_water"):
|
||||
var re: float = elevation.get(axial, t.elevation)
|
||||
if filled_elev.get(axial, re) - re > depth:
|
||||
lake_tiles[axial] = true
|
||||
|
|
@ -66,7 +65,7 @@ static func mark_rivers(
|
|||
var frozen_t: float = params.get("frozen_river_temperature", 0.10)
|
||||
for axial: Vector2i in topo_order:
|
||||
var t: Variant = game_map.tiles.get(axial)
|
||||
if t == null or _is_water(t):
|
||||
if t == null or BiomeRegistry.has_tag(t.biome_id, "is_water"):
|
||||
continue
|
||||
var a: float = acc.get(axial, 0.0)
|
||||
var temp: float = temperature.get(axial, t.temperature)
|
||||
|
|
@ -84,7 +83,7 @@ static func mark_rivers(
|
|||
t.river_flow[d] = fv
|
||||
t.river_flow["_flow_dir"] = d
|
||||
var opp: int = OPPOSITE_DIR[d]
|
||||
if not _is_water(ds):
|
||||
if not BiomeRegistry.has_tag(ds.biome_id, "is_water"):
|
||||
if opp not in ds.river_edges: ds.river_edges.append(opp)
|
||||
ds.river_flow[opp] = fv
|
||||
|
||||
|
|
@ -92,7 +91,7 @@ static func mark_rivers(
|
|||
static func _river_thresh(temp: float, biome_id: String, cfg: Dictionary) -> float:
|
||||
# Desert terrain uses its own threshold regardless of temperature band --
|
||||
# desert tiles are hot and would otherwise hit threshold_tropical, which is far too low.
|
||||
if biome_id == "desert":
|
||||
if BiomeRegistry.has_tag(biome_id, "is_dry"):
|
||||
return cfg.get("threshold_desert", 20.0)
|
||||
if temp > 0.65: return cfg.get("threshold_tropical", 4.0)
|
||||
if temp >= 0.25: return cfg.get("threshold_temperate", 6.0)
|
||||
|
|
@ -115,12 +114,12 @@ static func mark_deltas(
|
|||
var frozen_t: float = params.get("frozen_river_temperature", 0.10)
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var t: Variant = game_map.tiles.get(axial)
|
||||
if t == null or _is_water(t) or acc.get(axial, 0.0) < thresh:
|
||||
if t == null or BiomeRegistry.has_tag(t.biome_id, "is_water") or acc.get(axial, 0.0) < thresh:
|
||||
continue
|
||||
var water_dirs: Array[int] = []
|
||||
for di: int in 6:
|
||||
var nb: Variant = game_map.tiles.get(axial + HexUtilsScript.AXIAL_DIRECTIONS[di])
|
||||
if nb != null and _is_water(nb):
|
||||
if nb != null and BiomeRegistry.has_tag(nb.biome_id, "is_water"):
|
||||
water_dirs.append(di)
|
||||
if water_dirs.is_empty():
|
||||
continue
|
||||
|
|
@ -137,7 +136,7 @@ static func mark_deltas(
|
|||
var nb_pos: Vector2i = axial + HexUtilsScript.AXIAL_DIRECTIONS[di]
|
||||
if flow_dir.get(nb_pos, -1) == OPPOSITE_DIR[di]:
|
||||
var up: Variant = game_map.tiles.get(nb_pos)
|
||||
if up != null and not _is_water(up):
|
||||
if up != null and not BiomeRegistry.has_tag(up.biome_id, "is_water"):
|
||||
var d: int = water_dirs[br]
|
||||
var up_fv: float = acc.get(nb_pos, 0.0)
|
||||
if up.temperature <= frozen_t:
|
||||
|
|
@ -198,9 +197,6 @@ static func classify_sources(
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
static func _is_water(tile: Variant) -> bool:
|
||||
return tile.biome_id in WATER_TERRAINS
|
||||
|
||||
|
||||
static func _adj_terrain(axial: Vector2i, terrains: Array, game_map: RefCounted) -> bool:
|
||||
for dir: Vector2i in HexUtilsScript.AXIAL_DIRECTIONS:
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ func _compute_moisture(game_map: RefCounted) -> void:
|
|||
var queue: Array[Vector2i] = []
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var t: String = game_map.tiles[axial].biome_id
|
||||
if t == "ocean" or t == "coast":
|
||||
if BiomeRegistry.has_tag(t, "is_water"):
|
||||
dist[axial] = 0
|
||||
queue.append(axial)
|
||||
|
||||
|
|
@ -395,7 +395,7 @@ func _compute_moisture(game_map: RefCounted) -> void:
|
|||
|
||||
# Rain shadow: tiles in direction 0-1 rings downwind of each mountain
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
if game_map.tiles[axial].biome_id != "mountains":
|
||||
if not BiomeRegistry.has_tag(game_map.tiles[axial].biome_id, "is_elevated"):
|
||||
continue
|
||||
var wind: int = 0 # computed before quality, use base direction
|
||||
var downwind: Vector2i = HexUtilsScript.AXIAL_DIRECTIONS[wind]
|
||||
|
|
@ -420,7 +420,7 @@ func _derive_substrates(game_map: RefCounted) -> void:
|
|||
var moist: float = _moisture.get(axial, 0.5)
|
||||
|
||||
# Water tiles: classify by depth
|
||||
if tile.biome_id == "ocean" or tile.biome_id == "coast":
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
if elev < 0.15:
|
||||
tile.substrate_id = "deep_water"
|
||||
else:
|
||||
|
|
@ -435,7 +435,7 @@ func _derive_substrates(game_map: RefCounted) -> void:
|
|||
continue
|
||||
|
||||
# Volcanic (from terrain_refiner)
|
||||
if tile.biome_id == "volcano":
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_volcanic"):
|
||||
tile.substrate_id = "volcanic"
|
||||
tile.soil_type = "volcanic_ash"
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ func _get_wonder_candidates(game_map: RefCounted) -> Array[Vector2i]:
|
|||
var margin: int = 3
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
if tile.biome_id == "ocean" or tile.biome_id == "mountains":
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water") or BiomeRegistry.has_tag(tile.biome_id, "is_elevated"):
|
||||
continue
|
||||
var offset: Vector2i = HexUtilsScript.axial_to_offset(axial)
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ static func score_start_zone(
|
|||
continue
|
||||
land_tiles += 1
|
||||
biome_ids[tile.biome_id] = true
|
||||
if tile.biome_id == "hills":
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_elevated"):
|
||||
has_hills = true
|
||||
var yields: Dictionary = tile.get_quality_yields()
|
||||
var f: float = yields.get("food", 0)
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ func _score_all_start_candidates(
|
|||
continue
|
||||
if tile.is_natural_wonder():
|
||||
continue
|
||||
if tile.biome_id in ["mountains", "volcano", "ocean"]:
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_elevated") or BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
var score: float = _score_start_position(game_map, axial, prefer_coast)
|
||||
if score > 0:
|
||||
|
|
@ -310,7 +310,7 @@ func _score_start_position(
|
|||
food_total += yields.get("food", 0)
|
||||
production_total += yields.get("production", 0)
|
||||
terrain_variety[tile.biome_id] = true
|
||||
if tile.biome_id == "hills":
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_elevated"):
|
||||
has_hills = true
|
||||
else:
|
||||
has_coast = true
|
||||
|
|
@ -329,13 +329,13 @@ func _score_start_position(
|
|||
|
||||
var center_tile: Variant = game_map.get_tile(axial)
|
||||
if center_tile != null:
|
||||
if center_tile.biome_id == "desert":
|
||||
if BiomeRegistry.has_tag(center_tile.biome_id, "is_dry"):
|
||||
score -= 10.0
|
||||
elif center_tile.biome_id == "tundra":
|
||||
elif BiomeRegistry.has_tag(center_tile.biome_id, "is_frozen"):
|
||||
score -= 8.0
|
||||
elif center_tile.biome_id == "swamp":
|
||||
elif BiomeRegistry.has_tag(center_tile.biome_id, "is_wetland"):
|
||||
score -= 5.0
|
||||
elif center_tile.biome_id == "grassland":
|
||||
elif BiomeRegistry.has_tag(center_tile.biome_id, "is_grassland"):
|
||||
score += 3.0
|
||||
|
||||
var offset: Vector2i = HexUtilsScript.axial_to_offset(axial)
|
||||
|
|
|
|||
|
|
@ -33,14 +33,14 @@ static func smooth_coastlines(
|
|||
if neighbor == null:
|
||||
continue
|
||||
neighbor_count += 1
|
||||
if not _is_water_terrain(neighbor.biome_id):
|
||||
if not BiomeRegistry.has_tag(neighbor.biome_id, "is_water"):
|
||||
land_count += 1
|
||||
|
||||
var water_count: int = neighbor_count - land_count
|
||||
|
||||
if _is_water_terrain(tile.biome_id) and land_count >= 5:
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water") and land_count >= 5:
|
||||
changes.append({"pos": axial, "terrain": "grassland"})
|
||||
elif not _is_water_terrain(tile.biome_id) and water_count >= 5:
|
||||
elif not BiomeRegistry.has_tag(tile.biome_id, "is_water") and water_count >= 5:
|
||||
changes.append({"pos": axial, "terrain": "ocean"})
|
||||
|
||||
for change: Dictionary in changes:
|
||||
|
|
@ -61,16 +61,16 @@ static func assign_coast_tiles(game_map: RefCounted) -> void:
|
|||
var has_land_neighbor: bool = false
|
||||
for dir: Vector2i in HexUtilsScript.AXIAL_DIRECTIONS:
|
||||
var neighbor: Variant = game_map.tiles.get(axial + dir)
|
||||
if neighbor != null and not _is_water_terrain(neighbor.biome_id):
|
||||
if neighbor != null and not BiomeRegistry.has_tag(neighbor.biome_id, "is_water"):
|
||||
has_land_neighbor = true
|
||||
break
|
||||
if has_land_neighbor:
|
||||
tile.biome_id = "coast"
|
||||
|
||||
elif not _is_water_terrain(tile.biome_id):
|
||||
elif not BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
for dir: Vector2i in HexUtilsScript.AXIAL_DIRECTIONS:
|
||||
var neighbor: Variant = game_map.tiles.get(axial + dir)
|
||||
if neighbor != null and _is_water_terrain(neighbor.biome_id):
|
||||
if neighbor != null and BiomeRegistry.has_tag(neighbor.biome_id, "is_water"):
|
||||
tile.is_coastal = true
|
||||
break
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ static func place_tectonic_relief(
|
|||
var local_avg: Dictionary = {}
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
if tile.biome_id == "ocean" or tile.biome_id == "coast":
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
continue
|
||||
var nearby: Array[Vector2i] = HexUtilsScript.hex_spiral(axial, 3)
|
||||
var total: float = 0.0
|
||||
|
|
@ -109,7 +109,7 @@ static func place_tectonic_relief(
|
|||
var land_tiles: Array[Vector2i] = []
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
if tile.biome_id != "ocean" and tile.biome_id != "coast":
|
||||
if not BiomeRegistry.has_tag(tile.biome_id, "is_water"):
|
||||
land_tiles.append(axial)
|
||||
|
||||
var land_count: int = land_tiles.size()
|
||||
|
|
@ -129,9 +129,7 @@ static func place_tectonic_relief(
|
|||
var adj_ocean: bool = false
|
||||
for nb: Vector2i in HexUtilsScript.get_neighbors(axial):
|
||||
var nb_tile: Variant = game_map.tiles.get(nb)
|
||||
if nb_tile != null and (
|
||||
nb_tile.biome_id == "ocean" or nb_tile.biome_id == "coast"
|
||||
):
|
||||
if nb_tile != null and BiomeRegistry.has_tag(nb_tile.biome_id, "is_water"):
|
||||
adj_ocean = true
|
||||
break
|
||||
|
||||
|
|
@ -198,7 +196,7 @@ static func assign_terrain_patches(
|
|||
|
||||
for biome_id: String in order:
|
||||
var target_count: int = 0
|
||||
if biome_id == "tundra" or biome_id == "snow":
|
||||
if BiomeRegistry.has_tag(biome_id, "is_frozen"):
|
||||
var is_frozen: bool = biome_id == "snow"
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
if game_map.tiles[axial].biome_id != "land":
|
||||
|
|
@ -208,7 +206,7 @@ static func assign_terrain_patches(
|
|||
target_count += 1
|
||||
elif not is_frozen and t >= 0.10 and t < 0.25:
|
||||
target_count += 1
|
||||
elif biome_id == "jungle" or biome_id == "forest" or biome_id == "boreal_forest":
|
||||
elif BiomeRegistry.has_tag(biome_id, "has_vegetation"):
|
||||
# Forest types are placed by temperature zone, not fixed fraction count.
|
||||
# Count eligible land tiles in the correct temperature band.
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
|
|
@ -235,17 +233,17 @@ static func assign_terrain_patches(
|
|||
var forest_family_count: int = 0
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tid: String = game_map.tiles[axial].biome_id
|
||||
if tid in ["forest", "jungle", "boreal_forest"]:
|
||||
if BiomeRegistry.has_tag(tid, "has_vegetation"):
|
||||
forest_family_count += 1
|
||||
var base_count: int = forest_family_count if forest_family_count > 0 else land_count
|
||||
target_count = roundi(
|
||||
float(base_count) * fractions.get("enchanted_forest", 0.01)
|
||||
)
|
||||
elif biome_id == "grassland":
|
||||
elif BiomeRegistry.has_tag(biome_id, "is_grassland"):
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
if game_map.tiles[axial].biome_id == "land":
|
||||
target_count += 1
|
||||
elif biome_id == "volcano":
|
||||
elif BiomeRegistry.has_tag(biome_id, "is_volcanic"):
|
||||
var mt_count: int = game_map.get_tiles_by_terrain("mountains").size()
|
||||
target_count = roundi(float(mt_count) * fractions.get("volcano", 0.02))
|
||||
else:
|
||||
|
|
@ -269,7 +267,7 @@ static func assign_quality(game_map: RefCounted) -> void:
|
|||
## Set quality tier by same-terrain neighbour count (3+=mature, 1-2=nascent, 0=standard).
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
if tile.biome_id in ["ocean", "coast", "land"]:
|
||||
if BiomeRegistry.has_tag(tile.biome_id, "is_water") or tile.biome_id == "land":
|
||||
continue
|
||||
var same: int = 0
|
||||
for nb: Vector2i in HexUtilsScript.get_neighbors(axial):
|
||||
|
|
@ -294,8 +292,9 @@ static func _expand_patch(
|
|||
rng: RandomNumberGenerator,
|
||||
) -> void:
|
||||
var eligible: Array[Vector2i] = []
|
||||
var is_volcanic: bool = BiomeRegistry.has_tag(biome_id, "is_volcanic")
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var src: String = "mountains" if biome_id == "volcano" else "land"
|
||||
var src: String = "mountains" if is_volcanic else "land"
|
||||
if game_map.tiles[axial].biome_id == src:
|
||||
if _is_eligible(axial, biome_id, game_map, elevation, moisture, temperature):
|
||||
eligible.append(axial)
|
||||
|
|
@ -311,7 +310,7 @@ static func _expand_patch(
|
|||
var idx: int = rng.randi_range(0, eligible.size() - 1)
|
||||
var seed_pos: Vector2i = eligible[idx]
|
||||
var tile: Variant = game_map.tiles.get(seed_pos)
|
||||
var src_terrain: String = "mountains" if biome_id == "volcano" else "land"
|
||||
var src_terrain: String = "mountains" if is_volcanic else "land"
|
||||
if tile == null or tile.biome_id != src_terrain:
|
||||
eligible.remove_at(idx)
|
||||
continue
|
||||
|
|
@ -350,7 +349,7 @@ static func _is_eligible(
|
|||
"volcano":
|
||||
for nb: Vector2i in HexUtilsScript.get_neighbors(axial):
|
||||
var nb_tile: Variant = game_map.tiles.get(nb)
|
||||
if nb_tile != null and nb_tile.biome_id == "mountains":
|
||||
if nb_tile != null and BiomeRegistry.has_tag(nb_tile.biome_id, "is_elevated"):
|
||||
return false
|
||||
return true
|
||||
"jungle":
|
||||
|
|
@ -394,7 +393,4 @@ static func store_render_metadata(
|
|||
# -- Private helpers --
|
||||
|
||||
|
||||
static func _is_water_terrain(biome_id: String) -> bool:
|
||||
return biome_id == "ocean" or biome_id == "coast"
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue