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.
This commit is contained in:
Eryn Wells 2023-02-10 22:35:51 -08:00
parent f05dfdef55
commit 6c9d01771f
4 changed files with 80 additions and 29 deletions

View file

@ -68,7 +68,9 @@ class Engine:
self.map = Map(map_size, map_generator) self.map = Map(map_size, map_generator)
self.event_handler: 'EventHandler' = MainGameEventHandler(self) 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() self.entities: MutableSet[Entity] = set()
@ -103,6 +105,8 @@ class Engine:
def print_to_console(self, console): def print_to_console(self, console):
'''Print the whole game to the given console.''' '''Print the whole game to the given console.'''
self.map.highlight_points(self.__mouse_path_points or [])
self.map.print_to_console(console) self.map.print_to_console(console)
console.print(x=1, y=45, string='HP:') console.print(x=1, y=45, string='HP:')
@ -124,7 +128,7 @@ class Engine:
ent.print_to_console(console) 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) entities_at_mouse_position.append(ent)
if len(entities_at_mouse_position) > 0: if len(entities_at_mouse_position) > 0:
@ -141,12 +145,15 @@ class Engine:
self.event_handler.handle_events(context) self.event_handler.handle_events(context)
self.finish_turn() self.finish_turn()
def process_input_action(self, action: Action) -> ActionResult: def process_input_action(self, action: Action):
'''Process an Action from player input''' '''Process an Action from player input'''
log.ACTIONS_TREE.info('Processing Hero Actions') log.ACTIONS_TREE.info('Processing Hero Actions')
log.ACTIONS_TREE.info('|-> %s', action.actor) 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) result = self._perform_action_until_done(action)
# Player's action failed, don't proceed with turn. # Player's action failed, don't proceed with turn.
@ -225,7 +232,7 @@ class Engine:
return result 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.''' '''Compute visible area of the map based on the player's position and point of view.'''
# FIXME: Move this to the Map class # FIXME: Move this to the Map class
self.map.visible[:] = tcod.map.compute_fov( self.map.visible[:] = tcod.map.compute_fov(
@ -233,9 +240,37 @@ class Engine:
tuple(self.hero.position), tuple(self.hero.position),
radius=8) radius=8)
# Visible tiles should be added to the explored list # Add visible tiles to the explored grid
self.map.explored |= self.map.visible 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: def begin_turn(self) -> None:
'''Begin the current turn''' '''Begin the current turn'''
if self.did_begin_turn: if self.did_begin_turn:

View file

@ -28,7 +28,7 @@ class EventHandler(tcod.event.EventDispatch[Action]):
context.convert_event(event) context.convert_event(event)
self.handle_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 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. succeeds, also process actions from other Entities.
@ -95,7 +95,7 @@ class MainGameEventHandler(EventHandler):
mouse_point = Point(event.tile.x, event.tile.y) mouse_point = Point(event.tile.x, event.tile.y)
if not self.engine.map.tile_is_in_bounds(mouse_point): if not self.engine.map.tile_is_in_bounds(mouse_point):
mouse_point = None mouse_point = None
self.engine.current_mouse_point = mouse_point self.engine.update_mouse_point(mouse_point)
class GameOverEventHandler(EventHandler): class GameOverEventHandler(EventHandler):

View file

@ -6,6 +6,7 @@ parts of a map.
''' '''
import random import random
from typing import Iterable
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
@ -20,17 +21,17 @@ class Map:
def __init__(self, size: Size, generator: MapGenerator): def __init__(self, size: Size, generator: MapGenerator):
self.size = size self.size = size
self.generator = generator
self.tiles = np.full(tuple(size), fill_value=Empty, order='F') self.tiles = np.full(tuple(size), fill_value=Empty, order='F')
generator.generate(self.tiles) generator.generate(self.tiles)
self.up_stairs = generator.up_stairs self.up_stairs = generator.up_stairs
self.down_stairs = generator.down_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 # 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 # 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 self.__walkable_points = None
@ -47,7 +48,14 @@ class Map:
def tile_is_walkable(self, point: Point) -> bool: def tile_is_walkable(self, point: Point) -> bool:
'''Return True if the tile at the given point is walkable''' '''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: def print_to_console(self, console: tcod.Console) -> None:
'''Render the map to the console.''' '''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 # 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. # array, draw it with the "dark" color. Otherwise, draw it as Empty.
console.tiles_rgb[0:size.width, 0:size.height] = np.select( console.tiles_rgb[0:size.width, 0:size.height] = np.select(
condlist=[self.visible, self.explored], condlist=[self.highlighted, self.visible, self.explored],
choicelist=[self.tiles['light'], self.tiles['dark']], choicelist=[self.tiles['highlighted'], self.tiles['light'], self.tiles['dark']],
default=Shroud) default=Shroud)

