zombie-escape 1.13.1__py3-none-any.whl → 1.14.4__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.
Files changed (41) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/colors.py +7 -21
  3. zombie_escape/entities.py +100 -191
  4. zombie_escape/export_images.py +39 -33
  5. zombie_escape/gameplay/ambient.py +2 -6
  6. zombie_escape/gameplay/footprints.py +8 -11
  7. zombie_escape/gameplay/interactions.py +17 -58
  8. zombie_escape/gameplay/layout.py +20 -46
  9. zombie_escape/gameplay/movement.py +7 -21
  10. zombie_escape/gameplay/spawn.py +12 -40
  11. zombie_escape/gameplay/state.py +1 -0
  12. zombie_escape/gameplay/survivors.py +5 -16
  13. zombie_escape/gameplay/utils.py +4 -13
  14. zombie_escape/input_utils.py +8 -31
  15. zombie_escape/level_blueprints.py +112 -69
  16. zombie_escape/level_constants.py +8 -0
  17. zombie_escape/locales/ui.en.json +12 -0
  18. zombie_escape/locales/ui.ja.json +12 -0
  19. zombie_escape/localization.py +3 -11
  20. zombie_escape/models.py +26 -9
  21. zombie_escape/render/__init__.py +30 -0
  22. zombie_escape/render/core.py +992 -0
  23. zombie_escape/render/hud.py +444 -0
  24. zombie_escape/render/overview.py +218 -0
  25. zombie_escape/render/shadows.py +343 -0
  26. zombie_escape/render_assets.py +11 -33
  27. zombie_escape/rng.py +4 -8
  28. zombie_escape/screens/__init__.py +14 -30
  29. zombie_escape/screens/game_over.py +43 -15
  30. zombie_escape/screens/gameplay.py +41 -104
  31. zombie_escape/screens/settings.py +19 -104
  32. zombie_escape/screens/title.py +36 -176
  33. zombie_escape/stage_constants.py +192 -67
  34. zombie_escape/zombie_escape.py +1 -1
  35. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/METADATA +100 -39
  36. zombie_escape-1.14.4.dist-info/RECORD +53 -0
  37. zombie_escape/render.py +0 -1746
  38. zombie_escape-1.13.1.dist-info/RECORD +0 -49
  39. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/WHEEL +0 -0
  40. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/entry_points.txt +0 -0
  41. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,444 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Any
