scripts/fog_of_war.pyΒΆ
Part of Dungeon Explorer.
1"""Per-tile fog of war visibility tracking."""
2
3import numpy as np
4
5
6class FogOfWar:
7 """Tracks per-tile visibility for minimap and rendering.
8
9 States: 0=unexplored (black), 1=explored (dimmed), 2=visible (fully lit).
10 """
11
12 def __init__(self, width: int, height: int, reveal_radius: int = 5):
13 self.width = width
14 self.height = height
15 self.reveal_radius = reveal_radius
16 self._tiles = [[0] * width for _ in range(height)]
17 self._prev_visible: list[tuple[int, int]] = []
18
19 def update(self, player_grid_x: int, player_grid_y: int) -> None:
20 """Update visibility around the player position."""
21 r = self.reveal_radius
22 # Dim only previously visible tiles (not the full grid)
23 for x, y in self._prev_visible:
24 if self._tiles[y][x] == 2:
25 self._tiles[y][x] = 1
26
27 # Reveal around player and track visible tiles
28 visible: list[tuple[int, int]] = []
29 for dy in range(-r, r + 1):
30 for dx in range(-r, r + 1):
31 if dx * dx + dy * dy <= r * r:
32 nx, ny = player_grid_x + dx, player_grid_y + dy
33 if 0 <= nx < self.width and 0 <= ny < self.height:
34 self._tiles[ny][nx] = 2
35 visible.append((nx, ny))
36 self._prev_visible = visible
37
38 def get_state(self, x: int, y: int) -> int:
39 """Get visibility state at grid position (0/1/2)."""
40 if 0 <= x < self.width and 0 <= y < self.height:
41 return self._tiles[y][x]
42 return 0
43
44 def state_array(self) -> np.ndarray:
45 """Return the per-tile visibility state as an ``(H, W)`` int array.
46
47 The instanced fog overlay reads this to recompute its tint plane in one
48 vectorised pass (state -> dim alpha) whenever the fog changes. ``_tiles``
49 is row-major ``[y][x]``, so the array indexes as ``[y, x]`` and feeds
50 straight into ``TileMapLayer.set_cells`` (origin ``(0, 0)``).
51 """
52 return np.asarray(self._tiles, dtype=np.int32)
53
54 def is_visible(self, x: int, y: int) -> bool:
55 return self.get_state(x, y) == 2
56
57 def is_explored(self, x: int, y: int) -> bool:
58 return self.get_state(x, y) >= 1
59
60 def reveal_all(self) -> None:
61 """Reveal the entire map."""
62 for y in range(self.height):
63 for x in range(self.width):
64 self._tiles[y][x] = 2
65
66 def explored_fraction(self) -> float:
67 """Return fraction of map that has been explored."""
68 total = self.width * self.height
69 explored = sum(1 for row in self._tiles for cell in row if cell > 0)
70 return explored / max(1, total)
71
72 def to_dict(self) -> dict:
73 return {"width": self.width, "height": self.height, "tiles": self._tiles, "reveal_radius": self.reveal_radius}
74
75 @classmethod
76 def from_dict(cls, d: dict) -> FogOfWar:
77 fog = cls(d["width"], d["height"], d.get("reveal_radius", 5))
78 fog._tiles = d["tiles"]
79 return fog