From 3492eacd595bc51f6649c01f63545343073f38a0 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Wed, 4 Nov 2026 21:46:31 +0100 Subject: [PATCH] Fog of war lifts three tiles beyond sight --- VERSION | 2 +- src/fog.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/VERSION b/VERSION index ed0afb5..9cbe113 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v202611-00012 +v202611-00013 diff --git a/src/fog.rs b/src/fog.rs index f54f750..7aed1cd 100644 --- a/src/fog.rs +++ b/src/fog.rs @@ -7,7 +7,8 @@ use crate::map::*; use std::collections::VecDeque; const SIGHT: i32 = 6; // tiles a creature sees along open ground -const START_LIGHT: i32 = 5; // tiles of known ground around a master's core +const START_LIGHT: i32 = 4; // known core room: four tiles from the core centre +const SHADOW_LIFT: i32 = 3; // terrain is remembered this many tiles beyond anything currently in sight const FOG_EVERY: f32 = 0.4; // explored tiles in savegames: one bit per tile, base64 @@ -45,13 +46,32 @@ impl Map { } impl Game { + pub(crate) fn own_core_corner(&self) -> (i32, i32) { + if self.me_side == 0 { + return self.map.core; + } + let owner = Owner::of_side(self.me_side); + let mut core: Option<(i32, i32)> = None; + for i in 0..self.map.t.len() { + let t = self.map.t[i]; + if t.kind == Kind::Core && t.owner == owner { + let (x, y) = xy(i); + core = Some(match core { + Some((cx, cy)) => (cx.min(x), cy.min(y)), + None => (x, y), + }); + } + } + core.unwrap_or(self.map.core) + } + pub fn init_fog(&mut self) { let n = self.map.t.len(); self.map.explored = vec![false; n]; self.map.visible = vec![false; n]; self.fog_t = 0.0; // the nearest portal is known from the start (the dungeon feels where creatures come from) - let (cx, cy) = self.map.core; + let (cx, cy) = self.own_core_corner(); if let Some(&(px, py)) = self.map.portals.iter().min_by_key(|&&(px, py)| (px - cx).pow(2) + (py - cy).pow(2)) { for y in py - 3..=py + 5 { for x in px - 3..=px + 5 { @@ -79,21 +99,26 @@ impl Game { } let mut vis = vec![false; n]; let (w, h) = (mw() as i32, mh() as i32); - // own ground and everything around it + let (cx, cy) = self.own_core_corner(); + let core_start = (cx - 3, cy - 3, cx + 5, cy + 5); + let in_core_start = |x: i32, y: i32| x >= core_start.0 && x <= core_start.2 && y >= core_start.1 && y <= core_start.3; + // own ground and the ground around it, except straight outside the core's starting room for i in 0..n { if self.map.t[i].owner == self.me { let (x, y) = xy(i); + vis[i] = true; + let in_start = in_core_start(x, y); for dy in -1..=1 { for dx in -1..=1 { - if inb(x + dx, y + dy) { - vis[idx(x + dx, y + dy)] = true; + let (nx, ny) = (x + dx, y + dy); + if inb(nx, ny) && !(in_start && !in_core_start(nx, ny)) { + vis[idx(nx, ny)] = true; } } } } } - // the master is never blind at home: the shadow starts away from the core - let (cx, cy) = self.map.core; + // the master is never blind at home: the shadow begins five tiles from the core centre for dy in -START_LIGHT..=START_LIGHT { for dx in -START_LIGHT..=START_LIGHT { let (x, y) = (cx + 1 + dx, cy + 1 + dy); @@ -109,6 +134,8 @@ impl Game { let mut q = VecDeque::new(); for (sid, &(start, r)) in eyes.iter().enumerate() { let sid = sid as u32 + 1; + let (sx, sy) = xy(start); + let starts_in_core_start = in_core_start(sx, sy); q.clear(); q.push_back((start, 0)); stamp[start] = sid; @@ -120,7 +147,7 @@ impl Game { let (x, y) = xy(i); for &(dx, dy) in &DIR8 { let (nx, ny) = (x + dx, y + dy); - if nx >= 0 && ny >= 0 && nx < w && ny < h { + if nx >= 0 && ny >= 0 && nx < w && ny < h && !(starts_in_core_start && !in_core_start(nx, ny)) { let j = idx(nx, ny); if stamp[j] != sid { stamp[j] = sid; @@ -130,6 +157,19 @@ impl Game { } } } + let lifted = vis.clone(); + for i in 0..n { + if lifted[i] { + let (x, y) = xy(i); + for dy in -SHADOW_LIFT..=SHADOW_LIFT { + for dx in -SHADOW_LIFT..=SHADOW_LIFT { + if inb(x + dx, y + dy) { + self.map.explored[idx(x + dx, y + dy)] = true; + } + } + } + } + } for i in 0..n { if vis[i] { self.map.explored[i] = true; @@ -156,15 +196,27 @@ mod tests { #[test] fn fog_lifts_around_the_dungeon_and_is_saved() { - let mut g = Game::new(6, 64, 64); + let map = Map::generate_with(6, 64, 64, &GenOpts { portals: Some(0), treasures: Some(0), gate: false, core_y: None }); + let mut g = Game::from_map(map, 6); + g.init_fog(); let (cx, cy) = g.map.core; assert!(g.map.shown(idx(cx + 1, cy + 1))); + assert!(g.map.shown(idx(cx + 5, cy + 1)), "the core's starting room is known"); + assert!(g.map.shown(idx(cx + 6, cy + 1)), "the wall just beyond the start is lifted"); + assert!(g.map.shown(idx(cx + 8, cy + 1)), "three tiles beyond the wall are lifted"); + assert!(!g.map.shown(idx(cx + 9, cy + 1)), "the shadow starts beyond the lifted wall"); + assert!(g.map.shown(idx(cx - 6, cy + 1)), "three tiles beyond the wall are lifted"); + assert!(!g.map.shown(idx(cx - 7, cy + 1)), "the shadow starts beyond the lifted wall"); assert!(!g.map.shown(idx(60, 60)), "far away is unexplored"); let explored = g.map.explored.iter().filter(|&&b| b).count(); - // a creature walking out explores more - g.spawn(EK::Kobold, false, cx as f32 + 1.5, cy as f32 - 2.5, 1); + // claimed ground outside the lifted start area lifts the shadow there + let out = idx(cx + 12, cy + 1); + g.map.t[out].kind = Kind::Floor; + g.map.t[out].owner = Owner::Player; + g.spawn(EK::Kobold, false, cx as f32 + 12.5, cy as f32 + 1.5, 1); g.update_fog(1.0); - assert!(g.map.explored.iter().filter(|&&b| b).count() >= explored); + assert!(g.map.shown(out), "own ground beyond the lifted start is known"); + assert!(g.map.explored.iter().filter(|&&b| b).count() > explored); // savegame round trip let json = crate::save::to_json(&g, &crate::render3d::Cam::new(0.0, 0.0), &crate::settings::Settings::default()); let l = crate::save::from_bytes(json.as_bytes()).unwrap();