Figure out (finally!) the mouse coordinates in the MapWindow
This commit is contained in:
parent
1018febeab
commit
078520678d
4 changed files with 112 additions and 58 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'''
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue