From 6c9d01771f2679bb1b981f8e6f3e6dc659fe0718 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 10 Feb 2023 22:35:51 -0800 Subject: [PATCH] Draw a path from the hero to the current mouse point Add a highlight grid to Map, with locations set to True if that point should be drawn with a "highlight" treatment. Add the highlight graphic_dtype to all Tiles. --- erynrl/engine.py | 45 +++++++++++++++++++++++++++++++++++++----- erynrl/events.py | 4 ++-- erynrl/map/__init__.py | 20 +++++++++++++------ erynrl/map/tile.py | 40 ++++++++++++++++++++++--------------- 4 files changed, 80 insertions(+), 29 deletions(-) diff --git a/erynrl/engine.py b/erynrl/engine.py index 0c1a595..23ccbec 100644 --- a/erynrl/engine.py +++ b/erynrl/engine.py @@ -68,7 +68,9 @@ class Engine: self.map = Map(map_size, map_generator) self.event_handler: 'EventHandler' = MainGameEventHandler(self) - self.current_mouse_point: Optional[Point] = None + + self.__current_mouse_point: Optional[Point] = None + self.__mouse_path_points: Optional[List[Point]] = None self.entities: MutableSet[Entity] = set() @@ -103,6 +105,8 @@ class Engine: def print_to_console(self, console): '''Print the whole game to the given console.''' + self.map.highlight_points(self.__mouse_path_points or []) + self.map.print_to_console(console) console.print(x=1, y=45, string='HP:') @@ -124,7 +128,7 @@ class Engine: ent.print_to_console(console) - if ent.position == self.current_mouse_point: + if ent.position == self.__current_mouse_point: entities_at_mouse_position.append(ent) if len(entities_at_mouse_position) > 0: @@ -141,12 +145,15 @@ class Engine: self.event_handler.handle_events(context) self.finish_turn() - def process_input_action(self, action: Action) -> ActionResult: + def process_input_action(self, action: Action): '''Process an Action from player input''' log.ACTIONS_TREE.info('Processing Hero Actions') log.ACTIONS_TREE.info('|-> %s', action.actor) + # Clear the mouse path highlight before handling actions. + self.__mouse_path_points = None + result = self._perform_action_until_done(action) # Player's action failed, don't proceed with turn. @@ -225,7 +232,7 @@ class Engine: return result - def update_field_of_view(self) -> None: + def update_field_of_view(self): '''Compute visible area of the map based on the player's position and point of view.''' # FIXME: Move this to the Map class self.map.visible[:] = tcod.map.compute_fov( @@ -233,9 +240,37 @@ class Engine: tuple(self.hero.position), radius=8) - # Visible tiles should be added to the explored list + # Add visible tiles to the explored grid self.map.explored |= self.map.visible + def update_mouse_point(self, mouse_point: Optional[Point]): + if mouse_point == self.__current_mouse_point: + return + + should_render_mouse_path = ( + mouse_point + and self.map.tile_is_in_bounds(mouse_point) + and self.map.tile_is_walkable(mouse_point)) + + if not should_render_mouse_path: + self.__current_mouse_point = None + self.__mouse_path_points = None + return + + self.__current_mouse_point = mouse_point + + path_from_hero_to_mouse_point = tcod.los.bresenham(tuple(self.hero.position), tuple(self.__current_mouse_point)) + mouse_path_points = [Point(x, y) for x, y in path_from_hero_to_mouse_point.tolist()] + + all_mouse_path_points_are_walkable = all( + self.map.tile_is_walkable(pt) and self.map.point_is_explored(pt) for pt in mouse_path_points) + if not all_mouse_path_points_are_walkable: + self.__current_mouse_point = None + self.__mouse_path_points = None + return + + self.__mouse_path_points = mouse_path_points + def begin_turn(self) -> None: '''Begin the current turn''' if self.did_begin_turn: diff --git a/erynrl/events.py b/erynrl/events.py index fd3577f..dc0d5ac 100644 --- a/erynrl/events.py +++ b/erynrl/events.py @@ -28,7 +28,7 @@ class EventHandler(tcod.event.EventDispatch[Action]): context.convert_event(event) self.handle_event(event) - def handle_event(self, event: tcod.event.Event) -> None: + def handle_event(self, event: tcod.event.Event): ''' Handle an event by transforming it into an Action and processing it until it is completed. If the Action succeeds, also process actions from other Entities. @@ -95,7 +95,7 @@ class MainGameEventHandler(EventHandler): mouse_point = Point(event.tile.x, event.tile.y) if not self.engine.map.tile_is_in_bounds(mouse_point): mouse_point = None - self.engine.current_mouse_point = mouse_point + self.engine.update_mouse_point(mouse_point) class GameOverEventHandler(EventHandler): diff --git a/erynrl/map/__init__.py b/erynrl/map/__init__.py index 9d8c945..d19797f 100644 --- a/erynrl/map/__init__.py +++ b/erynrl/map/__init__.py @@ -6,6 +6,7 @@ parts of a map. ''' import random +from typing import Iterable import numpy as np import numpy.typing as npt @@ -20,17 +21,17 @@ class Map: def __init__(self, size: Size, generator: MapGenerator): self.size = size - self.generator = generator self.tiles = np.full(tuple(size), fill_value=Empty, order='F') generator.generate(self.tiles) self.up_stairs = generator.up_stairs self.down_stairs = generator.down_stairs + self.highlighted = np.full(tuple(self.size), fill_value=False, order='F') # Map tiles that are currently visible to the player - self.visible = np.full(tuple(self.size), fill_value=True, order='F') + self.visible = np.full(tuple(self.size), fill_value=False, order='F') # Map tiles that the player has explored - self.explored = np.full(tuple(self.size), fill_value=True, order='F') + self.explored = np.full(tuple(self.size), fill_value=False, order='F') self.__walkable_points = None @@ -47,7 +48,14 @@ class Map: def tile_is_walkable(self, point: Point) -> bool: '''Return True if the tile at the given point is walkable''' - return self.tiles[point.x, point.y]['walkable'] + return self.tile_is_in_bounds(point) and self.tiles[point.x, point.y]['walkable'] + + def highlight_points(self, points: Iterable[Point]): + '''Update the highlight graph with the list of points to highlight.''' + self.highlighted.fill(False) + + for pt in points if points: + self.highlighted[pt.x, pt.y] = True def print_to_console(self, console: tcod.Console) -> None: '''Render the map to the console.''' @@ -56,6 +64,6 @@ class Map: # If a tile is in the visible array, draw it with the "light" color. If it's not, but it's in the explored # array, draw it with the "dark" color. Otherwise, draw it as Empty. console.tiles_rgb[0:size.width, 0:size.height] = np.select( - condlist=[self.visible, self.explored], - choicelist=[self.tiles['light'], self.tiles['dark']], + condlist=[self.highlighted, self.visible, self.explored], + choicelist=[self.tiles['highlighted'], self.tiles['light'], self.tiles['dark']], default=Shroud) diff --git a/erynrl/map/tile.py b/erynrl/map/tile.py index 23c3eb7..d869920 100644 --- a/erynrl/map/tile.py +++ b/erynrl/map/tile.py @@ -6,9 +6,9 @@ graphic_datatype = np.dtype([ # Character, a Unicode codepoint represented as an int32 ('ch', np.int32), # Foreground color, three bytes - ('fg', '3B'), + ('fg', '4B'), # Background color, three bytes - ('bg', '3B'), + ('bg', '4B'), ]) tile_datatype = np.dtype([ @@ -20,37 +20,45 @@ tile_datatype = np.dtype([ ('dark', graphic_datatype), # A graphic struct (as above) defining the look of this tile when it's visible ('light', graphic_datatype), + # A graphic struct (as above) defining the look of this tile when it's highlighted + ('highlighted', graphic_datatype), ]) def tile(*, walkable: int, transparent: 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: - return np.array((walkable, transparent, dark, light), dtype=tile_datatype) + dark: Tuple[int, Tuple[int, int, int, int], Tuple[int, int, int, int]], + light: Tuple[int, Tuple[int, int, int, int], Tuple[int, int, int, int]], + highlighted: Tuple[int, Tuple[int, int, int, int], Tuple[int, int, int, int]]) -> np.ndarray: + return np.array((walkable, transparent, dark, light, highlighted), dtype=tile_datatype) # An overlay color for tiles that are not visible and have not been explored -Shroud = np.array((ord(' '), (255, 255, 255), (0, 0, 0)), dtype=graphic_datatype) +Shroud = np.array((ord(' '), (255, 255, 255, 255), (0, 0, 0, 0)), dtype=graphic_datatype) Empty = tile( walkable=False, transparent=False, - dark=(ord(' '), (255, 255, 255), (0, 0, 0)), - light=(ord(' '), (255, 255, 255), (0, 0, 0))) + dark=(ord('#'), (20, 20, 20, 255), (0, 0, 0, 0)), + light=(ord('#'), (20, 20, 20, 255), (0, 0, 0, 0)), + highlighted=(ord('#'), (20, 20, 20, 255), (30, 30, 30, 255))) Floor = tile( walkable=True, transparent=True, - dark=(ord('·'), (80, 80, 100), (50, 50, 50)), - light=(ord('·'), (100, 100, 120), (80, 80, 100))) + dark=(ord('·'), (80, 80, 100, 255), (50, 50, 50, 255)), + light=(ord('·'), (100, 100, 120, 255), (80, 80, 100, 255)), + highlighted=(ord('·'), (100, 100, 120, 255), (80, 80, 150, 255))) StairsUp = tile( walkable=True, transparent=True, - dark=(ord('<'), (80, 80, 100), (50, 50, 50)), - light=(ord('<'), (100, 100, 120), (80, 80, 100))) + dark=(ord('<'), (80, 80, 100, 255), (50, 50, 50, 255)), + light=(ord('<'), (100, 100, 120, 255), (80, 80, 100, 255)), + highlighted=(ord('<'), (100, 100, 120, 255), (80, 80, 150, 255))) StairsDown = tile( walkable=True, transparent=True, - dark=(ord('>'), (80, 80, 100), (50, 50, 50)), - light=(ord('>'), (100, 100, 120), (80, 80, 100))) + dark=(ord('>'), (80, 80, 100, 255), (50, 50, 50, 255)), + light=(ord('>'), (100, 100, 120, 255), (80, 80, 100, 255)), + highlighted=(ord('>'), (100, 100, 120, 255), (80, 80, 150, 255))) Wall = tile( walkable=False, transparent=False, - dark=(ord(' '), (255, 255, 255), (0, 0, 150)), - light=(ord(' '), (255, 255, 255), (50, 50, 200))) + dark=(ord('#'), (80, 80, 80, 255), (0, 0, 0, 255)), + light=(ord('#'), (100, 100, 100, 255), (20, 20, 20, 255)), + highlighted=(ord('#'), (100, 100, 100, 255), (20, 20, 20, 255)))