feat(@projects/@magic-civilization): 🌫️ p3-20 (consumer) — compute_vision shrinks sight under weather
The vision-consumer half of weather→scouting. compute_vision_with_penalties takes a per-tile vision-penalty map and reduces each unit's effective sight by the penalty at its tile (floored at WEATHER_MIN_VISION=1, so a unit always sees its own tile + immediate ring). compute_vision delegates with an empty map, so all existing callers are unchanged (no churn). refresh_for_player + compute_player_visible_set thread the penalty. Test: weather_vision_penalty_shrinks_unit_sight (radius-2 disk 19 → radius-1 disk 7 under penalty 1; floored under penalty 99); mc-vision 30/0. p3-20 stays partial — bridge/GDScript wiring (weather events → penalty map → compute_vision_with_penalties) + dylib/GUT next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
923af9c0ec
commit
c30b74523b
3 changed files with 70 additions and 9 deletions
|
|
@ -20,7 +20,7 @@ surprise — a missed gameplay lever for the weather system.
|
|||
|
||||
## Progress (2026-06-25)
|
||||
|
||||
Weather-producer side done: `WeatherEvent.vision_penalty: i32` added (`mc-climate`); `derive_events` sets it per kind (storm 1, blizzard 2, dust_storm 2; heat_wave/drought/flood 0). Tests assert the per-kind values (mc-climate 45/0). **Remaining:** `compute_vision` (mc-vision) consumes a per-tile vision-penalty to shrink a unit's effective sight under weather + the bridge/GDScript wiring (build the penalty map from live weather) + dylib/GUT. *(Per-kind values inline for now; data-drive to WeatherThresholds/climate_spec.json is a follow-up.)*
|
||||
Weather-producer side done: `WeatherEvent.vision_penalty: i32` added (`mc-climate`); `derive_events` sets it per kind (storm 1, blizzard 2, dust_storm 2; heat_wave/drought/flood 0). Tests assert the per-kind values (mc-climate 45/0). Vision-consumer side DONE: `compute_vision_with_penalties` (mc-vision) shrinks a unit's effective sight by the per-tile penalty (floored at `WEATHER_MIN_VISION`=1); `compute_vision` delegates with an empty map so existing callers are unchanged. Test `weather_vision_penalty_shrinks_unit_sight` (mc-vision 30/0). **Remaining:** the bridge/GDScript wiring — build the per-tile penalty map from live weather events + call `compute_vision_with_penalties` in the playable vision path + dylib/GUT. *(Per-kind values inline for now; data-drive to WeatherThresholds/climate_spec.json is a follow-up.)*
|
||||
|
||||
## Acceptance
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"generated_at": "2026-06-25T19:45:10Z",
|
||||
"generated_at": "2026-06-25T20:11:16Z",
|
||||
"totals": {
|
||||
"in_progress": 0,
|
||||
"oos": 31,
|
||||
"stub": 0,
|
||||
"missing": 0,
|
||||
"partial": 4,
|
||||
"done": 293,
|
||||
"stub": 0,
|
||||
"in_progress": 0,
|
||||
"missing": 0,
|
||||
"oos": 31,
|
||||
"partial": 4,
|
||||
"total": 328
|
||||
},
|
||||
"objectives": [
|
||||
|
|
|
|||
|
|
@ -117,6 +117,11 @@ pub type PlayerId = u8;
|
|||
/// and `GridState::idx`.
|
||||
pub type HexCoord = (i32, i32);
|
||||
|
||||
/// p3-20 — a unit under a vision-reducing weather event never drops below this
|
||||
/// effective sight radius, so it always sees its own tile + immediate ring even
|
||||
/// in the worst blizzard.
|
||||
pub const WEATHER_MIN_VISION: i32 = 1;
|
||||
|
||||
// ── Catalog ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Per-source vision lookup. Populated from JSON game packs by the
|
||||
|
|
@ -429,11 +434,31 @@ pub fn compute_vision(
|
|||
state: &GameState,
|
||||
catalog: &VisionCatalog,
|
||||
prior: Option<&VisionState>,
|
||||
) -> VisionState {
|
||||
compute_vision_with_penalties(state, catalog, prior, &BTreeMap::new())
|
||||
}
|
||||
|
||||
/// p3-20 — like [`compute_vision`], but `vision_penalties` (sight reduction per
|
||||
/// tile, e.g. built from live weather events) shrinks the effective sight of any
|
||||
/// unit standing on a penalised tile (floored at [`WEATHER_MIN_VISION`]). An
|
||||
/// empty map reproduces [`compute_vision`] exactly.
|
||||
#[must_use]
|
||||
pub fn compute_vision_with_penalties(
|
||||
state: &GameState,
|
||||
catalog: &VisionCatalog,
|
||||
prior: Option<&VisionState>,
|
||||
vision_penalties: &BTreeMap<HexCoord, i32>,
|
||||
) -> VisionState {
|
||||
let mut per_player: BTreeMap<PlayerId, PlayerVision> = BTreeMap::new();
|
||||
for player in &state.players {
|
||||
let prior_pv = prior.and_then(|p| p.per_player.get(&player.player_index));
|
||||
let pv = refresh_for_player(state, player.player_index, catalog, prior_pv);
|
||||
let pv = refresh_for_player(
|
||||
state,
|
||||
player.player_index,
|
||||
catalog,
|
||||
prior_pv,
|
||||
vision_penalties,
|
||||
);
|
||||
per_player.insert(player.player_index, pv);
|
||||
}
|
||||
// p1-60 J — union allied players' visible sets into each member.
|
||||
|
|
@ -547,6 +572,7 @@ pub fn refresh_for_player(
|
|||
player: PlayerId,
|
||||
catalog: &VisionCatalog,
|
||||
prior: Option<&PlayerVision>,
|
||||
vision_penalties: &BTreeMap<HexCoord, i32>,
|
||||
) -> PlayerVision {
|
||||
let Some(grid) = state.grid.as_ref() else {
|
||||
// No grid → no visibility. Returns empty for every player.
|
||||
|
|
@ -556,7 +582,7 @@ pub fn refresh_for_player(
|
|||
return PlayerVision::default();
|
||||
};
|
||||
|
||||
let visible = compute_player_visible_set(grid, ps, catalog);
|
||||
let visible = compute_player_visible_set(grid, ps, catalog, vision_penalties);
|
||||
|
||||
// Carry-forward last-seen snapshot.
|
||||
// Step 1: start from prior.last_seen (tiles still stale).
|
||||
|
|
@ -652,6 +678,7 @@ fn compute_player_visible_set(
|
|||
grid: &GridState,
|
||||
ps: &PlayerState,
|
||||
catalog: &VisionCatalog,
|
||||
vision_penalties: &BTreeMap<HexCoord, i32>,
|
||||
) -> BTreeSet<HexCoord> {
|
||||
let mut visible: BTreeSet<HexCoord> = BTreeSet::new();
|
||||
|
||||
|
|
@ -673,6 +700,10 @@ fn compute_player_visible_set(
|
|||
} else {
|
||||
(base_radius, 0)
|
||||
};
|
||||
// p3-20 — weather over the observer's tile cuts its sight (storms cut a
|
||||
// little, blizzards/dust storms cut more). Floored at WEATHER_MIN_VISION.
|
||||
let radius = (radius - vision_penalties.get(¢re).copied().unwrap_or(0))
|
||||
.max(WEATHER_MIN_VISION);
|
||||
accumulate_visible_from_with_pierce(grid, centre, radius, pierce, &mut visible);
|
||||
}
|
||||
|
||||
|
|
@ -1214,6 +1245,36 @@ mod tests {
|
|||
assert!(pv.last_seen.is_empty(), "first turn has no stale tiles");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weather_vision_penalty_shrinks_unit_sight() {
|
||||
// p3-20: a vision-penalty at the observer's tile (e.g. a blizzard) cuts
|
||||
// its effective sight; never below WEATHER_MIN_VISION.
|
||||
let mut state = GameState::default();
|
||||
state.grid = Some(make_flat_grid(11, 11, "grassland"));
|
||||
let mut ps = PlayerState::default();
|
||||
ps.player_index = 0;
|
||||
ps.units.push(unit_at("scout", 5, 5));
|
||||
state.players.push(ps);
|
||||
let mut cat = VisionCatalog::default();
|
||||
cat.insert_unit("scout", 2);
|
||||
|
||||
// No penalty → radius-2 disk = 19 tiles.
|
||||
assert_eq!(compute_vision(&state, &cat, None).for_player(0).unwrap().visible.len(), 19);
|
||||
|
||||
// Penalty 1 at the unit's tile → effective radius max(1, 2-1)=1 → 7 tiles.
|
||||
let mut pen: BTreeMap<HexCoord, i32> = BTreeMap::new();
|
||||
pen.insert((5, 5), 1);
|
||||
let reduced = compute_vision_with_penalties(&state, &cat, None, &pen);
|
||||
let pv = reduced.for_player(0).unwrap();
|
||||
assert_eq!(pv.visible.len(), 7, "penalty 1 → radius-1 disk = 7 tiles");
|
||||
assert!(pv.visible.contains(&(5, 5)), "still sees its own tile");
|
||||
|
||||
// Overwhelming penalty floors at WEATHER_MIN_VISION (radius 1), never 0.
|
||||
pen.insert((5, 5), 99);
|
||||
let floored = compute_vision_with_penalties(&state, &cat, None, &pen);
|
||||
assert_eq!(floored.for_player(0).unwrap().visible.len(), 7, "floored at radius 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_id_falls_back_to_default_when_absent_from_catalog() {
|
||||
let mut state = GameState::default();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue