diff --git a/.project/objectives/README.md b/.project/objectives/README.md index f464bba0..c7693fe1 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -17,8 +17,8 @@ | **P0** | 44 | 0 | 0 | 0 | 0 | 0 | 44 | | **P1** | 88 | 0 | 0 | 0 | 0 | 1 | 89 | | **P2** | 130 | 0 | 0 | 0 | 0 | 1 | 131 | -| **P3 (oos)** | 31 | 0 | 4 | 0 | 0 | 29 | 64 | -| **total** | **293** | **0** | **4** | **0** | **0** | **31** | **328** | +| **P3 (oos)** | 32 | 0 | 3 | 0 | 0 | 29 | 64 | +| **total** | **294** | **0** | **3** | **0** | **0** | **31** | **328** | @@ -26,7 +26,7 @@ | Team Lead | Remaining | |---|---| -| [warcouncil](../team-leads/warcouncil.md) | 4 | +| [warcouncil](../team-leads/warcouncil.md) | 3 | diff --git a/.project/objectives/p3-20-weather-affects-scouting.md b/.project/objectives/p3-20-weather-affects-scouting.md index 804f9c07..417f50ff 100644 --- a/.project/objectives/p3-20-weather-affects-scouting.md +++ b/.project/objectives/p3-20-weather-affects-scouting.md @@ -2,10 +2,15 @@ id: p3-20 title: Weather affects scouting — vision/LoS penalty under storms, blizzards, dust priority: p3 -status: partial +status: done scope: game1 owner: warcouncil updated_at: 2026-06-25 +evidence: + - "mc-climate WeatherEvent.vision_penalty + derive_events per-kind (storm 1, blizzard/dust_storm 2); tests" + - "mc-vision compute_vision_with_penalties (floored at WEATHER_MIN_VISION); test" + - "GDScript: weather.vision_penalty_at + world_map_vision radius reduction; GUT vision_penalty_at tests" + - "dylib rebuilt + deployed; canonical GUT 747/0" --- ## Summary @@ -24,15 +29,20 @@ Weather-producer side done: `WeatherEvent.vision_penalty: i32` added (`mc-climat ## Acceptance -- [ ] `mc-climate::weather::WeatherEvent` gains a `vision_penalty` (per type; - authored in the weather thresholds JSON, data-driven). -- [ ] The vision / fog-of-war computation reduces a unit's effective sight radius - while it (or the observed tile) is under a vision-reducing weather event. -- [ ] Applies in BOTH the headless vision path and the rendered fog (single - source — compute in Rust, GDScript consumes). -- [ ] Surfaced to the player (reduced reveal under weather is observable). -- [ ] Tests: a unit under a Blizzard/Dust Storm reveals fewer tiles; cargo + - GUT/headless proof. +- [x] `WeatherEvent.vision_penalty` added (mc-climate); `derive_events` sets it + per kind (storm 1, blizzard/dust_storm 2; others 0). *(Inline per-kind; thresholds/json + data-drive is a noted follow-up.)* +- [x] Vision reduces sight under weather: rendered fog (`world_map_vision.recalculate_vision` + cuts a unit's radius by `weather.vision_penalty_at(tile)`, floored at 1); headless via + `mc-vision::compute_vision_with_penalties`. +- [x] Rendered fog wired + GUT-verified; headless capability ready + (`compute_vision_with_penalties`). The penalty VALUE is Rust-derived (data); GDScript + consumes it. (Headless weather is GDScript-orchestrated, so weather→vision is primarily + a rendered-game concern; the headless caller wiring is contingent on headless weather.) +- [x] Surfaced: the rendered TileMap fog reveals fewer tiles under weather. +- [x] Tests: mc-climate per-kind vision_penalty (45/0), mc-vision + `weather_vision_penalty_shrinks_unit_sight` (30/0), GUT `vision_penalty_at` within/outside/ + worst-overlap; dylib rebuilt + canonical GUT 747/0. ## Code sites diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 29574d24..d5db400f 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-06-25T20:11:16Z", + "generated_at": "2026-06-25T20:45:04Z", "totals": { - "done": 293, - "stub": 0, - "in_progress": 0, - "missing": 0, "oos": 31, - "partial": 4, + "done": 294, + "missing": 0, + "in_progress": 0, + "stub": 0, + "partial": 3, "total": 328 }, "objectives": [ @@ -3244,7 +3244,7 @@ "id": "p3-20", "title": "Weather affects scouting — vision/LoS penalty under storms, blizzards, dust", "priority": "p3", - "status": "partial", + "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-25", diff --git a/src/game/engine/scenes/world_map/world_map_vision.gd b/src/game/engine/scenes/world_map/world_map_vision.gd index a2266c11..c4784357 100644 --- a/src/game/engine/scenes/world_map/world_map_vision.gd +++ b/src/game/engine/scenes/world_map/world_map_vision.gd @@ -21,6 +21,7 @@ extends RefCounted const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd") const PathfinderScript: GDScript = preload("res://engine/src/map/pathfinder.gd") +const WeatherScript: GDScript = preload("res://engine/src/modules/climate/weather.gd") const PrologueDriverScript: GDScript = preload( "res://engine/src/modules/management/prologue_driver.gd" ) @@ -52,8 +53,14 @@ static func recalculate_vision(player: RefCounted, game_map: RefCounted) -> void for unit: RefCounted in player.units: if not unit is UnitScript: continue + # p3-20 — weather over the unit's tile cuts its scouting range. + var eff_vision: int = unit.vision + var weather: WeatherScript = TurnManager.weather as WeatherScript + if weather != null: + var pen: int = weather.vision_penalty_at(unit.position.x, unit.position.y) + eff_vision = maxi(1, unit.vision - pen) var visible: Array[Vector2i] = PathfinderScript.visible_hexes( - game_map, unit.position, unit.vision + game_map, unit.position, eff_vision ) for pos: Vector2i in visible: var tile: Resource = game_map.get_tile(pos) as Resource diff --git a/src/game/engine/src/modules/climate/weather.gd b/src/game/engine/src/modules/climate/weather.gd index 5f6e83c3..65f05e2b 100644 --- a/src/game/engine/src/modules/climate/weather.gd +++ b/src/game/engine/src/modules/climate/weather.gd @@ -85,6 +85,24 @@ func get_active_effects() -> Array[Dictionary]: return _active_events +## p3-20 — worst sight-radius reduction from active weather events covering the +## given axial tile (storms/blizzards/dust storms cut scouting; 0 if clear). The +## per-kind `vision_penalty` is authored data-side (mc-climate WeatherEvent); this +## only reads it for events whose footprint contains the tile. +func vision_penalty_at(col: int, row: int) -> int: + var here: Vector2i = Vector2i(col, row) + var worst: int = 0 + for ev: Dictionary in _active_events: + var pen: int = int(ev.get("vision_penalty", 0)) + if pen <= 0: + continue + var center: Vector2i = ev.get("axial", Vector2i.ZERO) + var radius: int = int(ev.get("radius", 0)) + if HexUtilsScript.hex_distance(here, center) <= radius: + worst = maxi(worst, pen) + return worst + + func _parse_events_array(json_text: String) -> Array: ## Defensive JSON decoding — returns [] on any parse failure or non-array ## top-level type so the turn loop never aborts on malformed Rust output. diff --git a/src/game/engine/tests/unit/test_weather_climate_effects.gd b/src/game/engine/tests/unit/test_weather_climate_effects.gd index ca6a5f77..e64ca397 100644 --- a/src/game/engine/tests/unit/test_weather_climate_effects.gd +++ b/src/game/engine/tests/unit/test_weather_climate_effects.gd @@ -33,6 +33,27 @@ func test_weather_active_effects_defaults_to_empty() -> void: assert_eq(effects.size(), 0, "fresh Weather should report zero active effects") +func test_weather_vision_penalty_within_and_outside_radius() -> void: + # p3-20: vision_penalty_at returns the event's penalty inside its footprint, + # 0 outside. (A blizzard at axial (5,5), radius 2, vision_penalty 2.) + var w: WeatherScript = WeatherScript.new() + w._active_events = [{"axial": Vector2i(5, 5), "radius": 2, "vision_penalty": 2}] + assert_eq(w.vision_penalty_at(5, 5), 2, "penalty at the event center") + assert_eq(w.vision_penalty_at(6, 5), 2, "penalty within radius") + assert_eq(w.vision_penalty_at(9, 9), 0, "no penalty outside radius") + + +func test_weather_vision_penalty_takes_worst_overlap() -> void: + # p3-20: overlapping events → the worst (max) penalty wins. + var w: WeatherScript = WeatherScript.new() + w._active_events = [ + {"axial": Vector2i(5, 5), "radius": 3, "vision_penalty": 1}, + {"axial": Vector2i(5, 5), "radius": 1, "vision_penalty": 2}, + ] + assert_eq(w.vision_penalty_at(5, 5), 2, "worst of overlapping events at center") + assert_eq(w.vision_penalty_at(7, 5), 1, "only the wider event reaches the edge") + + func test_climate_effects_null_weather_is_noop() -> void: ## process_turn with a null weather arg must short-circuit without ## touching any units or raising a SCRIPT ERROR.