5
+
6
+ import pygame
7
+ from pygame import sprite, surface
8
+
9
+ from ..colors import LIGHT_GRAY, ORANGE, YELLOW
10
+ from ..entities import Camera, Car, Player
11
+ from ..entities_constants import (
12
+ FLASHLIGHT_HEIGHT,
13
+ FLASHLIGHT_WIDTH,
14
+ FUEL_CAN_HEIGHT,
15
+ FUEL_CAN_WIDTH,
16
+ SHOES_HEIGHT,
17
+ SHOES_WIDTH,
18
+ )
19
+ from ..font_utils import load_font
20
+ from ..gameplay_constants import SURVIVAL_FAKE_CLOCK_RATIO
21
+ from ..localization import get_font_settings
22
+ from ..localization import translate as tr
23
+ from ..models import Stage
24
+ from ..render_assets import (
25
+ RenderAssets,
26
+ build_flashlight_surface,
27
+ build_fuel_can_surface,
28
+ build_shoes_surface,
29
+ )
30
+ from ..render_constants import FLASHLIGHT_FOG_SCALE_ONE, FLASHLIGHT_FOG_SCALE_TWO
31
+
32
+ _HUD_ICON_CACHE: dict[str, surface.Surface] = {}
33
+
34
+ HUD_ICON_SIZE = 12
35
+
36
+
37
+ def _scale_icon_to_box(icon: surface.Surface, size: int) -> surface.Surface:
38
+ target_size = max(1, size)
39
+ width = max(1, icon.get_width())
40
+ height = max(1, icon.get_height())
41
+ scale = min(target_size / width, target_size / height)
42
+ target_width = max(1, int(width * scale))
43
+ target_height = max(1, int(height * scale))
44
+ scaled = pygame.transform.smoothscale(icon, (target_width, target_height))
45
+ boxed = pygame.Surface((target_size, target_size), pygame.SRCALPHA)
46
+ boxed.blit(
47
+ scaled,
48
+ (
49
+ (target_size - target_width) // 2,
50
+ (target_size - target_height) // 2,
51
+ ),
52
+ )
53
+ return boxed
54
+
55
+
56
+ def _get_hud_icon(kind: str) -> surface.Surface:
57
+ cached = _HUD_ICON_CACHE.get(kind)
58
+ if cached is not None:
59
+ return cached
60
+ if kind == "fuel":
61
+ icon = build_fuel_can_surface(FUEL_CAN_WIDTH, FUEL_CAN_HEIGHT)
62
+ elif kind == "flashlight":
63
+ icon = build_flashlight_surface(FLASHLIGHT_WIDTH, FLASHLIGHT_HEIGHT)
64
+ elif kind == "shoes":
65
+ icon = build_shoes_surface(SHOES_WIDTH, SHOES_HEIGHT)
66
+ else:
67
+ icon = pygame.Surface((1, 1), pygame.SRCALPHA)
68
+ icon = _scale_icon_to_box(icon, HUD_ICON_SIZE)
69
+ _HUD_ICON_CACHE[kind] = icon
70
+ return icon
71
+
72
+
73
+ def _draw_status_bar(
74
+ screen: surface.Surface,
75
+ assets: RenderAssets,
76
+ config: dict[str, Any],
77
+ *,
78
+ stage: Stage | None = None,
79
+ seed: int | None = None,
80
+ debug_mode: bool = False,
81
+ zombie_group: sprite.Group | None = None,
82
+ falling_spawn_carry: int | None = None,
83
+ show_fps: bool = False,
84
+ fps: float | None = None,
85
+ ) -> None:
86
+ """Render a compact status bar with current config flags and stage info."""
87
+ bar_rect = pygame.Rect(
88
+ 0,
89
+ assets.screen_height - assets.status_bar_height,
90
+ assets.screen_width,
91
+ assets.status_bar_height,
92
+ )
93
+ overlay = pygame.Surface((bar_rect.width, bar_rect.height), pygame.SRCALPHA)
94
+ overlay.fill((0, 0, 0, 140))
95
+ screen.blit(overlay, bar_rect.topleft)
96
+
97
+ footprints_on = config.get("footprints", {}).get("enabled", True)
98
+ fast_on = config.get("fast_zombies", {}).get("enabled", True)
99
+ hint_on = config.get("car_hint", {}).get("enabled", True)
100
+ steel_on = config.get("steel_beams", {}).get("enabled", False)
101
+ if stage:
102
+ # Keep the label compact for the status bar
103
+ if stage.id.startswith("stage"):
104
+ stage_suffix = stage.id.removeprefix("stage")
105
+ stage_label = f"#{stage_suffix}" if stage_suffix else stage.id
106
+ else:
107
+ stage_label = stage.id
108
+ else:
109
+ stage_label = "#1"
110
+
111
+ parts = [tr("status.stage", label=stage_label)]
112
+ if footprints_on:
113
+ parts.append(tr("status.footprints"))
114
+ if hint_on:
115
+ parts.append(tr("status.car_hint"))
116
+ if fast_on:
117
+ parts.append(tr("status.fast"))
118
+ if steel_on:
119
+ parts.append(tr("status.steel"))
120
+ if debug_mode:
121
+ if zombie_group is not None:
122
+ zombies = [z for z in zombie_group if z.alive()]
123
+ total = len(zombies)
124
+ tracker = sum(1 for z in zombies if z.tracker)
125
+ wall = sum(1 for z in zombies if z.wall_hugging)
126
+ normal = max(0, total - tracker - wall)
127
+ debug_counts = f"Z:{total} N:{normal} T:{tracker} W:{wall}"
128
+ if falling_spawn_carry is not None:
129
+ debug_counts = f"{debug_counts} C:{max(0, falling_spawn_carry)}"
130
+ parts.append(debug_counts)
131
+ status_text = " | ".join(parts)
132
+ color = LIGHT_GRAY
133
+
134
+ try:
135
+ font_settings = get_font_settings()
136
+ font = load_font(font_settings.resource, font_settings.scaled_size(11))
137
+ text_surface = font.render(status_text, False, color)
138
+ text_rect = text_surface.get_rect(left=12, centery=bar_rect.centery)
139
+ screen.blit(text_surface, text_rect)
140
+ if seed is not None:
141
+ seed_text = tr("status.seed", value=str(seed))
142
+ seed_surface = font.render(seed_text, False, LIGHT_GRAY)
143
+ seed_rect = seed_surface.get_rect(right=bar_rect.right - 12, centery=bar_rect.centery)
144
+ screen.blit(seed_surface, seed_rect)
145
+ if show_fps and fps is not None:
146
+ fps_text = f"FPS:{fps:.1f}"
147
+ 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
+ )
152
+ screen.blit(fps_surface, fps_rect)
153
+ except pygame.error as e:
154
+ print(f"Error rendering status bar: {e}")
155
+
156
+
157
+ def _draw_objective(lines: list[str], *, screen: surface.Surface) -> None:
158
+ try:
159
+ font_settings = get_font_settings()
160
+ font = load_font(font_settings.resource, font_settings.scaled_size(11))
161
+ y = 8
162
+ for line in lines:
163
+ text_surface = font.render(line, False, YELLOW)
164
+ text_rect = text_surface.get_rect(topleft=(12, y))
165
+ screen.blit(text_surface, text_rect)
166
+ y += text_rect.height + 4
167
+ except pygame.error as e:
168
+ print(f"Error rendering objective: {e}")
169
+
170
+
171
+ def _draw_inventory_icons(
172
+ screen: surface.Surface,
173
+ assets: RenderAssets,
174
+ *,
175
+ has_fuel: bool,
176
+ flashlight_count: int,
177
+ shoes_count: int,
178
+ ) -> None:
179
+ icons: list[surface.Surface] = []
180
+ if has_fuel:
181
+ icons.append(_get_hud_icon("fuel"))
182
+ for _ in range(max(0, int(flashlight_count))):
183
+ icons.append(_get_hud_icon("flashlight"))
184
+ for _ in range(max(0, int(shoes_count))):
185
+ icons.append(_get_hud_icon("shoes"))
186
+ if not icons:
187
+ return
188
+ spacing = 3
189
+ padding = 8
190
+ total_width = sum(icon.get_width() for icon in icons)
191
+ total_width += spacing * max(0, len(icons) - 1)
192
+ start_x = assets.screen_width - padding - total_width
193
+ y = 8
194
+ x = max(padding, start_x)
195
+ for icon in icons:
196
+ screen.blit(icon, (x, y))
197
+ x += icon.get_width() + spacing
198
+
199
+
200
+ def _draw_endurance_timer(
201
+ screen: surface.Surface,
202
+ assets: RenderAssets,
203
+ *,
204
+ stage: Stage | None,
205
+ state: Any,
206
+ ) -> None:
207
+ if not (stage and stage.endurance_stage):
208
+ return
209
+ goal_ms = state.endurance_goal_ms
210
+ if goal_ms <= 0:
211
+ return
212
+ elapsed_ms = max(0, min(goal_ms, state.endurance_elapsed_ms))
213
+ remaining_ms = max(0, goal_ms - elapsed_ms)
214
+ padding = 12
215
+ bar_height = 8
216
+ y_pos = assets.screen_height - assets.status_bar_height - bar_height - 10
217
+ bar_rect = pygame.Rect(
218
+ padding,
219
+ y_pos,
220
+ assets.screen_width - padding * 2,
221
+ bar_height,
222
+ )
223
+ track_surface = pygame.Surface((bar_rect.width, bar_rect.height), pygame.SRCALPHA)
224
+ track_surface.fill((0, 0, 0, 140))
225
+ screen.blit(track_surface, bar_rect.topleft)
226
+ progress_ratio = elapsed_ms / goal_ms if goal_ms else 0.0
227
+ progress_width = int(bar_rect.width * max(0.0, min(1.0, progress_ratio)))
228
+ if progress_width > 0:
229
+ fill_color = (120, 20, 20)
230
+ if state.dawn_ready:
231
+ fill_color = (25, 40, 120)
232
+ fill_rect = pygame.Rect(
233
+ bar_rect.left,
234
+ bar_rect.top,
235
+ progress_width,
236
+ bar_rect.height,
237
+ )
238
+ pygame.draw.rect(screen, fill_color, fill_rect)
239
+ display_ms = int(remaining_ms * SURVIVAL_FAKE_CLOCK_RATIO)
240
+ display_ms = max(0, display_ms)
241
+ display_hours = display_ms // 3_600_000
242
+ display_minutes = (display_ms % 3_600_000) // 60_000
243
+ display_label = f"{int(display_hours):02d}:{int(display_minutes):02d}"
244
+ timer_text = tr("hud.endurance_timer_label", time=display_label)
245
+ try:
246
+ font_settings = get_font_settings()
247
+ font = load_font(font_settings.resource, font_settings.scaled_size(12))
248
+ text_surface = font.render(timer_text, False, LIGHT_GRAY)
249
+ text_rect = text_surface.get_rect(left=bar_rect.left, bottom=bar_rect.top - 2)
250
+ screen.blit(text_surface, text_rect)
251
+ if state.time_accel_active:
252
+ accel_text = tr("hud.time_accel")
253
+ accel_surface = font.render(accel_text, False, YELLOW)
254
+ accel_rect = accel_surface.get_rect(right=bar_rect.right, bottom=bar_rect.top - 2)
255
+ screen.blit(accel_surface, accel_rect)
256
+ else:
257
+ hint_text = tr("hud.time_accel_hint")
258
+ hint_surface = font.render(hint_text, False, LIGHT_GRAY)
259
+ hint_rect = hint_surface.get_rect(right=bar_rect.right, bottom=bar_rect.top - 2)
260
+ screen.blit(hint_surface, hint_rect)
261
+ except pygame.error as e:
262
+ print(f"Error rendering endurance timer: {e}")
263
+
264
+
265
+ def _draw_time_accel_indicator(
266
+ screen: surface.Surface,
267
+ assets: RenderAssets,
268
+ *,
269
+ stage: Stage | None,
270
+ state: Any,
271
+ ) -> None:
272
+ if stage and stage.endurance_stage:
273
+ return
274
+ try:
275
+ font_settings = get_font_settings()
276
+ font = load_font(font_settings.resource, font_settings.scaled_size(12))
277
+ if state.time_accel_active:
278
+ text = tr("hud.time_accel")
279
+ color = YELLOW
280
+ else:
281
+ text = tr("hud.time_accel_hint")
282
+ color = LIGHT_GRAY
283
+ text_surface = font.render(text, False, color)
284
+ bottom_margin = assets.status_bar_height + 6
285
+ text_rect = text_surface.get_rect(
286
+ right=assets.screen_width - 12,
287
+ bottom=assets.screen_height - bottom_margin,
288
+ )
289
+ screen.blit(text_surface, text_rect)
290
+ except pygame.error as e:
291
+ print(f"Error rendering acceleration indicator: {e}")
292
+
293
+
294
+ def _draw_survivor_messages(
295
+ screen: surface.Surface,
296
+ assets: RenderAssets,
297
+ survivor_messages: list[dict[str, Any]],
298
+ ) -> None:
299
+ if not survivor_messages:
300
+ return
301
+ try:
302
+ font_settings = get_font_settings()
303
+ font = load_font(font_settings.resource, font_settings.scaled_size(14))
304
+ base_y = assets.screen_height // 2 - 70
305
+ for idx, message in enumerate(survivor_messages[:3]):
306
+ text = message.get("text", "")
307
+ if not text:
308
+ continue
309
+ msg_surface = font.render(text, False, ORANGE)
310
+ msg_rect = msg_surface.get_rect(center=(assets.screen_width // 2, base_y + idx * 18))
311
+ screen.blit(msg_surface, msg_rect)
312
+ except pygame.error as e:
313
+ print(f"Error rendering survivor message: {e}")
314
+
315
+
316
+ def _build_objective_lines(
317
+ *,
318
+ stage: Stage | None,
319
+ state: Any,
320
+ player: Player,
321
+ active_car: Car | None,
322
+ has_fuel: bool,
323
+ buddy_onboard: int,
324
+ buddy_required: int,
325
+ survivors_onboard: int,
326
+ ) -> list[str]:
327
+ objective_lines: list[str] = []
328
+ if stage and stage.endurance_stage:
329
+ if state.dawn_ready:
330
+ objective_lines.append(tr("objectives.get_outside"))
331
+ else:
332
+ objective_lines.append(tr("objectives.survive_until_dawn"))
333
+ elif stage and stage.buddy_required_count > 0:
334
+ buddy_ready = buddy_onboard >= buddy_required
335
+ if not active_car:
336
+ objective_lines.append(tr("objectives.pickup_buddy"))
337
+ if stage.requires_fuel and not has_fuel:
338
+ objective_lines.append(tr("objectives.find_fuel"))
339
+ else:
340
+ objective_lines.append(tr("objectives.find_car"))
341
+ else:
342
+ if stage.requires_fuel and not has_fuel:
343
+ objective_lines.append(tr("objectives.find_fuel"))
344
+ elif not buddy_ready:
345
+ objective_lines.append(tr("objectives.board_buddy"))
346
+ objective_lines.append(tr("objectives.buddy_onboard", count=buddy_onboard))
347
+ objective_lines.append(tr("objectives.escape"))
348
+ else:
349
+ objective_lines.append(tr("objectives.escape"))
350
+ elif stage and stage.requires_fuel and not has_fuel:
351
+ objective_lines.append(tr("objectives.find_fuel"))
352
+ elif stage and stage.rescue_stage:
353
+ if not player.in_car:
354
+ objective_lines.append(tr("objectives.find_car"))
355
+ else:
356
+ objective_lines.append(tr("objectives.escape_with_survivors"))
357
+ elif not player.in_car:
358
+ objective_lines.append(tr("objectives.find_car"))
359
+ else:
360
+ objective_lines.append(tr("objectives.escape"))
361
+
362
+ if stage and stage.rescue_stage and (survivors_onboard is not None):
363
+ limit = state.survivor_capacity
364
+ objective_lines.append(tr("objectives.survivors_onboard", count=survivors_onboard, limit=limit))
365
+ return objective_lines
366
+
367
+
368
+ def _get_fog_scale(
369
+ assets: RenderAssets,
370
+ flashlight_count: int,
371
+ ) -> float:
372
+ """Return current fog scale factoring in flashlight bonus."""
373
+ scale = assets.fog_radius_scale
374
+ flashlight_count = max(0, int(flashlight_count))
375
+ if flashlight_count <= 0:
376
+ return scale
377
+ if flashlight_count == 1:
378
+ return max(scale, FLASHLIGHT_FOG_SCALE_ONE)
379
+ return max(scale, FLASHLIGHT_FOG_SCALE_TWO)
380
+
381
+
382
+ def _draw_hint_arrow(
383
+ screen: surface.Surface,
384
+ camera: Camera,
385
+ assets: RenderAssets,
386
+ player: Player,
387
+ target_pos: tuple[int, int],
388
+ *,
389
+ color: tuple[int, int, int] | None = None,
390
+ ring_radius: float | None = None,
391
+ ) -> None:
392
+ """Draw a soft directional hint from player to a target position."""
393
+ color = color or YELLOW
394
+ player_screen = camera.apply(player).center
395
+ target_rect = pygame.Rect(target_pos[0], target_pos[1], 0, 0)
396
+ target_screen = camera.apply_rect(target_rect).center
397
+ dx = target_screen[0] - player_screen[0]
398
+ dy = target_screen[1] - player_screen[1]
399
+ dist = math.hypot(dx, dy)
400
+ if dist < assets.fov_radius * 0.7:
401
+ return
402
+ dir_x = dx / dist
403
+ dir_y = dy / dist
404
+ ring_radius = ring_radius if ring_radius is not None else assets.fov_radius * 0.5 * assets.fog_radius_scale
405
+ center_x = player_screen[0] + dir_x * ring_radius
406
+ center_y = player_screen[1] + dir_y * ring_radius
407
+ arrow_len = 6
408
+ tip = (center_x + dir_x * arrow_len, center_y + dir_y * arrow_len)
409
+ base = (center_x - dir_x * 6, center_y - dir_y * 6)
410
+ left = (
411
+ base[0] - dir_y * 5,
412
+ base[1] + dir_x * 5,
413
+ )
414
+ right = (
415
+ base[0] + dir_y * 5,
416
+ base[1] - dir_x * 5,
417
+ )
418
+ pygame.draw.polygon(screen, color, [tip, left, right])
419
+
420
+
421
+ def _draw_hint_indicator(
422
+ screen: surface.Surface,
423
+ camera: Camera,
424
+ assets: RenderAssets,
425
+ player: Player,
426
+ hint_target: tuple[int, int] | None,
427
+ *,
428
+ hint_color: tuple[int, int, int],
429
+ stage: Stage | None,
430
+ flashlight_count: int,
431
+ ) -> None:
432
+ if not hint_target:
433
+ return
434
+ current_fov_scale = _get_fog_scale(assets, flashlight_count)
435
+ hint_ring_radius = assets.fov_radius * 0.5 * current_fov_scale
436
+ _draw_hint_arrow(
437
+ screen,
438
+ camera,
439
+ assets,
440
+ player,
441
+ hint_target,
442
+ color=hint_color,
443
+ ring_radius=hint_ring_radius,
444
+ )
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ import pygame
4
+ from pygame import sprite, surface
5
+
6
+ from ..colors import BLACK, BLUE, FOOTPRINT_COLOR, WHITE, YELLOW, get_environment_palette
7
+ from ..entities import Car, Flashlight, FuelCan, Player, Shoes, SteelBeam, Survivor, Wall
8
+ from ..models import Footprint, GameData
9
+ from ..render_assets import RenderAssets, resolve_steel_beam_colors, resolve_wall_colors
10
+
11
+
12
+ def compute_floor_cells(
13
+ *,
14
+ cols: int,
15
+ rows: int,
16
+ wall_cells: set[tuple[int, int]],
17
+ outer_wall_cells: set[tuple[int, int]],
18
+ pitfall_cells: set[tuple[int, int]],
19
+ ) -> set[tuple[int, int]]:
20
+ """Return floor cells for the minimap base pass."""
21
+ # The layout wall sets are updated when walls are destroyed, so removing
22
+ # those cells here makes the minimap treat destroyed walls as floor.
23
+ blocked = wall_cells | outer_wall_cells | pitfall_cells
24
+ return {(x, y) for y in range(rows) for x in range(cols) if (x, y) not in blocked}
25
+
26
+
27
+ def draw_level_overview(
28
+ assets: RenderAssets,
29
+ surface: surface.Surface,
30
+ wall_group: sprite.Group,
31
+ floor_cells: set[tuple[int, int]],
32
+ player: Player | None,
33
+ car: Car | None,
34
+ waiting_cars: list[Car] | None,
35
+ footprints: list[Footprint],
36
+ *,
37
+ fuel: FuelCan | None = None,
38
+ flashlights: list[Flashlight] | None = None,
39
+ shoes: list[Shoes] | None = None,
40
+ buddies: list[Survivor] | None = None,
41
+ survivors: list[Survivor] | None = None,
42
+ palette_key: str | None = None,
43
+ ) -> None:
44
+ palette = get_environment_palette(palette_key)
45
+ base_floor = palette.floor_primary
46
+ dark_floor = tuple(max(0, int(channel * 0.35)) for channel in base_floor)
47
+ floor_color = tuple(max(0, int(channel * 0.65)) for channel in base_floor)
48
+ surface.fill(dark_floor)
49
+ cell_size = assets.internal_wall_grid_snap
50
+ if cell_size > 0:
51
+ for x, y in floor_cells:
52
+ pygame.draw.rect(
53
+ surface,
54
+ floor_color,
55
+ pygame.Rect(
56
+ x * cell_size,
57
+ y * cell_size,
58
+ cell_size,
59
+ cell_size,
60
+ ),
61
+ )
62
+
63
+ for wall in wall_group:
64
+ if wall.max_health > 0:
65
+ health_ratio = max(0.0, min(1.0, wall.health / wall.max_health))
66
+ else:
67
+ health_ratio = 0.0
68
+ if isinstance(wall, Wall):
69
+ if health_ratio <= 0.0:
70
+ pygame.draw.rect(surface, floor_color, wall.rect)
71
+ else:
72
+ fill_color, _ = resolve_wall_colors(
73
+ health_ratio=health_ratio,
74
+ palette_category=wall.palette_category,
75
+ palette=palette,
76
+ )
77
+ pygame.draw.rect(surface, fill_color, wall.rect)
78
+ elif isinstance(wall, SteelBeam):
79
+ if health_ratio <= 0.0:
80
+ pygame.draw.rect(surface, floor_color, wall.rect)
81
+ else:
82
+ fill_color, _ = resolve_steel_beam_colors(
83
+ health_ratio=health_ratio,
84
+ palette=palette,
85
+ )
86
+ pygame.draw.rect(surface, fill_color, wall.rect)
87
+ now = pygame.time.get_ticks()
88
+ for fp in footprints:
89
+ if not fp.visible:
90
+ continue
91
+ age = now - fp.time
92
+ fade = 1 - (age / assets.footprint_lifetime_ms)
93
+ fade = max(assets.footprint_min_fade, fade)
94
+ color = tuple(int(c * fade) for c in FOOTPRINT_COLOR)
95
+ pygame.draw.circle(
96
+ surface,
97
+ color,
98
+ (int(fp.pos[0]), int(fp.pos[1])),
99
+ assets.footprint_overview_radius,
100
+ )
101
+ if fuel and fuel.alive():
102
+ pygame.draw.rect(surface, YELLOW, fuel.rect, border_radius=3)
103
+ pygame.draw.rect(surface, BLACK, fuel.rect, width=2, border_radius=3)
104
+ if flashlights:
105
+ for flashlight in flashlights:
106
+ if flashlight.alive():
107
+ pygame.draw.rect(surface, YELLOW, flashlight.rect, border_radius=2)
108
+ pygame.draw.rect(surface, BLACK, flashlight.rect, width=2, border_radius=2)
109
+ if shoes:
110
+ for item in shoes:
111
+ if item.alive():
112
+ surface.blit(item.image, item.rect)
113
+ if survivors:
114
+ for survivor in survivors:
115
+ if survivor.alive():
116
+ pygame.draw.circle(
117
+ surface,
118
+ (220, 220, 255),
119
+ survivor.rect.center,
120
+ assets.player_radius * 2,
121
+ )
122
+ if player:
123
+ pygame.draw.circle(surface, BLUE, player.rect.center, assets.player_radius * 2)
124
+ if buddies:
125
+ buddy_color = (0, 200, 70)
126
+ for buddy in buddies:
127
+ if buddy.alive() and not buddy.rescued:
128
+ pygame.draw.circle(surface, buddy_color, buddy.rect.center, assets.player_radius * 2)
129
+ drawn_cars: list[Car] = []
130
+ if car and car.alive():
131
+ car_rect = car.image.get_rect(center=car.rect.center)
132
+ surface.blit(car.image, car_rect)
133
+ drawn_cars.append(car)
134
+ if waiting_cars:
135
+ for parked in waiting_cars:
136
+ if not parked.alive() or parked in drawn_cars:
137
+ continue
138
+ parked_rect = parked.image.get_rect(center=parked.rect.center)
139
+ surface.blit(parked.image, parked_rect)
140
+
141
+
142
+ def draw_debug_overview(
143
+ assets: RenderAssets,
144
+ screen: surface.Surface,
145
+ overview_surface: surface.Surface,
146
+ game_data: GameData,
147
+ config: dict[str, object],
148
+ *,
149
+ screen_width: int,
150
+ screen_height: int,
151
+ ) -> None:
152
+ cell_size = assets.internal_wall_grid_snap
153
+ floor_cells: set[tuple[int, int]] = set()
154
+ if cell_size > 0:
155
+ floor_cells = compute_floor_cells(
156
+ cols=max(0, game_data.layout.field_rect.width // cell_size),
157
+ rows=max(0, game_data.layout.field_rect.height // cell_size),
158
+ wall_cells=game_data.layout.wall_cells,
159
+ outer_wall_cells=game_data.layout.outer_wall_cells,
160
+ pitfall_cells=game_data.layout.pitfall_cells,
161
+ )
162
+ footprints_enabled = bool(config.get("footprints", {}).get("enabled", True))
163
+ footprints_to_draw = game_data.state.footprints if footprints_enabled else []
164
+ draw_level_overview(
165
+ assets,
166
+ overview_surface,
167
+ game_data.groups.wall_group,
168
+ floor_cells,
169
+ game_data.player,
170
+ game_data.car,
171
+ game_data.waiting_cars,
172
+ footprints_to_draw,
173
+ fuel=game_data.fuel,
174
+ flashlights=game_data.flashlights or [],
175
+ shoes=game_data.shoes or [],
176
+ buddies=[
177
+ survivor
178
+ for survivor in game_data.groups.survivor_group
179
+ if survivor.alive() and survivor.is_buddy and not survivor.rescued
180
+ ],
181
+ survivors=list(game_data.groups.survivor_group),
182
+ palette_key=game_data.state.ambient_palette_key,
183
+ )
184
+ zombie_color = (200, 80, 80)
185
+ zombie_radius = max(2, int(assets.player_radius * 1.2))
186
+ for zombie in game_data.groups.zombie_group:
187
+ if zombie.alive():
188
+ pygame.draw.circle(
189
+ overview_surface,
190
+ zombie_color,
191
+ zombie.rect.center,
192
+ zombie_radius,
193
+ )
194
+ cam_offset = game_data.camera.camera
195
+ camera_rect = pygame.Rect(
196
+ -cam_offset.x,
197
+ -cam_offset.y,
198
+ screen_width,
199
+ screen_height,
200
+ )
201
+ pygame.draw.rect(overview_surface, WHITE, camera_rect, width=1)
202
+ level_rect = game_data.layout.field_rect
203
+ level_aspect = level_rect.width / max(1, level_rect.height)
204
+ screen_aspect = screen_width / max(1, screen_height)
205
+ if level_aspect > screen_aspect:
206
+ scaled_w = screen_width - 40
207
+ scaled_h = int(scaled_w / level_aspect)
208
+ else:
209
+ scaled_h = screen_height - 40
210
+ scaled_w = int(scaled_h * level_aspect)
211
+ scaled_w = max(1, scaled_w)
212
+ scaled_h = max(1, scaled_h)
213
+ scaled_overview = pygame.transform.smoothscale(overview_surface, (scaled_w, scaled_h))
214
+ screen.fill(BLACK)
215
+ screen.blit(
216
+ scaled_overview,
217
+ scaled_overview.get_rect(center=(screen_width // 2, screen_height // 2)),
218
+ )