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:
parent
42cfb78ba3
commit
af3d93ba11
1 changed files with 86 additions and 26 deletions
|
@ -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')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue