zombie-escape 1.8.0__py3-none-any.whl → 1.10.1__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 +49 -14
- zombie_escape/entities.py +295 -332
- zombie_escape/entities_constants.py +6 -0
- zombie_escape/gameplay/__init__.py +2 -2
- zombie_escape/gameplay/constants.py +1 -1
- zombie_escape/gameplay/footprints.py +2 -2
- zombie_escape/gameplay/interactions.py +4 -10
- zombie_escape/gameplay/layout.py +38 -4
- zombie_escape/gameplay/movement.py +5 -7
- zombie_escape/gameplay/spawn.py +245 -12
- zombie_escape/gameplay/state.py +17 -16
- zombie_escape/gameplay/survivors.py +5 -4
- zombie_escape/gameplay/utils.py +19 -1
- zombie_escape/locales/ui.en.json +14 -2
- zombie_escape/locales/ui.ja.json +14 -2
- zombie_escape/models.py +52 -7
- zombie_escape/render.py +760 -284
- zombie_escape/render_constants.py +26 -8
- zombie_escape/screens/__init__.py +1 -0
- zombie_escape/screens/game_over.py +4 -4
- zombie_escape/screens/gameplay.py +10 -24
- zombie_escape/stage_constants.py +90 -2
- zombie_escape/world_grid.py +134 -0
- zombie_escape/zombie_escape.py +65 -61
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.1.dist-info}/METADATA +9 -1
- zombie_escape-1.10.1.dist-info/RECORD +47 -0
- zombie_escape-1.8.0.dist-info/RECORD +0 -46
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.1.dist-info}/WHEEL +0 -0
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.1.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.1.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/render.py
CHANGED
|
@@ -17,7 +17,17 @@ from .colors import (
|
|
|
17
17
|
YELLOW,
|
|
18
18
|
get_environment_palette,
|
|
19
19
|
)
|
|
20
|
-
from .entities import
|
|
20
|
+
from .entities import (
|
|
21
|
+
Camera,
|
|
22
|
+
Car,
|
|
23
|
+
Flashlight,
|
|
24
|
+
FuelCan,
|
|
25
|
+
Player,
|
|
26
|
+
SteelBeam,
|
|
27
|
+
Survivor,
|
|
28
|
+
Wall,
|
|
29
|
+
)
|
|
30
|
+
from .entities_constants import ZOMBIE_RADIUS
|
|
21
31
|
from .font_utils import load_font
|
|
22
32
|
from .gameplay_constants import (
|
|
23
33
|
DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
@@ -25,8 +35,82 @@ from .gameplay_constants import (
|
|
|
25
35
|
)
|
|
26
36
|
from .localization import get_font_settings
|
|
27
37
|
from .localization import translate as tr
|
|
28
|
-
from .models import GameData, Stage
|
|
29
|
-
from .render_assets import RenderAssets, resolve_wall_colors
|
|
38
|
+
from .models import DustRing, FallingZombie, Footprint, GameData, Stage
|
|
39
|
+
from .render_assets import RenderAssets, resolve_steel_beam_colors, resolve_wall_colors
|
|
40
|
+
from .render_constants import (
|
|
41
|
+
FALLING_DUST_COLOR,
|
|
42
|
+
FALLING_WHIRLWIND_COLOR,
|
|
43
|
+
FALLING_ZOMBIE_COLOR,
|
|
44
|
+
SHADOW_MIN_RATIO,
|
|
45
|
+
SHADOW_OVERSAMPLE,
|
|
46
|
+
SHADOW_RADIUS_RATIO,
|
|
47
|
+
SHADOW_STEPS,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_SHADOW_TILE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
|
|
51
|
+
_SHADOW_LAYER_CACHE: dict[tuple[int, int], surface.Surface] = {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_shadow_tile_surface(
|
|
55
|
+
cell_size: int,
|
|
56
|
+
alpha: int,
|
|
57
|
+
*,
|
|
58
|
+
edge_softness: float = 0.35,
|
|
59
|
+
) -> surface.Surface:
|
|
60
|
+
key = (max(1, cell_size), max(0, min(255, alpha)), edge_softness)
|
|
61
|
+
if key in _SHADOW_TILE_CACHE:
|
|
62
|
+
return _SHADOW_TILE_CACHE[key]
|
|
63
|
+
size = key[0]
|
|
64
|
+
oversample = SHADOW_OVERSAMPLE
|
|
65
|
+
render_size = size * oversample
|
|
66
|
+
render_surf = pygame.Surface((render_size, render_size), pygame.SRCALPHA)
|
|
67
|
+
base_alpha = key[1]
|
|
68
|
+
if edge_softness <= 0:
|
|
69
|
+
render_surf.fill((0, 0, 0, base_alpha))
|
|
70
|
+
if oversample > 1:
|
|
71
|
+
surf = pygame.transform.smoothscale(render_surf, (size, size))
|
|
72
|
+
else:
|
|
73
|
+
surf = render_surf
|
|
74
|
+
_SHADOW_TILE_CACHE[key] = surf
|
|
75
|
+
return surf
|
|
76
|
+
|
|
77
|
+
softness = max(0.0, min(1.0, edge_softness))
|
|
78
|
+
fade_band = max(1, int(render_size * softness))
|
|
79
|
+
base_radius = max(1, int(render_size * SHADOW_RADIUS_RATIO))
|
|
80
|
+
|
|
81
|
+
render_surf.fill((0, 0, 0, 0))
|
|
82
|
+
steps = SHADOW_STEPS
|
|
83
|
+
min_ratio = SHADOW_MIN_RATIO
|
|
84
|
+
for idx in range(steps):
|
|
85
|
+
t = idx / (steps - 1) if steps > 1 else 1.0
|
|
86
|
+
inset = int(fade_band * t)
|
|
87
|
+
rect_size = render_size - inset * 2
|
|
88
|
+
if rect_size <= 0:
|
|
89
|
+
continue
|
|
90
|
+
radius = max(0, base_radius - inset)
|
|
91
|
+
layer_alpha = int(base_alpha * (min_ratio + (1.0 - min_ratio) * t))
|
|
92
|
+
pygame.draw.rect(
|
|
93
|
+
render_surf,
|
|
94
|
+
(0, 0, 0, layer_alpha),
|
|
95
|
+
pygame.Rect(inset, inset, rect_size, rect_size),
|
|
96
|
+
border_radius=radius,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if oversample > 1:
|
|
100
|
+
surf = pygame.transform.smoothscale(render_surf, (size, size))
|
|
101
|
+
else:
|
|
102
|
+
surf = render_surf
|
|
103
|
+
_SHADOW_TILE_CACHE[key] = surf
|
|
104
|
+
return surf
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _get_shadow_layer(size: tuple[int, int]) -> surface.Surface:
|
|
108
|
+
key = (max(1, size[0]), max(1, size[1]))
|
|
109
|
+
if key in _SHADOW_LAYER_CACHE:
|
|
110
|
+
return _SHADOW_LAYER_CACHE[key]
|
|
111
|
+
layer = pygame.Surface(key, pygame.SRCALPHA)
|
|
112
|
+
_SHADOW_LAYER_CACHE[key] = layer
|
|
113
|
+
return layer
|
|
30
114
|
|
|
31
115
|
|
|
32
116
|
def show_message(
|
|
@@ -61,7 +145,7 @@ def draw_level_overview(
|
|
|
61
145
|
player: Player | None,
|
|
62
146
|
car: Car | None,
|
|
63
147
|
waiting_cars: list[Car] | None,
|
|
64
|
-
footprints: list[
|
|
148
|
+
footprints: list[Footprint],
|
|
65
149
|
*,
|
|
66
150
|
fuel: FuelCan | None = None,
|
|
67
151
|
flashlights: list[Flashlight] | None = None,
|
|
@@ -71,32 +155,37 @@ def draw_level_overview(
|
|
|
71
155
|
palette_key: str | None = None,
|
|
72
156
|
) -> None:
|
|
73
157
|
palette = get_environment_palette(palette_key)
|
|
74
|
-
base_floor =
|
|
158
|
+
base_floor = palette.floor_primary
|
|
75
159
|
dark_floor = tuple(max(0, int(channel * 0.55)) for channel in base_floor)
|
|
76
160
|
surface.fill(dark_floor)
|
|
77
161
|
for wall in wall_group:
|
|
78
|
-
if not isinstance(wall, Wall):
|
|
79
|
-
raise TypeError("wall_group must contain Wall instances")
|
|
80
162
|
if wall.max_health > 0:
|
|
81
163
|
health_ratio = max(0.0, min(1.0, wall.health / wall.max_health))
|
|
82
164
|
else:
|
|
83
165
|
health_ratio = 0.0
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
166
|
+
if isinstance(wall, Wall):
|
|
167
|
+
fill_color, _ = resolve_wall_colors(
|
|
168
|
+
health_ratio=health_ratio,
|
|
169
|
+
palette_category=wall.palette_category,
|
|
170
|
+
palette=palette,
|
|
171
|
+
)
|
|
172
|
+
pygame.draw.rect(surface, fill_color, wall.rect)
|
|
173
|
+
elif isinstance(wall, SteelBeam):
|
|
174
|
+
fill_color, _ = resolve_steel_beam_colors(
|
|
175
|
+
health_ratio=health_ratio,
|
|
176
|
+
palette=palette,
|
|
177
|
+
)
|
|
178
|
+
pygame.draw.rect(surface, fill_color, wall.rect)
|
|
90
179
|
now = pygame.time.get_ticks()
|
|
91
180
|
for fp in footprints:
|
|
92
|
-
age = now - fp
|
|
181
|
+
age = now - fp.time
|
|
93
182
|
fade = 1 - (age / assets.footprint_lifetime_ms)
|
|
94
183
|
fade = max(assets.footprint_min_fade, fade)
|
|
95
184
|
color = tuple(int(c * fade) for c in FOOTPRINT_COLOR)
|
|
96
185
|
pygame.draw.circle(
|
|
97
186
|
surface,
|
|
98
187
|
color,
|
|
99
|
-
(int(fp
|
|
188
|
+
(int(fp.pos[0]), int(fp.pos[1])),
|
|
100
189
|
assets.footprint_overview_radius,
|
|
101
190
|
)
|
|
102
191
|
if fuel and fuel.alive():
|
|
@@ -206,19 +295,21 @@ def _get_hatch_pattern(
|
|
|
206
295
|
fog_data: dict[str, Any],
|
|
207
296
|
thickness: int,
|
|
208
297
|
*,
|
|
209
|
-
pixel_scale: int = 1,
|
|
210
298
|
color: tuple[int, int, int, int] | None = None,
|
|
211
299
|
) -> surface.Surface:
|
|
212
|
-
"""Return cached
|
|
300
|
+
"""Return cached dot hatch tile surface (Bayer-ordered, optionally chunky)."""
|
|
213
301
|
cache = fog_data.setdefault("hatch_patterns", {})
|
|
214
|
-
|
|
215
|
-
key = (thickness, pixel_scale, color)
|
|
302
|
+
key = (thickness, color)
|
|
216
303
|
if key in cache:
|
|
217
304
|
return cache[key]
|
|
218
305
|
|
|
219
|
-
spacing =
|
|
306
|
+
spacing = 4
|
|
307
|
+
oversample = 3
|
|
220
308
|
density = max(1, min(thickness, 16))
|
|
221
|
-
|
|
309
|
+
pattern_size = spacing * 8
|
|
310
|
+
hi_spacing = spacing * oversample
|
|
311
|
+
hi_pattern_size = pattern_size * oversample
|
|
312
|
+
pattern = pygame.Surface((hi_pattern_size, hi_pattern_size), pygame.SRCALPHA)
|
|
222
313
|
|
|
223
314
|
# 8x8 Bayer matrix values 0..63 for ordered dithering
|
|
224
315
|
bayer = [
|
|
@@ -232,14 +323,20 @@ def _get_hatch_pattern(
|
|
|
232
323
|
[63, 31, 55, 23, 61, 29, 53, 21],
|
|
233
324
|
]
|
|
234
325
|
threshold = int((density / 16) * 64)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
326
|
+
dot_radius = max(
|
|
327
|
+
1,
|
|
328
|
+
min(hi_spacing, int(math.ceil((density / 16) * hi_spacing))),
|
|
329
|
+
)
|
|
330
|
+
dot_color = color or (0, 0, 0, 255)
|
|
331
|
+
for grid_y in range(8):
|
|
332
|
+
for grid_x in range(8):
|
|
333
|
+
if bayer[grid_y][grid_x] < threshold:
|
|
334
|
+
cx = grid_x * hi_spacing + hi_spacing // 2
|
|
335
|
+
cy = grid_y * hi_spacing + hi_spacing // 2
|
|
336
|
+
pygame.draw.circle(pattern, dot_color, (cx, cy), dot_radius)
|
|
239
337
|
|
|
240
|
-
if
|
|
241
|
-
|
|
242
|
-
pattern = pygame.transform.scale(pattern, scaled_size)
|
|
338
|
+
if oversample > 1:
|
|
339
|
+
pattern = pygame.transform.smoothscale(pattern, (pattern_size, pattern_size))
|
|
243
340
|
|
|
244
341
|
cache[key] = pattern
|
|
245
342
|
return pattern
|
|
@@ -258,6 +355,9 @@ def _get_fog_overlay_surfaces(
|
|
|
258
355
|
return overlays[key]
|
|
259
356
|
|
|
260
357
|
scale = profile._scale(assets, stage)
|
|
358
|
+
ring_scale = scale
|
|
359
|
+
if profile.flashlight_count >= 2:
|
|
360
|
+
ring_scale += max(0.0, assets.flashlight_hatch_extra_scale)
|
|
261
361
|
max_radius = int(assets.fov_radius * scale)
|
|
262
362
|
padding = 32
|
|
263
363
|
coverage_width = max(assets.screen_width * 2, max_radius * 2)
|
|
@@ -277,14 +377,13 @@ def _get_fog_overlay_surfaces(
|
|
|
277
377
|
pattern = _get_hatch_pattern(
|
|
278
378
|
fog_data,
|
|
279
379
|
ring.thickness,
|
|
280
|
-
pixel_scale=assets.fog_hatch_pixel_scale,
|
|
281
380
|
color=base_color,
|
|
282
381
|
)
|
|
283
382
|
p_w, p_h = pattern.get_size()
|
|
284
383
|
for y in range(0, height, p_h):
|
|
285
384
|
for x in range(0, width, p_w):
|
|
286
385
|
ring_surface.blit(pattern, (x, y))
|
|
287
|
-
radius = int(assets.fov_radius * ring.radius_factor *
|
|
386
|
+
radius = int(assets.fov_radius * ring.radius_factor * ring_scale)
|
|
288
387
|
pygame.draw.circle(ring_surface, (0, 0, 0, 0), center, radius)
|
|
289
388
|
ring_surfaces.append(ring_surface)
|
|
290
389
|
|
|
@@ -313,7 +412,7 @@ def _build_flashlight_fade_surface(
|
|
|
313
412
|
*,
|
|
314
413
|
start_ratio: float = 0.2,
|
|
315
414
|
max_alpha: int = 220,
|
|
316
|
-
outer_extension: int =
|
|
415
|
+
outer_extension: int = 30,
|
|
317
416
|
) -> surface.Surface:
|
|
318
417
|
"""Return a radial gradient so flashlight edges softly darken again."""
|
|
319
418
|
|
|
@@ -359,6 +458,96 @@ def _build_flashlight_fade_surface(
|
|
|
359
458
|
return fade_surface
|
|
360
459
|
|
|
361
460
|
|
|
461
|
+
def _draw_fall_whirlwind(
|
|
462
|
+
screen: surface.Surface,
|
|
463
|
+
camera: Camera,
|
|
464
|
+
center: tuple[int, int],
|
|
465
|
+
progress: float,
|
|
466
|
+
) -> None:
|
|
467
|
+
base_alpha = FALLING_WHIRLWIND_COLOR[3]
|
|
468
|
+
alpha = int(max(0, min(255, base_alpha * (1.0 - progress))))
|
|
469
|
+
if alpha <= 0:
|
|
470
|
+
return
|
|
471
|
+
color = (
|
|
472
|
+
FALLING_WHIRLWIND_COLOR[0],
|
|
473
|
+
FALLING_WHIRLWIND_COLOR[1],
|
|
474
|
+
FALLING_WHIRLWIND_COLOR[2],
|
|
475
|
+
alpha,
|
|
476
|
+
)
|
|
477
|
+
swirl_radius = max(2, int(ZOMBIE_RADIUS * 1.1))
|
|
478
|
+
offset = max(1, int(ZOMBIE_RADIUS * 0.6))
|
|
479
|
+
size = swirl_radius * 4
|
|
480
|
+
swirl = pygame.Surface((size, size), pygame.SRCALPHA)
|
|
481
|
+
cx = cy = size // 2
|
|
482
|
+
for idx in range(2):
|
|
483
|
+
angle = progress * math.tau * 0.3 + idx * (math.tau / 2)
|
|
484
|
+
ox = int(math.cos(angle) * offset)
|
|
485
|
+
oy = int(math.sin(angle) * offset)
|
|
486
|
+
pygame.draw.circle(swirl, color, (cx + ox, cy + oy), swirl_radius, width=2)
|
|
487
|
+
world_rect = pygame.Rect(0, 0, 1, 1)
|
|
488
|
+
world_rect.center = center
|
|
489
|
+
screen_center = camera.apply_rect(world_rect).center
|
|
490
|
+
screen.blit(swirl, swirl.get_rect(center=screen_center))
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _draw_falling_fx(
|
|
494
|
+
screen: surface.Surface,
|
|
495
|
+
camera: Camera,
|
|
496
|
+
falling_zombies: list[FallingZombie],
|
|
497
|
+
flashlight_count: int,
|
|
498
|
+
dust_rings: list[DustRing],
|
|
499
|
+
) -> None:
|
|
500
|
+
if not falling_zombies and not dust_rings:
|
|
501
|
+
return
|
|
502
|
+
now = pygame.time.get_ticks()
|
|
503
|
+
for fall in falling_zombies:
|
|
504
|
+
pre_fx_ms = max(0, fall.pre_fx_ms)
|
|
505
|
+
fall_duration_ms = max(1, fall.fall_duration_ms)
|
|
506
|
+
fall_start = fall.started_at_ms + pre_fx_ms
|
|
507
|
+
impact_at = fall_start + fall_duration_ms
|
|
508
|
+
if now < fall_start:
|
|
509
|
+
if flashlight_count > 0 and pre_fx_ms > 0:
|
|
510
|
+
fx_progress = max(0.0, min(1.0, (now - fall.started_at_ms) / pre_fx_ms))
|
|
511
|
+
_draw_fall_whirlwind(screen, camera, fall.target_pos, fx_progress)
|
|
512
|
+
continue
|
|
513
|
+
if now >= impact_at:
|
|
514
|
+
continue
|
|
515
|
+
fall_progress = max(0.0, min(1.0, (now - fall_start) / fall_duration_ms))
|
|
516
|
+
eased = 1.0 - (1.0 - fall_progress) * (1.0 - fall_progress)
|
|
517
|
+
x = fall.target_pos[0]
|
|
518
|
+
y = int(fall.start_pos[1] + (fall.target_pos[1] - fall.start_pos[1]) * eased)
|
|
519
|
+
world_rect = pygame.Rect(0, 0, ZOMBIE_RADIUS * 2, ZOMBIE_RADIUS * 2)
|
|
520
|
+
world_rect.center = (x, y)
|
|
521
|
+
screen_rect = camera.apply_rect(world_rect)
|
|
522
|
+
pygame.draw.circle(
|
|
523
|
+
screen,
|
|
524
|
+
FALLING_ZOMBIE_COLOR,
|
|
525
|
+
screen_rect.center,
|
|
526
|
+
ZOMBIE_RADIUS,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
for ring in list(dust_rings):
|
|
530
|
+
elapsed = now - ring.started_at_ms
|
|
531
|
+
if elapsed >= ring.duration_ms:
|
|
532
|
+
dust_rings.remove(ring)
|
|
533
|
+
continue
|
|
534
|
+
progress = max(0.0, min(1.0, elapsed / ring.duration_ms))
|
|
535
|
+
alpha = int(max(0, min(255, FALLING_DUST_COLOR[3] * (1.0 - progress))))
|
|
536
|
+
if alpha <= 0:
|
|
537
|
+
continue
|
|
538
|
+
radius = int(ZOMBIE_RADIUS * (0.7 + progress * 1.9))
|
|
539
|
+
color = (
|
|
540
|
+
FALLING_DUST_COLOR[0],
|
|
541
|
+
FALLING_DUST_COLOR[1],
|
|
542
|
+
FALLING_DUST_COLOR[2],
|
|
543
|
+
alpha,
|
|
544
|
+
)
|
|
545
|
+
world_rect = pygame.Rect(0, 0, 1, 1)
|
|
546
|
+
world_rect.center = ring.pos
|
|
547
|
+
screen_center = camera.apply_rect(world_rect).center
|
|
548
|
+
pygame.draw.circle(screen, color, screen_center, radius, width=2)
|
|
549
|
+
|
|
550
|
+
|
|
362
551
|
def _draw_hint_arrow(
|
|
363
552
|
screen: surface.Surface,
|
|
364
553
|
camera: Camera,
|
|
@@ -411,6 +600,7 @@ def _draw_status_bar(
|
|
|
411
600
|
seed: int | None = None,
|
|
412
601
|
debug_mode: bool = False,
|
|
413
602
|
zombie_group: sprite.Group | None = None,
|
|
603
|
+
falling_spawn_carry: int | None = None,
|
|
414
604
|
) -> None:
|
|
415
605
|
"""Render a compact status bar with current config flags and stage info."""
|
|
416
606
|
bar_rect = pygame.Rect(
|
|
@@ -429,7 +619,11 @@ def _draw_status_bar(
|
|
|
429
619
|
steel_on = config.get("steel_beams", {}).get("enabled", False)
|
|
430
620
|
if stage:
|
|
431
621
|
# Keep the label compact for the status bar
|
|
432
|
-
|
|
622
|
+
if stage.id.startswith("stage"):
|
|
623
|
+
stage_suffix = stage.id.removeprefix("stage")
|
|
624
|
+
stage_label = f"#{stage_suffix}" if stage_suffix else stage.id
|
|
625
|
+
else:
|
|
626
|
+
stage_label = stage.id
|
|
433
627
|
else:
|
|
434
628
|
stage_label = "#1"
|
|
435
629
|
|
|
@@ -443,14 +637,15 @@ def _draw_status_bar(
|
|
|
443
637
|
if steel_on:
|
|
444
638
|
parts.append(tr("status.steel"))
|
|
445
639
|
if debug_mode:
|
|
446
|
-
parts.append(tr("status.debug"))
|
|
447
640
|
if zombie_group is not None:
|
|
448
|
-
zombies = [z for z in zombie_group if
|
|
641
|
+
zombies = [z for z in zombie_group if z.alive()]
|
|
449
642
|
total = len(zombies)
|
|
450
|
-
tracker = sum(1 for z in zombies if
|
|
451
|
-
wall = sum(1 for z in zombies if
|
|
643
|
+
tracker = sum(1 for z in zombies if z.tracker)
|
|
644
|
+
wall = sum(1 for z in zombies if z.wall_follower)
|
|
452
645
|
normal = max(0, total - tracker - wall)
|
|
453
646
|
parts.append(f"Z:{total} N:{normal} T:{tracker} W:{wall}")
|
|
647
|
+
if falling_spawn_carry is not None:
|
|
648
|
+
parts.append(f"C:{max(0, falling_spawn_carry)}")
|
|
454
649
|
|
|
455
650
|
status_text = " | ".join(parts)
|
|
456
651
|
color = LIGHT_GRAY
|
|
@@ -472,45 +667,15 @@ def _draw_status_bar(
|
|
|
472
667
|
print(f"Error rendering status bar: {e}")
|
|
473
668
|
|
|
474
669
|
|
|
475
|
-
def
|
|
476
|
-
assets: RenderAssets,
|
|
670
|
+
def _draw_play_area(
|
|
477
671
|
screen: surface.Surface,
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
present_fn: Callable[[surface.Surface], None] | None = None,
|
|
486
|
-
) -> None:
|
|
487
|
-
hint_color = hint_color or YELLOW
|
|
488
|
-
state = game_data.state
|
|
489
|
-
player = game_data.player
|
|
490
|
-
if player is None:
|
|
491
|
-
raise ValueError("draw requires an active player on game_data")
|
|
492
|
-
|
|
493
|
-
camera = game_data.camera
|
|
494
|
-
stage = game_data.stage
|
|
495
|
-
outer_rect = game_data.layout.outer_rect
|
|
496
|
-
outside_rects = game_data.layout.outside_rects or []
|
|
497
|
-
all_sprites = game_data.groups.all_sprites
|
|
498
|
-
fog_surfaces = game_data.fog
|
|
499
|
-
footprints = state.footprints
|
|
500
|
-
has_fuel = state.has_fuel
|
|
501
|
-
flashlight_count = state.flashlight_count
|
|
502
|
-
elapsed_play_ms = state.elapsed_play_ms
|
|
503
|
-
fuel_message_until = state.fuel_message_until
|
|
504
|
-
buddy_onboard = state.buddy_onboard
|
|
505
|
-
buddy_required = stage.buddy_required_count if stage else 0
|
|
506
|
-
survivors_onboard = state.survivors_onboard
|
|
507
|
-
survivor_messages = list(state.survivor_messages)
|
|
508
|
-
zombie_group = game_data.groups.zombie_group
|
|
509
|
-
active_car = game_data.car if game_data.car and game_data.car.alive() else None
|
|
510
|
-
|
|
511
|
-
palette = get_environment_palette(state.ambient_palette_key)
|
|
512
|
-
screen.fill(palette.outside)
|
|
513
|
-
|
|
672
|
+
camera: Camera,
|
|
673
|
+
assets: RenderAssets,
|
|
674
|
+
palette: Any,
|
|
675
|
+
outer_rect: tuple[int, int, int, int],
|
|
676
|
+
outside_rects: list[pygame.Rect],
|
|
677
|
+
fall_spawn_cells: set[tuple[int, int]],
|
|
678
|
+
) -> tuple[int, int, int, int, set[tuple[int, int]]]:
|
|
514
679
|
xs, ys, xe, ye = outer_rect
|
|
515
680
|
xs //= assets.internal_wall_grid_snap
|
|
516
681
|
ys //= assets.internal_wall_grid_snap
|
|
@@ -556,38 +721,140 @@ def draw(
|
|
|
556
721
|
for x in range(start_x, end_x):
|
|
557
722
|
if (x, y) in outside_cells:
|
|
558
723
|
continue
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
lx,
|
|
566
|
-
ly,
|
|
567
|
-
assets.internal_wall_grid_snap,
|
|
568
|
-
assets.internal_wall_grid_snap,
|
|
724
|
+
use_secondary = ((x // 2) + (y // 2)) % 2 == 0
|
|
725
|
+
if (x, y) in fall_spawn_cells:
|
|
726
|
+
color = (
|
|
727
|
+
palette.fall_zone_secondary
|
|
728
|
+
if use_secondary
|
|
729
|
+
else palette.fall_zone_primary
|
|
569
730
|
)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
fp["pos"][1] - assets.footprint_radius,
|
|
584
|
-
assets.footprint_radius * 2,
|
|
585
|
-
assets.footprint_radius * 2,
|
|
731
|
+
elif not use_secondary:
|
|
732
|
+
continue
|
|
733
|
+
else:
|
|
734
|
+
color = palette.floor_secondary
|
|
735
|
+
lx, ly = (
|
|
736
|
+
x * assets.internal_wall_grid_snap,
|
|
737
|
+
y * assets.internal_wall_grid_snap,
|
|
738
|
+
)
|
|
739
|
+
r = pygame.Rect(
|
|
740
|
+
lx,
|
|
741
|
+
ly,
|
|
742
|
+
assets.internal_wall_grid_snap,
|
|
743
|
+
assets.internal_wall_grid_snap,
|
|
586
744
|
)
|
|
587
|
-
sr = camera.apply_rect(
|
|
588
|
-
if sr.colliderect(screen.get_rect()
|
|
589
|
-
pygame.draw.
|
|
745
|
+
sr = camera.apply_rect(r)
|
|
746
|
+
if sr.colliderect(screen.get_rect()):
|
|
747
|
+
pygame.draw.rect(screen, color, sr)
|
|
748
|
+
|
|
749
|
+
return xs, ys, xe, ye, outside_cells
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def abs_clip(value: float, min_v: float, max_v: float) -> float:
|
|
753
|
+
value_sign = 1.0 if value >= 0.0 else -1.0
|
|
754
|
+
value = abs(value)
|
|
755
|
+
if value < min_v:
|
|
756
|
+
value = min_v
|
|
757
|
+
elif value > max_v:
|
|
758
|
+
value = max_v
|
|
759
|
+
return value_sign * value
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _draw_wall_shadows(
|
|
763
|
+
screen: surface.Surface,
|
|
764
|
+
camera: Camera,
|
|
765
|
+
*,
|
|
766
|
+
wall_cells: set[tuple[int, int]],
|
|
767
|
+
wall_group: sprite.Group | None,
|
|
768
|
+
outer_wall_cells: set[tuple[int, int]] | None,
|
|
769
|
+
cell_size: int,
|
|
770
|
+
light_source_pos: tuple[int, int] | None,
|
|
771
|
+
alpha: int = 68,
|
|
772
|
+
) -> None:
|
|
773
|
+
if not wall_cells or cell_size <= 0 or light_source_pos is None:
|
|
774
|
+
return
|
|
775
|
+
inner_wall_cells = set(wall_cells)
|
|
776
|
+
if outer_wall_cells:
|
|
777
|
+
inner_wall_cells.difference_update(outer_wall_cells)
|
|
778
|
+
if wall_group and cell_size > 0:
|
|
779
|
+
for wall in wall_group:
|
|
780
|
+
if isinstance(wall, SteelBeam):
|
|
781
|
+
cell_x = int(wall.rect.centerx // cell_size)
|
|
782
|
+
cell_y = int(wall.rect.centery // cell_size)
|
|
783
|
+
inner_wall_cells.add((cell_x, cell_y))
|
|
784
|
+
if not inner_wall_cells:
|
|
785
|
+
return
|
|
786
|
+
base_shadow_size = max(cell_size + 2, int(cell_size * 1.35))
|
|
787
|
+
shadow_size = max(1, int(base_shadow_size * 1.5))
|
|
788
|
+
shadow_surface = _get_shadow_tile_surface(shadow_size, 255, edge_softness=0.12)
|
|
789
|
+
shadow_layer = _get_shadow_layer(screen.get_size())
|
|
790
|
+
shadow_layer.fill((0, 0, 0, 0))
|
|
791
|
+
screen_rect = screen.get_rect()
|
|
792
|
+
px, py = light_source_pos
|
|
793
|
+
drew = False
|
|
794
|
+
clip_max = shadow_size * 0.25
|
|
795
|
+
for cell_x, cell_y in inner_wall_cells:
|
|
796
|
+
world_x = cell_x * cell_size
|
|
797
|
+
world_y = cell_y * cell_size
|
|
798
|
+
wall_rect = pygame.Rect(world_x, world_y, cell_size, cell_size)
|
|
799
|
+
wall_screen_rect = camera.apply_rect(wall_rect)
|
|
800
|
+
if not wall_screen_rect.colliderect(screen_rect):
|
|
801
|
+
continue
|
|
802
|
+
center_x = world_x + cell_size / 2
|
|
803
|
+
center_y = world_y + cell_size / 2
|
|
804
|
+
dx = (center_x - px) * 0.5
|
|
805
|
+
dy = (center_y - py) * 0.5
|
|
806
|
+
dx = int(abs_clip(dx, 0, clip_max))
|
|
807
|
+
dy = int(abs_clip(dy, 0, clip_max))
|
|
808
|
+
shadow_rect = pygame.Rect(0, 0, shadow_size, shadow_size)
|
|
809
|
+
shadow_rect.center = (
|
|
810
|
+
int(center_x + dx),
|
|
811
|
+
int(center_y + dy),
|
|
812
|
+
)
|
|
813
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
814
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
815
|
+
continue
|
|
816
|
+
shadow_layer.blit(shadow_surface, shadow_screen_rect.topleft)
|
|
817
|
+
drew = True
|
|
818
|
+
if drew:
|
|
819
|
+
shadow_layer.set_alpha(alpha)
|
|
820
|
+
screen.blit(shadow_layer, (0, 0))
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _draw_footprints(
|
|
824
|
+
screen: surface.Surface,
|
|
825
|
+
camera: Camera,
|
|
826
|
+
assets: RenderAssets,
|
|
827
|
+
footprints: list[Footprint],
|
|
828
|
+
*,
|
|
829
|
+
config: dict[str, Any],
|
|
830
|
+
) -> None:
|
|
831
|
+
if not config.get("footprints", {}).get("enabled", True):
|
|
832
|
+
return
|
|
833
|
+
now = pygame.time.get_ticks()
|
|
834
|
+
for fp in footprints:
|
|
835
|
+
age = now - fp.time
|
|
836
|
+
fade = 1 - (age / assets.footprint_lifetime_ms)
|
|
837
|
+
fade = max(assets.footprint_min_fade, fade)
|
|
838
|
+
color = tuple(int(c * fade) for c in FOOTPRINT_COLOR)
|
|
839
|
+
fp_rect = pygame.Rect(
|
|
840
|
+
fp.pos[0] - assets.footprint_radius,
|
|
841
|
+
fp.pos[1] - assets.footprint_radius,
|
|
842
|
+
assets.footprint_radius * 2,
|
|
843
|
+
assets.footprint_radius * 2,
|
|
844
|
+
)
|
|
845
|
+
sr = camera.apply_rect(fp_rect)
|
|
846
|
+
if sr.colliderect(screen.get_rect().inflate(30, 30)):
|
|
847
|
+
pygame.draw.circle(screen, color, sr.center, assets.footprint_radius)
|
|
590
848
|
|
|
849
|
+
|
|
850
|
+
def _draw_entities(
|
|
851
|
+
screen: surface.Surface,
|
|
852
|
+
camera: Camera,
|
|
853
|
+
all_sprites: sprite.LayeredUpdates,
|
|
854
|
+
player: Player,
|
|
855
|
+
*,
|
|
856
|
+
has_fuel: bool,
|
|
857
|
+
) -> pygame.Rect:
|
|
591
858
|
screen_rect_inflated = screen.get_rect().inflate(100, 100)
|
|
592
859
|
player_screen_rect: pygame.Rect | None = None
|
|
593
860
|
for entity in all_sprites:
|
|
@@ -596,177 +863,273 @@ def draw(
|
|
|
596
863
|
screen.blit(entity.image, sprite_screen_rect)
|
|
597
864
|
if entity is player:
|
|
598
865
|
player_screen_rect = sprite_screen_rect
|
|
866
|
+
_draw_fuel_indicator(
|
|
867
|
+
screen,
|
|
868
|
+
player_screen_rect,
|
|
869
|
+
has_fuel=has_fuel,
|
|
870
|
+
in_car=player.in_car,
|
|
871
|
+
)
|
|
872
|
+
return player_screen_rect or camera.apply_rect(player.rect)
|
|
599
873
|
|
|
600
|
-
if player_screen_rect is None:
|
|
601
|
-
player_screen_rect = camera.apply_rect(player.rect)
|
|
602
|
-
|
|
603
|
-
if has_fuel and player_screen_rect and not player.in_car:
|
|
604
|
-
indicator_size = 4
|
|
605
|
-
padding = 1
|
|
606
|
-
indicator_rect = pygame.Rect(
|
|
607
|
-
player_screen_rect.right - indicator_size - padding,
|
|
608
|
-
player_screen_rect.bottom - indicator_size - padding,
|
|
609
|
-
indicator_size,
|
|
610
|
-
indicator_size,
|
|
611
|
-
)
|
|
612
|
-
pygame.draw.rect(screen, YELLOW, indicator_rect)
|
|
613
|
-
pygame.draw.rect(screen, (180, 160, 40), indicator_rect, width=1)
|
|
614
874
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
875
|
+
def _draw_fuel_indicator(
|
|
876
|
+
screen: surface.Surface,
|
|
877
|
+
player_screen_rect: pygame.Rect,
|
|
878
|
+
*,
|
|
879
|
+
has_fuel: bool,
|
|
880
|
+
in_car: bool,
|
|
881
|
+
) -> None:
|
|
882
|
+
if not has_fuel or in_car:
|
|
883
|
+
return
|
|
884
|
+
indicator_size = 4
|
|
885
|
+
padding = 1
|
|
886
|
+
indicator_rect = pygame.Rect(
|
|
887
|
+
player_screen_rect.right - indicator_size - padding,
|
|
888
|
+
player_screen_rect.bottom - indicator_size - padding,
|
|
889
|
+
indicator_size,
|
|
890
|
+
indicator_size,
|
|
891
|
+
)
|
|
892
|
+
pygame.draw.rect(screen, YELLOW, indicator_rect)
|
|
893
|
+
pygame.draw.rect(screen, (180, 160, 40), indicator_rect, width=1)
|
|
631
894
|
|
|
632
|
-
if fov_target is not None:
|
|
633
|
-
fov_center_on_screen = list(camera.apply(fov_target).center)
|
|
634
|
-
cam_rect = camera.camera
|
|
635
|
-
horizontal_span = camera.width - assets.screen_width
|
|
636
|
-
vertical_span = camera.height - assets.screen_height
|
|
637
|
-
if horizontal_span <= 0 or (cam_rect.x != 0 and cam_rect.x != -horizontal_span):
|
|
638
|
-
fov_center_on_screen[0] = assets.screen_width // 2
|
|
639
|
-
if vertical_span <= 0 or (cam_rect.y != 0 and cam_rect.y != -vertical_span):
|
|
640
|
-
fov_center_on_screen[1] = assets.screen_height // 2
|
|
641
|
-
fov_center_tuple = (int(fov_center_on_screen[0]), int(fov_center_on_screen[1]))
|
|
642
|
-
if state.dawn_ready:
|
|
643
|
-
profile = FogProfile.DAWN
|
|
644
|
-
else:
|
|
645
|
-
profile = FogProfile._from_flashlight_count(flashlight_count)
|
|
646
|
-
overlay = _get_fog_overlay_surfaces(
|
|
647
|
-
fog_surfaces,
|
|
648
|
-
assets,
|
|
649
|
-
profile,
|
|
650
|
-
stage=stage,
|
|
651
|
-
)
|
|
652
|
-
combined_surface: surface.Surface = overlay["combined"]
|
|
653
|
-
screen.blit(
|
|
654
|
-
combined_surface,
|
|
655
|
-
combined_surface.get_rect(center=fov_center_tuple),
|
|
656
|
-
)
|
|
657
895
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
896
|
+
def _draw_hint_indicator(
|
|
897
|
+
screen: surface.Surface,
|
|
898
|
+
camera: Camera,
|
|
899
|
+
assets: RenderAssets,
|
|
900
|
+
player: Player,
|
|
901
|
+
hint_target: tuple[int, int] | None,
|
|
902
|
+
*,
|
|
903
|
+
hint_color: tuple[int, int, int],
|
|
904
|
+
stage: Stage | None,
|
|
905
|
+
flashlight_count: int,
|
|
906
|
+
) -> None:
|
|
907
|
+
if not hint_target:
|
|
908
|
+
return
|
|
909
|
+
current_fov_scale = _get_fog_scale(
|
|
910
|
+
assets,
|
|
911
|
+
stage,
|
|
912
|
+
flashlight_count,
|
|
913
|
+
)
|
|
914
|
+
hint_ring_radius = assets.fov_radius * 0.5 * current_fov_scale
|
|
915
|
+
_draw_hint_arrow(
|
|
916
|
+
screen,
|
|
917
|
+
camera,
|
|
918
|
+
assets,
|
|
919
|
+
player,
|
|
920
|
+
hint_target,
|
|
921
|
+
color=hint_color,
|
|
922
|
+
ring_radius=hint_ring_radius,
|
|
923
|
+
)
|
|
666
924
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
925
|
+
|
|
926
|
+
def _draw_fog_of_war(
|
|
927
|
+
screen: surface.Surface,
|
|
928
|
+
camera: Camera,
|
|
929
|
+
assets: RenderAssets,
|
|
930
|
+
fog_surfaces: dict[str, Any],
|
|
931
|
+
fov_target: pygame.sprite.Sprite | None,
|
|
932
|
+
*,
|
|
933
|
+
stage: Stage | None,
|
|
934
|
+
flashlight_count: int,
|
|
935
|
+
dawn_ready: bool,
|
|
936
|
+
) -> None:
|
|
937
|
+
if fov_target is None:
|
|
938
|
+
return
|
|
939
|
+
fov_center_on_screen = list(camera.apply(fov_target).center)
|
|
940
|
+
cam_rect = camera.camera
|
|
941
|
+
horizontal_span = camera.width - assets.screen_width
|
|
942
|
+
vertical_span = camera.height - assets.screen_height
|
|
943
|
+
if horizontal_span <= 0 or (cam_rect.x != 0 and cam_rect.x != -horizontal_span):
|
|
944
|
+
fov_center_on_screen[0] = assets.screen_width // 2
|
|
945
|
+
if vertical_span <= 0 or (cam_rect.y != 0 and cam_rect.y != -vertical_span):
|
|
946
|
+
fov_center_on_screen[1] = assets.screen_height // 2
|
|
947
|
+
fov_center_tuple = (int(fov_center_on_screen[0]), int(fov_center_on_screen[1]))
|
|
948
|
+
if dawn_ready:
|
|
949
|
+
profile = FogProfile.DAWN
|
|
950
|
+
else:
|
|
951
|
+
profile = FogProfile._from_flashlight_count(flashlight_count)
|
|
952
|
+
overlay = _get_fog_overlay_surfaces(
|
|
953
|
+
fog_surfaces,
|
|
954
|
+
assets,
|
|
955
|
+
profile,
|
|
956
|
+
stage=stage,
|
|
957
|
+
)
|
|
958
|
+
combined_surface: surface.Surface = overlay["combined"]
|
|
959
|
+
screen.blit(
|
|
960
|
+
combined_surface,
|
|
961
|
+
combined_surface.get_rect(center=fov_center_tuple),
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _draw_need_fuel_message(
|
|
966
|
+
screen: surface.Surface,
|
|
967
|
+
assets: RenderAssets,
|
|
968
|
+
*,
|
|
969
|
+
has_fuel: bool,
|
|
970
|
+
fuel_message_until: int,
|
|
971
|
+
elapsed_play_ms: int,
|
|
972
|
+
) -> None:
|
|
973
|
+
if has_fuel or fuel_message_until <= elapsed_play_ms:
|
|
974
|
+
return
|
|
975
|
+
show_message(
|
|
976
|
+
screen,
|
|
977
|
+
tr("hud.need_fuel"),
|
|
978
|
+
18,
|
|
979
|
+
ORANGE,
|
|
980
|
+
(assets.screen_width // 2, assets.screen_height // 2),
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _draw_objective(lines: list[str], *, screen: surface.Surface) -> None:
|
|
985
|
+
try:
|
|
986
|
+
font_settings = get_font_settings()
|
|
987
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(11))
|
|
988
|
+
y = 8
|
|
989
|
+
for line in lines:
|
|
990
|
+
text_surface = font.render(line, False, YELLOW)
|
|
991
|
+
text_rect = text_surface.get_rect(topleft=(12, y))
|
|
992
|
+
screen.blit(text_surface, text_rect)
|
|
993
|
+
y += text_rect.height + 4
|
|
994
|
+
except pygame.error as e:
|
|
995
|
+
print(f"Error rendering objective: {e}")
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _draw_endurance_timer(
|
|
999
|
+
screen: surface.Surface,
|
|
1000
|
+
assets: RenderAssets,
|
|
1001
|
+
*,
|
|
1002
|
+
stage: Stage | None,
|
|
1003
|
+
state: Any,
|
|
1004
|
+
) -> None:
|
|
1005
|
+
if not (stage and stage.endurance_stage):
|
|
1006
|
+
return
|
|
1007
|
+
goal_ms = state.endurance_goal_ms
|
|
1008
|
+
if goal_ms <= 0:
|
|
1009
|
+
return
|
|
1010
|
+
elapsed_ms = max(0, min(goal_ms, state.endurance_elapsed_ms))
|
|
1011
|
+
remaining_ms = max(0, goal_ms - elapsed_ms)
|
|
1012
|
+
padding = 12
|
|
1013
|
+
bar_height = 8
|
|
1014
|
+
y_pos = assets.screen_height - assets.status_bar_height - bar_height - 10
|
|
1015
|
+
bar_rect = pygame.Rect(
|
|
1016
|
+
padding,
|
|
1017
|
+
y_pos,
|
|
1018
|
+
assets.screen_width - padding * 2,
|
|
1019
|
+
bar_height,
|
|
1020
|
+
)
|
|
1021
|
+
track_surface = pygame.Surface((bar_rect.width, bar_rect.height), pygame.SRCALPHA)
|
|
1022
|
+
track_surface.fill((0, 0, 0, 140))
|
|
1023
|
+
screen.blit(track_surface, bar_rect.topleft)
|
|
1024
|
+
progress_ratio = elapsed_ms / goal_ms if goal_ms else 0.0
|
|
1025
|
+
progress_width = int(bar_rect.width * max(0.0, min(1.0, progress_ratio)))
|
|
1026
|
+
if progress_width > 0:
|
|
1027
|
+
fill_color = (120, 20, 20)
|
|
1028
|
+
if state.dawn_ready:
|
|
1029
|
+
fill_color = (25, 40, 120)
|
|
1030
|
+
fill_rect = pygame.Rect(
|
|
1031
|
+
bar_rect.left,
|
|
1032
|
+
bar_rect.top,
|
|
1033
|
+
progress_width,
|
|
1034
|
+
bar_rect.height,
|
|
699
1035
|
)
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1036
|
+
pygame.draw.rect(screen, fill_color, fill_rect)
|
|
1037
|
+
display_ms = int(remaining_ms * SURVIVAL_FAKE_CLOCK_RATIO)
|
|
1038
|
+
display_ms = max(0, display_ms)
|
|
1039
|
+
display_hours = display_ms // 3_600_000
|
|
1040
|
+
display_minutes = (display_ms % 3_600_000) // 60_000
|
|
1041
|
+
display_label = f"{int(display_hours):02d}:{int(display_minutes):02d}"
|
|
1042
|
+
timer_text = tr("hud.endurance_timer_label", time=display_label)
|
|
1043
|
+
try:
|
|
1044
|
+
font_settings = get_font_settings()
|
|
1045
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(12))
|
|
1046
|
+
text_surface = font.render(timer_text, False, LIGHT_GRAY)
|
|
1047
|
+
text_rect = text_surface.get_rect(left=bar_rect.left, bottom=bar_rect.top - 2)
|
|
1048
|
+
screen.blit(text_surface, text_rect)
|
|
1049
|
+
if state.time_accel_active:
|
|
1050
|
+
accel_text = tr("hud.time_accel")
|
|
1051
|
+
accel_surface = font.render(accel_text, False, YELLOW)
|
|
1052
|
+
accel_rect = accel_surface.get_rect(
|
|
1053
|
+
right=bar_rect.right, bottom=bar_rect.top - 2
|
|
713
1054
|
)
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
timer_text = tr("hud.survival_timer_label", time=display_label)
|
|
721
|
-
try:
|
|
722
|
-
font_settings = get_font_settings()
|
|
723
|
-
font = load_font(font_settings.resource, font_settings.scaled_size(12))
|
|
724
|
-
text_surface = font.render(timer_text, False, LIGHT_GRAY)
|
|
725
|
-
text_rect = text_surface.get_rect(
|
|
726
|
-
left=bar_rect.left, bottom=bar_rect.top - 2
|
|
1055
|
+
screen.blit(accel_surface, accel_rect)
|
|
1056
|
+
else:
|
|
1057
|
+
hint_text = tr("hud.time_accel_hint")
|
|
1058
|
+
hint_surface = font.render(hint_text, False, LIGHT_GRAY)
|
|
1059
|
+
hint_rect = hint_surface.get_rect(
|
|
1060
|
+
right=bar_rect.right, bottom=bar_rect.top - 2
|
|
727
1061
|
)
|
|
728
|
-
screen.blit(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1062
|
+
screen.blit(hint_surface, hint_rect)
|
|
1063
|
+
except pygame.error as e:
|
|
1064
|
+
print(f"Error rendering endurance timer: {e}")
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _draw_time_accel_indicator(
|
|
1068
|
+
screen: surface.Surface,
|
|
1069
|
+
assets: RenderAssets,
|
|
1070
|
+
*,
|
|
1071
|
+
stage: Stage | None,
|
|
1072
|
+
state: Any,
|
|
1073
|
+
) -> None:
|
|
1074
|
+
if stage and stage.endurance_stage:
|
|
1075
|
+
return
|
|
1076
|
+
try:
|
|
1077
|
+
font_settings = get_font_settings()
|
|
1078
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(12))
|
|
1079
|
+
if state.time_accel_active:
|
|
1080
|
+
text = tr("hud.time_accel")
|
|
1081
|
+
color = YELLOW
|
|
1082
|
+
else:
|
|
1083
|
+
text = tr("hud.time_accel_hint")
|
|
1084
|
+
color = LIGHT_GRAY
|
|
1085
|
+
text_surface = font.render(text, False, color)
|
|
1086
|
+
bottom_margin = assets.status_bar_height + 6
|
|
1087
|
+
text_rect = text_surface.get_rect(
|
|
1088
|
+
right=assets.screen_width - 12,
|
|
1089
|
+
bottom=assets.screen_height - bottom_margin,
|
|
1090
|
+
)
|
|
1091
|
+
screen.blit(text_surface, text_rect)
|
|
1092
|
+
except pygame.error as e:
|
|
1093
|
+
print(f"Error rendering acceleration indicator: {e}")
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _draw_survivor_messages(
|
|
1097
|
+
screen: surface.Surface,
|
|
1098
|
+
assets: RenderAssets,
|
|
1099
|
+
survivor_messages: list[dict[str, Any]],
|
|
1100
|
+
) -> None:
|
|
1101
|
+
if not survivor_messages:
|
|
1102
|
+
return
|
|
1103
|
+
try:
|
|
1104
|
+
font_settings = get_font_settings()
|
|
1105
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(14))
|
|
1106
|
+
base_y = assets.screen_height // 2 - 70
|
|
1107
|
+
for idx, message in enumerate(survivor_messages[:3]):
|
|
1108
|
+
text = message.get("text", "")
|
|
1109
|
+
if not text:
|
|
1110
|
+
continue
|
|
1111
|
+
msg_surface = font.render(text, False, ORANGE)
|
|
1112
|
+
msg_rect = msg_surface.get_rect(
|
|
1113
|
+
center=(assets.screen_width // 2, base_y + idx * 18)
|
|
763
1114
|
)
|
|
764
|
-
screen.blit(
|
|
765
|
-
|
|
766
|
-
|
|
1115
|
+
screen.blit(msg_surface, msg_rect)
|
|
1116
|
+
except pygame.error as e:
|
|
1117
|
+
print(f"Error rendering survivor message: {e}")
|
|
767
1118
|
|
|
1119
|
+
|
|
1120
|
+
def _build_objective_lines(
|
|
1121
|
+
*,
|
|
1122
|
+
stage: Stage | None,
|
|
1123
|
+
state: Any,
|
|
1124
|
+
player: Player,
|
|
1125
|
+
active_car: Car | None,
|
|
1126
|
+
has_fuel: bool,
|
|
1127
|
+
buddy_onboard: int,
|
|
1128
|
+
buddy_required: int,
|
|
1129
|
+
survivors_onboard: int,
|
|
1130
|
+
) -> list[str]:
|
|
768
1131
|
objective_lines: list[str] = []
|
|
769
|
-
if stage and stage.
|
|
1132
|
+
if stage and stage.endurance_stage:
|
|
770
1133
|
if state.dawn_ready:
|
|
771
1134
|
objective_lines.append(tr("objectives.get_outside"))
|
|
772
1135
|
else:
|
|
@@ -807,29 +1170,141 @@ def draw(
|
|
|
807
1170
|
objective_lines.append(
|
|
808
1171
|
tr("objectives.survivors_onboard", count=survivors_onboard, limit=limit)
|
|
809
1172
|
)
|
|
1173
|
+
return objective_lines
|
|
810
1174
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1175
|
+
|
|
1176
|
+
def draw(
|
|
1177
|
+
assets: RenderAssets,
|
|
1178
|
+
screen: surface.Surface,
|
|
1179
|
+
game_data: GameData,
|
|
1180
|
+
*,
|
|
1181
|
+
config: dict[str, Any],
|
|
1182
|
+
hint_target: tuple[int, int] | None = None,
|
|
1183
|
+
hint_color: tuple[int, int, int] | None = None,
|
|
1184
|
+
do_flip: bool = True,
|
|
1185
|
+
present_fn: Callable[[surface.Surface], None] | None = None,
|
|
1186
|
+
) -> None:
|
|
1187
|
+
hint_color = hint_color or YELLOW
|
|
1188
|
+
state = game_data.state
|
|
1189
|
+
player = game_data.player
|
|
1190
|
+
if player is None:
|
|
1191
|
+
raise ValueError("draw requires an active player on game_data")
|
|
1192
|
+
|
|
1193
|
+
camera = game_data.camera
|
|
1194
|
+
stage = game_data.stage
|
|
1195
|
+
outer_rect = game_data.layout.outer_rect
|
|
1196
|
+
outside_rects = game_data.layout.outside_rects or []
|
|
1197
|
+
all_sprites = game_data.groups.all_sprites
|
|
1198
|
+
fog_surfaces = game_data.fog
|
|
1199
|
+
footprints = state.footprints
|
|
1200
|
+
has_fuel = state.has_fuel
|
|
1201
|
+
flashlight_count = state.flashlight_count
|
|
1202
|
+
elapsed_play_ms = state.elapsed_play_ms
|
|
1203
|
+
fuel_message_until = state.fuel_message_until
|
|
1204
|
+
buddy_onboard = state.buddy_onboard
|
|
1205
|
+
buddy_required = stage.buddy_required_count if stage else 0
|
|
1206
|
+
survivors_onboard = state.survivors_onboard
|
|
1207
|
+
survivor_messages = list(state.survivor_messages)
|
|
1208
|
+
zombie_group = game_data.groups.zombie_group
|
|
1209
|
+
active_car = game_data.car if game_data.car and game_data.car.alive() else None
|
|
1210
|
+
if player.in_car and game_data.car and game_data.car.alive():
|
|
1211
|
+
fov_target = game_data.car
|
|
831
1212
|
else:
|
|
832
|
-
|
|
1213
|
+
fov_target = player
|
|
1214
|
+
|
|
1215
|
+
palette = get_environment_palette(state.ambient_palette_key)
|
|
1216
|
+
screen.fill(palette.outside)
|
|
1217
|
+
|
|
1218
|
+
_draw_play_area(
|
|
1219
|
+
screen,
|
|
1220
|
+
camera,
|
|
1221
|
+
assets,
|
|
1222
|
+
palette,
|
|
1223
|
+
outer_rect,
|
|
1224
|
+
outside_rects,
|
|
1225
|
+
game_data.layout.fall_spawn_cells,
|
|
1226
|
+
)
|
|
1227
|
+
_draw_wall_shadows(
|
|
1228
|
+
screen,
|
|
1229
|
+
camera,
|
|
1230
|
+
wall_cells=game_data.layout.wall_cells,
|
|
1231
|
+
wall_group=game_data.groups.wall_group,
|
|
1232
|
+
outer_wall_cells=game_data.layout.outer_wall_cells,
|
|
1233
|
+
cell_size=game_data.cell_size,
|
|
1234
|
+
light_source_pos=(
|
|
1235
|
+
None
|
|
1236
|
+
if (stage and stage.endurance_stage and state.dawn_ready)
|
|
1237
|
+
else fov_target.rect.center
|
|
1238
|
+
)
|
|
1239
|
+
if fov_target
|
|
1240
|
+
else None,
|
|
1241
|
+
)
|
|
1242
|
+
_draw_footprints(
|
|
1243
|
+
screen,
|
|
1244
|
+
camera,
|
|
1245
|
+
assets,
|
|
1246
|
+
footprints,
|
|
1247
|
+
config=config,
|
|
1248
|
+
)
|
|
1249
|
+
_draw_entities(
|
|
1250
|
+
screen,
|
|
1251
|
+
camera,
|
|
1252
|
+
all_sprites,
|
|
1253
|
+
player,
|
|
1254
|
+
has_fuel=has_fuel,
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
_draw_falling_fx(
|
|
1258
|
+
screen,
|
|
1259
|
+
camera,
|
|
1260
|
+
state.falling_zombies,
|
|
1261
|
+
state.flashlight_count,
|
|
1262
|
+
state.dust_rings,
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
_draw_hint_indicator(
|
|
1266
|
+
screen,
|
|
1267
|
+
camera,
|
|
1268
|
+
assets,
|
|
1269
|
+
player,
|
|
1270
|
+
hint_target,
|
|
1271
|
+
hint_color=hint_color,
|
|
1272
|
+
stage=stage,
|
|
1273
|
+
flashlight_count=flashlight_count,
|
|
1274
|
+
)
|
|
1275
|
+
_draw_fog_of_war(
|
|
1276
|
+
screen,
|
|
1277
|
+
camera,
|
|
1278
|
+
assets,
|
|
1279
|
+
fog_surfaces,
|
|
1280
|
+
fov_target,
|
|
1281
|
+
stage=stage,
|
|
1282
|
+
flashlight_count=flashlight_count,
|
|
1283
|
+
dawn_ready=state.dawn_ready,
|
|
1284
|
+
)
|
|
1285
|
+
_draw_need_fuel_message(
|
|
1286
|
+
screen,
|
|
1287
|
+
assets,
|
|
1288
|
+
has_fuel=has_fuel,
|
|
1289
|
+
fuel_message_until=fuel_message_until,
|
|
1290
|
+
elapsed_play_ms=elapsed_play_ms,
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
objective_lines = _build_objective_lines(
|
|
1294
|
+
stage=stage,
|
|
1295
|
+
state=state,
|
|
1296
|
+
player=player,
|
|
1297
|
+
active_car=active_car,
|
|
1298
|
+
has_fuel=has_fuel,
|
|
1299
|
+
buddy_onboard=buddy_onboard,
|
|
1300
|
+
buddy_required=buddy_required,
|
|
1301
|
+
survivors_onboard=survivors_onboard,
|
|
1302
|
+
)
|
|
1303
|
+
if objective_lines:
|
|
1304
|
+
_draw_objective(objective_lines, screen=screen)
|
|
1305
|
+
_draw_survivor_messages(screen, assets, survivor_messages)
|
|
1306
|
+
_draw_endurance_timer(screen, assets, stage=stage, state=state)
|
|
1307
|
+
_draw_time_accel_indicator(screen, assets, stage=stage, state=state)
|
|
833
1308
|
_draw_status_bar(
|
|
834
1309
|
screen,
|
|
835
1310
|
assets,
|
|
@@ -838,6 +1313,7 @@ def draw(
|
|
|
838
1313
|
seed=state.seed,
|
|
839
1314
|
debug_mode=state.debug_mode,
|
|
840
1315
|
zombie_group=zombie_group,
|
|
1316
|
+
falling_spawn_carry=state.falling_spawn_carry,
|
|
841
1317
|
)
|
|
842
1318
|
if do_flip:
|
|
843
1319
|
if present_fn:
|