diff --git a/actions.py b/actions.py index 9e79fd1..a5ba58e 100644 --- a/actions.py +++ b/actions.py @@ -6,6 +6,7 @@ if TYPE_CHECKING: from engine import Engine from entity import Entity + class Action: def perform(self, engine: Engine, entity: Entity) -> None: """Perform this action with the objects needed to determine its scope. @@ -36,8 +37,8 @@ class MovementAction(Action): dest_y = entity.y + self.dy if not engine.game_map.in_bounds(dest_x, dest_y): - return # Destination is out of bounds + return # Destination is out of bounds if not engine.game_map.tiles["walkable"][dest_x, dest_y]: - return # Destination is blocked by a tile. + return # Destination is blocked by a tile. - entity.move(self.dx, self.dy); + entity.move(self.dx, self.dy) diff --git a/engine.py b/engine.py index d93383d..e639be2 100644 --- a/engine.py +++ b/engine.py @@ -2,6 +2,7 @@ from typing import Set, Iterable, Any from tcod.context import Context from tcod.console import Console +from tcod.map import compute_fov from entity import Entity from game_map import GameMap @@ -10,16 +11,17 @@ from input_handlers import EventHandler class Engine: def __init__( - self, - entities: Set[Entity], - event_handler: EventHandler, - game_map: GameMap, - player: Entity + self, + entities: Set[Entity], + event_handler: EventHandler, + game_map: GameMap, + player: Entity ): self.entities = entities self.event_handler = event_handler self.game_map = game_map self.player = player + self.update_fov() def handle_events(self, events: Iterable[Any]) -> None: for event in events: @@ -30,11 +32,27 @@ class Engine: action.perform(self, self.player) + # Update the FOV before the player's next action. + self.update_fov() + + def update_fov(self) -> None: + """Recompute the visible area based on the player's point of view.""" + self.game_map.visible[:] = compute_fov( + self.game_map.tiles["transparent"], + (self.player.x, self.player.y), + radius=8, + ) + + # If a tile is "visible" it should be added to "explored" + self.game_map.explored |= self.game_map.visible + def render(self, console: Console, context: Context) -> None: self.game_map.render(console) for entity in self.entities: - console.print(entity.x, entity.y, entity.char, fg=entity.color) + # Only print entities that are in FOV + if self.game_map.visible[entity.x, entity.y]: + console.print(entity.x, entity.y, entity.char, fg=entity.color) # Actually output to screen context.present(console) diff --git a/game_map.py b/game_map.py index 5587217..3e81a28 100644 --- a/game_map.py +++ b/game_map.py @@ -9,9 +9,25 @@ class GameMap: self.width, self.height = width, height self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F") + self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see + self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before + def in_bounds(self, x: int, y: int) -> bool: """Return True if x and y are inside the bounds of the map.""" return 0 <= x < self.width and 0 <= y < self.height def render(self, console: Console): - console.tiles_rgb[0: self.width, 0: self.height] = self.tiles["dark"] + """ + Renders the map. + + If a tile is in the "visible" array, then draw it with the "light" colors. + If it isn't, but it's in the "explored" array, then draw it with the "dark" colors. + Otherwise, the default is "SHROUD". + :param console: + :return: + """ + console.tiles_rgb[0: self.width, 0: self.height] = np.select( + condlist=[self.visible, self.explored], + choicelist=[self.tiles["light"], self.tiles["dark"]], + default=tile_types.SHROUD + ) diff --git a/tile_types.py b/tile_types.py index 37bf9ec..a90c550 100644 --- a/tile_types.py +++ b/tile_types.py @@ -17,6 +17,7 @@ tile_dt = np.dtype( ("walkable", np.bool), # True if this tile can be walked over. ("transparent", np.bool), # True if this tile doesn't block FOV. ("dark", graphic_dt), # Graphics for when this tile is not in FOV. + ("light", graphic_dt), # Graphics for when the tile is in FOV. ] ) @@ -25,20 +26,26 @@ def new_tile( *, # Enforce the use of keywords, so that parameter order doesn't matter walkable: int, transparent: int, - dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]] + dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]], + light: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]] ) -> np.ndarray: """Helper function for defining individual tile types""" - return np.array((walkable, transparent, dark), dtype=tile_dt) + return np.array((walkable, transparent, dark, light), dtype=tile_dt) +# SHROUD represents unexplored, unseen tiles +SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=graphic_dt) + floor = new_tile( walkable=True, transparent=True, - dark=(ord(" "), (255, 255, 255), (50, 50, 150)) + dark=(ord(" "), (255, 255, 255), (50, 50, 150)), + light=(ord(" "), (255, 255, 255), (200, 180, 50)), ) wall = new_tile( walkable=False, transparent=False, - dark=(ord(" "), (255, 255, 255), (0, 0, 100)) + dark=(ord(" "), (255, 255, 255), (0, 0, 100)), + light=(ord(" "), (255, 255, 255), (130, 110, 50)), )