2022-05-16 16:39:19 -07:00
|
|
|
# Eryn Wells <eryn@erynwells.me>
|
2023-02-11 01:21:52 -08:00
|
|
|
|
2023-02-15 08:25:40 -08:00
|
|
|
'''
|
|
|
|
The game's graphical user interface
|
|
|
|
'''
|
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
from typing import NoReturn
|
2023-02-11 01:21:52 -08:00
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
from tcod import event as tev
|
2023-02-11 01:21:52 -08:00
|
|
|
from tcod.console import Console
|
2023-03-07 21:29:05 -08:00
|
|
|
from tcod.context import Context
|
2023-02-11 01:21:52 -08:00
|
|
|
|
|
|
|
from .color import HealthBar
|
2023-03-07 21:29:05 -08:00
|
|
|
from .events import InterfaceEventHandler
|
2023-02-11 01:21:52 -08:00
|
|
|
from .percentage_bar import PercentageBar
|
2023-02-12 15:55:01 -08:00
|
|
|
from .window import Window, MapWindow
|
2023-03-07 21:29:05 -08:00
|
|
|
from ..engine import Engine
|
2023-02-11 01:21:52 -08:00
|
|
|
from ..geometry import Point, Rect, Size
|
|
|
|
from ..messages import MessageLog
|
2023-02-12 15:55:01 -08:00
|
|
|
from ..object import Entity, Hero
|
2023-02-11 01:21:52 -08:00
|
|
|
|
|
|
|
|
|
|
|
class Interface:
|
2023-02-12 15:55:01 -08:00
|
|
|
'''The game's user interface'''
|
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
def __init__(self, size: Size, engine: Engine):
|
|
|
|
self.engine = engine
|
2023-02-11 01:21:52 -08:00
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
self.console = Console(*size.numpy_shape, order='F')
|
|
|
|
|
|
|
|
self.map_window = MapWindow(
|
|
|
|
Rect.from_raw_values(0, 0, size.width, size.height - 5),
|
|
|
|
engine.map)
|
|
|
|
self.info_window = InfoWindow(
|
|
|
|
Rect.from_raw_values(0, size.height - 5, 28, 5))
|
|
|
|
self.message_window = MessageLogWindow(
|
|
|
|
Rect.from_raw_values(28, size.height - 5, size.width - 28, 5),
|
|
|
|
engine.message_log)
|
|
|
|
|
|
|
|
self.event_handler = InterfaceEventHandler(self)
|
|
|
|
|
|
|
|
def update(self):
|
2023-02-12 19:47:27 -08:00
|
|
|
'''Update game state that the interface needs to render'''
|
2023-03-07 21:29:05 -08:00
|
|
|
self.info_window.turn_count = self.engine.current_turn
|
2023-02-12 19:47:27 -08:00
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
hero = self.engine.hero
|
|
|
|
self.info_window.update_hero(hero)
|
2023-02-12 19:47:27 -08:00
|
|
|
self.map_window.update_drawable_map_bounds(hero)
|
2023-02-11 01:21:52 -08:00
|
|
|
|
2023-03-07 21:29:05 -08:00
|
|
|
sorted_entities = sorted(self.engine.entities, key=lambda e: e.render_order.value)
|
|
|
|
self.map_window.entities = sorted_entities
|
|
|
|
|
|
|
|
def draw(self):
|
2023-02-12 19:47:27 -08:00
|
|
|
'''Draw the UI to the console'''
|
2023-03-07 21:29:05 -08:00
|
|
|
self.map_window.draw(self.console)
|
|
|
|
self.info_window.draw(self.console)
|
|
|
|
self.message_window.draw(self.console)
|
|
|
|
|
|
|
|
def run_event_loop(self, context: Context) -> NoReturn:
|
|
|
|
'''Run the event loop forever. This method never returns.'''
|
|
|
|
while True:
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
self.console.clear()
|
|
|
|
self.draw()
|
|
|
|
context.present(self.console)
|
|
|
|
|
|
|
|
for event in tev.wait():
|
|
|
|
did_handle = self.event_handler.dispatch(event)
|
|
|
|
if did_handle:
|
|
|
|
continue
|
|
|
|
|
|
|
|
action = self.engine.event_handler.dispatch(event)
|
|
|
|
if not action:
|
|
|
|
# The engine didn't handle the event, so just drop it.
|
|
|
|
continue
|
|
|
|
|
|
|
|
self.engine.process_input_action(action)
|
2023-02-11 01:21:52 -08:00
|
|
|
|
|
|
|
|
|
|
|
class InfoWindow(Window):
|
2023-02-12 15:55:01 -08:00
|
|
|
'''A window that displays information about the player'''
|
|
|
|
|
2023-02-11 01:21:52 -08:00
|
|
|
def __init__(self, bounds: Rect):
|
|
|
|
super().__init__(bounds, framed=True)
|
|
|
|
|
|
|
|
self.turn_count: int = 0
|
|
|
|
|
2023-02-12 15:55:01 -08:00
|
|
|
drawable_area = self.drawable_bounds
|
2023-02-11 01:21:52 -08:00
|
|
|
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):
|
2023-02-12 19:47:27 -08:00
|
|
|
'''Update internal state for the hero'''
|
2023-02-12 15:55:01 -08:00
|
|
|
assert hero.fighter
|
|
|
|
|
|
|
|
fighter = hero.fighter
|
|
|
|
hp, max_hp = fighter.hit_points, fighter.maximum_hit_points
|
|
|
|
|
2023-02-11 01:21:52 -08:00
|
|
|
self.hit_points_bar.percent_filled = hp / max_hp
|
|
|
|
|
|
|
|
def draw(self, console):
|
|
|
|
super().draw(console)
|
|
|
|
|
2023-02-12 15:55:01 -08:00
|
|
|
drawable_bounds = self.drawable_bounds
|
|
|
|
console.print(x=drawable_bounds.min_x + 2, y=drawable_bounds.min_y, string='HP:')
|
2023-02-11 01:21:52 -08:00
|
|
|
self.hit_points_bar.render_to_console(console)
|
|
|
|
|
|
|
|
if self.turn_count:
|
2023-02-12 15:55:01 -08:00
|
|
|
console.print(x=drawable_bounds.min_x, y=drawable_bounds.min_y + 1, string=f'Turn: {self.turn_count}')
|
2023-02-11 01:21:52 -08:00
|
|
|
|
|
|
|
|
|
|
|
class MessageLogWindow(Window):
|
2023-02-12 15:55:01 -08:00
|
|
|
'''A window that displays a list of messages'''
|
|
|
|
|
2023-02-11 01:21:52 -08:00
|
|
|
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)
|
2023-02-12 15:55:01 -08:00
|
|
|
self.message_log.render_to_console(console, self.drawable_bounds)
|