feat(generation): Improve water terrain classification, placement, and start position logic by updating biome registry, map generator, and placer to handle legacy water terrains properly

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-11 06:21:28 -07:00
parent ede8a5b969
commit 268c93951f
4 changed files with 27 additions and 79 deletions

View file

@ -39,6 +39,27 @@ func rebuild_from_data() -> void:
_reverse_cache[tag] = [] as Array[String]
_reverse_cache[tag].append(biome_id)
# Fold in legacy water terrains from terrain.json (ocean, coast, lake,
# inland_sea) that may not have matching entries in biomes.json. Without
# this, map generation code that queries BiomeRegistry for "is_water"
# treats water tiles as land and mis-classifies whole maps.
for terrain: Dictionary in DataLoader.get_all_terrains():
var terrain_id: String = terrain.get("id", "")
if terrain_id.is_empty() or _tag_cache.has(terrain_id):
continue
var flags: Array = terrain.get("flags", [])
var derived: Array[String] = []
if "water" in flags:
derived.append("is_water")
if derived.is_empty():
continue
_tag_cache[terrain_id] = derived
for tag: String in derived:
if not _reverse_cache.has(tag):
_reverse_cache[tag] = [] as Array[String]
if terrain_id not in _reverse_cache[tag]:
_reverse_cache[tag].append(terrain_id)
_loaded = true

View file

@ -38,9 +38,6 @@ var _settings: Dictionary = {}
func generate(settings: Dictionary) -> RefCounted:
print("[mapgen] generate() called with map_type=%s map_size=%s" % [
settings.get("map_type", "?"), settings.get("map_size", "?"),
])
## Generate a complete map from settings.
##
## Settings keys:
@ -179,28 +176,11 @@ func _generate_terrain(game_map: RefCounted, type_data: Dictionary) -> void:
# Stage 7b: Derive substrate_id from elevation + geology
_derive_substrates(game_map)
var _dbg_land_before_patches: int = 0
for axial: Vector2i in game_map.tiles:
if not BiomeRegistry.has_tag(game_map.tiles[axial].biome_id, "is_water"):
_dbg_land_before_patches += 1
print("[mapgen] before_patches land=%d" % _dbg_land_before_patches)
# Stage 8: Terrain patch expansion
TerrainRefinerScript.assign_terrain_patches(
game_map, type_data, _elevation, _moisture, _temperature, _rng
)
var _dbg_land_after_patches: int = 0
var _dbg_biomes: Dictionary = {}
for axial: Vector2i in game_map.tiles:
var bid: String = game_map.tiles[axial].biome_id
_dbg_biomes[bid] = _dbg_biomes.get(bid, 0) + 1
if not BiomeRegistry.has_tag(bid, "is_water"):
_dbg_land_after_patches += 1
print("[mapgen] after_patches land=%d biomes=%s" % [
_dbg_land_after_patches, str(_dbg_biomes),
])
# Stage 9: Wind map (full 3-cell atmospheric model) + quality
WindCalculatorScript.compute_wind_map(game_map)
TerrainRefinerScript.assign_quality(game_map)
@ -346,27 +326,13 @@ func _assign_sea_level(
roundi(ocean_target * all_elevs.size()), 0, all_elevs.size() - 1
)
var sea_level: float = all_elevs[idx]
print("[mapgen] sea_level=%.3f ocean_target=%.3f total=%d elev_min=%.3f elev_max=%.3f" % [
sea_level, ocean_target, all_elevs.size(),
all_elevs[0], all_elevs[all_elevs.size() - 1],
])
var _dbg_land_pre: int = 0
for axial: Vector2i in game_map.tiles:
var is_land_pre: bool = _elevation.get(axial, 0.0) >= sea_level
if is_land_pre:
_dbg_land_pre += 1
game_map.tiles[axial].biome_id = (
"ocean" if _elevation.get(axial, 0.0) < sea_level else "land"
)
print("[mapgen] pre-smooth land=%d/%d" % [_dbg_land_pre, game_map.tiles.size()])
TerrainRefinerScript.smooth_coastlines(game_map, gen_params)
var _dbg_land_post: int = 0
for axial: Vector2i in game_map.tiles:
if game_map.tiles[axial].biome_id != "ocean":
_dbg_land_post += 1
print("[mapgen] post-smooth land=%d/%d" % [_dbg_land_post, game_map.tiles.size()])
TerrainRefinerScript.assign_coast_tiles(game_map)

View file

