From d3438ef69154cd73cf241cb94270781d4a30ecd7 Mon Sep 17 00:00:00 2001 From: autocommit Date: Sun, 12 Apr 2026 22:50:50 -0700 Subject: [PATCH] =?UTF-8?q?feat(turn-processor):=20=E2=9C=A8=20Reimplement?= =?UTF-8?q?=20culture=20processing=20in=20turn=20processor=20by=20adding?= =?UTF-8?q?=20HexUtilsScript=20dependency=20and=20reactivating=20stubbed-o?= =?UTF-8?q?ut=20culture=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../src/modules/management/turn_processor.gd | 89 ++++++++++++++++--- 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/src/game/engine/src/modules/management/turn_processor.gd b/src/game/engine/src/modules/management/turn_processor.gd index 302ebb11..1645e6b1 100644 --- a/src/game/engine/src/modules/management/turn_processor.gd +++ b/src/game/engine/src/modules/management/turn_processor.gd @@ -18,6 +18,7 @@ const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd") const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd") const CityScript: GDScript = preload("res://engine/src/entities/city.gd") const CultureScript: GDScript = preload("res://engine/src/modules/empire/culture.gd") +const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd") const HappinessScript: GDScript = preload("res://engine/src/modules/empire/happiness.gd") const AscensionRitualScript: GDScript = preload( "res://engine/src/modules/victory/ascension_ritual.gd" @@ -348,17 +349,83 @@ func _process_economy(player: RefCounted, game_map: RefCounted) -> void: # Play player.gold = 0 -func _process_culture(_player: RefCounted, _game_map: RefCounted) -> void: # Player, GameMap - ## DISABLED: CultureScript is an empty stub; both `process_turn` and - ## `process_global_culture` raise nonexistent-function errors. See the - ## top-of-file out-of-scope list. Revive once the culture module is rebuilt. - ## Original body: - ## player.culture_per_turn = 0 - ## for city in player.cities: - ## if city is CityScript: - ## CultureScript.process_turn(city, game_map, player) - ## CultureScript.process_global_culture(player) - pass +func _process_culture(player: RefCounted, game_map: RefCounted) -> void: + ## Accumulate culture in each city and expand borders when threshold met. + if game_map == null: + return + for city_ref: Variant in player.cities: + if not city_ref is CityScript: + continue + var c: CityScript = city_ref as CityScript + var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map) + var yields: Dictionary = c.get_yields(tile_json) + var culture: float = float(yields.get("culture", 0)) + if culture <= 0.0: + continue + # Accumulate culture + c.set_culture_stored(c.get_culture_stored() + culture) + # Check for border expansion + var threshold: float = _culture_expansion_threshold(c.owned_tiles.size()) + if c.get_culture_stored() >= threshold: + c.set_culture_stored(c.get_culture_stored() - threshold) + # Find best adjacent tile to claim + var best_tile: Vector2i = _pick_border_expansion_tile(c, game_map, player) + if best_tile != Vector2i(-1, -1): + c.owned_tiles.append(best_tile) + var tile: Resource = game_map.get_tile(best_tile) + if tile != null: + tile.owner = player.index + EventBus.city_border_expanded.emit(c, best_tile) + + +func _culture_expansion_threshold(tiles_owned: int) -> float: + ## Civ5-style: first ring at 10, then roughly 10 * 1.5^(tiles - 7) + if tiles_owned <= 7: + return 10.0 + return 10.0 * pow(1.5, tiles_owned - 7) + + +func _pick_border_expansion_tile( + city: CityScript, game_map: RefCounted, player: RefCounted +) -> Vector2i: + ## Pick the best unclaimed adjacent tile to add to the city's territory. + var candidates: Array[Vector2i] = [] + for owned_pos: Vector2i in city.owned_tiles: + var neighbors: Array[Vector2i] = HexUtilsScript.get_neighbors(owned_pos) + for n: Vector2i in neighbors: + var norm: Vector2i = HexUtilsScript.normalize_position( + n, game_map.width, game_map.height, game_map.wrap_mode + ) + if norm in city.owned_tiles: + continue + var tile: Resource = game_map.get_tile(norm) + if tile == null: + continue + if tile.owner != -1 and tile.owner != player.index: + continue # Already owned by another player + if norm not in candidates: + candidates.append(norm) + if candidates.is_empty(): + return Vector2i(-1, -1) + # Score by yield value — prefer high-production/food tiles + var best: Vector2i = candidates[0] + var best_score: float = 0.0 + for pos: Vector2i in candidates: + var tile: Resource = game_map.get_tile(pos) + if tile == null: + continue + var tile_yields: Dictionary = tile.get_yields(player.index) + var score: float = float(tile_yields.get("food", 0)) * 2.0 + score += float(tile_yields.get("production", 0)) * 1.5 + score += float(tile_yields.get("trade", 0)) + score += float(tile_yields.get("culture", 0)) + # Bonus for resources + if tile.resource_id != "": + score += 5.0 + if score > best_score: + best_score = score + best = pos + return best func _process_golden_age(player: RefCounted, game_map: RefCounted) -> void: # Player, GameMap