Refactor map rendering
- Move all map rendering to a new MapWindow class - Clip map rendering to the bounds of the Window - Pass in Entities list from the Engine and render them relative to the window The map doesn't scroll yet, so it'll always be clipped on the bottom and right.
This commit is contained in:
parent
ec28f984da
commit
8efd3ce207
4 changed files with 109 additions and 40 deletions
|
@ -106,23 +106,11 @@ class Engine:
|
|||
'''Print the whole game to the given console.'''
|
||||
self.map.highlight_points(self.__mouse_path_points or [])
|
||||
|
||||
self.interface.update(self.hero, self.current_turn)
|
||||
sorted_entities = sorted(self.entities, key=lambda e: e.render_order.value)
|
||||
self.interface.update(self.current_turn, self.hero, sorted_entities)
|
||||
|
||||
self.interface.draw(console)
|
||||
|
||||
entities_at_mouse_position = []
|
||||
for ent in sorted(self.entities, key=lambda e: e.render_order.value):
|
||||
# Only process entities that are in the field of view
|
||||
if not self.map.visible[tuple(ent.position)]:
|
||||
continue
|
||||
|
||||
ent.print_to_console(console)
|
||||
|
||||
if ent.position == self.__current_mouse_point:
|
||||
entities_at_mouse_position.append(ent)
|
||||
|
||||
if len(entities_at_mouse_position) > 0:
|
||||
console.print(x=1, y=43, string=', '.join(e.name for e in entities_at_mouse_position))
|
||||
|
||||
def run_event_loop(self, context: tcod.context.Context, console: tcod.Console) -> NoReturn:
|
||||
'''Run the event loop forever. This method never returns.'''
|
||||
while True:
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
# Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
from typing import Optional
|
||||
from typing import List
|
||||
|
||||
from tcod.console import Console
|
||||
|
||||
from .color import HealthBar
|
||||
from .percentage_bar import PercentageBar
|
||||
from .window import Window
|
||||
from .window import Window, MapWindow
|
||||
from ..geometry import Point, Rect, Size
|
||||
from ..map import Map
|
||||
from ..messages import MessageLog
|
||||
from ..object import Hero
|
||||
from ..object import Entity, Hero
|
||||
|
||||
|
||||
class Interface:
|
||||
'''The game's user interface'''
|
||||
|
||||
# pylint: disable=redefined-builtin
|
||||
def __init__(self, size: Size, map: Map, message_log: MessageLog):
|
||||
self.map_window = MapWindow(Rect(Point(0, 0), Size(size.width, size.height - 5)), map)
|
||||
self.info_window = InfoWindow(Rect(Point(0, size.height - 5), Size(28, 5)))
|
||||
self.message_window = MessageLogWindow(Rect(Point(28, size.height - 5), Size(size.width - 28, 5)), message_log)
|
||||
|
||||
def update(self, hero: Hero, turn_count: int):
|
||||
def update(self, turn_count: int, hero: Hero, entities: List[Entity]):
|
||||
self.info_window.turn_count = turn_count
|
||||
self.info_window.update_hero(hero)
|
||||
self.map_window.entities = entities
|
||||
|
||||
def draw(self, console: Console):
|
||||
self.map_window.draw(console)
|
||||
|
@ -29,51 +33,46 @@ class Interface:
|
|||
self.message_window.draw(console)
|
||||
|
||||
|
||||
class MapWindow(Window):
|
||||
def __init__(self, bounds: Rect, map: Map):
|
||||
super().__init__(bounds)
|
||||
self.map = map
|
||||
|
||||
def draw(self, console):
|
||||
super().draw(console)
|
||||
|
||||
# TODO: Get a 2D slice of tiles from the map given a rect based on the window's drawable area
|
||||
drawable_area = self.drawable_area
|
||||
self.map.print_to_console(console, drawable_area)
|
||||
|
||||
|
||||
class InfoWindow(Window):
|
||||
'''A window that displays information about the player'''
|
||||
|
||||
def __init__(self, bounds: Rect):
|
||||
super().__init__(bounds, framed=True)
|
||||
|
||||
self.turn_count: int = 0
|
||||
|
||||
drawable_area = self.drawable_area
|
||||
drawable_area = self.drawable_bounds
|
||||
self.hit_points_bar = PercentageBar(
|
||||
position=Point(drawable_area.min_x + 6, drawable_area.min_y),
|
||||
width=20,
|
||||
colors=list(HealthBar.bar_colors()))
|
||||
|
||||
def update_hero(self, hero: Hero):
|
||||
hp, max_hp = hero.fighter.hit_points, hero.fighter.maximum_hit_points
|
||||
assert hero.fighter
|
||||
|
||||
fighter = hero.fighter
|
||||
hp, max_hp = fighter.hit_points, fighter.maximum_hit_points
|
||||
|
||||
self.hit_points_bar.percent_filled = hp / max_hp
|
||||
|
||||
def draw(self, console):
|
||||
super().draw(console)
|
||||
|
||||
drawable_area = self.drawable_area
|
||||
console.print(x=drawable_area.min_x + 2, y=drawable_area.min_y, string='HP:')
|
||||
drawable_bounds = self.drawable_bounds
|
||||
console.print(x=drawable_bounds.min_x + 2, y=drawable_bounds.min_y, string='HP:')
|
||||
self.hit_points_bar.render_to_console(console)
|
||||
|
||||
if self.turn_count:
|
||||
console.print(x=drawable_area.min_x, y=drawable_area.min_y + 1, string=f'Turn: {self.turn_count}')
|
||||
console.print(x=drawable_bounds.min_x, y=drawable_bounds.min_y + 1, string=f'Turn: {self.turn_count}')
|
||||
|
||||
|
||||
class MessageLogWindow(Window):
|
||||
'''A window that displays a list of messages'''
|
||||
|
||||
def __init__(self, bounds: Rect, message_log: MessageLog):
|
||||
super().__init__(bounds, framed=True)
|
||||
self.message_log = message_log
|
||||
|
||||
def draw(self, console):
|
||||
super().draw(console)
|
||||
self.message_log.render_to_console(console, self.drawable_area)
|
||||
self.message_log.render_to_console(console, self.drawable_bounds)
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
# Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from tcod.console import Console
|
||||
|
||||
from ..geometry import Rect
|
||||
from ..object import Entity
|
||||
from ..geometry import Rect, Vector
|
||||
from ..map import Map
|
||||
|
||||
|
||||
class Window:
|
||||
'''A user interface window. It can be framed.'''
|
||||
|
||||
def __init__(self, bounds: Rect, *, framed: bool = True):
|
||||
self.bounds = bounds
|
||||
self.is_framed = framed
|
||||
|
||||
@property
|
||||
def drawable_area(self) -> Rect:
|
||||
def drawable_bounds(self) -> Rect:
|
||||
if self.is_framed:
|
||||
return self.bounds.inset_rect(1, 1, 1, 1)
|
||||
return self.bounds
|
||||
|
@ -23,3 +30,64 @@ class Window:
|
|||
self.bounds.origin.y,
|
||||
self.bounds.size.width,
|
||||
self.bounds.size.height)
|
||||
|
||||
|
||||
class MapWindow(Window):
|
||||
'''A Window that displays a game map'''
|
||||
|
||||
def __init__(self, bounds: Rect, map_: Map, **kwargs):
|
||||
super().__init__(bounds, **kwargs)
|
||||
self.map = map_
|
||||
|
||||
self.entities: List[Entity] = []
|
||||
|
||||
def draw(self, console: Console):
|
||||
super().draw(console)
|
||||
self._draw_map(console)
|
||||
self._draw_entities(console)
|
||||
|
||||
def _draw_map(self, console: Console):
|
||||
map_ = self.map
|
||||
map_size = map_.size
|
||||
|
||||
drawable_bounds = self.drawable_bounds
|
||||
|
||||
width = min(map_size.width, drawable_bounds.width)
|
||||
height = min(map_size.height, drawable_bounds.height)
|
||||
|
||||
# TODO: Adjust the slice according to where the hero is.
|
||||
map_slice = np.s_[0:width, 0:height]
|
||||
|
||||
min_x = drawable_bounds.min_x
|
||||
max_x = min_x + width
|
||||
min_y = drawable_bounds.min_y
|
||||
max_y = min_y + height
|
||||
|
||||
console.tiles_rgb[min_x:max_x, min_y:max_y] = self.map.composited_tiles[map_slice]
|
||||
|
||||
def _draw_entities(self, console):
|
||||
drawable_bounds_vector = Vector.from_point(self.drawable_bounds.origin)
|
||||
|
||||
for ent in self.entities:
|
||||
# Only process entities that are in the field of view
|
||||
if not self.map.visible[tuple(ent.position)]:
|
||||
continue
|
||||
|
||||
# Entity positions are 0-based relative to the (0, 0) point of the Map. In order to render them in the
|
||||
# correct position in the console, we need to offset the position.
|
||||
entity_position = ent.position
|
||||
map_tile_at_entity_position = self.map.composited_tiles[entity_position.x, entity_position.y]
|
||||
|
||||
position = ent.position + drawable_bounds_vector
|
||||
console.print(
|
||||
x=position.x,
|
||||
y=position.y,
|
||||
string=ent.symbol,
|
||||
fg=ent.foreground,
|
||||
bg=tuple(map_tile_at_entity_position['bg'][:3]))
|
||||
|
||||
# if ent.position == self.__current_mouse_point:
|
||||
# entities_at_mouse_position.append(ent)
|
||||
|
||||
# if len(entities_at_mouse_position) > 0:
|
||||
# console.print(x=1, y=43, string=', '.join(e.name for e in entities_at_mouse_position))
|
||||
|
|
|
@ -47,6 +47,20 @@ class Map:
|
|||
'''The size of the map'''
|
||||
return self.configuration.map_size
|
||||
|
||||
@property
|
||||
def composited_tiles(self) -> np.ndarray:
|
||||
# TODO: Hold onto the result here so that this doen't have to be done every time this property is called.
|
||||
return np.select(
|
||||
condlist=[
|
||||
self.highlighted,
|
||||
self.visible,
|
||||
self.explored],
|
||||
choicelist=[
|
||||
self.tiles['highlighted'],
|
||||
self.tiles['light'],
|
||||
self.tiles['dark']],
|
||||
default=Shroud)
|
||||
|
||||
def random_walkable_position(self) -> Point:
|
||||
'''Return a random walkable point on the map.'''
|
||||
if not self.__walkable_points:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue