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 import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, MutableSet, NoReturn, Optional from typing import TYPE_CHECKING, List, MutableSet, NoReturn, Optional
import tcod import tcod
@ -15,8 +15,7 @@ from .actions.result import ActionResult
from .ai import HostileEnemy from .ai import HostileEnemy
from .events import GameOverEventHandler, MainGameEventHandler from .events import GameOverEventHandler, MainGameEventHandler
from .geometry import Point, Rect, Size from .geometry import Point, Rect, Size
from .interface import color from .interface import Interface
from .interface.percentage_bar import PercentageBar
from .map import Map from .map import Map
from .map.generator import RoomsAndCorridorsGenerator from .map.generator import RoomsAndCorridorsGenerator
from .map.generator.room import BSPRoomGenerator from .map.generator.room import BSPRoomGenerator
@ -104,26 +103,15 @@ class Engine:
self.update_field_of_view() self.update_field_of_view()
# Interface elements # 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) self.message_log.add_message('Greetings adventurer!', fg=(127, 127, 255), stack=False)
def print_to_console(self, console): def print_to_console(self, console):
'''Print the whole game to the given console.''' '''Print the whole game to the given console.'''
self.map.highlight_points(self.__mouse_path_points or []) self.map.highlight_points(self.__mouse_path_points or [])
self.map.print_to_console(console) self.interface.update(self.hero, self.current_turn)
self.interface.draw(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)
entities_at_mouse_position = [] entities_at_mouse_position = []
for ent in sorted(self.entities, key=lambda e: e.render_order.value): for ent in sorted(self.entities, key=lambda e: e.render_order.value):

View file

@ -1 +1,79 @@
# Eryn Wells <eryn@erynwells.me> # 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 numpy.typing as npt
import tcod import tcod
from ..geometry import Point, Size from ..geometry import Point, Rect, Size
from .generator import MapGenerator from .generator import MapGenerator
from .tile import Empty, Shroud from .tile import Empty, Shroud
@ -57,7 +57,7 @@ class Map:
for pt in points if points: for pt in points if points:
self.highlighted[pt.x, pt.y] = True 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.''' '''Render the map to the console.'''
size = self.size size = self.size

View file

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