feat(map): Add dynamic tile property support and optimize tile rendering performance

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-25 23:53:21 -07:00
parent 12fd6ee7f4
commit a98b23cfe8
2 changed files with 82 additions and 33 deletions

View file

@ -118,11 +118,11 @@ func count_land_tiles() -> int:
return count
func get_tiles_by_terrain(terrain_id: String) -> Array: # Array[Tile]
func get_tiles_by_terrain(biome_id: String) -> Array: # Array[Tile]
## Return all tiles with the given terrain type.
var result: Array = []
for tile: Variant in tiles.values():
if tile.terrain_id == terrain_id:
if tile.biome_id == biome_id:
result.append(tile)
return result

View file

@ -5,9 +5,11 @@ extends Resource
const ImprovementScript: GDScript = preload("res://engine/src/entities/improvement.gd")
## Terrain type ID from terrain.json (e.g. "grassland", "mountains", "ocean")
var terrain_id: String = ""
var biome_id: String = "" # biome classification result
var substrate_id: String = "" # geological substrate from elevation
var water_body_id: int = -1 # water body index (-1 if land)
var depth_from_coast: int = -1 # BFS distance from land (-1 if land)
var soil_type: String = "" # rocky/sandy/clay/loam/peat/volcanic_ash/permafrost
## Axial coordinates (q, r) — primary position key
var position: Vector2i = Vector2i.ZERO
@ -21,9 +23,6 @@ var improvement: String = ""
## Owning player index, -1 = unclaimed
var owner: int = -1
## Whether this tile has an unvisited village
var village: bool = false
## Lair type ID from wilds.json (e.g. "beast_den"), empty if no lair
var lair_type: String = ""
@ -59,13 +58,9 @@ var quality_progress: int = 0
var wind_speed: float = 0.5
## Wind direction as hex edge index (0=E, 1=NE, 2=NW, 3=W, 4=SW, 5=SE)
var wind_direction: int = 0
## Corruption accumulation pressure [0,1]
var corruption_pressure: float = 0.0
## Cultural pressure accumulated from enemy cities (for ownership flip).
## Resets to 0 after a flip. Not bounded — flip_threshold determines ownership change.
var culture_pressure: float = 0.0
## Terrain ID before corruption (used for restoration)
var original_terrain_id: String = ""
## Per-turn heat forcing from active spells (transient, not saved)
var magic_heat_delta: float = 0.0
## Per-turn moisture forcing from active spells (transient, not saved)
@ -73,10 +68,6 @@ var magic_moisture_delta: float = 0.0
## City mitigation: aerosol cooling/drying scaled down by this fraction [0, 1].
## Written each turn by economy.gd from protection building effects. Not saved.
var aerosol_mitigation: float = 0.0
## City mitigation: corruption spread into this tile reduced by this fraction [0, 1].
## Written each turn by economy.gd from protection building effects. Not saved.
var corruption_resistance_pct: float = 0.0
## Ley line mana density [0,1]. 0.0 = dead zone, 1.0 = nexus.
var mana_density: float = 0.0
## Number of ley line edges passing through or adjacent to this tile.
@ -102,6 +93,32 @@ var marine_bloom_turns: int = 0
## Corrupted marine creature occupying this tile ("" = none, e.g. "wraith_eel").
var marine_creature: String = ""
## -- Flora fields (Layer 3) --
## Tree canopy density [0-1]. Drives biome classification, shade, lumber yield.
var canopy_cover: float = 0.0
## Ground-level vegetation [0-1]. Drives food yield, movement cost, habitat quality.
var undergrowth: float = 0.0
## Mycorrhizal network [0-1]. Drives ecosystem resilience, regrowth speed.
var fungi_network: float = 0.0
## -- Flora tracking --
## Consecutive turns with moisture < desertification_threshold.
var drought_counter: int = 0
## Turns at high canopy toward succession.
var succession_progress: int = 0
## Regrowth stage: -1=none, 0=barren, 1=scrub, 2=young_forest, 3=forest.
var regrowth_stage: int = -1
## Turns spent in current regrowth stage.
var regrowth_turns: int = 0
## -- Fauna tracking --
## Per-turn computed habitat suitability [0-1]. NOT serialized (recalculated each turn).
var habitat_suitability: float = 0.0
## Consecutive turns with habitat_suitability < abandon_threshold.
var habitat_low_turns: int = 0
## Landmark name set when tile crosses Q4 quality threshold.
var landmark_name: String = ""
## Ground loot: items dropped on this tile from dead units.
## Array of { "item_id": String, "charges_remaining": int, "turns_remaining": int }
var ground_items: Array = []
@ -149,16 +166,16 @@ var pressure_anomaly: float = 0.0
func _init(
p_position: Vector2i = Vector2i.ZERO,
p_terrain_id: String = "",
p_biome_id: String = "",
) -> void:
position = p_position
terrain_id = p_terrain_id
biome_id = p_biome_id
func get_movement_cost() -> int:
## Look up movement cost from terrain data via DataLoader.
## Roads reduce cost by 1 (minimum 1).
var terrain: Dictionary = DataLoader.get_terrain(terrain_id)
var terrain: Dictionary = DataLoader.get_terrain(biome_id)
var base: int = terrain.get("movement_cost", 1) if not terrain.is_empty() else 1
if improvement != "":
var modifier: int = int(ImprovementScript.get_movement_cost_modifier(improvement))
@ -168,7 +185,7 @@ func get_movement_cost() -> int:
func get_defense_bonus() -> int:
## Look up defense bonus from terrain data via DataLoader.
var terrain: Dictionary = DataLoader.get_terrain(terrain_id)
var terrain: Dictionary = DataLoader.get_terrain(biome_id)
if terrain.is_empty():
return 0
return terrain.get("defense_bonus", 0)
@ -190,7 +207,7 @@ func get_quality_yields(for_player: int = -1) -> Dictionary:
## culture is never penalized, only boosted at Q4/Q5.
## When for_player >= 0, tech-gated resources are excluded if player lacks the tech.
## mana: Dictionary[String, float] keyed by school name.
var terrain: Dictionary = DataLoader.get_terrain(terrain_id)
var terrain: Dictionary = DataLoader.get_terrain(biome_id)
var food: int = terrain.get("food", 0)
var production: int = terrain.get("production", 0)
var trade: int = terrain.get("trade", 0)
@ -305,7 +322,7 @@ func get_quality_defense() -> int:
func get_terrain_flags() -> Array:
## Return terrain flags (e.g. ["water", "naval_only"]).
var terrain: Dictionary = DataLoader.get_terrain(terrain_id)
var terrain: Dictionary = DataLoader.get_terrain(biome_id)
return terrain.get("flags", [])
@ -338,16 +355,22 @@ func to_dict() -> Dictionary:
## Serialize tile state for save files.
var data: Dictionary = {
"position": [position.x, position.y],
"terrain_id": terrain_id,
"biome_id": biome_id,
}
if substrate_id != "":
data["substrate_id"] = substrate_id
if water_body_id != -1:
data["water_body_id"] = water_body_id
if depth_from_coast != -1:
data["depth_from_coast"] = depth_from_coast
if soil_type != "":
data["soil_type"] = soil_type
if resource_id != "":
data["resource_id"] = resource_id
if improvement != "":
data["improvement"] = improvement
if owner != -1:
data["owner"] = owner
if village:
data["village"] = true
if lair_type != "":
data["lair_type"] = lair_type
if not visibility.is_empty():
@ -378,12 +401,8 @@ func to_dict() -> Dictionary:
data["quality_progress"] = quality_progress
if wind_speed != 0.5:
data["wind_speed"] = wind_speed
if corruption_pressure != 0.0:
data["corruption_pressure"] = corruption_pressure
if culture_pressure != 0.0:
data["culture_pressure"] = culture_pressure
if original_terrain_id != "":
data["original_terrain_id"] = original_terrain_id
# magic_heat_delta and magic_moisture_delta are per-turn transients — not saved
# relative_humidity, dew_point, cape are recalculated each turn — not saved
# ley fields
@ -412,6 +431,26 @@ func to_dict() -> Dictionary:
# marine_bloom_turns is transient (spell effect) — not saved
if marine_creature != "":
data["marine_creature"] = marine_creature
# Flora fields
if canopy_cover != 0.0:
data["canopy_cover"] = canopy_cover
if undergrowth != 0.0:
data["undergrowth"] = undergrowth
if fungi_network != 0.0:
data["fungi_network"] = fungi_network
if drought_counter != 0:
data["drought_counter"] = drought_counter
if succession_progress != 0:
data["succession_progress"] = succession_progress
if regrowth_stage != -1:
data["regrowth_stage"] = regrowth_stage
if regrowth_turns != 0:
data["regrowth_turns"] = regrowth_turns
# Fauna tracking (habitat_suitability is NOT serialized — recalculated per turn)
if habitat_low_turns != 0:
data["habitat_low_turns"] = habitat_low_turns
if landmark_name != "":
data["landmark_name"] = landmark_name
if not ground_items.is_empty():
data["ground_items"] = ground_items.duplicate(true)
if wonder_anchor_strength != 0.0:
@ -436,11 +475,14 @@ static func from_dict(data: Dictionary) -> Resource: # Tile
var pos_arr: Array = data.get("position", [0, 0])
var pos: Vector2i = Vector2i(pos_arr[0], pos_arr[1])
var SelfScript: GDScript = load("res://engine/src/map/tile.gd")
var tile: Resource = SelfScript.new(pos, data.get("terrain_id", ""))
var tile: Resource = SelfScript.new(pos, data.get("biome_id", ""))
tile.substrate_id = data.get("substrate_id", "")
tile.water_body_id = data.get("water_body_id", -1)
tile.depth_from_coast = data.get("depth_from_coast", -1)
tile.soil_type = data.get("soil_type", "")
tile.resource_id = data.get("resource_id", "")
tile.improvement = data.get("improvement", "")
tile.owner = data.get("owner", -1)
tile.village = data.get("village", false)
tile.lair_type = data.get("lair_type", "")
tile.visibility = data.get("visibility", {})
tile.variation_index = data.get("variation_index", 0)
@ -458,9 +500,7 @@ static func from_dict(data: Dictionary) -> Resource: # Tile
tile.quality = data.get("quality", 2)
tile.quality_progress = data.get("quality_progress", 0)
tile.wind_speed = data.get("wind_speed", 0.5)
tile.corruption_pressure = data.get("corruption_pressure", 0.0)
tile.culture_pressure = data.get("culture_pressure", 0.0)
tile.original_terrain_id = data.get("original_terrain_id", "")
tile.mana_density = data.get("mana_density", 0.0)
tile.ley_line_count = data.get("ley_line_count", 0)
tile.ley_school = data.get("ley_school", "")
@ -473,6 +513,15 @@ static func from_dict(data: Dictionary) -> Resource: # Tile
tile.fish_stock = data.get("fish_stock", -1)
tile.reef_health = data.get("reef_health", 1.0)
tile.marine_creature = data.get("marine_creature", "")
tile.canopy_cover = data.get("canopy_cover", 0.0)
tile.undergrowth = data.get("undergrowth", 0.0)
tile.fungi_network = data.get("fungi_network", 0.0)
tile.drought_counter = data.get("drought_counter", 0)
tile.succession_progress = data.get("succession_progress", 0)
tile.regrowth_stage = data.get("regrowth_stage", -1)
tile.regrowth_turns = data.get("regrowth_turns", 0)
tile.habitat_low_turns = data.get("habitat_low_turns", 0)
tile.landmark_name = data.get("landmark_name", "")
tile.ground_items = data.get("ground_items", [])
tile.wonder_anchor_strength = data.get("wonder_anchor_strength", 0.0)
tile.wonder_anchor_school = data.get("wonder_anchor_school", "")