zombie-escape 1.8.0__py3-none-any.whl → 1.10.0__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 +41 -8
- 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 +704 -267
- zombie_escape/render_constants.py +12 -0
- 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.0.dist-info}/METADATA +8 -1
- zombie_escape-1.10.0.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.0.dist-info}/WHEEL +0 -0
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.0.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.0.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,67 @@ 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
|
+
)
|
|
45
|
+
|
|
46
|
+
_SHADOW_TILE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
|
|
47
|
+
_SHADOW_LAYER_CACHE: dict[tuple[int, int], surface.Surface] = {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_shadow_tile_surface(
|
|
51
|
+
cell_size: int,
|
|
52
|
+
alpha: int,
|
|
53
|
+
*,
|
|
54
|
+
edge_softness: float = 0.35,
|
|
55
|
+
) -> surface.Surface:
|
|
56
|
+
key = (max(1, cell_size), max(0, min(255, alpha)), edge_softness)
|
|
57
|
+
if key in _SHADOW_TILE_CACHE:
|
|
58
|
+
return _SHADOW_TILE_CACHE[key]
|
|
59
|
+
size = key[0]
|
|
60
|
+
surf = pygame.Surface((size, size), pygame.SRCALPHA)
|
|
61
|
+
base_alpha = key[1]
|
|
62
|
+
if edge_softness <= 0:
|
|
63
|
+
surf.fill((0, 0, 0, base_alpha))
|
|
64
|
+
_SHADOW_TILE_CACHE[key] = surf
|
|
65
|
+
return surf
|
|
66
|
+
|
|
67
|
+
softness = max(0.0, min(1.0, edge_softness))
|
|
68
|
+
fade_band = max(1, int(size * softness))
|
|
69
|
+
base_radius = max(1, int(size * 0.18))
|
|
70
|
+
|
|
71
|
+
def draw_layer(inset: int, layer_alpha: int) -> None:
|
|
72
|
+
rect_size = size - inset * 2
|
|
73
|
+
if rect_size <= 0:
|
|
74
|
+
return
|
|
75
|
+
radius = max(0, base_radius - inset)
|
|
76
|
+
pygame.draw.rect(
|
|
77
|
+
surf,
|
|
78
|
+
(0, 0, 0, layer_alpha),
|
|
79
|
+
pygame.Rect(inset, inset, rect_size, rect_size),
|
|
80
|
+
border_radius=radius,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
outer_alpha = int(base_alpha * 0.45)
|
|
84
|
+
mid_alpha = int(base_alpha * 0.7)
|
|
85
|
+
draw_layer(0, outer_alpha)
|
|
86
|
+
draw_layer(max(1, fade_band // 2), mid_alpha)
|
|
87
|
+
draw_layer(fade_band, base_alpha)
|
|
88
|
+
_SHADOW_TILE_CACHE[key] = surf
|
|
89
|
+
return surf
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_shadow_layer(size: tuple[int, int]) -> surface.Surface:
|
|
93
|
+
key = (max(1, size[0]), max(1, size[1]))
|
|
94
|
+
if key in _SHADOW_LAYER_CACHE:
|
|
95
|
+
return _SHADOW_LAYER_CACHE[key]
|
|
96
|
+
layer = pygame.Surface(key, pygame.SRCALPHA)
|
|
97
|
+
_SHADOW_LAYER_CACHE[key] = layer
|
|
98
|
+
return layer
|
|
30
99
|
|
|
31
100
|
|
|
32
101
|
def show_message(
|
|
@@ -61,7 +130,7 @@ def draw_level_overview(
|
|
|
61
130
|
player: Player | None,
|
|
62
131
|
car: Car | None,
|
|
63
132
|
waiting_cars: list[Car] | None,
|
|
64
|
-
footprints: list[
|
|
133
|
+
footprints: list[Footprint],
|
|
65
134
|
*,
|
|
66
135
|
fuel: FuelCan | None = None,
|
|
67
136
|
flashlights: list[Flashlight] | None = None,
|
|
@@ -71,32 +140,37 @@ def draw_level_overview(
|
|
|
71
140
|
palette_key: str | None = None,
|
|
72
141
|
) -> None:
|
|
73
142
|
palette = get_environment_palette(palette_key)
|
|
74
|
-
base_floor =
|
|
143
|
+
base_floor = palette.floor_primary
|
|
75
144
|
dark_floor = tuple(max(0, int(channel * 0.55)) for channel in base_floor)
|
|
76
145
|
surface.fill(dark_floor)
|
|
77
146
|
for wall in wall_group:
|
|
78
|
-
if not isinstance(wall, Wall):
|
|
79
|
-
raise TypeError("wall_group must contain Wall instances")
|
|
80
147
|
if wall.max_health > 0:
|
|
81
148
|
health_ratio = max(0.0, min(1.0, wall.health / wall.max_health))
|
|
82
149
|
else:
|
|
83
150
|
health_ratio = 0.0
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
151
|
+
if isinstance(wall, Wall):
|
|
152
|
+
fill_color, _ = resolve_wall_colors(
|
|
153
|
+
health_ratio=health_ratio,
|
|
154
|
+
palette_category=wall.palette_category,
|
|
155
|
+
palette=palette,
|
|
156
|
+
)
|
|
157
|
+
pygame.draw.rect(surface, fill_color, wall.rect)
|
|
158
|
+
elif isinstance(wall, SteelBeam):
|
|
159
|
+
fill_color, _ = resolve_steel_beam_colors(
|
|
160
|
+
health_ratio=health_ratio,
|
|
161
|
+
palette=palette,
|
|
162
|
+
)
|
|
163
|
+
pygame.draw.rect(surface, fill_color, wall.rect)
|
|
90
164
|
now = pygame.time.get_ticks()
|
|
91
165
|
for fp in footprints:
|
|
92
|
-
age = now - fp
|
|
166
|
+
age = now - fp.time
|
|
93
167
|
fade = 1 - (age / assets.footprint_lifetime_ms)
|
|
94
168
|
fade = max(assets.footprint_min_fade, fade)
|
|
95
169
|
color = tuple(int(c * fade) for c in FOOTPRINT_COLOR)
|
|
96
170
|
pygame.draw.circle(
|
|
97
171
|
surface,
|
|
98
172
|
color,
|
|
99
|
-
(int(fp
|
|
173
|
+
(int(fp.pos[0]), int(fp.pos[1])),
|
|
100
174
|
assets.footprint_overview_radius,
|
|
101
175
|
)
|
|
102
176
|
if fuel and fuel.alive():
|
|
@@ -258,6 +332,9 @@ def _get_fog_overlay_surfaces(
|
|
|
258
332
|
return overlays[key]
|
|
259
333
|
|
|
260
334
|
scale = profile._scale(assets, stage)
|
|
335
|
+
ring_scale = scale
|
|
336
|
+
if profile.flashlight_count >= 2:
|
|
337
|
+
ring_scale += max(0.0, assets.flashlight_hatch_extra_scale)
|
|
261
338
|
max_radius = int(assets.fov_radius * scale)
|
|
262
339
|
padding = 32
|
|
263
340
|
coverage_width = max(assets.screen_width * 2, max_radius * 2)
|
|
@@ -284,7 +361,7 @@ def _get_fog_overlay_surfaces(
|
|
|
284
361
|
for y in range(0, height, p_h):
|
|
285
362
|
for x in range(0, width, p_w):
|
|
286
363
|
ring_surface.blit(pattern, (x, y))
|
|
287
|
-
radius = int(assets.fov_radius * ring.radius_factor *
|
|
364
|
+
radius = int(assets.fov_radius * ring.radius_factor * ring_scale)
|
|
288
365
|
pygame.draw.circle(ring_surface, (0, 0, 0, 0), center, radius)
|
|
289
366
|
ring_surfaces.append(ring_surface)
|
|
290
367
|
|
|
@@ -359,6 +436,96 @@ def _build_flashlight_fade_surface(
|
|
|
359
436
|
return fade_surface
|
|
360
437
|
|
|
361
438
|
|
|
439
|
+
def _draw_fall_whirlwind(
|
|
440
|
+
screen: surface.Surface,
|
|
441
|
+
camera: Camera,
|
|
442
|
+
center: tuple[int, int],
|
|
443
|
+
progress: float,
|
|
444
|
+
) -> None:
|
|
445
|
+
base_alpha = FALLING_WHIRLWIND_COLOR[3]
|
|
446
|
+
alpha = int(max(0, min(255, base_alpha * (1.0 - progress))))
|
|
447
|
+
if alpha <= 0:
|
|
448
|
+
return
|
|
449
|
+
color = (
|
|
450
|
+
FALLING_WHIRLWIND_COLOR[0],
|
|
451
|
+
FALLING_WHIRLWIND_COLOR[1],
|
|
452
|
+
FALLING_WHIRLWIND_COLOR[2],
|
|
453
|
+
alpha,
|
|
454
|
+
)
|
|
455
|
+
swirl_radius = max(2, int(ZOMBIE_RADIUS * 1.1))
|
|
456
|
+
offset = max(1, int(ZOMBIE_RADIUS * 0.6))
|
|
457
|
+
size = swirl_radius * 4
|
|
458
|
+
swirl = pygame.Surface((size, size), pygame.SRCALPHA)
|
|
459
|
+
cx = cy = size // 2
|
|
460
|
+
for idx in range(2):
|
|
461
|
+
angle = progress * math.tau * 0.3 + idx * (math.tau / 2)
|
|
462
|
+
ox = int(math.cos(angle) * offset)
|
|
463
|
+
oy = int(math.sin(angle) * offset)
|
|
464
|
+
pygame.draw.circle(swirl, color, (cx + ox, cy + oy), swirl_radius, width=2)
|
|
465
|
+
world_rect = pygame.Rect(0, 0, 1, 1)
|
|
466
|
+
world_rect.center = center
|
|
467
|
+
screen_center = camera.apply_rect(world_rect).center
|
|
468
|
+
screen.blit(swirl, swirl.get_rect(center=screen_center))
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _draw_falling_fx(
|
|
472
|
+
screen: surface.Surface,
|
|
473
|
+
camera: Camera,
|
|
474
|
+
falling_zombies: list[FallingZombie],
|
|
475
|
+
flashlight_count: int,
|
|
476
|
+
dust_rings: list[DustRing],
|
|
477
|
+
) -> None:
|
|
478
|
+
if not falling_zombies and not dust_rings:
|
|
479
|
+
return
|
|
480
|
+
now = pygame.time.get_ticks()
|
|
481
|
+
for fall in falling_zombies:
|
|
482
|
+
pre_fx_ms = max(0, fall.pre_fx_ms)
|
|
483
|
+
fall_duration_ms = max(1, fall.fall_duration_ms)
|
|
484
|
+
fall_start = fall.started_at_ms + pre_fx_ms
|
|
485
|
+
impact_at = fall_start + fall_duration_ms
|
|
486
|
+
if now < fall_start:
|
|
487
|
+
if flashlight_count > 0 and pre_fx_ms > 0:
|
|
488
|
+
fx_progress = max(0.0, min(1.0, (now - fall.started_at_ms) / pre_fx_ms))
|
|
489
|
+
_draw_fall_whirlwind(screen, camera, fall.target_pos, fx_progress)
|
|
490
|
+
continue
|
|
491
|
+
if now >= impact_at:
|
|
492
|
+
continue
|
|
493
|
+
fall_progress = max(0.0, min(1.0, (now - fall_start) / fall_duration_ms))
|
|
494
|
+
eased = 1.0 - (1.0 - fall_progress) * (1.0 - fall_progress)
|
|
495
|
+
x = fall.target_pos[0]
|
|
496
|
+
y = int(fall.start_pos[1] + (fall.target_pos[1] - fall.start_pos[1]) * eased)
|
|
497
|
+
world_rect = pygame.Rect(0, 0, ZOMBIE_RADIUS * 2, ZOMBIE_RADIUS * 2)
|
|
498
|
+
world_rect.center = (x, y)
|
|
499
|
+
screen_rect = camera.apply_rect(world_rect)
|
|
500
|
+
pygame.draw.circle(
|
|
501
|
+
screen,
|
|
502
|
+
FALLING_ZOMBIE_COLOR,
|
|
503
|
+
screen_rect.center,
|
|
504
|
+
ZOMBIE_RADIUS,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
for ring in list(dust_rings):
|
|
508
|
+
elapsed = now - ring.started_at_ms
|
|
509
|
+
if elapsed >= ring.duration_ms:
|
|
510
|
+
dust_rings.remove(ring)
|
|
511
|
+
continue
|
|
512
|
+
progress = max(0.0, min(1.0, elapsed / ring.duration_ms))
|
|
513
|
+
alpha = int(max(0, min(255, FALLING_DUST_COLOR[3] * (1.0 - progress))))
|
|
514
|
+
if alpha <= 0:
|
|
515
|
+
continue
|
|
516
|
+
radius = int(ZOMBIE_RADIUS * (0.7 + progress * 1.9))
|
|
517
|
+
color = (
|
|
518
|
+
FALLING_DUST_COLOR[0],
|
|
519
|
+
FALLING_DUST_COLOR[1],
|
|
520
|
+
FALLING_DUST_COLOR[2],
|
|
521
|
+
alpha,
|
|
522
|
+
)
|
|
523
|
+
world_rect = pygame.Rect(0, 0, 1, 1)
|
|
524
|
+
world_rect.center = ring.pos
|
|
525
|
+
screen_center = camera.apply_rect(world_rect).center
|
|
526
|
+
pygame.draw.circle(screen, color, screen_center, radius, width=2)
|
|
527
|
+
|
|
528
|
+
|
|
362
529
|
def _draw_hint_arrow(
|
|
363
530
|
screen: surface.Surface,
|
|
364
531
|
camera: Camera,
|
|
@@ -411,6 +578,7 @@ def _draw_status_bar(
|
|
|
411
578
|
seed: int | None = None,
|
|
412
579
|
debug_mode: bool = False,
|
|
413
580
|
zombie_group: sprite.Group | None = None,
|
|
581
|
+
falling_spawn_carry: int | None = None,
|
|
414
582
|
) -> None:
|
|
415
583
|
"""Render a compact status bar with current config flags and stage info."""
|
|
416
584
|
bar_rect = pygame.Rect(
|
|
@@ -443,14 +611,15 @@ def _draw_status_bar(
|
|
|
443
611
|
if steel_on:
|
|
444
612
|
parts.append(tr("status.steel"))
|
|
445
613
|
if debug_mode:
|
|
446
|
-
parts.append(tr("status.debug"))
|
|
447
614
|
if zombie_group is not None:
|
|
448
|
-
zombies = [z for z in zombie_group if
|
|
615
|
+
zombies = [z for z in zombie_group if z.alive()]
|
|
449
616
|
total = len(zombies)
|
|
450
|
-
tracker = sum(1 for z in zombies if
|
|
451
|
-
wall = sum(1 for z in zombies if
|
|
617
|
+
tracker = sum(1 for z in zombies if z.tracker)
|
|
618
|
+
wall = sum(1 for z in zombies if z.wall_follower)
|
|
452
619
|
normal = max(0, total - tracker - wall)
|
|
453
620
|
parts.append(f"Z:{total} N:{normal} T:{tracker} W:{wall}")
|
|
621
|
+
if falling_spawn_carry is not None:
|
|
622
|
+
parts.append(f"C:{max(0, falling_spawn_carry)}")
|
|
454
623
|
|
|
455
624
|
status_text = " | ".join(parts)
|
|
456
625
|
color = LIGHT_GRAY
|
|
@@ -472,45 +641,15 @@ def _draw_status_bar(
|
|
|
472
641
|
print(f"Error rendering status bar: {e}")
|
|
473
642
|
|
|
474
643
|
|
|
475
|
-
def
|
|
476
|
-
assets: RenderAssets,
|
|
644
|
+
def _draw_play_area(
|
|
477
645
|
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
|
-
|
|
646
|
+
camera: Camera,
|
|
647
|
+
assets: RenderAssets,
|
|
648
|
+
palette: Any,
|
|
649
|
+
outer_rect: tuple[int, int, int, int],
|
|
650
|
+
outside_rects: list[pygame.Rect],
|
|
651
|
+
fall_spawn_cells: set[tuple[int, int]],
|
|
652
|
+
) -> tuple[int, int, int, int, set[tuple[int, int]]]:
|
|
514
653
|
xs, ys, xe, ye = outer_rect
|
|
515
654
|
xs //= assets.internal_wall_grid_snap
|
|
516
655
|
ys //= assets.internal_wall_grid_snap
|
|
@@ -556,38 +695,129 @@ def draw(
|
|
|
556
695
|
for x in range(start_x, end_x):
|
|
557
696
|
if (x, y) in outside_cells:
|
|
558
697
|
continue
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
lx,
|
|
566
|
-
ly,
|
|
567
|
-
assets.internal_wall_grid_snap,
|
|
568
|
-
assets.internal_wall_grid_snap,
|
|
698
|
+
use_secondary = ((x // 2) + (y // 2)) % 2 == 0
|
|
699
|
+
if (x, y) in fall_spawn_cells:
|
|
700
|
+
color = (
|
|
701
|
+
palette.fall_zone_secondary
|
|
702
|
+
if use_secondary
|
|
703
|
+
else palette.fall_zone_primary
|
|
569
704
|
)
|
|
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,
|
|
705
|
+
elif not use_secondary:
|
|
706
|
+
continue
|
|
707
|
+
else:
|
|
708
|
+
color = palette.floor_secondary
|
|
709
|
+
lx, ly = (
|
|
710
|
+
x * assets.internal_wall_grid_snap,
|
|
711
|
+
y * assets.internal_wall_grid_snap,
|
|
712
|
+
)
|
|
713
|
+
r = pygame.Rect(
|
|
714
|
+
lx,
|
|
715
|
+
ly,
|
|
716
|
+
assets.internal_wall_grid_snap,
|
|
717
|
+
assets.internal_wall_grid_snap,
|
|
586
718
|
)
|
|
587
|
-
sr = camera.apply_rect(
|
|
588
|
-
if sr.colliderect(screen.get_rect()
|
|
589
|
-
pygame.draw.
|
|
719
|
+
sr = camera.apply_rect(r)
|
|
720
|
+
if sr.colliderect(screen.get_rect()):
|
|
721
|
+
pygame.draw.rect(screen, color, sr)
|
|
722
|
+
|
|
723
|
+
return xs, ys, xe, ye, outside_cells
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _draw_wall_shadows(
|
|
727
|
+
screen: surface.Surface,
|
|
728
|
+
camera: Camera,
|
|
729
|
+
*,
|
|
730
|
+
wall_cells: set[tuple[int, int]],
|
|
731
|
+
wall_group: sprite.Group | None,
|
|
732
|
+
outer_wall_cells: set[tuple[int, int]] | None,
|
|
733
|
+
cell_size: int,
|
|
734
|
+
light_source_pos: tuple[int, int] | None,
|
|
735
|
+
alpha: int = 54,
|
|
736
|
+
) -> None:
|
|
737
|
+
if not wall_cells or cell_size <= 0 or light_source_pos is None:
|
|
738
|
+
return
|
|
739
|
+
inner_wall_cells = set(wall_cells)
|
|
740
|
+
if outer_wall_cells:
|
|
741
|
+
inner_wall_cells.difference_update(outer_wall_cells)
|
|
742
|
+
if wall_group and cell_size > 0:
|
|
743
|
+
for wall in wall_group:
|
|
744
|
+
if isinstance(wall, SteelBeam):
|
|
745
|
+
cell_x = int(wall.rect.centerx // cell_size)
|
|
746
|
+
cell_y = int(wall.rect.centery // cell_size)
|
|
747
|
+
inner_wall_cells.add((cell_x, cell_y))
|
|
748
|
+
if not inner_wall_cells:
|
|
749
|
+
return
|
|
750
|
+
base_shadow_size = max(cell_size + 2, int(cell_size * 1.35))
|
|
751
|
+
shadow_size = max(1, int(base_shadow_size * 1.5))
|
|
752
|
+
shadow_surface = _get_shadow_tile_surface(shadow_size, 255, edge_softness=0.12)
|
|
753
|
+
shadow_layer = _get_shadow_layer(screen.get_size())
|
|
754
|
+
shadow_layer.fill((0, 0, 0, 0))
|
|
755
|
+
screen_rect = screen.get_rect()
|
|
756
|
+
px, py = light_source_pos
|
|
757
|
+
offset = max(2, int(cell_size * 0.3 * (shadow_size / cell_size) * 1.2))
|
|
758
|
+
drew = False
|
|
759
|
+
for cell_x, cell_y in inner_wall_cells:
|
|
760
|
+
world_x = cell_x * cell_size
|
|
761
|
+
world_y = cell_y * cell_size
|
|
762
|
+
center_x = world_x + cell_size / 2
|
|
763
|
+
center_y = world_y + cell_size / 2
|
|
764
|
+
dx = center_x - px
|
|
765
|
+
dy = center_y - py
|
|
766
|
+
dist = math.hypot(dx, dy)
|
|
767
|
+
if dist <= 1e-3:
|
|
768
|
+
continue
|
|
769
|
+
nx = dx / dist
|
|
770
|
+
ny = dy / dist
|
|
771
|
+
shadow_rect = pygame.Rect(0, 0, shadow_size, shadow_size)
|
|
772
|
+
shadow_rect.center = (
|
|
773
|
+
int(center_x + nx * offset),
|
|
774
|
+
int(center_y + ny * offset),
|
|
775
|
+
)
|
|
776
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
777
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
778
|
+
continue
|
|
779
|
+
shadow_layer.blit(shadow_surface, shadow_screen_rect.topleft)
|
|
780
|
+
drew = True
|
|
781
|
+
if drew:
|
|
782
|
+
shadow_layer.set_alpha(alpha)
|
|
783
|
+
screen.blit(shadow_layer, (0, 0))
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _draw_footprints(
|
|
787
|
+
screen: surface.Surface,
|
|
788
|
+
camera: Camera,
|
|
789
|
+
assets: RenderAssets,
|
|
790
|
+
footprints: list[Footprint],
|
|
791
|
+
*,
|
|
792
|
+
config: dict[str, Any],
|
|
793
|
+
) -> None:
|
|
794
|
+
if not config.get("footprints", {}).get("enabled", True):
|
|
795
|
+
return
|
|
796
|
+
now = pygame.time.get_ticks()
|
|
797
|
+
for fp in footprints:
|
|
798
|
+
age = now - fp.time
|
|
799
|
+
fade = 1 - (age / assets.footprint_lifetime_ms)
|
|
800
|
+
fade = max(assets.footprint_min_fade, fade)
|
|
801
|
+
color = tuple(int(c * fade) for c in FOOTPRINT_COLOR)
|
|
802
|
+
fp_rect = pygame.Rect(
|
|
803
|
+
fp.pos[0] - assets.footprint_radius,
|
|
804
|
+
fp.pos[1] - assets.footprint_radius,
|
|
805
|
+
assets.footprint_radius * 2,
|
|
806
|
+
assets.footprint_radius * 2,
|
|
807
|
+
)
|
|
808
|
+
sr = camera.apply_rect(fp_rect)
|
|
809
|
+
if sr.colliderect(screen.get_rect().inflate(30, 30)):
|
|
810
|
+
pygame.draw.circle(screen, color, sr.center, assets.footprint_radius)
|
|
811
|
+
|
|
590
812
|
|
|
813
|
+
def _draw_entities(
|
|
814
|
+
screen: surface.Surface,
|
|
815
|
+
camera: Camera,
|
|
816
|
+
all_sprites: sprite.LayeredUpdates,
|
|
817
|
+
player: Player,
|
|
818
|
+
*,
|
|
819
|
+
has_fuel: bool,
|
|
820
|
+
) -> pygame.Rect:
|
|
591
821
|
screen_rect_inflated = screen.get_rect().inflate(100, 100)
|
|
592
822
|
player_screen_rect: pygame.Rect | None = None
|
|
593
823
|
for entity in all_sprites:
|
|
@@ -596,177 +826,273 @@ def draw(
|
|
|
596
826
|
screen.blit(entity.image, sprite_screen_rect)
|
|
597
827
|
if entity is player:
|
|
598
828
|
player_screen_rect = sprite_screen_rect
|
|
829
|
+
_draw_fuel_indicator(
|
|
830
|
+
screen,
|
|
831
|
+
player_screen_rect,
|
|
832
|
+
has_fuel=has_fuel,
|
|
833
|
+
in_car=player.in_car,
|
|
834
|
+
)
|
|
835
|
+
return player_screen_rect or camera.apply_rect(player.rect)
|
|
599
836
|
|
|
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
837
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
838
|
+
def _draw_fuel_indicator(
|
|
839
|
+
screen: surface.Surface,
|
|
840
|
+
player_screen_rect: pygame.Rect,
|
|
841
|
+
*,
|
|
842
|
+
has_fuel: bool,
|
|
843
|
+
in_car: bool,
|
|
844
|
+
) -> None:
|
|
845
|
+
if not has_fuel or in_car:
|
|
846
|
+
return
|
|
847
|
+
indicator_size = 4
|
|
848
|
+
padding = 1
|
|
849
|
+
indicator_rect = pygame.Rect(
|
|
850
|
+
player_screen_rect.right - indicator_size - padding,
|
|
851
|
+
player_screen_rect.bottom - indicator_size - padding,
|
|
852
|
+
indicator_size,
|
|
853
|
+
indicator_size,
|
|
854
|
+
)
|
|
855
|
+
pygame.draw.rect(screen, YELLOW, indicator_rect)
|
|
856
|
+
pygame.draw.rect(screen, (180, 160, 40), indicator_rect, width=1)
|
|
631
857
|
|
|
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
858
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
859
|
+
def _draw_hint_indicator(
|
|
860
|
+
screen: surface.Surface,
|
|
861
|
+
camera: Camera,
|
|
862
|
+
assets: RenderAssets,
|
|
863
|
+
player: Player,
|
|
864
|
+
hint_target: tuple[int, int] | None,
|
|
865
|
+
*,
|
|
866
|
+
hint_color: tuple[int, int, int],
|
|
867
|
+
stage: Stage | None,
|
|
868
|
+
flashlight_count: int,
|
|
869
|
+
) -> None:
|
|
870
|
+
if not hint_target:
|
|
871
|
+
return
|
|
872
|
+
current_fov_scale = _get_fog_scale(
|
|
873
|
+
assets,
|
|
874
|
+
stage,
|
|
875
|
+
flashlight_count,
|
|
876
|
+
)
|
|
877
|
+
hint_ring_radius = assets.fov_radius * 0.5 * current_fov_scale
|
|
878
|
+
_draw_hint_arrow(
|
|
879
|
+
screen,
|
|
880
|
+
camera,
|
|
881
|
+
assets,
|
|
882
|
+
player,
|
|
883
|
+
hint_target,
|
|
884
|
+
color=hint_color,
|
|
885
|
+
ring_radius=hint_ring_radius,
|
|
886
|
+
)
|
|
666
887
|
|
|
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
|
-
|
|
888
|
+
|
|
889
|
+
def _draw_fog_of_war(
|
|
890
|
+
screen: surface.Surface,
|
|
891
|
+
camera: Camera,
|
|
892
|
+
assets: RenderAssets,
|
|
893
|
+
fog_surfaces: dict[str, Any],
|
|
894
|
+
fov_target: pygame.sprite.Sprite | None,
|
|
895
|
+
*,
|
|
896
|
+
stage: Stage | None,
|
|
897
|
+
flashlight_count: int,
|
|
898
|
+
dawn_ready: bool,
|
|
899
|
+
) -> None:
|
|
900
|
+
if fov_target is None:
|
|
901
|
+
return
|
|
902
|
+
fov_center_on_screen = list(camera.apply(fov_target).center)
|
|
903
|
+
cam_rect = camera.camera
|
|
904
|
+
horizontal_span = camera.width - assets.screen_width
|
|
905
|
+
vertical_span = camera.height - assets.screen_height
|
|
906
|
+
if horizontal_span <= 0 or (cam_rect.x != 0 and cam_rect.x != -horizontal_span):
|
|
907
|
+
fov_center_on_screen[0] = assets.screen_width // 2
|
|
908
|
+
if vertical_span <= 0 or (cam_rect.y != 0 and cam_rect.y != -vertical_span):
|
|
909
|
+
fov_center_on_screen[1] = assets.screen_height // 2
|
|
910
|
+
fov_center_tuple = (int(fov_center_on_screen[0]), int(fov_center_on_screen[1]))
|
|
911
|
+
if dawn_ready:
|
|
912
|
+
profile = FogProfile.DAWN
|
|
913
|
+
else:
|
|
914
|
+
profile = FogProfile._from_flashlight_count(flashlight_count)
|
|
915
|
+
overlay = _get_fog_overlay_surfaces(
|
|
916
|
+
fog_surfaces,
|
|
917
|
+
assets,
|
|
918
|
+
profile,
|
|
919
|
+
stage=stage,
|
|
920
|
+
)
|
|
921
|
+
combined_surface: surface.Surface = overlay["combined"]
|
|
922
|
+
screen.blit(
|
|
923
|
+
combined_surface,
|
|
924
|
+
combined_surface.get_rect(center=fov_center_tuple),
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _draw_need_fuel_message(
|
|
929
|
+
screen: surface.Surface,
|
|
930
|
+
assets: RenderAssets,
|
|
931
|
+
*,
|
|
932
|
+
has_fuel: bool,
|
|
933
|
+
fuel_message_until: int,
|
|
934
|
+
elapsed_play_ms: int,
|
|
935
|
+
) -> None:
|
|
936
|
+
if has_fuel or fuel_message_until <= elapsed_play_ms:
|
|
937
|
+
return
|
|
938
|
+
show_message(
|
|
939
|
+
screen,
|
|
940
|
+
tr("hud.need_fuel"),
|
|
941
|
+
18,
|
|
942
|
+
ORANGE,
|
|
943
|
+
(assets.screen_width // 2, assets.screen_height // 2),
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _draw_objective(lines: list[str], *, screen: surface.Surface) -> None:
|
|
948
|
+
try:
|
|
949
|
+
font_settings = get_font_settings()
|
|
950
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(11))
|
|
951
|
+
y = 8
|
|
952
|
+
for line in lines:
|
|
953
|
+
text_surface = font.render(line, False, YELLOW)
|
|
954
|
+
text_rect = text_surface.get_rect(topleft=(12, y))
|
|
955
|
+
screen.blit(text_surface, text_rect)
|
|
956
|
+
y += text_rect.height + 4
|
|
957
|
+
except pygame.error as e:
|
|
958
|
+
print(f"Error rendering objective: {e}")
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _draw_endurance_timer(
|
|
962
|
+
screen: surface.Surface,
|
|
963
|
+
assets: RenderAssets,
|
|
964
|
+
*,
|
|
965
|
+
stage: Stage | None,
|
|
966
|
+
state: Any,
|
|
967
|
+
) -> None:
|
|
968
|
+
if not (stage and stage.endurance_stage):
|
|
969
|
+
return
|
|
970
|
+
goal_ms = state.endurance_goal_ms
|
|
971
|
+
if goal_ms <= 0:
|
|
972
|
+
return
|
|
973
|
+
elapsed_ms = max(0, min(goal_ms, state.endurance_elapsed_ms))
|
|
974
|
+
remaining_ms = max(0, goal_ms - elapsed_ms)
|
|
975
|
+
padding = 12
|
|
976
|
+
bar_height = 8
|
|
977
|
+
y_pos = assets.screen_height - assets.status_bar_height - bar_height - 10
|
|
978
|
+
bar_rect = pygame.Rect(
|
|
979
|
+
padding,
|
|
980
|
+
y_pos,
|
|
981
|
+
assets.screen_width - padding * 2,
|
|
982
|
+
bar_height,
|
|
983
|
+
)
|
|
984
|
+
track_surface = pygame.Surface((bar_rect.width, bar_rect.height), pygame.SRCALPHA)
|
|
985
|
+
track_surface.fill((0, 0, 0, 140))
|
|
986
|
+
screen.blit(track_surface, bar_rect.topleft)
|
|
987
|
+
progress_ratio = elapsed_ms / goal_ms if goal_ms else 0.0
|
|
988
|
+
progress_width = int(bar_rect.width * max(0.0, min(1.0, progress_ratio)))
|
|
989
|
+
if progress_width > 0:
|
|
990
|
+
fill_color = (120, 20, 20)
|
|
991
|
+
if state.dawn_ready:
|
|
992
|
+
fill_color = (25, 40, 120)
|
|
993
|
+
fill_rect = pygame.Rect(
|
|
994
|
+
bar_rect.left,
|
|
995
|
+
bar_rect.top,
|
|
996
|
+
progress_width,
|
|
997
|
+
bar_rect.height,
|
|
699
998
|
)
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
999
|
+
pygame.draw.rect(screen, fill_color, fill_rect)
|
|
1000
|
+
display_ms = int(remaining_ms * SURVIVAL_FAKE_CLOCK_RATIO)
|
|
1001
|
+
display_ms = max(0, display_ms)
|
|
1002
|
+
display_hours = display_ms // 3_600_000
|
|
1003
|
+
display_minutes = (display_ms % 3_600_000) // 60_000
|
|
1004
|
+
display_label = f"{int(display_hours):02d}:{int(display_minutes):02d}"
|
|
1005
|
+
timer_text = tr("hud.endurance_timer_label", time=display_label)
|
|
1006
|
+
try:
|
|
1007
|
+
font_settings = get_font_settings()
|
|
1008
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(12))
|
|
1009
|
+
text_surface = font.render(timer_text, False, LIGHT_GRAY)
|
|
1010
|
+
text_rect = text_surface.get_rect(left=bar_rect.left, bottom=bar_rect.top - 2)
|
|
1011
|
+
screen.blit(text_surface, text_rect)
|
|
1012
|
+
if state.time_accel_active:
|
|
1013
|
+
accel_text = tr("hud.time_accel")
|
|
1014
|
+
accel_surface = font.render(accel_text, False, YELLOW)
|
|
1015
|
+
accel_rect = accel_surface.get_rect(
|
|
1016
|
+
right=bar_rect.right, bottom=bar_rect.top - 2
|
|
713
1017
|
)
|
|
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
|
|
1018
|
+
screen.blit(accel_surface, accel_rect)
|
|
1019
|
+
else:
|
|
1020
|
+
hint_text = tr("hud.time_accel_hint")
|
|
1021
|
+
hint_surface = font.render(hint_text, False, LIGHT_GRAY)
|
|
1022
|
+
hint_rect = hint_surface.get_rect(
|
|
1023
|
+
right=bar_rect.right, bottom=bar_rect.top - 2
|
|
727
1024
|
)
|
|
728
|
-
screen.blit(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
accel_surface = font.render(accel_text, False, YELLOW)
|
|
732
|
-
accel_rect = accel_surface.get_rect(
|
|
733
|
-
right=bar_rect.right, bottom=bar_rect.top - 2
|
|
734
|
-
)
|
|
735
|
-
screen.blit(accel_surface, accel_rect)
|
|
736
|
-
else:
|
|
737
|
-
hint_text = tr("hud.time_accel_hint")
|
|
738
|
-
hint_surface = font.render(hint_text, False, LIGHT_GRAY)
|
|
739
|
-
hint_rect = hint_surface.get_rect(
|
|
740
|
-
right=bar_rect.right, bottom=bar_rect.top - 2
|
|
741
|
-
)
|
|
742
|
-
screen.blit(hint_surface, hint_rect)
|
|
743
|
-
except pygame.error as e:
|
|
744
|
-
print(f"Error rendering survival timer: {e}")
|
|
1025
|
+
screen.blit(hint_surface, hint_rect)
|
|
1026
|
+
except pygame.error as e:
|
|
1027
|
+
print(f"Error rendering endurance timer: {e}")
|
|
745
1028
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1029
|
+
|
|
1030
|
+
def _draw_time_accel_indicator(
|
|
1031
|
+
screen: surface.Surface,
|
|
1032
|
+
assets: RenderAssets,
|
|
1033
|
+
*,
|
|
1034
|
+
stage: Stage | None,
|
|
1035
|
+
state: Any,
|
|
1036
|
+
) -> None:
|
|
1037
|
+
if stage and stage.endurance_stage:
|
|
1038
|
+
return
|
|
1039
|
+
try:
|
|
1040
|
+
font_settings = get_font_settings()
|
|
1041
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(12))
|
|
1042
|
+
if state.time_accel_active:
|
|
1043
|
+
text = tr("hud.time_accel")
|
|
1044
|
+
color = YELLOW
|
|
1045
|
+
else:
|
|
1046
|
+
text = tr("hud.time_accel_hint")
|
|
1047
|
+
color = LIGHT_GRAY
|
|
1048
|
+
text_surface = font.render(text, False, color)
|
|
1049
|
+
bottom_margin = assets.status_bar_height + 6
|
|
1050
|
+
text_rect = text_surface.get_rect(
|
|
1051
|
+
right=assets.screen_width - 12,
|
|
1052
|
+
bottom=assets.screen_height - bottom_margin,
|
|
1053
|
+
)
|
|
1054
|
+
screen.blit(text_surface, text_rect)
|
|
1055
|
+
except pygame.error as e:
|
|
1056
|
+
print(f"Error rendering acceleration indicator: {e}")
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _draw_survivor_messages(
|
|
1060
|
+
screen: surface.Surface,
|
|
1061
|
+
assets: RenderAssets,
|
|
1062
|
+
survivor_messages: list[dict[str, Any]],
|
|
1063
|
+
) -> None:
|
|
1064
|
+
if not survivor_messages:
|
|
1065
|
+
return
|
|
1066
|
+
try:
|
|
1067
|
+
font_settings = get_font_settings()
|
|
1068
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(14))
|
|
1069
|
+
base_y = assets.screen_height // 2 - 70
|
|
1070
|
+
for idx, message in enumerate(survivor_messages[:3]):
|
|
1071
|
+
text = message.get("text", "")
|
|
1072
|
+
if not text:
|
|
1073
|
+
continue
|
|
1074
|
+
msg_surface = font.render(text, False, ORANGE)
|
|
1075
|
+
msg_rect = msg_surface.get_rect(
|
|
1076
|
+
center=(assets.screen_width // 2, base_y + idx * 18)
|
|
763
1077
|
)
|
|
764
|
-
screen.blit(
|
|
765
|
-
|
|
766
|
-
|
|
1078
|
+
screen.blit(msg_surface, msg_rect)
|
|
1079
|
+
except pygame.error as e:
|
|
1080
|
+
print(f"Error rendering survivor message: {e}")
|
|
767
1081
|
|
|
1082
|
+
|
|
1083
|
+
def _build_objective_lines(
|
|
1084
|
+
*,
|
|
1085
|
+
stage: Stage | None,
|
|
1086
|
+
state: Any,
|
|
1087
|
+
player: Player,
|
|
1088
|
+
active_car: Car | None,
|
|
1089
|
+
has_fuel: bool,
|
|
1090
|
+
buddy_onboard: int,
|
|
1091
|
+
buddy_required: int,
|
|
1092
|
+
survivors_onboard: int,
|
|
1093
|
+
) -> list[str]:
|
|
768
1094
|
objective_lines: list[str] = []
|
|
769
|
-
if stage and stage.
|
|
1095
|
+
if stage and stage.endurance_stage:
|
|
770
1096
|
if state.dawn_ready:
|
|
771
1097
|
objective_lines.append(tr("objectives.get_outside"))
|
|
772
1098
|
else:
|
|
@@ -807,29 +1133,139 @@ def draw(
|
|
|
807
1133
|
objective_lines.append(
|
|
808
1134
|
tr("objectives.survivors_onboard", count=survivors_onboard, limit=limit)
|
|
809
1135
|
)
|
|
1136
|
+
return objective_lines
|
|
810
1137
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1138
|
+
|
|
1139
|
+
def draw(
|
|
1140
|
+
assets: RenderAssets,
|
|
1141
|
+
screen: surface.Surface,
|
|
1142
|
+
game_data: GameData,
|
|
1143
|
+
*,
|
|
1144
|
+
config: dict[str, Any],
|
|
1145
|
+
hint_target: tuple[int, int] | None = None,
|
|
1146
|
+
hint_color: tuple[int, int, int] | None = None,
|
|
1147
|
+
do_flip: bool = True,
|
|
1148
|
+
present_fn: Callable[[surface.Surface], None] | None = None,
|
|
1149
|
+
) -> None:
|
|
1150
|
+
hint_color = hint_color or YELLOW
|
|
1151
|
+
state = game_data.state
|
|
1152
|
+
player = game_data.player
|
|
1153
|
+
if player is None:
|
|
1154
|
+
raise ValueError("draw requires an active player on game_data")
|
|
1155
|
+
|
|
1156
|
+
camera = game_data.camera
|
|
1157
|
+
stage = game_data.stage
|
|
1158
|
+
outer_rect = game_data.layout.outer_rect
|
|
1159
|
+
outside_rects = game_data.layout.outside_rects or []
|
|
1160
|
+
all_sprites = game_data.groups.all_sprites
|
|
1161
|
+
fog_surfaces = game_data.fog
|
|
1162
|
+
footprints = state.footprints
|
|
1163
|
+
has_fuel = state.has_fuel
|
|
1164
|
+
flashlight_count = state.flashlight_count
|
|
1165
|
+
elapsed_play_ms = state.elapsed_play_ms
|
|
1166
|
+
fuel_message_until = state.fuel_message_until
|
|
1167
|
+
buddy_onboard = state.buddy_onboard
|
|
1168
|
+
buddy_required = stage.buddy_required_count if stage else 0
|
|
1169
|
+
survivors_onboard = state.survivors_onboard
|
|
1170
|
+
survivor_messages = list(state.survivor_messages)
|
|
1171
|
+
zombie_group = game_data.groups.zombie_group
|
|
1172
|
+
active_car = game_data.car if game_data.car and game_data.car.alive() else None
|
|
1173
|
+
if player.in_car and game_data.car and game_data.car.alive():
|
|
1174
|
+
fov_target = game_data.car
|
|
831
1175
|
else:
|
|
832
|
-
|
|
1176
|
+
fov_target = player
|
|
1177
|
+
|
|
1178
|
+
palette = get_environment_palette(state.ambient_palette_key)
|
|
1179
|
+
screen.fill(palette.outside)
|
|
1180
|
+
|
|
1181
|
+
_draw_play_area(
|
|
1182
|
+
screen,
|
|
1183
|
+
camera,
|
|
1184
|
+
assets,
|
|
1185
|
+
palette,
|
|
1186
|
+
outer_rect,
|
|
1187
|
+
outside_rects,
|
|
1188
|
+
game_data.layout.fall_spawn_cells,
|
|
1189
|
+
)
|
|
1190
|
+
_draw_wall_shadows(
|
|
1191
|
+
screen,
|
|
1192
|
+
camera,
|
|
1193
|
+
wall_cells=game_data.layout.wall_cells,
|
|
1194
|
+
wall_group=game_data.groups.wall_group,
|
|
1195
|
+
outer_wall_cells=game_data.layout.outer_wall_cells,
|
|
1196
|
+
cell_size=game_data.cell_size,
|
|
1197
|
+
light_source_pos=(
|
|
1198
|
+
None if (stage and stage.endurance_stage and state.dawn_ready) else fov_target.rect.center
|
|
1199
|
+
)
|
|
1200
|
+
if fov_target
|
|
1201
|
+
else None,
|
|
1202
|
+
)
|
|
1203
|
+
_draw_footprints(
|
|
1204
|
+
screen,
|
|
1205
|
+
camera,
|
|
1206
|
+
assets,
|
|
1207
|
+
footprints,
|
|
1208
|
+
config=config,
|
|
1209
|
+
)
|
|
1210
|
+
_draw_entities(
|
|
1211
|
+
screen,
|
|
1212
|
+
camera,
|
|
1213
|
+
all_sprites,
|
|
1214
|
+
player,
|
|
1215
|
+
has_fuel=has_fuel,
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
_draw_falling_fx(
|
|
1219
|
+
screen,
|
|
1220
|
+
camera,
|
|
1221
|
+
state.falling_zombies,
|
|
1222
|
+
state.flashlight_count,
|
|
1223
|
+
state.dust_rings,
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
_draw_hint_indicator(
|
|
1227
|
+
screen,
|
|
1228
|
+
camera,
|
|
1229
|
+
assets,
|
|
1230
|
+
player,
|
|
1231
|
+
hint_target,
|
|
1232
|
+
hint_color=hint_color,
|
|
1233
|
+
stage=stage,
|
|
1234
|
+
flashlight_count=flashlight_count,
|
|
1235
|
+
)
|
|
1236
|
+
_draw_fog_of_war(
|
|
1237
|
+
screen,
|
|
1238
|
+
camera,
|
|
1239
|
+
assets,
|
|
1240
|
+
fog_surfaces,
|
|
1241
|
+
fov_target,
|
|
1242
|
+
stage=stage,
|
|
1243
|
+
flashlight_count=flashlight_count,
|
|
1244
|
+
dawn_ready=state.dawn_ready,
|
|
1245
|
+
)
|
|
1246
|
+
_draw_need_fuel_message(
|
|
1247
|
+
screen,
|
|
1248
|
+
assets,
|
|
1249
|
+
has_fuel=has_fuel,
|
|
1250
|
+
fuel_message_until=fuel_message_until,
|
|
1251
|
+
elapsed_play_ms=elapsed_play_ms,
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
objective_lines = _build_objective_lines(
|
|
1255
|
+
stage=stage,
|
|
1256
|
+
state=state,
|
|
1257
|
+
player=player,
|
|
1258
|
+
active_car=active_car,
|
|
1259
|
+
has_fuel=has_fuel,
|
|
1260
|
+
buddy_onboard=buddy_onboard,
|
|
1261
|
+
buddy_required=buddy_required,
|
|
1262
|
+
survivors_onboard=survivors_onboard,
|
|
1263
|
+
)
|
|
1264
|
+
if objective_lines:
|
|
1265
|
+
_draw_objective(objective_lines, screen=screen)
|
|
1266
|
+
_draw_survivor_messages(screen, assets, survivor_messages)
|
|
1267
|
+
_draw_endurance_timer(screen, assets, stage=stage, state=state)
|
|
1268
|
+
_draw_time_accel_indicator(screen, assets, stage=stage, state=state)
|
|
833
1269
|
_draw_status_bar(
|
|
834
1270
|
screen,
|
|
835
1271
|
assets,
|
|
@@ -838,6 +1274,7 @@ def draw(
|
|
|
838
1274
|
seed=state.seed,
|
|
839
1275
|
debug_mode=state.debug_mode,
|
|
840
1276
|
zombie_group=zombie_group,
|
|
1277
|
+
falling_spawn_carry=state.falling_spawn_carry,
|
|
841
1278
|
)
|
|
842
1279
|
if do_flip:
|
|
843
1280
|
if present_fn:
|