zombie-escape 1.10.0__py3-none-any.whl → 1.12.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 +14 -12
- zombie_escape/entities.py +13 -0
- zombie_escape/entities_constants.py +9 -3
- zombie_escape/gameplay/__init__.py +2 -0
- zombie_escape/gameplay/interactions.py +19 -0
- zombie_escape/gameplay/layout.py +22 -1
- zombie_escape/gameplay/movement.py +15 -1
- zombie_escape/gameplay/spawn.py +95 -4
- zombie_escape/gameplay/state.py +2 -0
- zombie_escape/gameplay_constants.py +8 -0
- zombie_escape/level_blueprints.py +54 -22
- zombie_escape/locales/ui.en.json +9 -1
- zombie_escape/locales/ui.ja.json +8 -0
- zombie_escape/models.py +6 -1
- zombie_escape/render.py +420 -69
- zombie_escape/render_assets.py +104 -52
- zombie_escape/render_constants.py +28 -12
- zombie_escape/screens/game_over.py +1 -1
- zombie_escape/screens/gameplay.py +27 -1
- zombie_escape/stage_constants.py +31 -14
- {zombie_escape-1.10.0.dist-info → zombie_escape-1.12.0.dist-info}/METADATA +5 -2
- zombie_escape-1.12.0.dist-info/RECORD +47 -0
- zombie_escape-1.10.0.dist-info/RECORD +0 -47
- {zombie_escape-1.10.0.dist-info → zombie_escape-1.12.0.dist-info}/WHEEL +0 -0
- {zombie_escape-1.10.0.dist-info → zombie_escape-1.12.0.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.10.0.dist-info → zombie_escape-1.12.0.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/render.py
CHANGED
|
@@ -23,11 +23,21 @@ from .entities import (
|
|
|
23
23
|
Flashlight,
|
|
24
24
|
FuelCan,
|
|
25
25
|
Player,
|
|
26
|
+
Shoes,
|
|
26
27
|
SteelBeam,
|
|
27
28
|
Survivor,
|
|
28
29
|
Wall,
|
|
30
|
+
Zombie,
|
|
31
|
+
)
|
|
32
|
+
from .entities_constants import (
|
|
33
|
+
FLASHLIGHT_HEIGHT,
|
|
34
|
+
FLASHLIGHT_WIDTH,
|
|
35
|
+
FUEL_CAN_HEIGHT,
|
|
36
|
+
FUEL_CAN_WIDTH,
|
|
37
|
+
SHOES_HEIGHT,
|
|
38
|
+
SHOES_WIDTH,
|
|
39
|
+
ZOMBIE_RADIUS,
|
|
29
40
|
)
|
|
30
|
-
from .entities_constants import ZOMBIE_RADIUS
|
|
31
41
|
from .font_utils import load_font
|
|
32
42
|
from .gameplay_constants import (
|
|
33
43
|
DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
@@ -36,15 +46,37 @@ from .gameplay_constants import (
|
|
|
36
46
|
from .localization import get_font_settings
|
|
37
47
|
from .localization import translate as tr
|
|
38
48
|
from .models import DustRing, FallingZombie, Footprint, GameData, Stage
|
|
39
|
-
from .render_assets import
|
|
49
|
+
from .render_assets import (
|
|
50
|
+
RenderAssets,
|
|
51
|
+
build_flashlight_surface,
|
|
52
|
+
build_fuel_can_surface,
|
|
53
|
+
build_shoes_surface,
|
|
54
|
+
resolve_steel_beam_colors,
|
|
55
|
+
resolve_wall_colors,
|
|
56
|
+
)
|
|
40
57
|
from .render_constants import (
|
|
58
|
+
ENTITY_SHADOW_ALPHA,
|
|
59
|
+
ENTITY_SHADOW_EDGE_SOFTNESS,
|
|
60
|
+
ENTITY_SHADOW_RADIUS_MULT,
|
|
41
61
|
FALLING_DUST_COLOR,
|
|
42
62
|
FALLING_WHIRLWIND_COLOR,
|
|
43
63
|
FALLING_ZOMBIE_COLOR,
|
|
64
|
+
FLASHLIGHT_FOG_SCALE_ONE,
|
|
65
|
+
FLASHLIGHT_FOG_SCALE_TWO,
|
|
66
|
+
PLAYER_SHADOW_ALPHA_MULT,
|
|
67
|
+
PLAYER_SHADOW_RADIUS_MULT,
|
|
68
|
+
SHADOW_MIN_RATIO,
|
|
69
|
+
SHADOW_OVERSAMPLE,
|
|
70
|
+
SHADOW_RADIUS_RATIO,
|
|
71
|
+
SHADOW_STEPS,
|
|
44
72
|
)
|
|
45
73
|
|
|
46
74
|
_SHADOW_TILE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
|
|
47
75
|
_SHADOW_LAYER_CACHE: dict[tuple[int, int], surface.Surface] = {}
|
|
76
|
+
_SHADOW_CIRCLE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
|
|
77
|
+
_HUD_ICON_CACHE: dict[str, surface.Surface] = {}
|
|
78
|
+
|
|
79
|
+
HUD_ICON_SIZE = 12
|
|
48
80
|
|
|
49
81
|
|
|
50
82
|
def _get_shadow_tile_surface(
|
|
@@ -57,34 +89,45 @@ def _get_shadow_tile_surface(
|
|
|
57
89
|
if key in _SHADOW_TILE_CACHE:
|
|
58
90
|
return _SHADOW_TILE_CACHE[key]
|
|
59
91
|
size = key[0]
|
|
60
|
-
|
|
92
|
+
oversample = SHADOW_OVERSAMPLE
|
|
93
|
+
render_size = size * oversample
|
|
94
|
+
render_surf = pygame.Surface((render_size, render_size), pygame.SRCALPHA)
|
|
61
95
|
base_alpha = key[1]
|
|
62
96
|
if edge_softness <= 0:
|
|
63
|
-
|
|
97
|
+
render_surf.fill((0, 0, 0, base_alpha))
|
|
98
|
+
if oversample > 1:
|
|
99
|
+
surf = pygame.transform.smoothscale(render_surf, (size, size))
|
|
100
|
+
else:
|
|
101
|
+
surf = render_surf
|
|
64
102
|
_SHADOW_TILE_CACHE[key] = surf
|
|
65
103
|
return surf
|
|
66
104
|
|
|
67
105
|
softness = max(0.0, min(1.0, edge_softness))
|
|
68
|
-
fade_band = max(1, int(
|
|
69
|
-
base_radius = max(1, int(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
106
|
+
fade_band = max(1, int(render_size * softness))
|
|
107
|
+
base_radius = max(1, int(render_size * SHADOW_RADIUS_RATIO))
|
|
108
|
+
|
|
109
|
+
render_surf.fill((0, 0, 0, 0))
|
|
110
|
+
steps = SHADOW_STEPS
|
|
111
|
+
min_ratio = SHADOW_MIN_RATIO
|
|
112
|
+
for idx in range(steps):
|
|
113
|
+
t = idx / (steps - 1) if steps > 1 else 1.0
|
|
114
|
+
inset = int(fade_band * t)
|
|
115
|
+
rect_size = render_size - inset * 2
|
|
73
116
|
if rect_size <= 0:
|
|
74
|
-
|
|
117
|
+
continue
|
|
75
118
|
radius = max(0, base_radius - inset)
|
|
119
|
+
layer_alpha = int(base_alpha * (min_ratio + (1.0 - min_ratio) * t))
|
|
76
120
|
pygame.draw.rect(
|
|
77
|
-
|
|
121
|
+
render_surf,
|
|
78
122
|
(0, 0, 0, layer_alpha),
|
|
79
123
|
pygame.Rect(inset, inset, rect_size, rect_size),
|
|
80
124
|
border_radius=radius,
|
|
81
125
|
)
|
|
82
126
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
draw_layer(fade_band, base_alpha)
|
|
127
|
+
if oversample > 1:
|
|
128
|
+
surf = pygame.transform.smoothscale(render_surf, (size, size))
|
|
129
|
+
else:
|
|
130
|
+
surf = render_surf
|
|
88
131
|
_SHADOW_TILE_CACHE[key] = surf
|
|
89
132
|
return surf
|
|
90
133
|
|
|
@@ -98,6 +141,98 @@ def _get_shadow_layer(size: tuple[int, int]) -> surface.Surface:
|
|
|
98
141
|
return layer
|
|
99
142
|
|
|
100
143
|
|
|
144
|
+
def _scale_icon_to_box(icon: surface.Surface, size: int) -> surface.Surface:
|
|
145
|
+
target_size = max(1, size)
|
|
146
|
+
width = max(1, icon.get_width())
|
|
147
|
+
height = max(1, icon.get_height())
|
|
148
|
+
scale = min(target_size / width, target_size / height)
|
|
149
|
+
target_width = max(1, int(width * scale))
|
|
150
|
+
target_height = max(1, int(height * scale))
|
|
151
|
+
scaled = pygame.transform.smoothscale(icon, (target_width, target_height))
|
|
152
|
+
boxed = pygame.Surface((target_size, target_size), pygame.SRCALPHA)
|
|
153
|
+
boxed.blit(
|
|
154
|
+
scaled,
|
|
155
|
+
(
|
|
156
|
+
(target_size - target_width) // 2,
|
|
157
|
+
(target_size - target_height) // 2,
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
return boxed
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_hud_icon(kind: str) -> surface.Surface:
|
|
164
|
+
cached = _HUD_ICON_CACHE.get(kind)
|
|
165
|
+
if cached is not None:
|
|
166
|
+
return cached
|
|
167
|
+
if kind == "fuel":
|
|
168
|
+
icon = build_fuel_can_surface(FUEL_CAN_WIDTH, FUEL_CAN_HEIGHT)
|
|
169
|
+
elif kind == "flashlight":
|
|
170
|
+
icon = build_flashlight_surface(FLASHLIGHT_WIDTH, FLASHLIGHT_HEIGHT)
|
|
171
|
+
elif kind == "shoes":
|
|
172
|
+
icon = build_shoes_surface(SHOES_WIDTH, SHOES_HEIGHT)
|
|
173
|
+
else:
|
|
174
|
+
icon = pygame.Surface((1, 1), pygame.SRCALPHA)
|
|
175
|
+
icon = _scale_icon_to_box(icon, HUD_ICON_SIZE)
|
|
176
|
+
_HUD_ICON_CACHE[kind] = icon
|
|
177
|
+
return icon
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _get_shadow_circle_surface(
|
|
181
|
+
radius: int,
|
|
182
|
+
alpha: int,
|
|
183
|
+
*,
|
|
184
|
+
edge_softness: float = 0.12,
|
|
185
|
+
) -> surface.Surface:
|
|
186
|
+
key = (max(1, radius), max(0, min(255, alpha)), edge_softness)
|
|
187
|
+
if key in _SHADOW_CIRCLE_CACHE:
|
|
188
|
+
return _SHADOW_CIRCLE_CACHE[key]
|
|
189
|
+
radius = key[0]
|
|
190
|
+
oversample = SHADOW_OVERSAMPLE
|
|
191
|
+
render_radius = radius * oversample
|
|
192
|
+
render_size = render_radius * 2
|
|
193
|
+
render_surf = pygame.Surface((render_size, render_size), pygame.SRCALPHA)
|
|
194
|
+
base_alpha = key[1]
|
|
195
|
+
if edge_softness <= 0:
|
|
196
|
+
pygame.draw.circle(
|
|
197
|
+
render_surf,
|
|
198
|
+
(0, 0, 0, base_alpha),
|
|
199
|
+
(render_radius, render_radius),
|
|
200
|
+
render_radius,
|
|
201
|
+
)
|
|
202
|
+
if oversample > 1:
|
|
203
|
+
surf = pygame.transform.smoothscale(render_surf, (radius * 2, radius * 2))
|
|
204
|
+
else:
|
|
205
|
+
surf = render_surf
|
|
206
|
+
_SHADOW_CIRCLE_CACHE[key] = surf
|
|
207
|
+
return surf
|
|
208
|
+
|
|
209
|
+
softness = max(0.0, min(1.0, edge_softness))
|
|
210
|
+
fade_band = max(1, int(render_radius * softness))
|
|
211
|
+
steps = SHADOW_STEPS
|
|
212
|
+
min_ratio = SHADOW_MIN_RATIO
|
|
213
|
+
render_surf.fill((0, 0, 0, 0))
|
|
214
|
+
for idx in range(steps):
|
|
215
|
+
t = idx / (steps - 1) if steps > 1 else 1.0
|
|
216
|
+
inset = int(fade_band * t)
|
|
217
|
+
circle_radius = render_radius - inset
|
|
218
|
+
if circle_radius <= 0:
|
|
219
|
+
continue
|
|
220
|
+
layer_alpha = int(base_alpha * (min_ratio + (1.0 - min_ratio) * t))
|
|
221
|
+
pygame.draw.circle(
|
|
222
|
+
render_surf,
|
|
223
|
+
(0, 0, 0, layer_alpha),
|
|
224
|
+
(render_radius, render_radius),
|
|
225
|
+
circle_radius,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if oversample > 1:
|
|
229
|
+
surf = pygame.transform.smoothscale(render_surf, (radius * 2, radius * 2))
|
|
230
|
+
else:
|
|
231
|
+
surf = render_surf
|
|
232
|
+
_SHADOW_CIRCLE_CACHE[key] = surf
|
|
233
|
+
return surf
|
|
234
|
+
|
|
235
|
+
|
|
101
236
|
def show_message(
|
|
102
237
|
screen: surface.Surface,
|
|
103
238
|
text: str,
|
|
@@ -134,7 +269,7 @@ def draw_level_overview(
|
|
|
134
269
|
*,
|
|
135
270
|
fuel: FuelCan | None = None,
|
|
136
271
|
flashlights: list[Flashlight] | None = None,
|
|
137
|
-
|
|
272
|
+
shoes: list[Shoes] | None = None,
|
|
138
273
|
buddies: list[Survivor] | None = None,
|
|
139
274
|
survivors: list[Survivor] | None = None,
|
|
140
275
|
palette_key: str | None = None,
|
|
@@ -180,11 +315,15 @@ def draw_level_overview(
|
|
|
180
315
|
for flashlight in flashlights:
|
|
181
316
|
if flashlight.alive():
|
|
182
317
|
pygame.draw.rect(
|
|
183
|
-
surface,
|
|
318
|
+
surface, YELLOW, flashlight.rect, border_radius=2
|
|
184
319
|
)
|
|
185
320
|
pygame.draw.rect(
|
|
186
321
|
surface, BLACK, flashlight.rect, width=2, border_radius=2
|
|
187
322
|
)
|
|
323
|
+
if shoes:
|
|
324
|
+
for item in shoes:
|
|
325
|
+
if item.alive():
|
|
326
|
+
surface.blit(item.image, item.rect)
|
|
188
327
|
if survivors:
|
|
189
328
|
for survivor in survivors:
|
|
190
329
|
if survivor.alive():
|
|
@@ -218,7 +357,6 @@ def draw_level_overview(
|
|
|
218
357
|
|
|
219
358
|
def _get_fog_scale(
|
|
220
359
|
assets: RenderAssets,
|
|
221
|
-
stage: Stage | None,
|
|
222
360
|
flashlight_count: int,
|
|
223
361
|
) -> float:
|
|
224
362
|
"""Return current fog scale factoring in flashlight bonus."""
|
|
@@ -226,8 +364,9 @@ def _get_fog_scale(
|
|
|
226
364
|
flashlight_count = max(0, int(flashlight_count))
|
|
227
365
|
if flashlight_count <= 0:
|
|
228
366
|
return scale
|
|
229
|
-
|
|
230
|
-
|
|
367
|
+
if flashlight_count == 1:
|
|
368
|
+
return max(scale, FLASHLIGHT_FOG_SCALE_ONE)
|
|
369
|
+
return max(scale, FLASHLIGHT_FOG_SCALE_TWO)
|
|
231
370
|
|
|
232
371
|
|
|
233
372
|
def _max_flashlight_pickups() -> int:
|
|
@@ -247,7 +386,7 @@ class FogProfile(Enum):
|
|
|
247
386
|
|
|
248
387
|
def _scale(self, assets: RenderAssets, stage: Stage | None) -> float:
|
|
249
388
|
count = max(0, min(self.flashlight_count, _max_flashlight_pickups()))
|
|
250
|
-
return _get_fog_scale(assets,
|
|
389
|
+
return _get_fog_scale(assets, count)
|
|
251
390
|
|
|
252
391
|
@staticmethod
|
|
253
392
|
def _from_flashlight_count(count: int) -> "FogProfile":
|
|
@@ -280,19 +419,21 @@ def _get_hatch_pattern(
|
|
|
280
419
|
fog_data: dict[str, Any],
|
|
281
420
|
thickness: int,
|
|
282
421
|
*,
|
|
283
|
-
pixel_scale: int = 1,
|
|
284
422
|
color: tuple[int, int, int, int] | None = None,
|
|
285
423
|
) -> surface.Surface:
|
|
286
|
-
"""Return cached
|
|
424
|
+
"""Return cached dot hatch tile surface (Bayer-ordered, optionally chunky)."""
|
|
287
425
|
cache = fog_data.setdefault("hatch_patterns", {})
|
|
288
|
-
|
|
289
|
-
key = (thickness, pixel_scale, color)
|
|
426
|
+
key = (thickness, color)
|
|
290
427
|
if key in cache:
|
|
291
428
|
return cache[key]
|
|
292
429
|
|
|
293
|
-
spacing =
|
|
430
|
+
spacing = 4
|
|
431
|
+
oversample = 3
|
|
294
432
|
density = max(1, min(thickness, 16))
|
|
295
|
-
|
|
433
|
+
pattern_size = spacing * 8
|
|
434
|
+
hi_spacing = spacing * oversample
|
|
435
|
+
hi_pattern_size = pattern_size * oversample
|
|
436
|
+
pattern = pygame.Surface((hi_pattern_size, hi_pattern_size), pygame.SRCALPHA)
|
|
296
437
|
|
|
297
438
|
# 8x8 Bayer matrix values 0..63 for ordered dithering
|
|
298
439
|
bayer = [
|
|
@@ -306,14 +447,20 @@ def _get_hatch_pattern(
|
|
|
306
447
|
[63, 31, 55, 23, 61, 29, 53, 21],
|
|
307
448
|
]
|
|
308
449
|
threshold = int((density / 16) * 64)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
450
|
+
dot_radius = max(
|
|
451
|
+
1,
|
|
452
|
+
min(hi_spacing, int(math.ceil((density / 16) * hi_spacing))),
|
|
453
|
+
)
|
|
454
|
+
dot_color = color or (0, 0, 0, 255)
|
|
455
|
+
for grid_y in range(8):
|
|
456
|
+
for grid_x in range(8):
|
|
457
|
+
if bayer[grid_y][grid_x] < threshold:
|
|
458
|
+
cx = grid_x * hi_spacing + hi_spacing // 2
|
|
459
|
+
cy = grid_y * hi_spacing + hi_spacing // 2
|
|
460
|
+
pygame.draw.circle(pattern, dot_color, (cx, cy), dot_radius)
|
|
313
461
|
|
|
314
|
-
if
|
|
315
|
-
|
|
316
|
-
pattern = pygame.transform.scale(pattern, scaled_size)
|
|
462
|
+
if oversample > 1:
|
|
463
|
+
pattern = pygame.transform.smoothscale(pattern, (pattern_size, pattern_size))
|
|
317
464
|
|
|
318
465
|
cache[key] = pattern
|
|
319
466
|
return pattern
|
|
@@ -354,7 +501,6 @@ def _get_fog_overlay_surfaces(
|
|
|
354
501
|
pattern = _get_hatch_pattern(
|
|
355
502
|
fog_data,
|
|
356
503
|
ring.thickness,
|
|
357
|
-
pixel_scale=assets.fog_hatch_pixel_scale,
|
|
358
504
|
color=base_color,
|
|
359
505
|
)
|
|
360
506
|
p_w, p_h = pattern.get_size()
|
|
@@ -390,7 +536,7 @@ def _build_flashlight_fade_surface(
|
|
|
390
536
|
*,
|
|
391
537
|
start_ratio: float = 0.2,
|
|
392
538
|
max_alpha: int = 220,
|
|
393
|
-
outer_extension: int =
|
|
539
|
+
outer_extension: int = 30,
|
|
394
540
|
) -> surface.Surface:
|
|
395
541
|
"""Return a radial gradient so flashlight edges softly darken again."""
|
|
396
542
|
|
|
@@ -579,6 +725,7 @@ def _draw_status_bar(
|
|
|
579
725
|
debug_mode: bool = False,
|
|
580
726
|
zombie_group: sprite.Group | None = None,
|
|
581
727
|
falling_spawn_carry: int | None = None,
|
|
728
|
+
fps: float | None = None,
|
|
582
729
|
) -> None:
|
|
583
730
|
"""Render a compact status bar with current config flags and stage info."""
|
|
584
731
|
bar_rect = pygame.Rect(
|
|
@@ -597,7 +744,11 @@ def _draw_status_bar(
|
|
|
597
744
|
steel_on = config.get("steel_beams", {}).get("enabled", False)
|
|
598
745
|
if stage:
|
|
599
746
|
# Keep the label compact for the status bar
|
|
600
|
-
|
|
747
|
+
if stage.id.startswith("stage"):
|
|
748
|
+
stage_suffix = stage.id.removeprefix("stage")
|
|
749
|
+
stage_label = f"#{stage_suffix}" if stage_suffix else stage.id
|
|
750
|
+
else:
|
|
751
|
+
stage_label = stage.id
|
|
601
752
|
else:
|
|
602
753
|
stage_label = "#1"
|
|
603
754
|
|
|
@@ -620,6 +771,8 @@ def _draw_status_bar(
|
|
|
620
771
|
parts.append(f"Z:{total} N:{normal} T:{tracker} W:{wall}")
|
|
621
772
|
if falling_spawn_carry is not None:
|
|
622
773
|
parts.append(f"C:{max(0, falling_spawn_carry)}")
|
|
774
|
+
if fps is not None:
|
|
775
|
+
parts.append(f"FPS:{fps:.1f}")
|
|
623
776
|
|
|
624
777
|
status_text = " | ".join(parts)
|
|
625
778
|
color = LIGHT_GRAY
|
|
@@ -637,6 +790,14 @@ def _draw_status_bar(
|
|
|
637
790
|
right=bar_rect.right - 12, centery=bar_rect.centery
|
|
638
791
|
)
|
|
639
792
|
screen.blit(seed_surface, seed_rect)
|
|
793
|
+
if debug_mode and fps is not None:
|
|
794
|
+
fps_text = f"FPS:{fps:.1f}"
|
|
795
|
+
fps_surface = font.render(fps_text, False, LIGHT_GRAY)
|
|
796
|
+
fps_rect = fps_surface.get_rect(
|
|
797
|
+
left=12,
|
|
798
|
+
bottom=max(2, bar_rect.top - 4),
|
|
799
|
+
)
|
|
800
|
+
screen.blit(fps_surface, fps_rect)
|
|
640
801
|
except pygame.error as e:
|
|
641
802
|
print(f"Error rendering status bar: {e}")
|
|
642
803
|
|
|
@@ -723,8 +884,18 @@ def _draw_play_area(
|
|
|
723
884
|
return xs, ys, xe, ye, outside_cells
|
|
724
885
|
|
|
725
886
|
|
|
887
|
+
def abs_clip(value: float, min_v: float, max_v: float) -> float:
|
|
888
|
+
value_sign = 1.0 if value >= 0.0 else -1.0
|
|
889
|
+
value = abs(value)
|
|
890
|
+
if value < min_v:
|
|
891
|
+
value = min_v
|
|
892
|
+
elif value > max_v:
|
|
893
|
+
value = max_v
|
|
894
|
+
return value_sign * value
|
|
895
|
+
|
|
896
|
+
|
|
726
897
|
def _draw_wall_shadows(
|
|
727
|
-
|
|
898
|
+
shadow_layer: surface.Surface,
|
|
728
899
|
camera: Camera,
|
|
729
900
|
*,
|
|
730
901
|
wall_cells: set[tuple[int, int]],
|
|
@@ -732,10 +903,10 @@ def _draw_wall_shadows(
|
|
|
732
903
|
outer_wall_cells: set[tuple[int, int]] | None,
|
|
733
904
|
cell_size: int,
|
|
734
905
|
light_source_pos: tuple[int, int] | None,
|
|
735
|
-
alpha: int =
|
|
736
|
-
) ->
|
|
906
|
+
alpha: int = 68,
|
|
907
|
+
) -> bool:
|
|
737
908
|
if not wall_cells or cell_size <= 0 or light_source_pos is None:
|
|
738
|
-
return
|
|
909
|
+
return False
|
|
739
910
|
inner_wall_cells = set(wall_cells)
|
|
740
911
|
if outer_wall_cells:
|
|
741
912
|
inner_wall_cells.difference_update(outer_wall_cells)
|
|
@@ -746,41 +917,153 @@ def _draw_wall_shadows(
|
|
|
746
917
|
cell_y = int(wall.rect.centery // cell_size)
|
|
747
918
|
inner_wall_cells.add((cell_x, cell_y))
|
|
748
919
|
if not inner_wall_cells:
|
|
749
|
-
return
|
|
920
|
+
return False
|
|
750
921
|
base_shadow_size = max(cell_size + 2, int(cell_size * 1.35))
|
|
751
922
|
shadow_size = max(1, int(base_shadow_size * 1.5))
|
|
752
|
-
shadow_surface = _get_shadow_tile_surface(
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
923
|
+
shadow_surface = _get_shadow_tile_surface(
|
|
924
|
+
shadow_size,
|
|
925
|
+
alpha,
|
|
926
|
+
edge_softness=0.12,
|
|
927
|
+
)
|
|
928
|
+
screen_rect = shadow_layer.get_rect()
|
|
756
929
|
px, py = light_source_pos
|
|
757
|
-
offset = max(2, int(cell_size * 0.3 * (shadow_size / cell_size) * 1.2))
|
|
758
930
|
drew = False
|
|
931
|
+
clip_max = shadow_size * 0.25
|
|
759
932
|
for cell_x, cell_y in inner_wall_cells:
|
|
760
933
|
world_x = cell_x * cell_size
|
|
761
934
|
world_y = cell_y * cell_size
|
|
935
|
+
wall_rect = pygame.Rect(world_x, world_y, cell_size, cell_size)
|
|
936
|
+
wall_screen_rect = camera.apply_rect(wall_rect)
|
|
937
|
+
if not wall_screen_rect.colliderect(screen_rect):
|
|
938
|
+
continue
|
|
762
939
|
center_x = world_x + cell_size / 2
|
|
763
940
|
center_y = world_y + cell_size / 2
|
|
764
|
-
dx = center_x - px
|
|
765
|
-
dy = center_y - py
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
continue
|
|
769
|
-
nx = dx / dist
|
|
770
|
-
ny = dy / dist
|
|
941
|
+
dx = (center_x - px) * 0.5
|
|
942
|
+
dy = (center_y - py) * 0.5
|
|
943
|
+
dx = int(abs_clip(dx, 0, clip_max))
|
|
944
|
+
dy = int(abs_clip(dy, 0, clip_max))
|
|
771
945
|
shadow_rect = pygame.Rect(0, 0, shadow_size, shadow_size)
|
|
772
946
|
shadow_rect.center = (
|
|
773
|
-
int(center_x +
|
|
774
|
-
int(center_y +
|
|
947
|
+
int(center_x + dx),
|
|
948
|
+
int(center_y + dy),
|
|
775
949
|
)
|
|
776
950
|
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
777
951
|
if not shadow_screen_rect.colliderect(screen_rect):
|
|
778
952
|
continue
|
|
779
|
-
shadow_layer.blit(
|
|
953
|
+
shadow_layer.blit(
|
|
954
|
+
shadow_surface,
|
|
955
|
+
shadow_screen_rect.topleft,
|
|
956
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
957
|
+
)
|
|
780
958
|
drew = True
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
959
|
+
return drew
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _draw_entity_shadows(
|
|
963
|
+
shadow_layer: surface.Surface,
|
|
964
|
+
camera: Camera,
|
|
965
|
+
all_sprites: sprite.LayeredUpdates,
|
|
966
|
+
*,
|
|
967
|
+
light_source_pos: tuple[int, int] | None,
|
|
968
|
+
exclude_car: Car | None,
|
|
969
|
+
shadow_radius: int = int(ZOMBIE_RADIUS * ENTITY_SHADOW_RADIUS_MULT),
|
|
970
|
+
alpha: int = ENTITY_SHADOW_ALPHA,
|
|
971
|
+
) -> bool:
|
|
972
|
+
if light_source_pos is None or shadow_radius <= 0:
|
|
973
|
+
return False
|
|
974
|
+
shadow_surface = _get_shadow_circle_surface(
|
|
975
|
+
shadow_radius,
|
|
976
|
+
alpha,
|
|
977
|
+
edge_softness=ENTITY_SHADOW_EDGE_SOFTNESS,
|
|
978
|
+
)
|
|
979
|
+
screen_rect = shadow_layer.get_rect()
|
|
980
|
+
px, py = light_source_pos
|
|
981
|
+
offset_dist = max(1.0, shadow_radius * 0.6)
|
|
982
|
+
drew = False
|
|
983
|
+
for entity in all_sprites:
|
|
984
|
+
if not entity.alive():
|
|
985
|
+
continue
|
|
986
|
+
if isinstance(entity, Player):
|
|
987
|
+
continue
|
|
988
|
+
if isinstance(entity, Car):
|
|
989
|
+
if exclude_car is not None and entity is exclude_car:
|
|
990
|
+
continue
|
|
991
|
+
if not isinstance(entity, (Zombie, Survivor, Car)):
|
|
992
|
+
continue
|
|
993
|
+
cx, cy = entity.rect.center
|
|
994
|
+
dx = cx - px
|
|
995
|
+
dy = cy - py
|
|
996
|
+
dist = math.hypot(dx, dy)
|
|
997
|
+
if dist > 0.001:
|
|
998
|
+
scale = offset_dist / dist
|
|
999
|
+
offset_x = dx * scale
|
|
1000
|
+
offset_y = dy * scale
|
|
1001
|
+
else:
|
|
1002
|
+
offset_x = 0.0
|
|
1003
|
+
offset_y = 0.0
|
|
1004
|
+
shadow_rect = shadow_surface.get_rect(
|
|
1005
|
+
center=(int(cx + offset_x), int(cy + offset_y))
|
|
1006
|
+
)
|
|
1007
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
1008
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
1009
|
+
continue
|
|
1010
|
+
shadow_layer.blit(
|
|
1011
|
+
shadow_surface,
|
|
1012
|
+
shadow_screen_rect.topleft,
|
|
1013
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
1014
|
+
)
|
|
1015
|
+
drew = True
|
|
1016
|
+
return drew
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _draw_single_entity_shadow(
|
|
1020
|
+
shadow_layer: surface.Surface,
|
|
1021
|
+
camera: Camera,
|
|
1022
|
+
*,
|
|
1023
|
+
entity: pygame.sprite.Sprite | None,
|
|
1024
|
+
light_source_pos: tuple[int, int] | None,
|
|
1025
|
+
shadow_radius: int,
|
|
1026
|
+
alpha: int,
|
|
1027
|
+
edge_softness: float = ENTITY_SHADOW_EDGE_SOFTNESS,
|
|
1028
|
+
) -> bool:
|
|
1029
|
+
if (
|
|
1030
|
+
entity is None
|
|
1031
|
+
or not entity.alive()
|
|
1032
|
+
or light_source_pos is None
|
|
1033
|
+
or shadow_radius <= 0
|
|
1034
|
+
):
|
|
1035
|
+
return False
|
|
1036
|
+
shadow_surface = _get_shadow_circle_surface(
|
|
1037
|
+
shadow_radius,
|
|
1038
|
+
alpha,
|
|
1039
|
+
edge_softness=edge_softness,
|
|
1040
|
+
)
|
|
1041
|
+
screen_rect = shadow_layer.get_rect()
|
|
1042
|
+
px, py = light_source_pos
|
|
1043
|
+
cx, cy = entity.rect.center
|
|
1044
|
+
dx = cx - px
|
|
1045
|
+
dy = cy - py
|
|
1046
|
+
dist = math.hypot(dx, dy)
|
|
1047
|
+
offset_dist = max(1.0, shadow_radius * 0.6)
|
|
1048
|
+
if dist > 0.001:
|
|
1049
|
+
scale = offset_dist / dist
|
|
1050
|
+
offset_x = dx * scale
|
|
1051
|
+
offset_y = dy * scale
|
|
1052
|
+
else:
|
|
1053
|
+
offset_x = 0.0
|
|
1054
|
+
offset_y = 0.0
|
|
1055
|
+
shadow_rect = shadow_surface.get_rect(
|
|
1056
|
+
center=(int(cx + offset_x), int(cy + offset_y))
|
|
1057
|
+
)
|
|
1058
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
1059
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
1060
|
+
return False
|
|
1061
|
+
shadow_layer.blit(
|
|
1062
|
+
shadow_surface,
|
|
1063
|
+
shadow_screen_rect.topleft,
|
|
1064
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
1065
|
+
)
|
|
1066
|
+
return True
|
|
784
1067
|
|
|
785
1068
|
|
|
786
1069
|
def _draw_footprints(
|
|
@@ -869,11 +1152,7 @@ def _draw_hint_indicator(
|
|
|
869
1152
|
) -> None:
|
|
870
1153
|
if not hint_target:
|
|
871
1154
|
return
|
|
872
|
-
current_fov_scale = _get_fog_scale(
|
|
873
|
-
assets,
|
|
874
|
-
stage,
|
|
875
|
-
flashlight_count,
|
|
876
|
-
)
|
|
1155
|
+
current_fov_scale = _get_fog_scale(assets, flashlight_count)
|
|
877
1156
|
hint_ring_radius = assets.fov_radius * 0.5 * current_fov_scale
|
|
878
1157
|
_draw_hint_arrow(
|
|
879
1158
|
screen,
|
|
@@ -958,6 +1237,35 @@ def _draw_objective(lines: list[str], *, screen: surface.Surface) -> None:
|
|
|
958
1237
|
print(f"Error rendering objective: {e}")
|
|
959
1238
|
|
|
960
1239
|
|
|
1240
|
+
def _draw_inventory_icons(
|
|
1241
|
+
screen: surface.Surface,
|
|
1242
|
+
assets: RenderAssets,
|
|
1243
|
+
*,
|
|
1244
|
+
has_fuel: bool,
|
|
1245
|
+
flashlight_count: int,
|
|
1246
|
+
shoes_count: int,
|
|
1247
|
+
) -> None:
|
|
1248
|
+
icons: list[surface.Surface] = []
|
|
1249
|
+
if has_fuel:
|
|
1250
|
+
icons.append(_get_hud_icon("fuel"))
|
|
1251
|
+
for _ in range(max(0, int(flashlight_count))):
|
|
1252
|
+
icons.append(_get_hud_icon("flashlight"))
|
|
1253
|
+
for _ in range(max(0, int(shoes_count))):
|
|
1254
|
+
icons.append(_get_hud_icon("shoes"))
|
|
1255
|
+
if not icons:
|
|
1256
|
+
return
|
|
1257
|
+
spacing = 3
|
|
1258
|
+
padding = 8
|
|
1259
|
+
total_width = sum(icon.get_width() for icon in icons)
|
|
1260
|
+
total_width += spacing * max(0, len(icons) - 1)
|
|
1261
|
+
start_x = assets.screen_width - padding - total_width
|
|
1262
|
+
y = 8
|
|
1263
|
+
x = max(padding, start_x)
|
|
1264
|
+
for icon in icons:
|
|
1265
|
+
screen.blit(icon, (x, y))
|
|
1266
|
+
x += icon.get_width() + spacing
|
|
1267
|
+
|
|
1268
|
+
|
|
961
1269
|
def _draw_endurance_timer(
|
|
962
1270
|
screen: surface.Surface,
|
|
963
1271
|
assets: RenderAssets,
|
|
@@ -1146,6 +1454,7 @@ def draw(
|
|
|
1146
1454
|
hint_color: tuple[int, int, int] | None = None,
|
|
1147
1455
|
do_flip: bool = True,
|
|
1148
1456
|
present_fn: Callable[[surface.Surface], None] | None = None,
|
|
1457
|
+
fps: float | None = None,
|
|
1149
1458
|
) -> None:
|
|
1150
1459
|
hint_color = hint_color or YELLOW
|
|
1151
1460
|
state = game_data.state
|
|
@@ -1162,6 +1471,7 @@ def draw(
|
|
|
1162
1471
|
footprints = state.footprints
|
|
1163
1472
|
has_fuel = state.has_fuel
|
|
1164
1473
|
flashlight_count = state.flashlight_count
|
|
1474
|
+
shoes_count = state.shoes_count
|
|
1165
1475
|
elapsed_play_ms = state.elapsed_play_ms
|
|
1166
1476
|
fuel_message_until = state.fuel_message_until
|
|
1167
1477
|
buddy_onboard = state.buddy_onboard
|
|
@@ -1187,19 +1497,52 @@ def draw(
|
|
|
1187
1497
|
outside_rects,
|
|
1188
1498
|
game_data.layout.fall_spawn_cells,
|
|
1189
1499
|
)
|
|
1190
|
-
|
|
1191
|
-
|
|
1500
|
+
shadow_layer = _get_shadow_layer(screen.get_size())
|
|
1501
|
+
shadow_layer.fill((0, 0, 0, 0))
|
|
1502
|
+
drew_shadow = _draw_wall_shadows(
|
|
1503
|
+
shadow_layer,
|
|
1192
1504
|
camera,
|
|
1193
1505
|
wall_cells=game_data.layout.wall_cells,
|
|
1194
1506
|
wall_group=game_data.groups.wall_group,
|
|
1195
1507
|
outer_wall_cells=game_data.layout.outer_wall_cells,
|
|
1196
1508
|
cell_size=game_data.cell_size,
|
|
1197
1509
|
light_source_pos=(
|
|
1198
|
-
None
|
|
1510
|
+
None
|
|
1511
|
+
if (stage and stage.endurance_stage and state.dawn_ready)
|
|
1512
|
+
else fov_target.rect.center
|
|
1199
1513
|
)
|
|
1200
1514
|
if fov_target
|
|
1201
1515
|
else None,
|
|
1202
1516
|
)
|
|
1517
|
+
drew_shadow |= _draw_entity_shadows(
|
|
1518
|
+
shadow_layer,
|
|
1519
|
+
camera,
|
|
1520
|
+
all_sprites,
|
|
1521
|
+
light_source_pos=fov_target.rect.center if fov_target else None,
|
|
1522
|
+
exclude_car=active_car if player.in_car else None,
|
|
1523
|
+
)
|
|
1524
|
+
player_shadow_alpha = max(1, int(ENTITY_SHADOW_ALPHA * PLAYER_SHADOW_ALPHA_MULT))
|
|
1525
|
+
player_shadow_radius = int(ZOMBIE_RADIUS * PLAYER_SHADOW_RADIUS_MULT)
|
|
1526
|
+
if player.in_car:
|
|
1527
|
+
drew_shadow |= _draw_single_entity_shadow(
|
|
1528
|
+
shadow_layer,
|
|
1529
|
+
camera,
|
|
1530
|
+
entity=active_car,
|
|
1531
|
+
light_source_pos=fov_target.rect.center if fov_target else None,
|
|
1532
|
+
shadow_radius=player_shadow_radius,
|
|
1533
|
+
alpha=player_shadow_alpha,
|
|
1534
|
+
)
|
|
1535
|
+
else:
|
|
1536
|
+
drew_shadow |= _draw_single_entity_shadow(
|
|
1537
|
+
shadow_layer,
|
|
1538
|
+
camera,
|
|
1539
|
+
entity=player,
|
|
1540
|
+
light_source_pos=fov_target.rect.center if fov_target else None,
|
|
1541
|
+
shadow_radius=player_shadow_radius,
|
|
1542
|
+
alpha=player_shadow_alpha,
|
|
1543
|
+
)
|
|
1544
|
+
if drew_shadow:
|
|
1545
|
+
screen.blit(shadow_layer, (0, 0))
|
|
1203
1546
|
_draw_footprints(
|
|
1204
1547
|
screen,
|
|
1205
1548
|
camera,
|
|
@@ -1263,6 +1606,13 @@ def draw(
|
|
|
1263
1606
|
)
|
|
1264
1607
|
if objective_lines:
|
|
1265
1608
|
_draw_objective(objective_lines, screen=screen)
|
|
1609
|
+
_draw_inventory_icons(
|
|
1610
|
+
screen,
|
|
1611
|
+
assets,
|
|
1612
|
+
has_fuel=has_fuel,
|
|
1613
|
+
flashlight_count=flashlight_count,
|
|
1614
|
+
shoes_count=shoes_count,
|
|
1615
|
+
)
|
|
1266
1616
|
_draw_survivor_messages(screen, assets, survivor_messages)
|
|
1267
1617
|
_draw_endurance_timer(screen, assets, stage=stage, state=state)
|
|
1268
1618
|
_draw_time_accel_indicator(screen, assets, stage=stage, state=state)
|
|
@@ -1275,6 +1625,7 @@ def draw(
|
|
|
1275
1625
|
debug_mode=state.debug_mode,
|
|
1276
1626
|
zombie_group=zombie_group,
|
|
1277
1627
|
falling_spawn_carry=state.falling_spawn_carry,
|
|
1628
|
+
fps=fps,
|
|
1278
1629
|
)
|
|
1279
1630
|
if do_flip:
|
|
1280
1631
|
if present_fn:
|