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.
- zombie_escape/__about__.py +1 -1
- zombie_escape/colors.py +7 -21
- zombie_escape/entities.py +100 -191
- zombie_escape/export_images.py +39 -33
- zombie_escape/gameplay/ambient.py +2 -6
- zombie_escape/gameplay/footprints.py +8 -11
- zombie_escape/gameplay/interactions.py +17 -58
- zombie_escape/gameplay/layout.py +20 -46
- zombie_escape/gameplay/movement.py +7 -21
- zombie_escape/gameplay/spawn.py +12 -40
- zombie_escape/gameplay/state.py +1 -0
- zombie_escape/gameplay/survivors.py +5 -16
- zombie_escape/gameplay/utils.py +4 -13
- zombie_escape/input_utils.py +8 -31
- zombie_escape/level_blueprints.py +112 -69
- zombie_escape/level_constants.py +8 -0
- zombie_escape/locales/ui.en.json +12 -0
- zombie_escape/locales/ui.ja.json +12 -0
- zombie_escape/localization.py +3 -11
- zombie_escape/models.py +26 -9
- zombie_escape/render/__init__.py +30 -0
- zombie_escape/render/core.py +992 -0
- zombie_escape/render/hud.py +444 -0
- zombie_escape/render/overview.py +218 -0
- zombie_escape/render/shadows.py +343 -0
- zombie_escape/render_assets.py +11 -33
- zombie_escape/rng.py +4 -8
- zombie_escape/screens/__init__.py +14 -30
- zombie_escape/screens/game_over.py +43 -15
- zombie_escape/screens/gameplay.py +41 -104
- zombie_escape/screens/settings.py +19 -104
- zombie_escape/screens/title.py +36 -176
- zombie_escape/stage_constants.py +192 -67
- zombie_escape/zombie_escape.py +1 -1
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/METADATA +100 -39
- zombie_escape-1.14.4.dist-info/RECORD +53 -0
- zombie_escape/render.py +0 -1746
- zombie_escape-1.13.1.dist-info/RECORD +0 -49
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/WHEEL +0 -0
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|