Figure out (finally!) the mouse coordinates in the MapWindow

This commit is contained in:
Eryn Wells 2023-03-10 23:53:08 -08:00
parent 1018febeab
commit 078520678d
4 changed files with 112 additions and 58 deletions

View file

@ -205,6 +205,16 @@ class Rect:
'''Maximum y-value that is still within the bounds of this rectangle.''' '''Maximum y-value that is still within the bounds of this rectangle.'''
return self.origin.y + self.size.height - 1 return self.origin.y + self.size.height - 1
@property
def end_x(self) -> int:
'''X-value beyond the end of the rectangle.'''
return self.origin.x + self.size.width
@property
def end_y(self) -> int:
'''Y-value beyond the end of the rectangle.'''
return self.origin.y + self.size.height
@property @property
def width(self) -> int: def width(self) -> int:
'''The width of the rectangle. A convenience property for accessing `self.size.width`.''' '''The width of the rectangle. A convenience property for accessing `self.size.width`.'''
@ -299,8 +309,7 @@ class Rect:
raise TypeError(f'{self.__class__.__name__} cannot contain value of type {other.__class__.__name__}') raise TypeError(f'{self.__class__.__name__} cannot contain value of type {other.__class__.__name__}')
def __contains_point(self, pt: Point) -> bool: def __contains_point(self, pt: Point) -> bool:
return (pt.x >= self.min_x and pt.x <= self.max_x return self.min_x <= pt.x <= self.max_x and self.min_y <= pt.y <= self.max_y
and pt.y >= self.min_y and pt.y <= self.max_y)
def __contains_rect(self, other: 'Rect') -> bool: def __contains_rect(self, other: 'Rect') -> bool:
return (self.min_x <= other.min_x return (self.min_x <= other.min_x

View file

@ -44,7 +44,6 @@ class Interface:
hero = self.engine.hero hero = self.engine.hero
self.info_window.update_hero(hero) self.info_window.update_hero(hero)
self.map_window.update_drawable_map_bounds()
sorted_entities = sorted(self.engine.entities, key=lambda e: e.render_order.value) sorted_entities = sorted(self.engine.entities, key=lambda e: e.render_order.value)
self.map_window.entities = sorted_entities self.map_window.entities = sorted_entities

View file

@ -1,5 +1,9 @@
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
'''
Declares the Window class.
'''
from typing import Generic, Optional, TypeVar from typing import Generic, Optional, TypeVar
from tcod import event as tev from tcod import event as tev
@ -49,26 +53,34 @@ class Window:
def __init__(self, bounds: Rect, *, framed: bool = True, event_handler: Optional['EventHandler'] = None): def __init__(self, bounds: Rect, *, framed: bool = True, event_handler: Optional['EventHandler'] = None):
self.bounds = bounds self.bounds = bounds
'''The window's bounds in console coordinates'''
self.is_framed = framed self.is_framed = framed
'''A `bool` indicating whether the window has a frame'''
self.event_handler = event_handler or self.__class__.EventHandler(self) self.event_handler = event_handler or self.__class__.EventHandler(self)
'''The window's event handler'''
@property @property
def drawable_bounds(self) -> Rect: def drawable_bounds(self) -> Rect:
''' '''
The bounds of the window that is drawable, inset by its frame if A rectangle in console coordinates defining the area of the window that
`is_framed` is `True`. is drawable, inset by the window's frame if it has one.
''' '''
if self.is_framed: if self.is_framed:
return self.bounds.inset_rect(1, 1, 1, 1) return self.bounds.inset_rect(1, 1, 1, 1)
return self.bounds return self.bounds
def convert_console_point(self, point: Point) -> Optional[Point]: def convert_console_point_to_window(self, point: Point, *, use_drawable_bounds: bool = False) -> Optional[Point]:
''' '''
Converts a point in console coordinates to window-relative coordinates. Converts a point in console coordinates to window coordinates. If the
If the point is out of bounds of the window, return None. point is out of bounds of the window, return None.
''' '''
converted_point = point - Vector.from_point(self.bounds.origin) bounds = self.drawable_bounds if use_drawable_bounds else self.bounds
return converted_point if converted_point in self.bounds else None if point in bounds:
return point - Vector.from_point(bounds.origin)
return None
def draw(self, console: Console): def draw(self, console: Console):
'''Draw the window to the conole''' '''Draw the window to the conole'''

View file

@ -1,6 +1,10 @@
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
from typing import List, Optional '''
Declares the MapWindow class.
'''
from typing import List
import numpy as np import numpy as np
import tcod.event as tev import tcod.event as tev
@ -8,7 +12,7 @@ from tcod.console import Console
from . import Window from . import Window
from ... import log from ... import log
from ...geometry import Point, Rect, Size, Vector from ...geometry import Point, Rect, Vector
from ...map import Map from ...map import Map
from ...object import Entity, Hero from ...object import Entity, Hero
@ -20,18 +24,18 @@ class MapWindow(Window):
'''An event handler for the MapWindow.''' '''An event handler for the MapWindow.'''
def ev_mousemotion(self, event: tev.MouseMotion) -> bool: def ev_mousemotion(self, event: tev.MouseMotion) -> bool:
mouse_point = self.window.convert_console_point(self.mouse_point_for_event(event)) mouse_point = self.mouse_point_for_event(event)
if not mouse_point:
return False
log.UI.info('Mouse point in window %s', mouse_point) converted_point = self.window.convert_console_point_to_window(mouse_point, use_drawable_bounds=True)
if not converted_point:
return False
hero = self.window.hero hero = self.window.hero
if not hero: if not hero:
return False return False
map_point = self.window.convert_window_point_to_map(mouse_point) map_point = self.window.convert_console_point_to_map(mouse_point)
log.UI.info('Mouse point in map %s', map_point) log.UI.info('Mouse moved; finding path from hero to %s', map_point)
map_ = self.window.map map_ = self.window.map
path = map_.find_walkable_path_from_point_to_point(hero.position, map_point) path = map_.find_walkable_path_from_point_to_point(hero.position, map_point)
@ -39,30 +43,51 @@ class MapWindow(Window):
return False return False
# pylint: disable=redefined-builtin def ev_mousebuttondown(self, event: tev.MouseButtonDown) -> bool:
mouse_point = self.mouse_point_for_event(event)
converted_point = self.window.convert_console_point_to_window(mouse_point, use_drawable_bounds=True)
if not converted_point:
return False
map_point = self.window.convert_console_point_to_map(mouse_point)
log.UI.info('Mouse button down at %s', map_point)
return False
def __init__(self, bounds: Rect, map: Map, hero: Hero, **kwargs): def __init__(self, bounds: Rect, map: Map, hero: Hero, **kwargs):
super().__init__(bounds, event_handler=self.__class__.EventHandler(self), **kwargs) super().__init__(bounds, event_handler=self.__class__.EventHandler(self), **kwargs)
self.map = map self.map = map
'''The game map'''
self.visible_map_bounds = map.bounds
'''A rectangle in map coordinates defining the visible area of the map in the window'''
self.drawable_map_bounds = map.bounds
self.hero = hero self.hero = hero
'''The hero entity'''
self.entities: List[Entity] = [] self.entities: List[Entity] = []
'''A list of all game entities to render on the map'''
self._draw_bounds = self.drawable_bounds self._draw_bounds = self.drawable_bounds
def convert_window_point_to_map(self, point: Point) -> Point:
''' '''
Convert a point in window coordinates to a point relative to the map's A rectangle in console coordinates where the map will actually be drawn.
This area should always be entirely contained within the window's
drawable bounds.
'''
def convert_console_point_to_map(self, point: Point) -> Point:
'''
Convert a point in console coordinates to a point relative to the map's
origin point. origin point.
''' '''
return point - Vector.from_point(self._draw_bounds.origin) return point - Vector.from_point(self._draw_bounds.origin) + Vector.from_point(self.visible_map_bounds.origin)
def update_drawable_map_bounds(self): def _update_visible_map_bounds(self) -> Rect:
''' '''
Figure out what portion of the map is drawable and update Figure out what portion of the map is visible. This method attempts to
`self.drawable_map_bounds`. This method attempts to keep the hero keep the hero centered in the map viewport, while not overscrolling the
centered in the map viewport, while not overscrolling the map in either map in either direction.
direction.
''' '''
bounds = self.drawable_bounds bounds = self.drawable_bounds
map_bounds = self.map.bounds map_bounds = self.map.bounds
@ -72,65 +97,71 @@ class MapWindow(Window):
if viewport_is_wider_than_map and viewport_is_taller_than_map: if viewport_is_wider_than_map and viewport_is_taller_than_map:
# The whole map fits within the window's drawable bounds # The whole map fits within the window's drawable bounds
self.drawable_map_bounds = map_bounds return map_bounds
return
# Attempt to keep the player centered in the viewport. # Attempt to keep the player centered in the viewport.
hero_point = self.hero.position hero_point = self.hero.position
if viewport_is_wider_than_map: if viewport_is_wider_than_map:
x = 0 x = 0
width = map_bounds.width
else: else:
x = min(max(0, hero_point.x - bounds.mid_x), map_bounds.max_x - bounds.width) half_width = bounds.width // 2
x = min(max(0, hero_point.x - half_width), map_bounds.end_x - bounds.width)
width = bounds.width
if viewport_is_taller_than_map: if viewport_is_taller_than_map:
y = 0 y = 0
height = map_bounds.height
else: else:
y = min(max(0, hero_point.y - bounds.mid_y), map_bounds.max_y - bounds.height) half_height = bounds.height // 2
y = min(max(0, hero_point.y - half_height), map_bounds.end_y - bounds.height)
height = bounds.height
origin = Point(x, y) return Rect.from_raw_values(x, y, width, height)
size = Size(min(bounds.width, map_bounds.width), min(bounds.height, map_bounds.height))
self.drawable_map_bounds = Rect(origin, size)
def _update_draw_bounds(self): def _update_draw_bounds(self):
''' '''
The area where the map should actually be drawn, accounting for the size 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`). of the viewport (`drawable_bounds`) and the size of the map (`self.map.bounds`).
''' '''
drawable_map_bounds = self.drawable_map_bounds visible_map_bounds = self.visible_map_bounds
drawable_bounds = self.drawable_bounds drawable_bounds = self.drawable_bounds
viewport_is_wider_than_map = drawable_bounds.width >= drawable_map_bounds.width viewport_is_wider_than_map = drawable_bounds.width >= visible_map_bounds.width
viewport_is_taller_than_map = drawable_bounds.height >= drawable_map_bounds.height viewport_is_taller_than_map = drawable_bounds.height >= visible_map_bounds.height
if viewport_is_wider_than_map: if viewport_is_wider_than_map:
# Center the map horizontally in the viewport # Center the map horizontally in the viewport
origin_x = drawable_bounds.min_x + (drawable_bounds.width - drawable_map_bounds.width) // 2 x = drawable_bounds.min_x + (drawable_bounds.width - visible_map_bounds.width) // 2
width = drawable_map_bounds.width width = visible_map_bounds.width
else: else:
origin_x = drawable_bounds.min_x x = drawable_bounds.min_x
width = drawable_bounds.width width = drawable_bounds.width
if viewport_is_taller_than_map: if viewport_is_taller_than_map:
# Center the map vertically in the viewport # Center the map vertically in the viewport
origin_y = drawable_bounds.min_y + (drawable_bounds.height - drawable_map_bounds.height) // 2 y = drawable_bounds.min_y + (drawable_bounds.height - visible_map_bounds.height) // 2
height = drawable_map_bounds.height height = visible_map_bounds.height
else: else:
origin_y = drawable_bounds.min_y y = drawable_bounds.min_y
height = drawable_bounds.height height = drawable_bounds.height
self._draw_bounds = Rect(Point(origin_x, origin_y), Size(width, height)) draw_bounds = Rect.from_raw_values(x, y, width, height)
assert draw_bounds in self.drawable_bounds
return draw_bounds
def draw(self, console: Console): def draw(self, console: Console):
super().draw(console) super().draw(console)
self._update_draw_bounds()
self.visible_map_bounds = self._update_visible_map_bounds()
self._draw_bounds = self._update_draw_bounds()
self._draw_map(console) self._draw_map(console)
self._draw_entities(console) self._draw_entities(console)
def _draw_map(self, console: Console): def _draw_map(self, console: Console):
drawable_map_bounds = self.drawable_map_bounds drawable_map_bounds = self.visible_map_bounds
drawable_bounds = self.drawable_bounds
map_slice = np.s_[ map_slice = np.s_[
drawable_map_bounds.min_x: drawable_map_bounds.max_x + 1, drawable_map_bounds.min_x: drawable_map_bounds.max_x + 1,
@ -143,26 +174,29 @@ class MapWindow(Window):
console.tiles_rgb[console_slice] = self.map.composited_tiles[map_slice] console.tiles_rgb[console_slice] = self.map.composited_tiles[map_slice]
def _draw_entities(self, console): def _draw_entities(self, console: Console):
map_bounds_vector = Vector.from_point(self.drawable_map_bounds.origin) visible_map_bounds = self.visible_map_bounds
map_bounds_vector = Vector.from_point(self.visible_map_bounds.origin)
draw_bounds_vector = Vector.from_point(self._draw_bounds.origin) draw_bounds_vector = Vector.from_point(self._draw_bounds.origin)
for ent in self.entities: for ent in self.entities:
entity_position = ent.position
# Only draw entities that are within the visible map bounds
if entity_position not in visible_map_bounds:
continue
# Only draw entities that are in the field of view # Only draw entities that are in the field of view
if not self.map.point_is_visible(ent.position): if not self.map.point_is_visible(entity_position):
continue continue
# Entity positions are relative to the (0, 0) point of the Map. In # 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 # order to render them in the correct position in the console, we
# need to transform them into viewport-relative coordinates. # need to transform them into viewport-relative coordinates.
entity_position = ent.position
map_tile_at_entity_position = self.map.composited_tiles[entity_position.numpy_index] map_tile_at_entity_position = self.map.composited_tiles[entity_position.numpy_index]
position = ent.position - map_bounds_vector + draw_bounds_vector position = ent.position - map_bounds_vector + draw_bounds_vector
if isinstance(ent, Hero):
log.UI.debug('Hero position: map=%s, window=%s', entity_position, position)
console.print( console.print(
x=position.x, x=position.x,
y=position.y, y=position.y,