@ -28,10 +28,8 @@ func place_all(
game_map: RefCounted, settings: Dictionary,
num_players: int, wonder_count: int, type_data: Dictionary,
) -> void:
print("[placer] place_all start tiles=%d" % game_map.tiles.size())
## Run the full placement pipeline on a terrain-generated map.
place_natural_wonders(game_map, wonder_count)
print("[placer] after_wonders")
var resource_mult: float = _get_density_multiplier(
settings.get("resource_density", "standard"), "resources"
@ -39,14 +37,6 @@ func place_all(
place_resources(game_map, resource_mult)
var start_strategy: String = settings.get("start_strategy", "")
var _dbg_biomes: Dictionary = {}
var _dbg_is_land: int = 0
for _axial: Vector2i in game_map.tiles:
var _bid: String = game_map.tiles[_axial].biome_id
_dbg_biomes[_bid] = _dbg_biomes.get(_bid, 0) + 1
if game_map.tiles[_axial].is_land():
_dbg_is_land += 1
print("[placer] biomes=%s is_land_count=%d" % [str(_dbg_biomes), _dbg_is_land])
var start_positions: Array[Vector2i] = _start_position.select_start_positions(
game_map, num_players, type_data, start_strategy
)

View file

@ -21,27 +21,19 @@ func select_start_positions(
var prefer_coast: bool = gen_params.get(
"start_position_prefer_coast", true
)
print("[start] dispatch strategy='%s' type_id='%s' num_players=%d min_dist=%d" % [
start_strategy, type_data.get("id", ""), num_players,
gen_params.get("start_position_min_distance", 10),
])
# Dispatch to specialised strategy if provided
var result: Array[Vector2i]
match start_strategy:
"quadrant":
result = _select_starts_quadrant(game_map, num_players, prefer_coast)
return _select_starts_quadrant(game_map, num_players, prefer_coast)
"per_continent":
result = _select_starts_per_continent(game_map, num_players, prefer_coast)
return _select_starts_per_continent(game_map, num_players, prefer_coast)
"arm_tips":
var type_id: String = type_data.get("id", "")
result = _select_starts_arm_tips(game_map, num_players, prefer_coast, type_id)
_:
result = _select_starts_greedy(game_map, num_players, gen_params, prefer_coast)
print("[start] result size=%d positions=%s" % [result.size(), str(result)])
if result.size() == 2:
print("[start] pair_dist=%d" % HexUtilsScript.hex_distance(result[0], result[1]))
return result
return _select_starts_arm_tips(game_map, num_players, prefer_coast, type_id)
# Default: greedy score-based with min-distance constraint
return _select_starts_greedy(game_map, num_players, gen_params, prefer_coast)
func _select_starts_greedy(
@ -85,13 +77,6 @@ func _select_starts_greedy(
if far_enough:
selected.append(pos)
# DEBUG: report selected positions and pair distance for arena investigation
if selected.size() == 2:
print("[start] greedy min_distance=%d scored_count=%d selected=%s pair_dist=%d" % [
min_distance, scored_tiles.size(), str(selected),
HexUtilsScript.hex_distance(selected[0], selected[1]),
])
return selected
@ -287,34 +272,20 @@ func _score_all_start_candidates(
) -> Array[Dictionary]:
## Score all eligible start tiles and return sorted descending by score.
var scored: Array[Dictionary] = []
var _dbg_total: int = 0
var _dbg_non_land: int = 0
var _dbg_nw: int = 0
var _dbg_tag: int = 0
var _dbg_zero: int = 0
for axial: Vector2i in game_map.tiles:
_dbg_total += 1
var tile: Variant = game_map.tiles[axial]
if not tile.is_land():
_dbg_non_land += 1
continue
if tile.is_natural_wonder():
_dbg_nw += 1
continue
if (
BiomeRegistry.has_tag(tile.biome_id, "is_elevated")
or BiomeRegistry.has_tag(tile.biome_id, "is_water")
):
_dbg_tag += 1
continue
var score: float = _score_start_position(game_map, axial, prefer_coast)
if score > 0:
scored.append({"position": axial, "score": score})
else:
_dbg_zero += 1
print("[start] candidates total=%d non_land=%d nw=%d tag=%d zero=%d qualifying=%d" % [
_dbg_total, _dbg_non_land, _dbg_nw, _dbg_tag, _dbg_zero, scored.size()
])
scored.sort_custom(
func(a: Dictionary, b: Dictionary) -> bool:
return a["score"] > b["score"]