feat(@projects/@magic-civilization): add river gap blocking for bunkers

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-10 02:27:17 -07:00
parent 161213a795
commit 6588c0cc76
3 changed files with 83 additions and 3 deletions

View file

@ -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")

View file

@ -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:

View file

@ -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),
)