// Fog of war: everything except the start area is black until the dungeon's ground, creatures // or towers come close; explored tiles stay known. Enemies are only seen while they are in // sight of the dungeon's units, towers or ground. The seer's eye opens an area for a while. use crate::game::*; use crate::map::*; use std::collections::VecDeque; const SIGHT: i32 = 6; // tiles a creature sees along open ground 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 pub mod bits { use serde::{Deserialize, Deserializer, Serializer}; pub fn serialize(v: &[bool], s: S) -> Result { let mut bytes = vec![0u8; (v.len() + 7) / 8]; for (i, &b) in v.iter().enumerate() { if b { bytes[i / 8] |= 1 << (i % 8); } } s.serialize_str(&format!("{}:{}", v.len(), crate::map::b64_encode(&bytes))) } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { let s = String::deserialize(d)?; let Some((n, data)) = s.split_once(':') else { return Ok(Vec::new()) }; let n: usize = n.parse().unwrap_or(0); let bytes = crate::map::b64_decode(data).unwrap_or_default(); Ok((0..n).map(|i| bytes.get(i / 8).map_or(false, |b| b & (1 << (i % 8)) != 0)).collect()) } } impl Map { // drawn at all: explored, or seen right now (no fog: everything) pub fn shown(&self, i: usize) -> bool { self.explored.is_empty() || self.explored.get(i).copied().unwrap_or(true) || self.visible.get(i).copied().unwrap_or(false) } // in sight right now (enemies are only drawn there) pub fn in_sight(&self, i: usize) -> bool { self.explored.is_empty() || self.visible.get(i).copied().unwrap_or(false) } } 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.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 { if inb(x, y) { self.map.explored[idx(x, y)] = true; } } } } self.update_fog(1.0); } pub(crate) fn update_fog(&mut self, dt: f32) { if self.map.explored.is_empty() { return; } self.fog_t -= dt; if self.fog_t > 0.0 { return; } self.fog_t = FOG_EVERY; let n = self.map.t.len(); if self.map.visible.len() != n { self.map.visible = vec![false; n]; } let mut vis = vec![false; n]; let (w, h) = (mw() as i32, mh() as i32); 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 { 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 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); if inb(x, y) { vis[idx(x, y)] = true; } } } // what the creatures and towers see along open ground (walls stop the view) let mut eyes: Vec<(usize, i32)> = self.ents.iter().filter(|e| e.alive && !e.held && !e.hero && self.is_master(e.side) && !e.chained).map(|e| (e.tile(), SIGHT)).collect(); eyes.extend(self.towers.iter().filter(|t| !t.hero).map(|t| (t.tile, t.range() as i32 + 1))); let mut stamp = vec![0u32; n]; 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; while let Some((i, d)) = q.pop_front() { vis[i] = true; if (self.map.t[i].solid() && i != start) || d >= r { continue; } 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 && !(starts_in_core_start && !in_core_start(nx, ny)) { let j = idx(nx, ny); if stamp[j] != sid { stamp[j] = sid; q.push_back((j, d + 1)); } } } } } 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; } } // the seer's eye: a wide view for a while (not remembered) for &(ex, ey, _) in &self.reveals { let r = crate::spells::EYE_RADIUS as i32; for y in ey as i32 - r..=ey as i32 + r { for x in ex as i32 - r..=ex as i32 + r { if inb(x, y) && ((x as f32 + 0.5 - ex).powi(2) + (y as f32 + 0.5 - ey).powi(2)) <= (r * r) as f32 { vis[idx(x, y)] = true; } } } } self.map.visible = vis; } } #[cfg(test)] mod tests { use super::*; #[test] fn fog_lifts_around_the_dungeon_and_is_saved() { 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(); // 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.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(); assert_eq!(l.game.map.explored, g.map.explored); // the seer's eye opens a far area for a while g.reveals.push((40.0, 40.0, 5.0)); g.update_fog(1.0); assert!(g.map.in_sight(idx(40, 40))); } }