2023-02-11 01:21:52 -08:00
|
|
|
# Eryn Wells <eryn@erynwells.me>
|
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
from typing import List, Optional
|
2023-02-12 15:55:01 -08:00
|
|
|
|
|
|
|
import numpy as np
|
2023-03-07 21:29:05 -08:00
|
|
|
from tcod import event as tev
|
2023-02-11 01:21:52 -08:00
|
|
|
from tcod.console import Console
|
|
|
|
|
2023-02-12 19:47:27 -08:00
|
|
|
from .. import log
|
2023-03-05 13:37:51 -08:00
|
|
|
from ..geometry import Point, Rect, Size, Vector
|
2023-02-12 15:55:01 -08:00
|
|
|
from ..map import Map
|
2023-02-12 19:47:27 -08:00
|
|
|
from ..object import Entity, Hero
|
2023-02-11 01:21:52 -08:00
|
|
|
|
|
|
|
|
|
|
|
class Window:
|
2023-03-07 21:29:05 -08:00
|
|
|
'''A user interface window. It can be framed and it can handle events.'''
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
class EventHandler(tev.EventDispatch[bool]):
|
|
|
|
'''
|
|
|
|
Handles events for a Window. Event dispatch methods return True if the event
|
|
|
|
was handled and no further action is needed.
|
|
|
|
'''
|
|
|
|
|
|
|
|
def __init__(self, window: 'Window'):
|
|
|
|
super().__init__()
|
|
|
|
self.window = window
|
|
|
|
|
|
|
|
def mouse_point_for_event(self, event: tev.MouseState) -> Point:
|
|
|
|
'''
|
|
|
|
Return the mouse point in tiles for a window event. Raises a ValueError
|
|
|
|
if the event is not a mouse event.
|
|
|
|
'''
|
|
|
|
if not isinstance(event, tev.MouseState):
|
|
|
|
raise ValueError("Can't get mouse point for non-mouse event")
|
|
|
|
|
|
|
|
return Point(event.tile.x, event.tile.y)
|
|
|
|
|
|
|
|
def ev_keydown(self, event: tev.KeyDown) -> bool:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def ev_keyup(self, event: tev.KeyUp) -> bool:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def ev_mousemotion(self, event: tev.MouseMotion) -> bool:
|
|
|
|
mouse_point = self.mouse_point_for_event(event)
|
|
|
|
|
|
|
|
if mouse_point not in self.window.bounds:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def __init__(self, bounds: Rect, *, framed: bool = True, event_handler: Optional['EventHandler'] = None):
|
2023-02-11 01:21:52 -08:00
|
|
|
self.bounds = bounds
|
|
|
|
self.is_framed = framed
|
2023-03-07 21:29:05 -08:00
|
|
|
self.event_handler = event_handler or self.__class__.EventHandler(self)
|
2023-02-11 01:21:52 -08:00
|
|
|
|
|
|
|
@property
|
2023-02-12 15:55:01 -08:00
|
|
|
def drawable_bounds(self) -> Rect:
|
2023-03-05 13:37:51 -08:00
|
|
|
'''
|
|
|
|
The bounds of the window that is drawable, inset by its frame if
|
|
|
|
`is_framed` is `True`.
|
|
|
|
'''
|
2023-02-11 01:21:52 -08:00
|
|
|
if self.is_framed:
|
|
|
|
return self.bounds.inset_rect(1, 1, 1, 1)
|
|
|
|
return self.bounds
|
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
def convert_console_point(self, point: Point) -> Optional[Point]:
|
|
|
|
'''
|
|
|
|
Converts a point in console coordinates to window-relative coordinates.
|
|
|
|
If the point is out of bounds of the window, return None.
|
|
|
|
'''
|
|
|
|
converted_point = point - Vector.from_point(self.bounds.origin)
|
|
|
|
return converted_point if converted_point in self.bounds else None
|
|
|
|
|
2023-02-11 01:21:52 -08:00
|
|
|
def draw(self, console: Console):
|
2023-02-12 19:47:27 -08:00
|
|
|
'''Draw the window to the conole'''
|
2023-02-11 01:21:52 -08:00
|
|
|
if self.is_framed:
|
|
|
|
console.draw_frame(
|
|
|
|
self.bounds.origin.x,
|
|
|
|
self.bounds.origin.y,
|
|
|
|
self.bounds.size.width,
|
|
|
|
self.bounds.size.height)
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
drawable_bounds = self.drawable_bounds
|
|
|
|
console.draw_rect(drawable_bounds.min_x, drawable_bounds.min_y,
|
|
|
|
drawable_bounds.width, drawable_bounds.height,
|
|
|
|
ord(' '), (255, 255, 255), (0, 0, 0))
|
|
|
|
|
2023-02-12 15:55:01 -08:00
|
|
|
|
|
|
|
class MapWindow(Window):
|
|
|
|
'''A Window that displays a game map'''
|
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
class EventHandler(Window.EventHandler):
|
|
|
|
'''An event handler for the MapWindow.'''
|
|
|
|
|
|
|
|
def ev_mousemotion(self, event: tev.MouseMotion) -> bool:
|
|
|
|
mouse_point = self.window.convert_console_point(self.mouse_point_for_event(event))
|
|
|
|
if not mouse_point:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# TODO: Convert window point to map point
|
|
|
|
# TODO: Perform a path finding operation from the hero to the mouse point
|
|
|
|
# TODO: Highlight those points on the map
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
2023-02-12 19:47:27 -08:00
|
|
|
# pylint: disable=redefined-builtin
|
|
|
|
def __init__(self, bounds: Rect, map: Map, **kwargs):
|
2023-02-12 15:55:01 -08:00
|
|
|
super().__init__(bounds, **kwargs)
|
2023-02-12 19:47:27 -08:00
|
|
|
self.map = map
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-02-12 19:47:27 -08:00
|
|
|
self.drawable_map_bounds = map.bounds
|
2023-02-12 15:55:01 -08:00
|
|
|
self.entities: List[Entity] = []
|
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
self._draw_bounds = self.drawable_bounds
|
|
|
|
|
2023-02-12 19:47:27 -08:00
|
|
|
def update_drawable_map_bounds(self, hero: Hero):
|
2023-03-05 13:37:51 -08:00
|
|
|
'''
|
|
|
|
Figure out what portion of the map is drawable and update
|
|
|
|
`self.drawable_map_bounds`. This method attempts to keep the hero
|
|
|
|
centered in the map viewport, while not overscrolling the map in either
|
|
|
|
direction.
|
|
|
|
'''
|
2023-02-12 19:47:27 -08:00
|
|
|
bounds = self.drawable_bounds
|
|
|
|
map_bounds = self.map.bounds
|
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
viewport_is_wider_than_map = bounds.width > map_bounds.width
|
|
|
|
viewport_is_taller_than_map = bounds.height > map_bounds.height
|
|
|
|
|
|
|
|
if viewport_is_wider_than_map and viewport_is_taller_than_map:
|
|
|
|
# The whole map fits within the window's drawable bounds
|
2023-02-12 19:47:27 -08:00
|
|
|
self.drawable_map_bounds = map_bounds
|
2023-03-05 13:37:51 -08:00
|
|
|
return
|
2023-02-12 19:47:27 -08:00
|
|
|
|
|
|
|
# Attempt to keep the player centered in the viewport.
|
|
|
|
hero_point = hero.position
|
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
if viewport_is_wider_than_map:
|
|
|
|
x = 0
|
|
|
|
else:
|
|
|
|
x = min(max(0, hero_point.x - bounds.mid_x), map_bounds.max_x - bounds.width)
|
|
|
|
|
|
|
|
if viewport_is_taller_than_map:
|
|
|
|
y = 0
|
|
|
|
else:
|
|
|
|
y = min(max(0, hero_point.y - bounds.mid_y), map_bounds.max_y - bounds.height)
|
|
|
|
|
2023-02-12 19:47:27 -08:00
|
|
|
origin = Point(x, y)
|
2023-03-05 13:37:51 -08:00
|
|
|
size = Size(min(bounds.width, map_bounds.width), min(bounds.height, map_bounds.height))
|
|
|
|
|
|
|
|
self.drawable_map_bounds = Rect(origin, size)
|
2023-02-12 19:47:27 -08:00
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
def _update_draw_bounds(self):
|
|
|
|
'''
|
|
|
|
The area where the map should actually be drawn, accounting for the size
|
|
|
|
of the viewport (`drawable_bounds`)and the size of the map (`self.map.bounds`).
|
|
|
|
'''
|
|
|
|
drawable_map_bounds = self.drawable_map_bounds
|
|
|
|
drawable_bounds = self.drawable_bounds
|
|
|
|
|
|
|
|
viewport_is_wider_than_map = drawable_bounds.width >= drawable_map_bounds.width
|
|
|
|
viewport_is_taller_than_map = drawable_bounds.height >= drawable_map_bounds.height
|
|
|
|
|
|
|
|
if viewport_is_wider_than_map:
|
|
|
|
# Center the map horizontally in the viewport
|
|
|
|
origin_x = drawable_bounds.min_x + (drawable_bounds.width - drawable_map_bounds.width) // 2
|
|
|
|
width = drawable_map_bounds.width
|
|
|
|
else:
|
|
|
|
origin_x = drawable_bounds.min_x
|
|
|
|
width = drawable_bounds.width
|
|
|
|
|
|
|
|
if viewport_is_taller_than_map:
|
|
|
|
# Center the map vertically in the viewport
|
|
|
|
origin_y = drawable_bounds.min_y + (drawable_bounds.height - drawable_map_bounds.height) // 2
|
|
|
|
height = drawable_map_bounds.height
|
|
|
|
else:
|
|
|
|
origin_y = drawable_bounds.min_y
|
|
|
|
height = drawable_bounds.height
|
|
|
|
|
|
|
|
self._draw_bounds = Rect(Point(origin_x, origin_y), Size(width, height))
|
2023-02-12 19:47:27 -08:00
|
|
|
|
2023-02-12 15:55:01 -08:00
|
|
|
def draw(self, console: Console):
|
|
|
|
super().draw(console)
|
2023-03-05 13:37:51 -08:00
|
|
|
self._update_draw_bounds()
|
2023-02-12 15:55:01 -08:00
|
|
|
self._draw_map(console)
|
|
|
|
self._draw_entities(console)
|
|
|
|
|
|
|
|
def _draw_map(self, console: Console):
|
2023-02-12 19:47:27 -08:00
|
|
|
drawable_map_bounds = self.drawable_map_bounds
|
2023-02-12 15:55:01 -08:00
|
|
|
drawable_bounds = self.drawable_bounds
|
|
|
|
|
2023-02-12 19:47:27 -08:00
|
|
|
log.UI.info('Drawing map')
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-02-12 19:47:27 -08:00
|
|
|
map_slice = np.s_[
|
2023-03-05 13:37:51 -08:00
|
|
|
drawable_map_bounds.min_x: drawable_map_bounds.max_x + 1,
|
|
|
|
drawable_map_bounds.min_y: drawable_map_bounds.max_y + 1]
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
console_draw_bounds = self._draw_bounds
|
2023-02-12 19:47:27 -08:00
|
|
|
console_slice = np.s_[
|
2023-03-05 13:37:51 -08:00
|
|
|
console_draw_bounds.min_x: console_draw_bounds.max_x + 1,
|
|
|
|
console_draw_bounds.min_y: console_draw_bounds.max_y + 1]
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
log.UI.debug('Map bounds=%s, slice=%s', drawable_map_bounds, map_slice)
|
|
|
|
log.UI.debug('Console bounds=%s, slice=%s', drawable_bounds, console_slice)
|
2023-02-12 19:47:27 -08:00
|
|
|
|
|
|
|
console.tiles_rgb[console_slice] = self.map.composited_tiles[map_slice]
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
log.UI.info('Done drawing map')
|
|
|
|
|
2023-02-12 15:55:01 -08:00
|
|
|
def _draw_entities(self, console):
|
2023-02-12 19:47:27 -08:00
|
|
|
map_bounds_vector = Vector.from_point(self.drawable_map_bounds.origin)
|
2023-03-05 13:37:51 -08:00
|
|
|
draw_bounds_vector = Vector.from_point(self._draw_bounds.origin)
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-02-12 19:47:27 -08:00
|
|
|
log.UI.info('Drawing entities')
|
|
|
|
|
2023-02-12 15:55:01 -08:00
|
|
|
for ent in self.entities:
|
2023-02-12 19:47:27 -08:00
|
|
|
# Only draw entities that are in the field of view
|
2023-03-05 13:37:51 -08:00
|
|
|
if not self.map.point_is_visible(ent.position):
|
2023-02-12 15:55:01 -08:00
|
|
|
continue
|
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
# Entity positions are relative to the (0, 0) point of the Map. In
|
|
|
|
# order to render them in the correct position in the console, we
|
|
|
|
# need to transform them into viewport-relative coordinates.
|
2023-02-12 15:55:01 -08:00
|
|
|
entity_position = ent.position
|
2023-03-05 13:37:51 -08:00
|
|
|
map_tile_at_entity_position = self.map.composited_tiles[entity_position.numpy_index]
|
2023-02-12 15:55:01 -08:00
|
|
|
|
2023-03-05 13:37:51 -08:00
|
|
|
position = ent.position - map_bounds_vector + draw_bounds_vector
|
2023-02-12 19:47:27 -08:00
|
|
|
|
|
|
|
if isinstance(ent, Hero):
|
2023-03-05 13:37:51 -08:00
|
|
|
log.UI.debug('Hero position: map=%s, window=%s', entity_position, position)
|
2023-02-12 19:47:27 -08:00
|
|
|
|
2023-02-12 15:55:01 -08:00
|
|
|
console.print(
|
|
|
|
x=position.x,
|
|
|
|
y=position.y,
|
|
|
|
string=ent.symbol,
|
|
|
|
fg=ent.foreground,
|
|
|
|
bg=tuple(map_tile_at_entity_position['bg'][:3]))
|
2023-03-05 13:37:51 -08:00
|
|
|
|
|
|
|
log.UI.info('Done drawing entities')
|