Fix up how Maps are rendered in MapWindows

There was a bug in how MapWindow was calculating the numpy array slices
when drawing the map. Redo how this works so that MapWindow can draw
maps of arbitrary size and center maps that are smaller than the window's
drawable area.
This commit is contained in:
Eryn Wells 2023-03-05 13:37:51 -08:00
parent 42cfb78ba3
commit af3d93ba11

View file

@ -6,7 +6,7 @@ import numpy as np
from tcod.console import Console from tcod.console import Console
from .. import log from .. import log
from ..geometry import Point, Rect, Vector from ..geometry import Point, Rect, Size, Vector
from ..map import Map from ..map import Map
from ..object import Entity, Hero from ..object import Entity, Hero
@ -20,7 +20,10 @@ class Window:
@property @property
def drawable_bounds(self) -> Rect: def drawable_bounds(self) -> Rect:
'''The bounds of the window that is drawable, inset by any frame''' '''
The bounds of the window that is drawable, inset by its frame if
`is_framed` is `True`.
'''
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
@ -34,6 +37,11 @@ class Window:
self.bounds.size.width, self.bounds.size.width,
self.bounds.size.height) self.bounds.size.height)
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))
class MapWindow(Window): class MapWindow(Window):
'''A Window that displays a game map''' '''A Window that displays a game map'''
@ -44,29 +52,78 @@ class MapWindow(Window):
self.map = map self.map = map
self.drawable_map_bounds = map.bounds self.drawable_map_bounds = map.bounds
self.offset = Vector()
self.entities: List[Entity] = [] self.entities: List[Entity] = []
self._draw_bounds = self.drawable_bounds
def update_drawable_map_bounds(self, hero: Hero): def update_drawable_map_bounds(self, hero: Hero):
'''
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.
'''
bounds = self.drawable_bounds bounds = self.drawable_bounds
map_bounds = self.map.bounds map_bounds = self.map.bounds
if map_bounds.width < bounds.width and map_bounds.height < bounds.height: viewport_is_wider_than_map = bounds.width > map_bounds.width
# We can draw the whole map in the drawable bounds 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
self.drawable_map_bounds = map_bounds self.drawable_map_bounds = map_bounds
return
# Attempt to keep the player centered in the viewport. # Attempt to keep the player centered in the viewport.
hero_point = hero.position hero_point = hero.position
x = min(max(0, hero_point.x - bounds.mid_x), map_bounds.max_x - bounds.width) if viewport_is_wider_than_map:
y = min(max(0, hero_point.y - bounds.mid_y), map_bounds.max_y - bounds.height) x = 0
origin = Point(x, y) else:
x = min(max(0, hero_point.x - bounds.mid_x), map_bounds.max_x - bounds.width)
self.drawable_map_bounds = Rect(origin, bounds.size) 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)
origin = Point(x, y)
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):
'''
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))
def draw(self, console: Console): def draw(self, console: Console):
super().draw(console) super().draw(console)
self._update_draw_bounds()
self._draw_map(console) self._draw_map(console)
self._draw_entities(console) self._draw_entities(console)
@ -75,43 +132,44 @@ class MapWindow(Window):
drawable_bounds = self.drawable_bounds drawable_bounds = self.drawable_bounds
log.UI.info('Drawing map') log.UI.info('Drawing map')
log.UI.info('|- map bounds: %s', drawable_map_bounds)
log.UI.info('|- window bounds: %s', 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,
drawable_map_bounds.min_y:drawable_map_bounds.max_y + 1] drawable_map_bounds.min_y: drawable_map_bounds.max_y + 1]
console_draw_bounds = self._draw_bounds
console_slice = np.s_[ console_slice = np.s_[
drawable_bounds.min_x:drawable_bounds.max_x + 1, console_draw_bounds.min_x: console_draw_bounds.max_x + 1,
drawable_bounds.min_y:drawable_bounds.max_y + 1] console_draw_bounds.min_y: console_draw_bounds.max_y + 1]
log.UI.info('|- map slice: %s', map_slice) log.UI.debug('Map bounds=%s, slice=%s', drawable_map_bounds, map_slice)
log.UI.info('`- console slice: %s', console_slice) log.UI.debug('Console bounds=%s, slice=%s', drawable_bounds, console_slice)
console.tiles_rgb[console_slice] = self.map.composited_tiles[map_slice] console.tiles_rgb[console_slice] = self.map.composited_tiles[map_slice]
log.UI.info('Done drawing map')
def _draw_entities(self, console): def _draw_entities(self, console):
map_bounds_vector = Vector.from_point(self.drawable_map_bounds.origin) map_bounds_vector = Vector.from_point(self.drawable_map_bounds.origin)
drawable_bounds_vector = Vector.from_point(self.drawable_bounds.origin) draw_bounds_vector = Vector.from_point(self._draw_bounds.origin)
log.UI.info('Drawing entities') log.UI.info('Drawing entities')
for ent in self.entities: for ent in self.entities:
# Only draw entities that are in the field of view # Only draw entities that are in the field of view
if not self.map.visible[tuple(ent.position)]: if not self.map.point_is_visible(ent.position):
continue continue
# Entity positions are 0-based relative to the (0, 0) point of the Map. In order to render them in the # Entity positions are relative to the (0, 0) point of the Map. In
# correct position in the console, we need to offset the position. # order to render them in the correct position in the console, we
# need to transform them into viewport-relative coordinates.
entity_position = ent.position entity_position = ent.position
map_tile_at_entity_position = self.map.composited_tiles[entity_position.x, entity_position.y] map_tile_at_entity_position = self.map.composited_tiles[entity_position.numpy_index]
position = ent.position - map_bounds_vector + drawable_bounds_vector position = ent.position - map_bounds_vector + draw_bounds_vector
if isinstance(ent, Hero): if isinstance(ent, Hero):
log.UI.info('|- hero position on map %s', entity_position) log.UI.debug('Hero position: map=%s, window=%s', entity_position, position)
log.UI.info('`- position in window %s', position)
console.print( console.print(
x=position.x, x=position.x,
@ -119,3 +177,5 @@ class MapWindow(Window):
string=ent.symbol, string=ent.symbol,
fg=ent.foreground, fg=ent.foreground,
bg=tuple(map_tile_at_entity_position['bg'][:3])) bg=tuple(map_tile_at_entity_position['bg'][:3]))
log.UI.info('Done drawing entities')