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