-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdisplay_manager.py
More file actions
276 lines (234 loc) · 10.2 KB
/
Copy pathdisplay_manager.py
File metadata and controls
276 lines (234 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tdl
import textwrap
from entities import Stairs
from misc import Singleton, Vector, Colors, get_abs_path
from dungeon import Dungeon
class DisplayManager(metaclass=Singleton):
"""
Handles the display of entities and objects on the map.
This class is a Singleton, and as such can be called from anywhere, its
members and functions/methods can be accessed from anywhere.
The main "feature" of this class is the refresh() method which will trigger
a complete rendering of the game and the UI and blit them to the screen,
effectively performing an update to the latest game state
"""
# TODO: move these to an appropriate, globally-accessible place
SCREEN_WIDTH = 100
SCREEN_HEIGHT = 50
GAME_TITLE = "Tonzo Studios Roguelike"
BAR_WIDTH = 20
PANEL_HEIGHT = 6
PANEL_Y = SCREEN_HEIGHT - PANEL_HEIGHT
MSG_X = BAR_WIDTH + 2
MSG_WIDTH = SCREEN_WIDTH - BAR_WIDTH - 2
MSG_HEIGHT = PANEL_HEIGHT - 1
BACKPACK_WIDTH = 20
game_msgs = []
def __init__(cls, player, dungeon):
# TODO: give consoles a better name
tdl.set_font(get_abs_path('lucida10x10_gs_tc.png'), greyscale=True, altLayout=True)
# TODO: Instead of using the level width, use views with fixed width
# FIXME: Since we don't have views yet, hardcode level width and height
# Initialize consoles
cls.console = tdl.Console(80, 40)
cls.panel = tdl.Console(cls.SCREEN_HEIGHT, cls.PANEL_HEIGHT)
cls.backpack = tdl.Console(cls.BACKPACK_WIDTH, cls.SCREEN_HEIGHT)
cls.root_console = tdl.init(cls.SCREEN_WIDTH, cls.SCREEN_HEIGHT, title=cls.GAME_TITLE,
fullscreen=False)
# Initialize references to other needed objects
cls.player = player
cls.dungeon = dungeon
@classmethod
def add_message(cls, new_msg, color=Colors.WHITE):
"""
Adds a text message to the UI log.
Examples:
* Damage dealt to enemy
* Status effects applied
* Items looted
* Dialogues
Messages are added to a queue and they will be rendered and discarded
in a FIFO manner.
Note:
/!\ Attention, this method shouldn't be used through the
DisplayManager itself, use misc.add_message instead which is a
standalone function /!\
Args:
new_msg (str): Message to display in the UI log, no size limit
for the string, and it will automatically be wrapped, however
if the string is too big for the console it might get clipped.
color (Colors): Color of the text to be displayed, white by
default.
"""
wrapped = textwrap.wrap(new_msg, cls.MSG_WIDTH)
for line in wrapped:
if len(cls.game_msgs) == cls.MSG_HEIGHT:
del cls.game_msgs[0]
cls.game_msgs.append((line, color))
def _render_messages(cls):
"""
Renders all messages found in the DisplayManager.game_msgs queue
"""
for y, msg in enumerate(cls.game_msgs):
line, color = msg
cls.panel.draw_str(cls.MSG_X, y + 1, line, color, None)
def _render_backpack(cls):
for y, item in enumerate(cls.player.backpack.contents):
cls.backpack.draw_str(
2, y + 1,
f"{cls.player.backpack.contents[item]} of {item.name}",
Colors.WHITE, None
)
def add_bar(cls, x, y, total_w, name, val, maxi, fg_color, bg_color,
text_color=Colors.WHITE):
"""
Adds a bar to the UI in a chosen color with chosen text.
Useful for displaying stuff like HP, MP, EXP and any other thing that
the user might want to track through the UI in a min-max model.
The bar's color depends on the ratio of val to maxi, effectively
creating a visual representation of the stat.
Note:
If the fg/bg color are the same as the text color, the text
won't be visible.
Args:
x (int): X coordinate relative to the container (panel console).
y (int): Y coordinate relative to the container (panel console).
total_w (int): Total width of the bar, in pixels.
name (str): Name of the stat to track, to be displayed inside
the bar in a format such as {name}: {val}/{maxi}.
val (int): Current value of this stat.
maxi (int): Max value of this stat.
fg_color (Colors): Color of the bar when "full".
bg_color (Colors): Color of the bar when "empty".
text_color (Colors, optional): Color of the text inside the bar.
Colors.WHITE by default.
"""
bar_width = int(float(val) / maxi * total_w)
cls.panel.draw_rect(x, y, total_w, 1, None, bg=bg_color)
if bar_width > 0:
cls.panel.draw_rect(x, y, bar_width, 1, None, bg=fg_color)
# FIXME: make val be an int or a properly truncated float, don't coerce
text = f"{name}: {int(val)}/{maxi}"
x_centered = x + (total_w - len(text)) // 2
cls.panel.draw_str(x_centered, y, text, fg=text_color, bg=None)
def _render_bars(cls):
"""
Render all UI bars.
"""
cls.add_bar(1, 1, cls.BAR_WIDTH, 'HP', cls.player.hp,
cls.player.max_hp, Colors.RED, (150, 0, 0))
cls.add_bar(1, 3, cls.BAR_WIDTH, 'MP', cls.player.mp,
cls.player.max_mp, Colors.BLUE, (0, 0, 150))
def _render_map(cls):
"""
Renders the current game map if necessary.
"""
cur_map = cls.dungeon.current_level
if cls.dungeon.fov_recomputed:
cls.dungeon.fov_recomputed = False
# First clear the old console before re-draw
cls.console.clear(fg=Colors.WHITE, bg=Colors.BLACK)
for x in range(cur_map.width):
for y in range(cur_map.height):
pos = Vector(x, y)
wall = not cur_map.transparent[pos]
# If position is visible, draw a bright tile
if cur_map.fov[pos]:
if wall:
cls.console.draw_char(
x, y, None, fg=None, bg=Colors.WALL_VISIBLE
)
else:
cls.console.draw_char(
x, y, None, fg=None, bg=Colors.GROUND_VISIBLE
)
# Tiles in FOV will be remembered after they get out
# of sight, out of mind :^)
cur_map.explored[pos] = True
# Position is not visible, but has been explored before
elif cur_map.explored[pos]:
if wall:
cls.console.draw_char(
x, y, None, fg=None, bg=Colors.WALL_DARK
)
else:
cls.console.draw_char(
x, y, None, fg=None, bg=Colors.GROUND_DARK
)
def _render_entities(cls):
"""
Render visible entities by render layer to the buffer console.
"""
entities_sorted = sorted(cls.dungeon.current_level.entities,
key=lambda x: x.render_priority.value)
for entity in entities_sorted:
# Draw visible entities
if cls.dungeon.current_level.fov[entity.pos]:
cls.console.draw_char(
entity.pos.x, entity.pos.y, entity.char, entity.color,
bg=None
)
# Remember stairs location
if isinstance(entity, Stairs) and cls.dungeon.current_level.explored[entity.pos]:
cls.console.draw_char(
entity.pos.x, entity.pos.y, entity.char, entity.color,
bg=None
)
def _display_game(cls):
"""
Renders the game world and displays it in the main screen.
The game world consists of the current game map and the entities that
are within it, player included.
"""
cls._render_map()
cls._render_entities()
cls.root_console.blit(
cls.console, 0, 0, cls.SCREEN_WIDTH, cls.SCREEN_HEIGHT, 0, 0
)
def _display_ui(cls):
"""
Renders the UI and displays it in the main screen.
The UI consists of stat bars (HP, MP, EXP, ...) and of messages
(Dialog, Combat, ...).
"""
cls._render_bars()
cls._render_messages()
cls._render_backpack()
cls.root_console.blit(
cls.panel, 0, cls.PANEL_Y, cls.SCREEN_WIDTH, cls.PANEL_HEIGHT, 0, 0
)
# TODO: Instead of using the level width, use views with fixed width
cls.root_console.blit(
cls.backpack, cls.dungeon.LEVEL_WIDTH, 0, cls.BACKPACK_WIDTH, cls.SCREEN_HEIGHT, 0, 0
)
def _clear_entities(cls):
"""
Clears all of the entities in the current game map from the buffer console.
"""
for entity in cls.dungeon.current_level.entities:
cls.console.draw_char(
entity.pos.x, entity.pos.y, ' ', entity.color, bg=None
)
def _clear_all(cls):
"""
Clears the whole screen (Game and UI) from the buffer console.
"""
cls._clear_entities()
cls.panel.clear(fg=Colors.WHITE, bg=Colors.BLACK)
def refresh(cls):
"""
Refreshes the display after every "turn" (player action).
This method will perform the following tasks in the following order:
1. Recompute the player's FOV.
2. Render the game map if necessary.
3. Render any entities within the player's FOV.
4. Render UI elements such as stat bars, logs, etc.
5. Display everything that's been rendered to the screen.
6. Prepare for the next call (flushing and clearing).
"""
cls._display_game()
cls._display_ui()
tdl.flush()
cls._clear_all()