Move all the interface stuff to interface.Interface

Draw three windows with frames:
- map window
- info window (hit point bar; turn count)
- message window

Clean up the UI code in the Engine.
This commit is contained in:
Eryn Wells 2023-02-11 01:21:52 -08:00
parent df4df06013
commit 6780b0495c
5 changed files with 114 additions and 21 deletions

View file

@ -4,7 +4,7 @@
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, MutableSet, NoReturn, Optional
from typing import TYPE_CHECKING, List, MutableSet, NoReturn, Optional
import tcod
@ -15,8 +15,7 @@ from .actions.result import ActionResult
from .ai import HostileEnemy
from .events import GameOverEventHandler, MainGameEventHandler
from .geometry import Point, Rect, Size
from .interface import color
from .interface.percentage_bar import PercentageBar
from .interface import Interface
from .map import Map
from .map.generator import RoomsAndCorridorsGenerator
from .map.generator.room import BSPRoomGenerator
@ -104,26 +103,15 @@ class Engine:
self.update_field_of_view()
# Interface elements
self.hit_points_bar = PercentageBar(position=Point(4, 45), width=20, colors=list(color.HealthBar.bar_colors()))
self.interface = Interface(Size(80, 50), self.map, self.message_log)
self.message_log.add_message('Greetings adventurer!', fg=(127, 127, 255), stack=False)
def print_to_console(self, console):
'''Print the whole game to the given console.'''
self.map.highlight_points(self.__mouse_path_points or [])
self.map.print_to_console(console)
console.print(x=1, y=45, string='HP:')
hp, max_hp = self.hero.fighter.hit_points, self.hero.fighter.maximum_hit_points
self.hit_points_bar.percent_filled = hp / max_hp
self.hit_points_bar.render_to_console(console)
console.print(x=6, y=45, string=f'{hp}/{max_hp}', fg=color.WHITE)
console.print(x=1, y=46, string=f'Turn: {self.current_turn}')
messages_rect = Rect(Point(x=27, y=45), Size(width=40, height=5))
self.message_log.render_to_console(console, messages_rect)
self.interface.update(self.hero, self.current_turn)
self.interface.draw(console)
entities_at_mouse_position = []
for ent in sorted(self.entities, key=lambda e: e.render_order.value):

View file

@ -1 +1,79 @@
# Eryn Wells <eryn@erynwells.me>
from typing import Optional
from tcod.console import Console
from .color import HealthBar
from .percentage_bar import PercentageBar
from .window import Window
from ..geometry import Point, Rect, Size
from ..map import Map
from ..messages import MessageLog
from ..object import Hero
class Interface:
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):
self.info_window.turn_count = turn_count
self.info_window.update_hero(hero)
def draw(self, console: Console):
self.map_window.draw(console)
self.info_window.draw(console)
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):
def __init__(self, bounds: Rect):
super().__init__(bounds, framed=True)
self.turn_count: int = 0
drawable_area = self.drawable_area
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
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:')
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}')
class MessageLogWindow(Window):
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)

View file

@ -0,0 +1,25 @@
# Eryn Wells <eryn@erynwells.me>
from tcod.console import Console
from ..geometry import Rect
class Window:
def __init__(self, bounds: Rect, *, framed: bool = True):
self.bounds = bounds
self.is_framed = framed
@property
def drawable_area(self) -> Rect:
if self.is_framed:
return self.bounds.inset_rect(1, 1, 1, 1)
return self.bounds
def draw(self, console: Console):
if self.is_framed:
console.draw_frame(
self.bounds.origin.x,
self.bounds.origin.y,
self.bounds.size.width,
self.bounds.size.height)

View file

@ -12,7 +12,7 @@ import numpy as np
import numpy.typing as npt
import tcod
from ..geometry import Point, Size
from ..geometry import Point, Rect, Size
from .generator import MapGenerator
from .tile import Empty, Shroud
@ -57,7 +57,7 @@ class Map:
for pt in points if points:
self.highlighted[pt.x, pt.y] = True
def print_to_console(self, console: tcod.Console) -> None:
def print_to_console(self, console: tcod.Console, bounds: Rect) -> None:
'''Render the map to the console.'''
size = self.size

View file

@ -13,6 +13,7 @@ import tcod
from .geometry import Rect
class Message:
'''A message in the message log
@ -44,6 +45,7 @@ class Message:
def __repr__(self) -> str:
return f'{self.__class__.__name__}({repr(self.text)}, fg={self.foreground})'
class MessageLog:
'''A buffer of messages sent to the player by the game'''
@ -76,12 +78,12 @@ class MessageLog:
@staticmethod
def render_messages(console: tcod.console.Console, rect: Rect, messages: Reversible[Message]):
'''Render a list of messages to the console in the given rect'''
y_offset = min(rect.size.height, len(messages))
y_offset = min(rect.size.height, len(messages)) - 1
for message in reversed(messages):
wrapped_text = textwrap.wrap(message.full_text, rect.size.width)
for line in wrapped_text:
console.print(x=rect.min_x, y=rect.min_y + y_offset - 1, string=line, fg=message.foreground)
console.print(x=rect.min_x, y=rect.min_y + y_offset, string=line, fg=message.foreground)
y_offset -= 1
if y_offset < 0: