diff --git a/src/game/engine/src/autoloads/data_loader.gd b/src/game/engine/src/autoloads/data_loader.gd index 1fbd40f1..9659cc13 100644 --- a/src/game/engine/src/autoloads/data_loader.gd +++ b/src/game/engine/src/autoloads/data_loader.gd @@ -205,6 +205,13 @@ func _log_load_summary() -> void: total += count print("DataLoader: Loaded %d entries from theme '%s'" % [total, _active_theme]) +# -- Bulk category access -- + +func get_data(category: String) -> Dictionary: + ## Return the full id→entry dictionary for a data category. + ## Returns an empty dictionary for unknown or empty categories. + return _data.get(category, {}) + # -- Single-item lookups -- func get_terrain(id: String) -> Dictionary: diff --git a/src/game/engine/src/map/tile.gd b/src/game/engine/src/map/tile.gd index 39595491..cc04d417 100644 --- a/src/game/engine/src/map/tile.gd +++ b/src/game/engine/src/map/tile.gd @@ -6,7 +6,13 @@ extends Resource const ImprovementScript: GDScript = preload("res://engine/src/entities/improvement.gd") const TileSerializerScript: GDScript = preload("res://engine/src/map/tile_serializer.gd") -var biome_id: String = "" # biome classification result +var biome_id: String = "": # biome classification result + set(value): + if value == null: + push_warning("Tile: null biome_id assignment at %s, defaulting to 'plains'" % str(position)) + biome_id = "plains" + else: + biome_id = value var substrate_id: String = "" # geological substrate from elevation var water_body_id: int = -1 # water body index (-1 if land) var water_body_type: String = "" # pond/river/lake/large_lake/ocean diff --git a/src/game/engine/src/modules/management/unit_manager.gd b/src/game/engine/src/modules/management/unit_manager.gd index 5e832dd6..75cf9a29 100644 --- a/src/game/engine/src/modules/management/unit_manager.gd +++ b/src/game/engine/src/modules/management/unit_manager.gd @@ -1,2 +1,60 @@ class_name UnitManager extends RefCounted +## Manages per-turn unit lifecycle: movement refresh, vision calculation, +## and unit queries for the active player. + +const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd") +const PathfinderScript: GDScript = preload("res://engine/src/map/pathfinder.gd") +const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd") +const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd") +const GameMapScript: GDScript = preload("res://engine/src/map/game_map.gd") +const TileScript: GDScript = preload("res://engine/src/map/tile.gd") + +## Default vision radius when a unit's JSON data has no "vision" field. +const DEFAULT_VISION_RADIUS: int = 2 + + +func refresh_player_units(player: RefCounted) -> void: + ## Reset per-turn state for all units owned by this player. + ## Called at the start of each player's turn by TurnManager. + for raw_unit: RefCounted in player.units: + var unit: UnitScript = raw_unit as UnitScript + if unit != null: + unit.refresh_turn() + + +func recalculate_vision(player: RefCounted, game_map: RefCounted) -> void: + ## Update tile visibility for this player based on unit positions. + ## Tiles previously visible become fog-of-war (1); tiles in range + ## of any unit become currently visible (2). + var gm: GameMapScript = game_map as GameMapScript + if gm == null: + return + _dim_current_vision(player, gm) + for raw_unit: RefCounted in player.units: + var unit: UnitScript = raw_unit as UnitScript + if unit == null: + continue + var vision_radius: int = _get_unit_vision(unit) + var visible_tiles: Array[Vector2i] = PathfinderScript.visible_hexes( + gm, unit.position, vision_radius + ) + for pos: Vector2i in visible_tiles: + var tile: TileScript = gm.get_tile(pos) as TileScript + if tile != null: + tile.set_visibility(player.index, 2) + + +func _dim_current_vision(player: RefCounted, game_map: GameMapScript) -> void: + ## Downgrade all currently-visible (2) tiles to fog-of-war (1). + for axial: Vector2i in game_map.tiles: + var tile: TileScript = game_map.tiles[axial] as TileScript + if tile != null and tile.get_visibility(player.index) == 2: + tile.set_visibility(player.index, 1) + + +func _get_unit_vision(unit: UnitScript) -> int: + ## Read the vision radius from the unit's JSON data. + ## Falls back to DEFAULT_VISION_RADIUS when the field is absent. + var data: Dictionary = DataLoader.get_unit(unit.unit_id) + return data.get("vision", DEFAULT_VISION_RADIUS) diff --git a/src/game/engine/src/rendering/unit_renderer.gd b/src/game/engine/src/rendering/unit_renderer.gd index e6f71767..289f73b5 100644 --- a/src/game/engine/src/rendering/unit_renderer.gd +++ b/src/game/engine/src/rendering/unit_renderer.gd @@ -18,6 +18,19 @@ const SELECTION_COLOR: Color = Color(1.0, 1.0, 0.0, 0.9) const MOVE_RANGE_COLOR: Color = Color(0.2, 0.4, 0.9, 0.25) const MOVE_RANGE_BORDER_COLOR: Color = Color(0.3, 0.5, 1.0, 0.5) +## Combat type to marker label mapping for placeholder rendering. +const COMBAT_TYPE_LABELS: Dictionary = { + "settler": "F", "founder": "F", + "worker": "W", + "ranged": "R", + "siege": "S", + "flying": "^", + "melee": "M", + "cavalry": "C", + "naval": "N", + "civilian": "V", +} + ## Unit display data: unit_id -> { "position": Vector2i, "color": Color, "label": String } var _units: Dictionary = {} @@ -51,15 +64,17 @@ func sync_units(units: Array) -> void: var unit: UnitScript = raw_unit as UnitScript if unit == null: continue - if unit.id == "": + var uid: String = _resolve_unit_id(unit) + if uid == "": continue - _units[unit.id] = { + var tid: String = _resolve_type_id(unit) + _units[uid] = { "position": unit.position, "color": _get_unit_color(unit), "label": _get_unit_label(unit), - "type_id": unit.type_id, + "type_id": tid, } - _cache_unit_sprite(unit.type_id) + _cache_unit_sprite(tid) queue_redraw() @@ -188,12 +203,33 @@ func _get_unit_color(unit: UnitScript) -> Color: func _get_unit_label(unit: UnitScript) -> String: - ## Get a short label for the unit (first character of type_id, uppercased). - if unit.type_id.length() > 0: - return unit.type_id[0].to_upper() + ## Get a marker label based on the unit's combat type. + ## Falls back to first character of unit_id if combat type is unknown. + var data: Dictionary = DataLoader.get_unit(unit.unit_id) + var combat_type: String = data.get("combat_type", "") + if COMBAT_TYPE_LABELS.has(combat_type): + return COMBAT_TYPE_LABELS[combat_type] + if unit.unit_id.length() > 0: + return unit.unit_id[0].to_upper() return "?" +func _resolve_unit_id(unit: UnitScript) -> String: + ## Return the instance identifier for a unit. + ## Checks 'id' first (if set by world_map), falls back to 'unit_id'. + if "id" in unit and unit.get("id") != "": + return str(unit.get("id")) + return unit.unit_id + + +func _resolve_type_id(unit: UnitScript) -> String: + ## Return the type identifier for sprite/label lookup. + ## Checks 'type_id' first, falls back to 'unit_id'. + if "type_id" in unit and unit.get("type_id") != "": + return str(unit.get("type_id")) + return unit.unit_id + + func _cache_unit_sprite(type_id: String) -> void: ## Pre-cache a unit sprite via ThemeAssets. Caches null on miss. if type_id == "" or _unit_sprite_cache.has(type_id): @@ -219,7 +255,10 @@ func _get_unit_sprite(type_id: String) -> Texture2D: func _on_unit_moved( unit: RefCounted, from: Vector2i, to: Vector2i ) -> void: - var uid: String = unit.id if "id" in unit else "" + var u: UnitScript = unit as UnitScript + if u == null: + return + var uid: String = _resolve_unit_id(u) if uid == "" or not _units.has(uid): return animate_move(uid, from, to) @@ -227,24 +266,32 @@ func _on_unit_moved( func _on_unit_created(unit: RefCounted, _player_index: int) -> void: var u: UnitScript = unit as UnitScript - if u == null or u.id == "": + if u == null: return - _units[u.id] = { + var uid: String = _resolve_unit_id(u) + if uid == "": + return + var tid: String = _resolve_type_id(u) + _units[uid] = { "position": u.position, "color": _get_unit_color(u), "label": _get_unit_label(u), - "type_id": u.type_id, + "type_id": tid, } - _cache_unit_sprite(u.type_id) + _cache_unit_sprite(tid) queue_redraw() func _on_unit_destroyed(unit: RefCounted, _killer: RefCounted) -> void: - var uid: String = unit.id if "id" in unit else "" - if uid != "" and _units.has(uid): - _units.erase(uid) - _anim_pixels.erase(uid) - if _selected_id == uid: - _selected_id = "" - _movement_range.clear() - queue_redraw() + var u: UnitScript = unit as UnitScript + if u == null: + return + var uid: String = _resolve_unit_id(u) + if uid == "" or not _units.has(uid): + return + _units.erase(uid) + _anim_pixels.erase(uid) + if _selected_id == uid: + _selected_id = "" + _movement_range.clear() + queue_redraw()