View file

@ -6,9 +6,9 @@ graphic_datatype = np.dtype([
# Character, a Unicode codepoint represented as an int32 # Character, a Unicode codepoint represented as an int32
('ch', np.int32), ('ch', np.int32),
# Foreground color, three bytes # Foreground color, three bytes
('fg', '3B'), ('fg', '4B'),
# Background color, three bytes # Background color, three bytes
('bg', '3B'), ('bg', '4B'),
]) ])
tile_datatype = np.dtype([ tile_datatype = np.dtype([
@ -20,37 +20,45 @@ tile_datatype = np.dtype([
('dark', graphic_datatype), ('dark', graphic_datatype),
# A graphic struct (as above) defining the look of this tile when it's visible # A graphic struct (as above) defining the look of this tile when it's visible
('light', graphic_datatype), ('light', graphic_datatype),
# A graphic struct (as above) defining the look of this tile when it's highlighted
('highlighted', graphic_datatype),
]) ])
def tile(*, def tile(*,
walkable: int, walkable: int,
transparent: int, transparent: int,
dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]], dark: Tuple[int, Tuple[int, int, int, int], Tuple[int, int, int, int]],
light: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]]) -> np.ndarray: light: Tuple[int, Tuple[int, int, int, int], Tuple[int, int, int, int]],
return np.array((walkable, transparent, dark, light), dtype=tile_datatype) 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 # 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( Empty = tile(
walkable=False, transparent=False, walkable=False, transparent=False,
dark=(ord(' '), (255, 255, 255), (0, 0, 0)), dark=(ord('#'), (20, 20, 20, 255), (0, 0, 0, 0)),
light=(ord(' '), (255, 255, 255), (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( Floor = tile(
walkable=True, transparent=True, walkable=True, transparent=True,
dark=(ord('·'), (80, 80, 100), (50, 50, 50)), dark=(ord('·'), (80, 80, 100, 255), (50, 50, 50, 255)),
light=(ord('·'), (100, 100, 120), (80, 80, 100))) light=(ord('·'), (100, 100, 120, 255), (80, 80, 100, 255)),
highlighted=(ord('·'), (100, 100, 120, 255), (80, 80, 150, 255)))
StairsUp = tile( StairsUp = tile(
walkable=True, transparent=True, walkable=True, transparent=True,
dark=(ord('<'), (80, 80, 100), (50, 50, 50)), dark=(ord('<'), (80, 80, 100, 255), (50, 50, 50, 255)),
light=(ord('<'), (100, 100, 120), (80, 80, 100))) light=(ord('<'), (100, 100, 120, 255), (80, 80, 100, 255)),
highlighted=(ord('<'), (100, 100, 120, 255), (80, 80, 150, 255)))
StairsDown = tile( StairsDown = tile(
walkable=True, transparent=True, walkable=True, transparent=True,
dark=(ord('>'), (80, 80, 100), (50, 50, 50)), dark=(ord('>'), (80, 80, 100, 255), (50, 50, 50, 255)),
light=(ord('>'), (100, 100, 120), (80, 80, 100))) light=(ord('>'), (100, 100, 120, 255), (80, 80, 100, 255)),
highlighted=(ord('>'), (100, 100, 120, 255), (80, 80, 150, 255)))
Wall = tile( Wall = tile(
walkable=False, transparent=False, walkable=False, transparent=False,
dark=(ord(' '), (255, 255, 255), (0, 0, 150)), dark=(ord('#'), (80, 80, 80, 255), (0, 0, 0, 255)),
light=(ord(' '), (255, 255, 255), (50, 50, 200))) light=(ord('#'), (100, 100, 100, 255), (20, 20, 20, 255)),
highlighted=(ord('#'), (100, 100, 100, 255), (20, 20, 20, 255)))