Fog of war: unexplored darkness until the dungeon's ground, creatures or towers come close
- explored tiles stay known (saved compactly as bits), enemies are only drawn while in sight; sight runs along open ground, walls block it - unexplored tiles are black blocks; units, towers, torches, furniture, portals, gate and rival cores in the dark are hidden; minimap and tile info too - the nearest portal is known from the start; the seer's eye opens a wide area for 20 seconds without remembering it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7f4d5ca7b4
commit
240e89d9cb
11 files changed
+244
-17
No files matched your search
+166
@@ -0,0 +1,166 @@
|
||||
// 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 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<S: Serializer>(v: &[bool], s: S) -> Result<S::Ok, S::Error> {
|
||||
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<Vec<bool>, 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 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;
|
||||
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);
|
||||
// own ground and everything around it
|
||||
for i in 0..n {
|
||||
if self.map.t[i].owner == Owner::Player {
|
||||
let (x, y) = xy(i);
|
||||
for dy in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
if inb(x + dx, y + dy) {
|
||||
vis[idx(x + dx, y + dy)] = 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 && e.side == 0 && !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;
|
||||
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 {
|
||||
let j = idx(nx, ny);
|
||||
if stamp[j] != sid {
|
||||
stamp[j] = sid;
|
||||
q.push_back((j, d + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 mut g = Game::new(6, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
assert!(g.map.shown(idx(cx + 1, cy + 1)));
|
||||
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);
|
||||
g.update_fog(1.0);
|
||||
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)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user