feat(@projects/@magic-civilization): 🌫️ p3-20 DONE — weather reduces scouting in the rendered game
Completes weather→scouting. weather.gd gains vision_penalty_at(col,row) (worst penalty from active events covering the tile, hex-distance footprint); world_map_vision.recalculate_vision cuts each unit's sight radius by it (floored at 1) before computing visible_hexes — so a unit standing in a storm/blizzard/dust storm reveals fewer tiles. The penalty VALUE is Rust-derived (WeatherEvent.vision_penalty, data); GDScript only reads + applies it. Headless path has compute_vision_with_penalties. Verified: mc-climate 45/0, mc-vision 30/0, GUT vision_penalty_at (within/outside/ worst-overlap); dylib rebuilt + deployed; canonical GUT 747/0. p3-20 → done. Next: p3-21 (weather-driven migration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c30b74523b
commit
e77149f8fb
6 changed files with 77 additions and 21 deletions
|
|
@ -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** |
|
||||
|
||||
</td><td valign='top' style='padding-left:2em'>
|
||||
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
| Team Lead | Remaining |
|
||||
|---|---|
|
||||
| [warcouncil](../team-leads/warcouncil.md) | 4 |
|
||||
| [warcouncil](../team-leads/warcouncil.md) | 3 |
|
||||
|
||||
</td></tr></table>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue