zombie-escape 1.14.4__py3-none-any.whl → 1.15.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zombie_escape/__about__.py +1 -1
- zombie_escape/config.py +1 -0
- zombie_escape/entities.py +126 -199
- zombie_escape/entities_constants.py +11 -1
- zombie_escape/export_images.py +4 -4
- zombie_escape/font_utils.py +47 -0
- zombie_escape/gameplay/__init__.py +2 -1
- zombie_escape/gameplay/constants.py +4 -0
- zombie_escape/gameplay/interactions.py +83 -16
- zombie_escape/gameplay/layout.py +9 -15
- zombie_escape/gameplay/movement.py +45 -29
- zombie_escape/gameplay/spawn.py +15 -29
- zombie_escape/gameplay/state.py +62 -7
- zombie_escape/gameplay/survivors.py +61 -10
- zombie_escape/gameplay/utils.py +33 -0
- zombie_escape/level_blueprints.py +35 -31
- zombie_escape/level_constants.py +2 -2
- zombie_escape/locales/ui.en.json +19 -8
- zombie_escape/locales/ui.ja.json +19 -8
- zombie_escape/localization.py +7 -1
- zombie_escape/models.py +21 -6
- zombie_escape/render/__init__.py +2 -2
- zombie_escape/render/core.py +113 -81
- zombie_escape/render/hud.py +112 -40
- zombie_escape/render/overview.py +93 -2
- zombie_escape/render/shadows.py +2 -2
- zombie_escape/render_constants.py +12 -0
- zombie_escape/screens/__init__.py +6 -189
- zombie_escape/screens/game_over.py +8 -21
- zombie_escape/screens/gameplay.py +71 -26
- zombie_escape/screens/settings.py +114 -43
- zombie_escape/screens/title.py +128 -47
- zombie_escape/stage_constants.py +37 -8
- zombie_escape/windowing.py +508 -0
- zombie_escape/world_grid.py +7 -5
- zombie_escape/zombie_escape.py +26 -13
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/METADATA +24 -24
- zombie_escape-1.15.2.dist-info/RECORD +54 -0
- zombie_escape-1.14.4.dist-info/RECORD +0 -53
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/WHEEL +0 -0
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/render/hud.py
CHANGED
|
@@ -7,7 +7,7 @@ import pygame
|
|
|
7
7
|
from pygame import sprite, surface
|
|
8
8
|
|
|
9
9
|
from ..colors import LIGHT_GRAY, ORANGE, YELLOW
|
|
10
|
-
from ..entities import
|
|
10
|
+
from ..entities import Car, Player
|
|
11
11
|
from ..entities_constants import (
|
|
12
12
|
FLASHLIGHT_HEIGHT,
|
|
13
13
|
FLASHLIGHT_WIDTH,
|
|
@@ -20,18 +20,25 @@ from ..font_utils import load_font
|
|
|
20
20
|
from ..gameplay_constants import SURVIVAL_FAKE_CLOCK_RATIO
|
|
21
21
|
from ..localization import get_font_settings
|
|
22
22
|
from ..localization import translate as tr
|
|
23
|
-
from ..models import Stage
|
|
23
|
+
from ..models import Stage, TimedMessage
|
|
24
24
|
from ..render_assets import (
|
|
25
25
|
RenderAssets,
|
|
26
26
|
build_flashlight_surface,
|
|
27
27
|
build_fuel_can_surface,
|
|
28
28
|
build_shoes_surface,
|
|
29
29
|
)
|
|
30
|
-
from ..render_constants import
|
|
30
|
+
from ..render_constants import (
|
|
31
|
+
FLASHLIGHT_FOG_SCALE_ONE,
|
|
32
|
+
FLASHLIGHT_FOG_SCALE_TWO,
|
|
33
|
+
GAMEPLAY_FONT_SIZE,
|
|
34
|
+
HUD_ICON_SIZE,
|
|
35
|
+
TIMED_MESSAGE_BAND_ALPHA,
|
|
36
|
+
TIMED_MESSAGE_LEFT_X,
|
|
37
|
+
TIMED_MESSAGE_TOP_Y,
|
|
38
|
+
)
|
|
31
39
|
|
|
32
40
|
_HUD_ICON_CACHE: dict[str, surface.Surface] = {}
|
|
33
41
|
|
|
34
|
-
HUD_ICON_SIZE = 12
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
def _scale_icon_to_box(icon: surface.Surface, size: int) -> surface.Surface:
|
|
@@ -133,7 +140,7 @@ def _draw_status_bar(
|
|
|
133
140
|
|
|
134
141
|
try:
|
|
135
142
|
font_settings = get_font_settings()
|
|
136
|
-
font = load_font(font_settings.resource, font_settings.scaled_size(
|
|
143
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(GAMEPLAY_FONT_SIZE))
|
|
137
144
|
text_surface = font.render(status_text, False, color)
|
|
138
145
|
text_rect = text_surface.get_rect(left=12, centery=bar_rect.centery)
|
|
139
146
|
screen.blit(text_surface, text_rect)
|
|
@@ -145,10 +152,7 @@ def _draw_status_bar(
|
|
|
145
152
|
if show_fps and fps is not None:
|
|
146
153
|
fps_text = f"FPS:{fps:.1f}"
|
|
147
154
|
fps_surface = font.render(fps_text, False, LIGHT_GRAY)
|
|
148
|
-
fps_rect = fps_surface.get_rect(
|
|
149
|
-
left=12,
|
|
150
|
-
bottom=max(2, bar_rect.top - 4),
|
|
151
|
-
)
|
|
155
|
+
fps_rect = fps_surface.get_rect(left=12, bottom=max(2, bar_rect.top))
|
|
152
156
|
screen.blit(fps_surface, fps_rect)
|
|
153
157
|
except pygame.error as e:
|
|
154
158
|
print(f"Error rendering status bar: {e}")
|
|
@@ -157,7 +161,7 @@ def _draw_status_bar(
|
|
|
157
161
|
def _draw_objective(lines: list[str], *, screen: surface.Surface) -> None:
|
|
158
162
|
try:
|
|
159
163
|
font_settings = get_font_settings()
|
|
160
|
-
font = load_font(font_settings.resource, font_settings.scaled_size(
|
|
164
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(GAMEPLAY_FONT_SIZE))
|
|
161
165
|
y = 8
|
|
162
166
|
for line in lines:
|
|
163
167
|
text_surface = font.render(line, False, YELLOW)
|
|
@@ -212,8 +216,10 @@ def _draw_endurance_timer(
|
|
|
212
216
|
elapsed_ms = max(0, min(goal_ms, state.endurance_elapsed_ms))
|
|
213
217
|
remaining_ms = max(0, goal_ms - elapsed_ms)
|
|
214
218
|
padding = 12
|
|
215
|
-
bar_height =
|
|
216
|
-
|
|
219
|
+
bar_height = 6
|
|
220
|
+
text_bottom = assets.screen_height - assets.status_bar_height - bar_height - 8
|
|
221
|
+
bar_overlap = 6
|
|
222
|
+
y_pos = text_bottom + 2 - bar_overlap
|
|
217
223
|
bar_rect = pygame.Rect(
|
|
218
224
|
padding,
|
|
219
225
|
y_pos,
|
|
@@ -226,16 +232,18 @@ def _draw_endurance_timer(
|
|
|
226
232
|
progress_ratio = elapsed_ms / goal_ms if goal_ms else 0.0
|
|
227
233
|
progress_width = int(bar_rect.width * max(0.0, min(1.0, progress_ratio)))
|
|
228
234
|
if progress_width > 0:
|
|
229
|
-
fill_color = (120, 20, 20)
|
|
235
|
+
fill_color = (120, 20, 20, 160)
|
|
230
236
|
if state.dawn_ready:
|
|
231
|
-
fill_color = (25, 40, 120)
|
|
237
|
+
fill_color = (25, 40, 120, 160)
|
|
232
238
|
fill_rect = pygame.Rect(
|
|
233
239
|
bar_rect.left,
|
|
234
240
|
bar_rect.top,
|
|
235
241
|
progress_width,
|
|
236
242
|
bar_rect.height,
|
|
237
243
|
)
|
|
238
|
-
pygame.
|
|
244
|
+
fill_surface = pygame.Surface((progress_width, bar_rect.height), pygame.SRCALPHA)
|
|
245
|
+
fill_surface.fill(fill_color)
|
|
246
|
+
screen.blit(fill_surface, fill_rect.topleft)
|
|
239
247
|
display_ms = int(remaining_ms * SURVIVAL_FAKE_CLOCK_RATIO)
|
|
240
248
|
display_ms = max(0, display_ms)
|
|
241
249
|
display_hours = display_ms // 3_600_000
|
|
@@ -244,19 +252,19 @@ def _draw_endurance_timer(
|
|
|
244
252
|
timer_text = tr("hud.endurance_timer_label", time=display_label)
|
|
245
253
|
try:
|
|
246
254
|
font_settings = get_font_settings()
|
|
247
|
-
font = load_font(font_settings.resource, font_settings.scaled_size(
|
|
255
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(GAMEPLAY_FONT_SIZE))
|
|
248
256
|
text_surface = font.render(timer_text, False, LIGHT_GRAY)
|
|
249
|
-
text_rect = text_surface.get_rect(left=bar_rect.left, bottom=
|
|
257
|
+
text_rect = text_surface.get_rect(left=bar_rect.left, bottom=text_bottom)
|
|
250
258
|
screen.blit(text_surface, text_rect)
|
|
251
259
|
if state.time_accel_active:
|
|
252
260
|
accel_text = tr("hud.time_accel")
|
|
253
261
|
accel_surface = font.render(accel_text, False, YELLOW)
|
|
254
|
-
accel_rect = accel_surface.get_rect(right=bar_rect.right, bottom=
|
|
262
|
+
accel_rect = accel_surface.get_rect(right=bar_rect.right, bottom=text_bottom)
|
|
255
263
|
screen.blit(accel_surface, accel_rect)
|
|
256
264
|
else:
|
|
257
265
|
hint_text = tr("hud.time_accel_hint")
|
|
258
266
|
hint_surface = font.render(hint_text, False, LIGHT_GRAY)
|
|
259
|
-
hint_rect = hint_surface.get_rect(right=bar_rect.right, bottom=
|
|
267
|
+
hint_rect = hint_surface.get_rect(right=bar_rect.right, bottom=text_bottom)
|
|
260
268
|
screen.blit(hint_surface, hint_rect)
|
|
261
269
|
except pygame.error as e:
|
|
262
270
|
print(f"Error rendering endurance timer: {e}")
|
|
@@ -273,7 +281,7 @@ def _draw_time_accel_indicator(
|
|
|
273
281
|
return
|
|
274
282
|
try:
|
|
275
283
|
font_settings = get_font_settings()
|
|
276
|
-
font = load_font(font_settings.resource, font_settings.scaled_size(
|
|
284
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(GAMEPLAY_FONT_SIZE))
|
|
277
285
|
if state.time_accel_active:
|
|
278
286
|
text = tr("hud.time_accel")
|
|
279
287
|
color = YELLOW
|
|
@@ -300,19 +308,69 @@ def _draw_survivor_messages(
|
|
|
300
308
|
return
|
|
301
309
|
try:
|
|
302
310
|
font_settings = get_font_settings()
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
font_size = font_settings.scaled_size(GAMEPLAY_FONT_SIZE * 2)
|
|
312
|
+
font = load_font(font_settings.resource, font_size)
|
|
313
|
+
line_height = int(round(font.get_linesize() * 2))
|
|
314
|
+
base_y = assets.screen_height // 2 - (line_height * 2)
|
|
315
|
+
for idx, message in enumerate(survivor_messages[:5]):
|
|
306
316
|
text = message.get("text", "")
|
|
307
317
|
if not text:
|
|
308
318
|
continue
|
|
309
319
|
msg_surface = font.render(text, False, ORANGE)
|
|
310
|
-
msg_rect = msg_surface.get_rect(center=(assets.screen_width // 2, base_y + idx *
|
|
320
|
+
msg_rect = msg_surface.get_rect(center=(assets.screen_width // 2, base_y + idx * line_height))
|
|
311
321
|
screen.blit(msg_surface, msg_rect)
|
|
312
322
|
except pygame.error as e:
|
|
313
323
|
print(f"Error rendering survivor message: {e}")
|
|
314
324
|
|
|
315
325
|
|
|
326
|
+
def _draw_timed_message(
|
|
327
|
+
screen: surface.Surface,
|
|
328
|
+
assets: RenderAssets,
|
|
329
|
+
*,
|
|
330
|
+
message: TimedMessage | None,
|
|
331
|
+
elapsed_play_ms: int,
|
|
332
|
+
) -> None:
|
|
333
|
+
if not message:
|
|
334
|
+
return
|
|
335
|
+
if message.expires_at_ms <= 0:
|
|
336
|
+
return
|
|
337
|
+
if elapsed_play_ms > message.expires_at_ms:
|
|
338
|
+
return
|
|
339
|
+
try:
|
|
340
|
+
font_settings = get_font_settings()
|
|
341
|
+
font_size = font_settings.scaled_size(GAMEPLAY_FONT_SIZE * 2)
|
|
342
|
+
font = load_font(font_settings.resource, font_size)
|
|
343
|
+
text_color = message.color or LIGHT_GRAY
|
|
344
|
+
line_height = int(round(font.get_linesize() * font_settings.line_height_scale))
|
|
345
|
+
lines = message.text.splitlines() or [message.text]
|
|
346
|
+
rendered_lines = [font.render(line, False, text_color) for line in lines]
|
|
347
|
+
max_width = max(surface.get_width() for surface in rendered_lines)
|
|
348
|
+
total_height = line_height * len(rendered_lines)
|
|
349
|
+
if message.align == "left":
|
|
350
|
+
text_rect = pygame.Rect(TIMED_MESSAGE_LEFT_X, TIMED_MESSAGE_TOP_Y, max_width, total_height)
|
|
351
|
+
else:
|
|
352
|
+
center_x = assets.screen_width // 2
|
|
353
|
+
center_y = assets.screen_height // 2
|
|
354
|
+
text_rect = pygame.Rect(0, 0, max_width, total_height)
|
|
355
|
+
text_rect.center = (center_x, center_y)
|
|
356
|
+
padding_x = 16
|
|
357
|
+
padding_y = max(8, int(round(line_height * 0.35)))
|
|
358
|
+
band_rect = text_rect.inflate(padding_x * 2, padding_y * 2)
|
|
359
|
+
band_surface = pygame.Surface(band_rect.size, pygame.SRCALPHA)
|
|
360
|
+
band_surface.fill((0, 0, 0, TIMED_MESSAGE_BAND_ALPHA))
|
|
361
|
+
screen.blit(band_surface, band_rect.topleft)
|
|
362
|
+
y = text_rect.top
|
|
363
|
+
for surface in rendered_lines:
|
|
364
|
+
if message.align == "left":
|
|
365
|
+
line_rect = surface.get_rect(topleft=(text_rect.left, y))
|
|
366
|
+
else:
|
|
367
|
+
line_rect = surface.get_rect(centerx=text_rect.centerx, y=y)
|
|
368
|
+
screen.blit(surface, line_rect)
|
|
369
|
+
y += line_height
|
|
370
|
+
except pygame.error as e:
|
|
371
|
+
print(f"Error rendering timed message: {e}")
|
|
372
|
+
|
|
373
|
+
|
|
316
374
|
def _build_objective_lines(
|
|
317
375
|
*,
|
|
318
376
|
stage: Stage | None,
|
|
@@ -320,7 +378,7 @@ def _build_objective_lines(
|
|
|
320
378
|
player: Player,
|
|
321
379
|
active_car: Car | None,
|
|
322
380
|
has_fuel: bool,
|
|
323
|
-
|
|
381
|
+
buddy_merged_count: int,
|
|
324
382
|
buddy_required: int,
|
|
325
383
|
survivors_onboard: int,
|
|
326
384
|
) -> list[str]:
|
|
@@ -330,23 +388,37 @@ def _build_objective_lines(
|
|
|
330
388
|
objective_lines.append(tr("objectives.get_outside"))
|
|
331
389
|
else:
|
|
332
390
|
objective_lines.append(tr("objectives.survive_until_dawn"))
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
391
|
+
if stage.buddy_required_count > 0:
|
|
392
|
+
buddy_ready = buddy_merged_count >= buddy_required
|
|
393
|
+
if not buddy_ready:
|
|
394
|
+
if buddy_required == 1:
|
|
395
|
+
objective_lines.append(tr("objectives.merge_buddy_single"))
|
|
396
|
+
else:
|
|
397
|
+
objective_lines.append(
|
|
398
|
+
tr("objectives.merge_buddy_multi", count=buddy_merged_count, limit=buddy_required)
|
|
399
|
+
)
|
|
400
|
+
return objective_lines
|
|
401
|
+
|
|
402
|
+
if stage and stage.buddy_required_count > 0:
|
|
403
|
+
buddy_ready = buddy_merged_count >= buddy_required
|
|
404
|
+
if not buddy_ready:
|
|
405
|
+
if buddy_required == 1:
|
|
406
|
+
objective_lines.append(tr("objectives.merge_buddy_single"))
|
|
339
407
|
else:
|
|
340
|
-
objective_lines.append(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
408
|
+
objective_lines.append(
|
|
409
|
+
tr("objectives.merge_buddy_multi", count=buddy_merged_count, limit=buddy_required)
|
|
410
|
+
)
|
|
411
|
+
if not stage.endurance_stage:
|
|
412
|
+
if not active_car:
|
|
413
|
+
if stage.requires_fuel and not has_fuel:
|
|
414
|
+
objective_lines.append(tr("objectives.find_fuel"))
|
|
415
|
+
else:
|
|
416
|
+
objective_lines.append(tr("objectives.find_car"))
|
|
348
417
|
else:
|
|
349
|
-
|
|
418
|
+
if stage.requires_fuel and not has_fuel:
|
|
419
|
+
objective_lines.append(tr("objectives.find_fuel"))
|
|
420
|
+
else:
|
|
421
|
+
objective_lines.append(tr("objectives.escape"))
|
|
350
422
|
elif stage and stage.requires_fuel and not has_fuel:
|
|
351
423
|
objective_lines.append(tr("objectives.find_fuel"))
|
|
352
424
|
elif stage and stage.rescue_stage:
|
zombie_escape/render/overview.py
CHANGED
|
@@ -3,10 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import pygame
|
|
4
4
|
from pygame import sprite, surface
|
|
5
5
|
|
|
6
|
-
from ..colors import BLACK, BLUE, FOOTPRINT_COLOR,
|
|
6
|
+
from ..colors import BLACK, BLUE, FOOTPRINT_COLOR, YELLOW, WHITE, get_environment_palette
|
|
7
7
|
from ..entities import Car, Flashlight, FuelCan, Player, Shoes, SteelBeam, Survivor, Wall
|
|
8
|
+
from ..font_utils import load_font
|
|
9
|
+
from ..localization import get_font_settings
|
|
8
10
|
from ..models import Footprint, GameData
|
|
9
11
|
from ..render_assets import RenderAssets, resolve_steel_beam_colors, resolve_wall_colors
|
|
12
|
+
from .hud import _get_fog_scale
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
def compute_floor_cells(
|
|
@@ -24,6 +27,31 @@ def compute_floor_cells(
|
|
|
24
27
|
return {(x, y) for y in range(rows) for x in range(cols) if (x, y) not in blocked}
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
def _draw_overview_tag(
|
|
31
|
+
surface: surface.Surface,
|
|
32
|
+
font: pygame.font.Font,
|
|
33
|
+
text: str,
|
|
34
|
+
item_rect: pygame.Rect,
|
|
35
|
+
*,
|
|
36
|
+
fg: tuple[int, int, int] = YELLOW,
|
|
37
|
+
padding: tuple[int, int] = (4, 2),
|
|
38
|
+
) -> None:
|
|
39
|
+
label = font.render(text, False, fg)
|
|
40
|
+
label_rect = label.get_rect()
|
|
41
|
+
padded = label_rect.inflate(padding[0] * 2, padding[1] * 2)
|
|
42
|
+
top_left = (item_rect.left, item_rect.top)
|
|
43
|
+
bottom_left = (item_rect.left, item_rect.bottom - padded.height)
|
|
44
|
+
if top_left[1] < 0 or top_left[1] + padded.height > surface.get_height():
|
|
45
|
+
x, y = bottom_left
|
|
46
|
+
else:
|
|
47
|
+
x, y = top_left
|
|
48
|
+
x = max(0, min(surface.get_width() - padded.width, x))
|
|
49
|
+
y = max(0, min(surface.get_height() - padded.height, y))
|
|
50
|
+
padded.topleft = (x, y)
|
|
51
|
+
label_rect.center = padded.center
|
|
52
|
+
surface.blit(label, label_rect)
|
|
53
|
+
|
|
54
|
+
|
|
27
55
|
def draw_level_overview(
|
|
28
56
|
assets: RenderAssets,
|
|
29
57
|
surface: surface.Surface,
|
|
@@ -39,12 +67,14 @@ def draw_level_overview(
|
|
|
39
67
|
shoes: list[Shoes] | None = None,
|
|
40
68
|
buddies: list[Survivor] | None = None,
|
|
41
69
|
survivors: list[Survivor] | None = None,
|
|
70
|
+
fall_spawn_cells: set[tuple[int, int]] | None = None,
|
|
42
71
|
palette_key: str | None = None,
|
|
43
72
|
) -> None:
|
|
44
73
|
palette = get_environment_palette(palette_key)
|
|
45
74
|
base_floor = palette.floor_primary
|
|
46
75
|
dark_floor = tuple(max(0, int(channel * 0.35)) for channel in base_floor)
|
|
47
76
|
floor_color = tuple(max(0, int(channel * 0.65)) for channel in base_floor)
|
|
77
|
+
fall_floor = tuple(max(0, int(channel * 0.55)) for channel in palette.fall_zone_primary)
|
|
48
78
|
surface.fill(dark_floor)
|
|
49
79
|
cell_size = assets.internal_wall_grid_snap
|
|
50
80
|
if cell_size > 0:
|
|
@@ -59,6 +89,18 @@ def draw_level_overview(
|
|
|
59
89
|
cell_size,
|
|
60
90
|
),
|
|
61
91
|
)
|
|
92
|
+
if fall_spawn_cells:
|
|
93
|
+
for x, y in fall_spawn_cells:
|
|
94
|
+
pygame.draw.rect(
|
|
95
|
+
surface,
|
|
96
|
+
fall_floor,
|
|
97
|
+
pygame.Rect(
|
|
98
|
+
x * cell_size,
|
|
99
|
+
y * cell_size,
|
|
100
|
+
cell_size,
|
|
101
|
+
cell_size,
|
|
102
|
+
),
|
|
103
|
+
)
|
|
62
104
|
|
|
63
105
|
for wall in wall_group:
|
|
64
106
|
if wall.max_health > 0:
|
|
@@ -179,6 +221,7 @@ def draw_debug_overview(
|
|
|
179
221
|
if survivor.alive() and survivor.is_buddy and not survivor.rescued
|
|
180
222
|
],
|
|
181
223
|
survivors=list(game_data.groups.survivor_group),
|
|
224
|
+
fall_spawn_cells=game_data.layout.fall_spawn_cells,
|
|
182
225
|
palette_key=game_data.state.ambient_palette_key,
|
|
183
226
|
)
|
|
184
227
|
zombie_color = (200, 80, 80)
|
|
@@ -191,6 +234,21 @@ def draw_debug_overview(
|
|
|
191
234
|
zombie.rect.center,
|
|
192
235
|
zombie_radius,
|
|
193
236
|
)
|
|
237
|
+
fov_target = None
|
|
238
|
+
if game_data.player and game_data.player.in_car and game_data.car and game_data.car.alive():
|
|
239
|
+
fov_target = game_data.car
|
|
240
|
+
elif game_data.player:
|
|
241
|
+
fov_target = game_data.player
|
|
242
|
+
if fov_target:
|
|
243
|
+
fov_scale = _get_fog_scale(assets, game_data.state.flashlight_count)
|
|
244
|
+
fov_radius = max(1, int(assets.fov_radius * fov_scale))
|
|
245
|
+
pygame.draw.circle(
|
|
246
|
+
overview_surface,
|
|
247
|
+
(255, 255, 120),
|
|
248
|
+
fov_target.rect.center,
|
|
249
|
+
fov_radius,
|
|
250
|
+
width=2,
|
|
251
|
+
)
|
|
194
252
|
cam_offset = game_data.camera.camera
|
|
195
253
|
camera_rect = pygame.Rect(
|
|
196
254
|
-cam_offset.x,
|
|
@@ -212,7 +270,40 @@ def draw_debug_overview(
|
|
|
212
270
|
scaled_h = max(1, scaled_h)
|
|
213
271
|
scaled_overview = pygame.transform.smoothscale(overview_surface, (scaled_w, scaled_h))
|
|
214
272
|
screen.fill(BLACK)
|
|
273
|
+
scaled_rect = scaled_overview.get_rect(center=(screen_width // 2, screen_height // 2))
|
|
215
274
|
screen.blit(
|
|
216
275
|
scaled_overview,
|
|
217
|
-
|
|
276
|
+
scaled_rect,
|
|
218
277
|
)
|
|
278
|
+
try:
|
|
279
|
+
font_settings = get_font_settings()
|
|
280
|
+
label_font = load_font(font_settings.resource, font_settings.scaled_size(11))
|
|
281
|
+
except pygame.error as e:
|
|
282
|
+
print(f"Error loading overview font: {e}")
|
|
283
|
+
return
|
|
284
|
+
scale_x = scaled_w / max(1, level_rect.width)
|
|
285
|
+
scale_y = scaled_h / max(1, level_rect.height)
|
|
286
|
+
|
|
287
|
+
def _scaled_rect(rect: pygame.Rect) -> pygame.Rect:
|
|
288
|
+
return pygame.Rect(
|
|
289
|
+
int(scaled_rect.left + rect.left * scale_x),
|
|
290
|
+
int(scaled_rect.top + rect.top * scale_y),
|
|
291
|
+
max(1, int(rect.width * scale_x)),
|
|
292
|
+
max(1, int(rect.height * scale_y)),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if game_data.car and game_data.car.alive():
|
|
296
|
+
_draw_overview_tag(screen, label_font, "C", _scaled_rect(game_data.car.rect))
|
|
297
|
+
for parked in game_data.waiting_cars:
|
|
298
|
+
if parked.alive():
|
|
299
|
+
_draw_overview_tag(screen, label_font, "C", _scaled_rect(parked.rect))
|
|
300
|
+
if game_data.fuel and game_data.fuel.alive():
|
|
301
|
+
_draw_overview_tag(screen, label_font, "F", _scaled_rect(game_data.fuel.rect))
|
|
302
|
+
if game_data.flashlights:
|
|
303
|
+
for flashlight in game_data.flashlights:
|
|
304
|
+
if flashlight.alive():
|
|
305
|
+
_draw_overview_tag(screen, label_font, "L", _scaled_rect(flashlight.rect))
|
|
306
|
+
if game_data.shoes:
|
|
307
|
+
for item in game_data.shoes:
|
|
308
|
+
if item.alive():
|
|
309
|
+
_draw_overview_tag(screen, label_font, "S", _scaled_rect(item.rect))
|
zombie_escape/render/shadows.py
CHANGED
|
@@ -22,7 +22,7 @@ _SHADOW_LAYER_CACHE: dict[tuple[int, int], surface.Surface] = {}
|
|
|
22
22
|
_SHADOW_CIRCLE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def
|
|
25
|
+
def _get_shadow_cell_surface(
|
|
26
26
|
cell_size: int,
|
|
27
27
|
alpha: int,
|
|
28
28
|
*,
|
|
@@ -176,7 +176,7 @@ def _draw_wall_shadows(
|
|
|
176
176
|
return False
|
|
177
177
|
base_shadow_size = max(cell_size + 2, int(cell_size * 1.35))
|
|
178
178
|
shadow_size = max(1, int(base_shadow_size * 1.5))
|
|
179
|
-
shadow_surface =
|
|
179
|
+
shadow_surface = _get_shadow_cell_surface(
|
|
180
180
|
shadow_size,
|
|
181
181
|
alpha,
|
|
182
182
|
edge_softness=0.12,
|
|
@@ -20,6 +20,12 @@ FALLING_WHIRLWIND_COLOR = (200, 200, 200, 120)
|
|
|
20
20
|
FALLING_DUST_COLOR = (70, 70, 70, 130)
|
|
21
21
|
ANGLE_BINS = 16
|
|
22
22
|
HAND_SPREAD_RAD = math.radians(75)
|
|
23
|
+
GAMEPLAY_FONT_SIZE = 11
|
|
24
|
+
HUD_ICON_SIZE = 12
|
|
25
|
+
FADE_IN_DURATION_MS = 900
|
|
26
|
+
TIMED_MESSAGE_LEFT_X = 20
|
|
27
|
+
TIMED_MESSAGE_TOP_Y = 48
|
|
28
|
+
TIMED_MESSAGE_BAND_ALPHA = 80
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
@dataclass(frozen=True)
|
|
@@ -113,6 +119,12 @@ __all__ = [
|
|
|
113
119
|
"ZOMBIE_NOSE_COLOR",
|
|
114
120
|
"ANGLE_BINS",
|
|
115
121
|
"HAND_SPREAD_RAD",
|
|
122
|
+
"GAMEPLAY_FONT_SIZE",
|
|
123
|
+
"HUD_ICON_SIZE",
|
|
124
|
+
"FADE_IN_DURATION_MS",
|
|
125
|
+
"TIMED_MESSAGE_LEFT_X",
|
|
126
|
+
"TIMED_MESSAGE_TOP_Y",
|
|
127
|
+
"TIMED_MESSAGE_BAND_ALPHA",
|
|
116
128
|
"HUMANOID_OUTLINE_COLOR",
|
|
117
129
|
"HUMANOID_OUTLINE_WIDTH",
|
|
118
130
|
"SURVIVOR_COLOR",
|
|
@@ -10,21 +10,10 @@ from dataclasses import dataclass
|
|
|
10
10
|
from enum import Enum
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
import pygame
|
|
14
|
-
from pygame import surface
|
|
15
|
-
|
|
16
13
|
try: # pragma: no cover - version fallback not critical for tests
|
|
17
14
|
from ..__about__ import __version__
|
|
18
15
|
except Exception: # pragma: no cover - fallback version
|
|
19
16
|
__version__ = "0.0.0-unknown"
|
|
20
|
-
from ..screen_constants import (
|
|
21
|
-
DEFAULT_WINDOW_SCALE,
|
|
22
|
-
SCREEN_HEIGHT,
|
|
23
|
-
SCREEN_WIDTH,
|
|
24
|
-
WINDOW_SCALE_MAX,
|
|
25
|
-
WINDOW_SCALE_MIN,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
17
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
29
18
|
from ..models import GameData, Stage
|
|
30
19
|
|
|
@@ -52,186 +41,14 @@ class ScreenTransition:
|
|
|
52
41
|
seed_is_auto: bool = False
|
|
53
42
|
|
|
54
43
|
|
|
55
|
-
current_window_scale = DEFAULT_WINDOW_SCALE # Applied to the OS window only
|
|
56
|
-
current_maximized = False
|
|
57
|
-
last_window_scale = DEFAULT_WINDOW_SCALE
|
|
58
|
-
current_window_size = (
|
|
59
|
-
int(SCREEN_WIDTH * DEFAULT_WINDOW_SCALE),
|
|
60
|
-
int(SCREEN_HEIGHT * DEFAULT_WINDOW_SCALE),
|
|
61
|
-
)
|
|
62
|
-
last_logged_window_size = current_window_size
|
|
63
|
-
|
|
64
44
|
__all__ = [
|
|
65
45
|
"ScreenID",
|
|
66
46
|
"ScreenTransition",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"toggle_fullscreen",
|
|
71
|
-
"sync_window_size",
|
|
47
|
+
"TITLE_FONT_SCALE",
|
|
48
|
+
"TITLE_HEADER_Y",
|
|
49
|
+
"TITLE_SECTION_TOP",
|
|
72
50
|
]
|
|
73
51
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
window = pygame.display.get_surface()
|
|
78
|
-
if window is None:
|
|
79
|
-
return
|
|
80
|
-
window_size = _fetch_window_size(window)
|
|
81
|
-
_update_window_size(window_size, source="frame")
|
|
82
|
-
logical_size = logical_surface.get_size()
|
|
83
|
-
if window_size == logical_size:
|
|
84
|
-
window.blit(logical_surface, (0, 0))
|
|
85
|
-
else:
|
|
86
|
-
# Preserve aspect ratio with letterboxing.
|
|
87
|
-
scale_x = window_size[0] / max(1, logical_size[0])
|
|
88
|
-
scale_y = window_size[1] / max(1, logical_size[1])
|
|
89
|
-
scale = min(scale_x, scale_y)
|
|
90
|
-
scaled_width = max(1, int(logical_size[0] * scale))
|
|
91
|
-
scaled_height = max(1, int(logical_size[1] * scale))
|
|
92
|
-
window.fill((0, 0, 0))
|
|
93
|
-
if (scaled_width, scaled_height) == logical_size:
|
|
94
|
-
scaled_surface = logical_surface
|
|
95
|
-
else:
|
|
96
|
-
scaled_surface = pygame.transform.scale(logical_surface, (scaled_width, scaled_height))
|
|
97
|
-
offset_x = (window_size[0] - scaled_width) // 2
|
|
98
|
-
offset_y = (window_size[1] - scaled_height) // 2
|
|
99
|
-
window.blit(scaled_surface, (offset_x, offset_y))
|
|
100
|
-
pygame.display.flip()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def apply_window_scale(scale: float, *, game_data: "GameData | None" = None) -> surface.Surface:
|
|
104
|
-
"""Resize the OS window; logical render surface stays constant."""
|
|
105
|
-
global current_window_scale, current_maximized, last_window_scale
|
|
106
|
-
|
|
107
|
-
clamped_scale = max(WINDOW_SCALE_MIN, min(WINDOW_SCALE_MAX, scale))
|
|
108
|
-
current_window_scale = clamped_scale
|
|
109
|
-
last_window_scale = clamped_scale
|
|
110
|
-
current_maximized = False
|
|
111
|
-
|
|
112
|
-
window_width = max(1, int(SCREEN_WIDTH * current_window_scale))
|
|
113
|
-
window_height = max(1, int(SCREEN_HEIGHT * current_window_scale))
|
|
114
|
-
|
|
115
|
-
new_window = pygame.display.set_mode((window_width, window_height), pygame.RESIZABLE)
|
|
116
|
-
_update_window_size((window_width, window_height), source="apply_scale")
|
|
117
|
-
_update_window_caption()
|
|
118
|
-
|
|
119
|
-
if game_data is not None:
|
|
120
|
-
game_data.state.overview_created = False
|
|
121
|
-
|
|
122
|
-
return new_window
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def nudge_window_scale(multiplier: float, *, game_data: "GameData | None" = None) -> surface.Surface:
|
|
126
|
-
"""Scale the window relative to the current zoom level."""
|
|
127
|
-
target_scale = current_window_scale * multiplier
|
|
128
|
-
return apply_window_scale(target_scale, game_data=game_data)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def toggle_fullscreen(*, game_data: "GameData | None" = None) -> surface.Surface | None:
|
|
132
|
-
"""Toggle a maximized window without persisting the setting."""
|
|
133
|
-
global current_maximized, last_window_scale
|
|
134
|
-
if current_maximized:
|
|
135
|
-
current_maximized = False
|
|
136
|
-
window_width = max(1, int(SCREEN_WIDTH * last_window_scale))
|
|
137
|
-
window_height = max(1, int(SCREEN_HEIGHT * last_window_scale))
|
|
138
|
-
window = pygame.display.set_mode((window_width, window_height), pygame.RESIZABLE)
|
|
139
|
-
_restore_window()
|
|
140
|
-
_update_window_caption()
|
|
141
|
-
_update_window_size((window_width, window_height), source="toggle_windowed")
|
|
142
|
-
else:
|
|
143
|
-
last_window_scale = current_window_scale
|
|
144
|
-
current_maximized = True
|
|
145
|
-
window = pygame.display.set_mode(_fetch_window_size(None), pygame.RESIZABLE)
|
|
146
|
-
_maximize_window()
|
|
147
|
-
window_width, window_height = _fetch_window_size(window)
|
|
148
|
-
_update_window_caption()
|
|
149
|
-
_update_window_size((window_width, window_height), source="toggle_fullscreen")
|
|
150
|
-
pygame.mouse.set_visible(not current_maximized)
|
|
151
|
-
if game_data is not None:
|
|
152
|
-
game_data.state.overview_created = False
|
|
153
|
-
return window
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def sync_window_size(event: pygame.event.Event, *, game_data: "GameData | None" = None) -> None:
|
|
157
|
-
"""Synchronize tracked window size with SDL window events."""
|
|
158
|
-
global current_window_scale, last_window_scale
|
|
159
|
-
size = getattr(event, "size", None)
|
|
160
|
-
if not size:
|
|
161
|
-
width = getattr(event, "x", None)
|
|
162
|
-
height = getattr(event, "y", None)
|
|
163
|
-
if width is not None and height is not None:
|
|
164
|
-
size = (width, height)
|
|
165
|
-
if not size:
|
|
166
|
-
return
|
|
167
|
-
window_width, window_height = _normalize_window_size(size)
|
|
168
|
-
_update_window_size((window_width, window_height), source="window_event")
|
|
169
|
-
if not current_maximized:
|
|
170
|
-
scale_x = window_width / max(1, SCREEN_WIDTH)
|
|
171
|
-
scale_y = window_height / max(1, SCREEN_HEIGHT)
|
|
172
|
-
scale = max(WINDOW_SCALE_MIN, min(WINDOW_SCALE_MAX, min(scale_x, scale_y)))
|
|
173
|
-
current_window_scale = scale
|
|
174
|
-
last_window_scale = scale
|
|
175
|
-
_update_window_caption()
|
|
176
|
-
if game_data is not None:
|
|
177
|
-
game_data.state.overview_created = False
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def _fetch_window_size(window: surface.Surface | None) -> tuple[int, int]:
|
|
181
|
-
if hasattr(pygame.display, "get_window_size"):
|
|
182
|
-
size = pygame.display.get_window_size()
|
|
183
|
-
if size != (0, 0):
|
|
184
|
-
return _normalize_window_size(size)
|
|
185
|
-
if window is not None:
|
|
186
|
-
return _normalize_window_size(window.get_size())
|
|
187
|
-
window_width = max(1, int(SCREEN_WIDTH * last_window_scale))
|
|
188
|
-
window_height = max(1, int(SCREEN_HEIGHT * last_window_scale))
|
|
189
|
-
return window_width, window_height
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _normalize_window_size(size: tuple[int, int]) -> tuple[int, int]:
|
|
193
|
-
width = max(1, int(size[0]))
|
|
194
|
-
height = max(1, int(size[1]))
|
|
195
|
-
return width, height
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _update_window_size(size: tuple[int, int], *, source: str) -> None:
|
|
199
|
-
global current_window_size, last_logged_window_size
|
|
200
|
-
current_window_size = size
|
|
201
|
-
if size != last_logged_window_size:
|
|
202
|
-
print(f"WINDOW_SIZE {source}={size[0]}x{size[1]}")
|
|
203
|
-
last_logged_window_size = size
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _update_window_caption() -> None:
|
|
207
|
-
pygame.display.set_caption("Zombie Escape")
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _maximize_window() -> None:
|
|
211
|
-
try:
|
|
212
|
-
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
213
|
-
except Exception:
|
|
214
|
-
return
|
|
215
|
-
try:
|
|
216
|
-
window = sdl2.Window.from_display_module()
|
|
217
|
-
except Exception:
|
|
218
|
-
return
|
|
219
|
-
try:
|
|
220
|
-
window.maximize()
|
|
221
|
-
except Exception:
|
|
222
|
-
return
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _restore_window() -> None:
|
|
226
|
-
try:
|
|
227
|
-
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
228
|
-
except Exception:
|
|
229
|
-
return
|
|
230
|
-
try:
|
|
231
|
-
window = sdl2.Window.from_display_module()
|
|
232
|
-
except Exception:
|
|
233
|
-
return
|
|
234
|
-
try:
|
|
235
|
-
window.restore()
|
|
236
|
-
except Exception:
|
|
237
|
-
return
|
|
52
|
+
TITLE_FONT_SCALE = 1
|
|
53
|
+
TITLE_HEADER_Y = 20
|
|
54
|
+
TITLE_SECTION_TOP = 45
|