refactor(ecology): ♻️ Restructure fauna tracking and optimize water body detection with improved database handling

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 00:29:34 -07:00
parent da7a90f646
commit c78443fa4b
3 changed files with 109 additions and 3 deletions

View file

@ -94,6 +94,7 @@ func add_species(data: Dictionary) -> int:
"maturity_age": data.get("maturity_age", 5),
"max_age": data.get("max_age", 50),
"quality_up_threshold": data.get("quality_up_threshold", 20),
"migration_pattern": data.get("migration_pattern", ""),
}
_species[id] = row

View file

@ -9,6 +9,8 @@ const LV_SUBSTEPS: int = 3
const PERTURBATION: float = 0.05
const MIN_VIABLE_POPULATION: int = 2
const QUALITY_MISMATCH_TIERS: int = 2
## Seasonal migration occurs every N turns (approximate season length).
const SEASONAL_MIGRATION_INTERVAL: int = 12
const LOOT_DROP_CHANCE: Dictionary = {1: 0.0, 2: 0.0, 3: 0.25, 4: 0.60, 5: 1.0}
const GRAZING_BY_SIZE: Dictionary = {
"tiny": 0.005, "small": 0.01, "medium": 0.02, "large": 0.04, "huge": 0.08,
@ -24,7 +26,9 @@ func invalidate_region_cache() -> void:
_biome_regions.clear()
func process_turn(game_map: Variant, ecology_db: Variant, seed: int) -> void:
func process_turn(
game_map: Variant, ecology_db: Variant, seed: int, turn_number: int = 0
) -> void:
rng.seed = seed
var lp: Variant = DataLoader.get_land_fauna_params()
var mp: Variant = DataLoader.get_marine_fauna_params()
@ -37,6 +41,8 @@ func process_turn(game_map: Variant, ecology_db: Variant, seed: int) -> void:
_process_herbivore_grazing(game_map, ecology_db)
_enforce_carrying_capacity(ecology_db, mp)
_process_migration(game_map, ecology_db, mp)
if turn_number > 0 and turn_number % SEASONAL_MIGRATION_INTERVAL == 0:
_process_seasonal_migration(game_map, ecology_db)
_process_lair_habitat(game_map, ecology_db, lp)
@ -333,7 +339,11 @@ func _find_target(
if not game_map.tiles.has(c):
continue
var ct: Variant = game_map.tiles[c]
if not ct.is_water() and ct.biome_id != ob:
# Amphibious species can cross land/water boundaries
var habitat: String = sp.get("habitat", "terrestrial")
if habitat == "amphibious":
pass # amphibious can go anywhere (land or water)
elif not ct.is_water() and ct.biome_id != ob:
continue
if _is_whale(sp) and mp != null:
if not ct.is_water() or ct.depth_from_coast < mp.whale_min_depth or ct.depth_from_coast > mp.whale_max_depth or ct.quality < mp.whale_min_quality:
@ -387,6 +397,58 @@ func _abandon_lair(pos: Vector2i, tile: Variant, ecology_db: Variant) -> void:
EventBus.lair_abandoned.emit(pos)
# -- Seasonal migration (aerial herd/swarm move to warmer biome) --
func _process_seasonal_migration(game_map: Variant, ecology_db: Variant) -> void:
## Seasonal migrants move toward warmer tiles within migration_range.
## Simulates flying herds/swarms following temperature gradients.
var migrated: Dictionary = {} # creature_id -> true (prevent double-move)
for pos: Vector2i in game_map.tiles:
var creatures: Array = ecology_db.get_creatures_on_tile(pos.x, pos.y)
for c: Dictionary in creatures:
var cid: int = c.get("id", -1)
if migrated.has(cid):
continue
var sp: Dictionary = ecology_db.get_species(c.get("species_id", -1))
if sp.is_empty():
continue
if sp.get("migration_pattern", "") != "seasonal":
continue
var mrange: int = sp.get("migration_range", 3)
var target: Vector2i = _find_seasonal_target(pos, sp, game_map, ecology_db, mrange)
if target != Vector2i(-1, -1) and target != pos:
ecology_db.update_creature(cid, {
"tile_col": target.x, "tile_row": target.y,
})
migrated[cid] = true
func _find_seasonal_target(
origin: Vector2i, sp: Dictionary, game_map: Variant,
ecology_db: Variant, search_range: int,
) -> Vector2i:
## Find the warmest tile within range that has capacity for this species.
var otile: Variant = game_map.tiles.get(origin)
if otile == null:
return Vector2i(-1, -1)
var best: Vector2i = Vector2i(-1, -1)
var best_temp: float = otile.temperature
for r: int in range(1, search_range + 1):
for c: Vector2i in HexUtilsScript.hex_ring(origin, r):
if not game_map.tiles.has(c):
continue
var ct: Variant = game_map.tiles[c]
if ct.temperature <= best_temp:
continue
var count: int = ecology_db.get_creature_count_on_tile(c.x, c.y)
var cap: float = sp.get("carrying_capacity", 10.0)
if float(count) >= cap:
continue
best_temp = ct.temperature
best = c
return best
# -- Biome region cache (flood-fill connected same-biome tiles) --
func _rebuild_biome_regions(game_map: Variant) -> void:

View file

@ -75,15 +75,19 @@ static func identify_water_bodies(
# Store in ecology_db
ecology_db.add_water_body(body.id, body.type, body.size, body.tiles.size())
# Set water_body_id on each tile in this body
# Set water_body_id and water_body_type on each tile
for tile_pos: Vector2i in body.tiles:
var tile: Variant = game_map.tiles.get(tile_pos)
if tile != null:
tile.water_body_id = body.id
tile.water_body_type = body.type
# Compute depth_from_coast for all water tiles
_compute_depth_from_coast(game_map, water_bodies, ecology_db)
# Detect river mouths: ocean tiles at depth <= 1 adjacent to river tiles
_detect_river_mouths(game_map, water_bodies)
return water_bodies
@ -152,3 +156,42 @@ static func _compute_depth_from_coast(
# Store in ecology_db
ecology_db.add_water_body_tile(body_id, pos.x, pos.y, depth)
## Detect river mouths: ocean/sea tiles at depth <= 1 with adjacent river tiles.
## Sets is_river_mouth = true on qualifying tiles.
static func _detect_river_mouths(
game_map: Variant,
water_bodies: Array,
) -> void:
# Build set of river tile positions
var river_positions: Dictionary = {}
for wb: Variant in water_bodies:
if wb.type == "river":
for pos: Vector2i in wb.tiles:
river_positions[pos] = true
# Also consider land tiles with river_edges as river-adjacent
for pos: Vector2i in game_map.tiles:
var tile: Variant = game_map.tiles[pos]
if not tile.river_edges.is_empty():
river_positions[pos] = true
if river_positions.is_empty():
return
# Check ocean tiles at depth <= 1 for river adjacency
for wb: Variant in water_bodies:
if wb.type != "ocean":
continue
for pos: Vector2i in wb.tiles:
var tile: Variant = game_map.tiles.get(pos)
if tile == null or tile.depth_from_coast > 1:
continue
var nbs: Array[Vector2i] = (
game_map.get_valid_neighbor_positions(pos)
)
for nb: Vector2i in nbs:
if nb in river_positions:
tile.is_river_mouth = true
break