diff --git a/src/game/engine/scenes/tests/bunker_proof.tscn b/src/game/engine/scenes/tests/bunker_proof.tscn new file mode 100644 index 00000000..a5cc104a --- /dev/null +++ b/src/game/engine/scenes/tests/bunker_proof.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bnkr76proof1"] + +[ext_resource type="Script" path="res://engine/scenes/tests/bunker_proof.gd" id="1_script"] + +[node name="BunkerProof" type="Node2D"] +script = ExtResource("1_script") diff --git a/src/game/engine/src/modules/management/improvement_manager.gd b/src/game/engine/src/modules/management/improvement_manager.gd index 6d11c7c7..5823f9dc 100644 --- a/src/game/engine/src/modules/management/improvement_manager.gd +++ b/src/game/engine/src/modules/management/improvement_manager.gd @@ -34,10 +34,15 @@ func get_buildable_improvements( continue var data: Dictionary = ImprovementScript._get_data(imp_id) - var tech_req: String = str(data.get("tech_required", "")) + # `requires_tech` is the canonical field per improvements.schema.json + # (units/buildings use `tech_required`; improvements do not). + var tech_req: String = str(data.get("requires_tech", "")) if tech_req != "" and tech_req != "null" and not player.has_tech(tech_req): continue + if _river_gap_blocked(data, unit.position): + continue + result.append({ "id": imp_id, "name": data.get("name", imp_id), @@ -47,6 +52,20 @@ func get_buildable_improvements( return result +func _river_gap_blocked(data: Dictionary, tile_pos: Vector2i) -> bool: + ## Deposit-destroying improvements (bunker) cannot be sited on a + ## river-course tile until p2-78 lands the windowed hydrology re-solve. + ## The verdict comes from Rust (`GdGameState.bunker_river_gap_blocked`); + ## this is only the build-validity consultation. + var effects: Dictionary = data.get("effects", {}) as Dictionary + if not bool(effects.get("destroys_deposit", false)): + return false + var gd_state: RefCounted = GameState.get_gd_state() + if gd_state == null: + return false + return bool(gd_state.call("bunker_river_gap_blocked", tile_pos.x, tile_pos.y)) + + func _can_unit_build_at( unit: RefCounted, game_map: RefCounted, player: RefCounted ) -> bool: diff --git a/src/game/engine/tests/unit/test_worker_improvement_tech_gate.gd b/src/game/engine/tests/unit/test_worker_improvement_tech_gate.gd index acc3e9e0..25b03be0 100644 --- a/src/game/engine/tests/unit/test_worker_improvement_tech_gate.gd +++ b/src/game/engine/tests/unit/test_worker_improvement_tech_gate.gd @@ -1,5 +1,5 @@ extends GutTest -## p0-16 acceptance test: when an improvement declares `tech_required`, the +## p0-16 acceptance test: when an improvement declares `requires_tech`, the ## worker's candidate list MUST exclude it for a player that lacks the tech. ## ## ImprovementManager.get_buildable_improvements applies the tech gate via @@ -40,7 +40,7 @@ func before_all() -> void: "valid_terrain": ["grassland", "plains"], "yields": {"food": 1, "production": 1}, "effects": {}, - "tech_required": FIXTURE_TECH_ID, + "requires_tech": FIXTURE_TECH_ID, } @@ -116,3 +116,58 @@ func test_worker_with_required_tech_includes_gated_improvement() -> void: ("worker WITH '%s' tech must be offered '%s'; candidate list " + "was %s") % [FIXTURE_TECH_ID, FIXTURE_IMP_ID, str(ids)], ) + + +func test_bunker_gated_by_terrain_and_pneumatic_construction() -> void: + ## p2-76 acceptance: the SHIPPING bunker is offered only on hills/mountains + ## AND only with `pneumatic_construction` (both gates read from bunker.json). + var bunker_data: Dictionary = DataLoader.get_improvement("bunker") + assert_false(bunker_data.is_empty(), "bunker.json must be loaded by the pack") + + var mgr: RefCounted = ImprovementManagerScript.new() + var game_map: RefCounted = GameMapScript.new() + game_map.initialize(4, 4, 0) + var tile: Resource = TileScript.new() + tile.biome_id = "hills" + tile.owner = 0 + tile.improvement = "" + tile.position = Vector2i(0, 0) + game_map.set_tile(Vector2i(0, 0), tile) + + var player: RefCounted = PlayerScript.new() + player.index = 0 + player.researched_techs.clear() + + var worker: RefCounted = UnitScript.new("worker", 0, Vector2i(0, 0)) + worker.can_build_improvements = true + worker.movement_remaining = 1 + + var without_tech: Array = mgr.get_buildable_improvements(worker, game_map, player) + var ids_without: Array[String] = [] + for entry: Dictionary in without_tech: + ids_without.append(str(entry.get("id", ""))) + assert_false( + "bunker" in ids_without, + "bunker must NOT be offered without pneumatic_construction; got %s" % str(ids_without), + ) + + player.researched_techs.append("pneumatic_construction") + var with_tech: Array = mgr.get_buildable_improvements(worker, game_map, player) + var ids_with: Array[String] = [] + for entry: Dictionary in with_tech: + ids_with.append(str(entry.get("id", ""))) + assert_true( + "bunker" in ids_with, + "bunker MUST be offered on hills with pneumatic_construction; got %s" % str(ids_with), + ) + + # Terrain gate: same teched player on grassland must not see the bunker. + tile.biome_id = "grassland" + var on_grass: Array = mgr.get_buildable_improvements(worker, game_map, player) + var ids_grass: Array[String] = [] + for entry: Dictionary in on_grass: + ids_grass.append(str(entry.get("id", ""))) + assert_false( + "bunker" in ids_grass, + "bunker must NOT be offered on grassland; got %s" % str(ids_grass), + )