zombie-escape 1.10.1__py3-none-any.whl → 1.12.3__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 +22 -14
- zombie_escape/entities.py +247 -47
- zombie_escape/entities_constants.py +15 -5
- zombie_escape/gameplay/__init__.py +2 -0
- zombie_escape/gameplay/footprints.py +4 -0
- zombie_escape/gameplay/interactions.py +38 -7
- zombie_escape/gameplay/layout.py +40 -15
- zombie_escape/gameplay/movement.py +38 -2
- zombie_escape/gameplay/spawn.py +174 -41
- zombie_escape/gameplay/state.py +17 -9
- zombie_escape/gameplay/survivors.py +9 -1
- zombie_escape/gameplay/utils.py +40 -21
- zombie_escape/gameplay_constants.py +8 -0
- zombie_escape/level_blueprints.py +139 -24
- zombie_escape/locales/ui.en.json +9 -1
- zombie_escape/locales/ui.ja.json +8 -0
- zombie_escape/models.py +11 -5
- zombie_escape/render.py +390 -43
- zombie_escape/render_assets.py +427 -174
- zombie_escape/render_constants.py +25 -4
- zombie_escape/screens/game_over.py +4 -4
- zombie_escape/screens/gameplay.py +31 -1
- zombie_escape/stage_constants.py +33 -16
- zombie_escape/zombie_escape.py +1 -1
- {zombie_escape-1.10.1.dist-info → zombie_escape-1.12.3.dist-info}/METADATA +7 -4
- zombie_escape-1.12.3.dist-info/RECORD +47 -0
- zombie_escape-1.10.1.dist-info/RECORD +0 -47
- {zombie_escape-1.10.1.dist-info → zombie_escape-1.12.3.dist-info}/WHEEL +0 -0
- {zombie_escape-1.10.1.dist-info → zombie_escape-1.12.3.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.10.1.dist-info → zombie_escape-1.12.3.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,11 +46,25 @@ 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,
|
|
44
68
|
SHADOW_MIN_RATIO,
|
|
45
69
|
SHADOW_OVERSAMPLE,
|
|
46
70
|
SHADOW_RADIUS_RATIO,
|
|
@@ -49,6 +73,10 @@ from .render_constants import (
|
|
|
49
73
|
|
|
50
74
|
_SHADOW_TILE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
|
|
51
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
|
|
52
80
|
|
|
53
81
|
|
|
54
82
|
def _get_shadow_tile_surface(
|
|
@@ -113,6 +141,98 @@ def _get_shadow_layer(size: tuple[int, int]) -> surface.Surface:
|
|
|
113
141
|
return layer
|
|
114
142
|
|
|
115
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
|
+
|
|
116
236
|
def show_message(
|
|
117
237
|
screen: surface.Surface,
|
|
118
238
|
text: str,
|
|
@@ -149,7 +269,7 @@ def draw_level_overview(
|
|
|
149
269
|
*,
|
|
150
270
|
fuel: FuelCan | None = None,
|
|
151
271
|
flashlights: list[Flashlight] | None = None,
|
|
152
|
-
|
|
272
|
+
shoes: list[Shoes] | None = None,
|
|
153
273
|
buddies: list[Survivor] | None = None,
|
|
154
274
|
survivors: list[Survivor] | None = None,
|
|
155
275
|
palette_key: str | None = None,
|
|
@@ -195,11 +315,15 @@ def draw_level_overview(
|
|
|
195
315
|
for flashlight in flashlights:
|
|
196
316
|
if flashlight.alive():
|
|
197
317
|
pygame.draw.rect(
|
|
198
|
-
surface,
|
|
318
|
+
surface, YELLOW, flashlight.rect, border_radius=2
|
|
199
319
|
)
|
|
200
320
|
pygame.draw.rect(
|
|
201
321
|
surface, BLACK, flashlight.rect, width=2, border_radius=2
|
|
202
322
|
)
|
|
323
|
+
if shoes:
|
|
324
|
+
for item in shoes:
|
|
325
|
+
if item.alive():
|
|
326
|
+
surface.blit(item.image, item.rect)
|
|
203
327
|
if survivors:
|
|
204
328
|
for survivor in survivors:
|
|
205
329
|
if survivor.alive():
|
|
@@ -233,7 +357,6 @@ def draw_level_overview(
|
|
|
233
357
|
|
|
234
358
|
def _get_fog_scale(
|
|
235
359
|
assets: RenderAssets,
|
|
236
|
-
stage: Stage | None,
|
|
237
360
|
flashlight_count: int,
|
|
238
361
|
) -> float:
|
|
239
362
|
"""Return current fog scale factoring in flashlight bonus."""
|
|
@@ -241,8 +364,9 @@ def _get_fog_scale(
|
|
|
241
364
|
flashlight_count = max(0, int(flashlight_count))
|
|
242
365
|
if flashlight_count <= 0:
|
|
243
366
|
return scale
|
|
244
|
-
|
|
245
|
-
|
|
367
|
+
if flashlight_count == 1:
|
|
368
|
+
return max(scale, FLASHLIGHT_FOG_SCALE_ONE)
|
|
369
|
+
return max(scale, FLASHLIGHT_FOG_SCALE_TWO)
|
|
246
370
|
|
|
247
371
|
|
|
248
372
|
def _max_flashlight_pickups() -> int:
|
|
@@ -262,7 +386,7 @@ class FogProfile(Enum):
|
|
|
262
386
|
|
|
263
387
|
def _scale(self, assets: RenderAssets, stage: Stage | None) -> float:
|
|
264
388
|
count = max(0, min(self.flashlight_count, _max_flashlight_pickups()))
|
|
265
|
-
return _get_fog_scale(assets,
|
|
389
|
+
return _get_fog_scale(assets, count)
|
|
266
390
|
|
|
267
391
|
@staticmethod
|
|
268
392
|
def _from_flashlight_count(count: int) -> "FogProfile":
|
|
@@ -601,6 +725,7 @@ def _draw_status_bar(
|
|
|
601
725
|
debug_mode: bool = False,
|
|
602
726
|
zombie_group: sprite.Group | None = None,
|
|
603
727
|
falling_spawn_carry: int | None = None,
|
|
728
|
+
fps: float | None = None,
|
|
604
729
|
) -> None:
|
|
605
730
|
"""Render a compact status bar with current config flags and stage info."""
|
|
606
731
|
bar_rect = pygame.Rect(
|
|
@@ -646,6 +771,8 @@ def _draw_status_bar(
|
|
|
646
771
|
parts.append(f"Z:{total} N:{normal} T:{tracker} W:{wall}")
|
|
647
772
|
if falling_spawn_carry is not None:
|
|
648
773
|
parts.append(f"C:{max(0, falling_spawn_carry)}")
|
|
774
|
+
if fps is not None:
|
|
775
|
+
parts.append(f"FPS:{fps:.1f}")
|
|
649
776
|
|
|
650
777
|
status_text = " | ".join(parts)
|
|
651
778
|
color = LIGHT_GRAY
|
|
@@ -663,6 +790,14 @@ def _draw_status_bar(
|
|
|
663
790
|
right=bar_rect.right - 12, centery=bar_rect.centery
|
|
664
791
|
)
|
|
665
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)
|
|
666
801
|
except pygame.error as e:
|
|
667
802
|
print(f"Error rendering status bar: {e}")
|
|
668
803
|
|
|
@@ -672,11 +807,16 @@ def _draw_play_area(
|
|
|
672
807
|
camera: Camera,
|
|
673
808
|
assets: RenderAssets,
|
|
674
809
|
palette: Any,
|
|
675
|
-
|
|
676
|
-
|
|
810
|
+
field_rect: pygame.Rect,
|
|
811
|
+
outside_cells: set[tuple[int, int]],
|
|
677
812
|
fall_spawn_cells: set[tuple[int, int]],
|
|
678
813
|
) -> tuple[int, int, int, int, set[tuple[int, int]]]:
|
|
679
|
-
xs, ys, xe, ye =
|
|
814
|
+
xs, ys, xe, ye = (
|
|
815
|
+
field_rect.left,
|
|
816
|
+
field_rect.top,
|
|
817
|
+
field_rect.right,
|
|
818
|
+
field_rect.bottom,
|
|
819
|
+
)
|
|
680
820
|
xs //= assets.internal_wall_grid_snap
|
|
681
821
|
ys //= assets.internal_wall_grid_snap
|
|
682
822
|
xe //= assets.internal_wall_grid_snap
|
|
@@ -691,15 +831,6 @@ def _draw_play_area(
|
|
|
691
831
|
play_area_screen_rect = camera.apply_rect(play_area_rect)
|
|
692
832
|
pygame.draw.rect(screen, palette.floor_primary, play_area_screen_rect)
|
|
693
833
|
|
|
694
|
-
outside_cells = {
|
|
695
|
-
(r.x // assets.internal_wall_grid_snap, r.y // assets.internal_wall_grid_snap)
|
|
696
|
-
for r in outside_rects
|
|
697
|
-
}
|
|
698
|
-
for rect_obj in outside_rects:
|
|
699
|
-
sr = camera.apply_rect(rect_obj)
|
|
700
|
-
if sr.colliderect(screen.get_rect()):
|
|
701
|
-
pygame.draw.rect(screen, palette.outside, sr)
|
|
702
|
-
|
|
703
834
|
view_world = pygame.Rect(
|
|
704
835
|
-camera.camera.x,
|
|
705
836
|
-camera.camera.y,
|
|
@@ -720,6 +851,19 @@ def _draw_play_area(
|
|
|
720
851
|
for y in range(start_y, end_y):
|
|
721
852
|
for x in range(start_x, end_x):
|
|
722
853
|
if (x, y) in outside_cells:
|
|
854
|
+
lx, ly = (
|
|
855
|
+
x * assets.internal_wall_grid_snap,
|
|
856
|
+
y * assets.internal_wall_grid_snap,
|
|
857
|
+
)
|
|
858
|
+
r = pygame.Rect(
|
|
859
|
+
lx,
|
|
860
|
+
ly,
|
|
861
|
+
assets.internal_wall_grid_snap,
|
|
862
|
+
assets.internal_wall_grid_snap,
|
|
863
|
+
)
|
|
864
|
+
sr = camera.apply_rect(r)
|
|
865
|
+
if sr.colliderect(screen.get_rect()):
|
|
866
|
+
pygame.draw.rect(screen, palette.outside, sr)
|
|
723
867
|
continue
|
|
724
868
|
use_secondary = ((x // 2) + (y // 2)) % 2 == 0
|
|
725
869
|
if (x, y) in fall_spawn_cells:
|
|
@@ -760,7 +904,7 @@ def abs_clip(value: float, min_v: float, max_v: float) -> float:
|
|
|
760
904
|
|
|
761
905
|
|
|
762
906
|
def _draw_wall_shadows(
|
|
763
|
-
|
|
907
|
+
shadow_layer: surface.Surface,
|
|
764
908
|
camera: Camera,
|
|
765
909
|
*,
|
|
766
910
|
wall_cells: set[tuple[int, int]],
|
|
@@ -769,9 +913,9 @@ def _draw_wall_shadows(
|
|
|
769
913
|
cell_size: int,
|
|
770
914
|
light_source_pos: tuple[int, int] | None,
|
|
771
915
|
alpha: int = 68,
|
|
772
|
-
) ->
|
|
916
|
+
) -> bool:
|
|
773
917
|
if not wall_cells or cell_size <= 0 or light_source_pos is None:
|
|
774
|
-
return
|
|
918
|
+
return False
|
|
775
919
|
inner_wall_cells = set(wall_cells)
|
|
776
920
|
if outer_wall_cells:
|
|
777
921
|
inner_wall_cells.difference_update(outer_wall_cells)
|
|
@@ -782,13 +926,15 @@ def _draw_wall_shadows(
|
|
|
782
926
|
cell_y = int(wall.rect.centery // cell_size)
|
|
783
927
|
inner_wall_cells.add((cell_x, cell_y))
|
|
784
928
|
if not inner_wall_cells:
|
|
785
|
-
return
|
|
929
|
+
return False
|
|
786
930
|
base_shadow_size = max(cell_size + 2, int(cell_size * 1.35))
|
|
787
931
|
shadow_size = max(1, int(base_shadow_size * 1.5))
|
|
788
|
-
shadow_surface = _get_shadow_tile_surface(
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
932
|
+
shadow_surface = _get_shadow_tile_surface(
|
|
933
|
+
shadow_size,
|
|
934
|
+
alpha,
|
|
935
|
+
edge_softness=0.12,
|
|
936
|
+
)
|
|
937
|
+
screen_rect = shadow_layer.get_rect()
|
|
792
938
|
px, py = light_source_pos
|
|
793
939
|
drew = False
|
|
794
940
|
clip_max = shadow_size * 0.25
|
|
@@ -813,11 +959,140 @@ def _draw_wall_shadows(
|
|
|
813
959
|
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
814
960
|
if not shadow_screen_rect.colliderect(screen_rect):
|
|
815
961
|
continue
|
|
816
|
-
shadow_layer.blit(
|
|
962
|
+
shadow_layer.blit(
|
|
963
|
+
shadow_surface,
|
|
964
|
+
shadow_screen_rect.topleft,
|
|
965
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
966
|
+
)
|
|
817
967
|
drew = True
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
968
|
+
return drew
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _draw_entity_shadows(
|
|
972
|
+
shadow_layer: surface.Surface,
|
|
973
|
+
camera: Camera,
|
|
974
|
+
all_sprites: sprite.LayeredUpdates,
|
|
975
|
+
*,
|
|
976
|
+
light_source_pos: tuple[int, int] | None,
|
|
977
|
+
exclude_car: Car | None,
|
|
978
|
+
outside_cells: set[tuple[int, int]] | None,
|
|
979
|
+
cell_size: int,
|
|
980
|
+
shadow_radius: int = int(ZOMBIE_RADIUS * ENTITY_SHADOW_RADIUS_MULT),
|
|
981
|
+
alpha: int = ENTITY_SHADOW_ALPHA,
|
|
982
|
+
) -> bool:
|
|
983
|
+
if light_source_pos is None or shadow_radius <= 0:
|
|
984
|
+
return False
|
|
985
|
+
if cell_size <= 0:
|
|
986
|
+
outside_cells = None
|
|
987
|
+
shadow_surface = _get_shadow_circle_surface(
|
|
988
|
+
shadow_radius,
|
|
989
|
+
alpha,
|
|
990
|
+
edge_softness=ENTITY_SHADOW_EDGE_SOFTNESS,
|
|
991
|
+
)
|
|
992
|
+
screen_rect = shadow_layer.get_rect()
|
|
993
|
+
px, py = light_source_pos
|
|
994
|
+
offset_dist = max(1.0, shadow_radius * 0.6)
|
|
995
|
+
drew = False
|
|
996
|
+
for entity in all_sprites:
|
|
997
|
+
if not entity.alive():
|
|
998
|
+
continue
|
|
999
|
+
if isinstance(entity, Player):
|
|
1000
|
+
continue
|
|
1001
|
+
if isinstance(entity, Car):
|
|
1002
|
+
if exclude_car is not None and entity is exclude_car:
|
|
1003
|
+
continue
|
|
1004
|
+
if not isinstance(entity, (Zombie, Survivor, Car)):
|
|
1005
|
+
continue
|
|
1006
|
+
if outside_cells:
|
|
1007
|
+
cell = (
|
|
1008
|
+
int(entity.rect.centerx // cell_size),
|
|
1009
|
+
int(entity.rect.centery // cell_size),
|
|
1010
|
+
)
|
|
1011
|
+
if cell in outside_cells:
|
|
1012
|
+
continue
|
|
1013
|
+
cx, cy = entity.rect.center
|
|
1014
|
+
dx = cx - px
|
|
1015
|
+
dy = cy - py
|
|
1016
|
+
dist = math.hypot(dx, dy)
|
|
1017
|
+
if dist > 0.001:
|
|
1018
|
+
scale = offset_dist / dist
|
|
1019
|
+
offset_x = dx * scale
|
|
1020
|
+
offset_y = dy * scale
|
|
1021
|
+
else:
|
|
1022
|
+
offset_x = 0.0
|
|
1023
|
+
offset_y = 0.0
|
|
1024
|
+
shadow_rect = shadow_surface.get_rect(
|
|
1025
|
+
center=(int(cx + offset_x), int(cy + offset_y))
|
|
1026
|
+
)
|
|
1027
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
1028
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
1029
|
+
continue
|
|
1030
|
+
shadow_layer.blit(
|
|
1031
|
+
shadow_surface,
|
|
1032
|
+
shadow_screen_rect.topleft,
|
|
1033
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
1034
|
+
)
|
|
1035
|
+
drew = True
|
|
1036
|
+
return drew
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _draw_single_entity_shadow(
|
|
1040
|
+
shadow_layer: surface.Surface,
|
|
1041
|
+
camera: Camera,
|
|
1042
|
+
*,
|
|
1043
|
+
entity: pygame.sprite.Sprite | None,
|
|
1044
|
+
light_source_pos: tuple[int, int] | None,
|
|
1045
|
+
outside_cells: set[tuple[int, int]] | None,
|
|
1046
|
+
cell_size: int,
|
|
1047
|
+
shadow_radius: int,
|
|
1048
|
+
alpha: int,
|
|
1049
|
+
edge_softness: float = ENTITY_SHADOW_EDGE_SOFTNESS,
|
|
1050
|
+
) -> bool:
|
|
1051
|
+
if (
|
|
1052
|
+
entity is None
|
|
1053
|
+
or not entity.alive()
|
|
1054
|
+
or light_source_pos is None
|
|
1055
|
+
or shadow_radius <= 0
|
|
1056
|
+
):
|
|
1057
|
+
return False
|
|
1058
|
+
if outside_cells and cell_size > 0:
|
|
1059
|
+
cell = (
|
|
1060
|
+
int(entity.rect.centerx // cell_size),
|
|
1061
|
+
int(entity.rect.centery // cell_size),
|
|
1062
|
+
)
|
|
1063
|
+
if cell in outside_cells:
|
|
1064
|
+
return False
|
|
1065
|
+
shadow_surface = _get_shadow_circle_surface(
|
|
1066
|
+
shadow_radius,
|
|
1067
|
+
alpha,
|
|
1068
|
+
edge_softness=edge_softness,
|
|
1069
|
+
)
|
|
1070
|
+
screen_rect = shadow_layer.get_rect()
|
|
1071
|
+
px, py = light_source_pos
|
|
1072
|
+
cx, cy = entity.rect.center
|
|
1073
|
+
dx = cx - px
|
|
1074
|
+
dy = cy - py
|
|
1075
|
+
dist = math.hypot(dx, dy)
|
|
1076
|
+
offset_dist = max(1.0, shadow_radius * 0.6)
|
|
1077
|
+
if dist > 0.001:
|
|
1078
|
+
scale = offset_dist / dist
|
|
1079
|
+
offset_x = dx * scale
|
|
1080
|
+
offset_y = dy * scale
|
|
1081
|
+
else:
|
|
1082
|
+
offset_x = 0.0
|
|
1083
|
+
offset_y = 0.0
|
|
1084
|
+
shadow_rect = shadow_surface.get_rect(
|
|
1085
|
+
center=(int(cx + offset_x), int(cy + offset_y))
|
|
1086
|
+
)
|
|
1087
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
1088
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
1089
|
+
return False
|
|
1090
|
+
shadow_layer.blit(
|
|
1091
|
+
shadow_surface,
|
|
1092
|
+
shadow_screen_rect.topleft,
|
|
1093
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
1094
|
+
)
|
|
1095
|
+
return True
|
|
821
1096
|
|
|
822
1097
|
|
|
823
1098
|
def _draw_footprints(
|
|
@@ -906,11 +1181,7 @@ def _draw_hint_indicator(
|
|
|
906
1181
|
) -> None:
|
|
907
1182
|
if not hint_target:
|
|
908
1183
|
return
|
|
909
|
-
current_fov_scale = _get_fog_scale(
|
|
910
|
-
assets,
|
|
911
|
-
stage,
|
|
912
|
-
flashlight_count,
|
|
913
|
-
)
|
|
1184
|
+
current_fov_scale = _get_fog_scale(assets, flashlight_count)
|
|
914
1185
|
hint_ring_radius = assets.fov_radius * 0.5 * current_fov_scale
|
|
915
1186
|
_draw_hint_arrow(
|
|
916
1187
|
screen,
|
|
@@ -995,6 +1266,35 @@ def _draw_objective(lines: list[str], *, screen: surface.Surface) -> None:
|
|
|
995
1266
|
print(f"Error rendering objective: {e}")
|
|
996
1267
|
|
|
997
1268
|
|
|
1269
|
+
def _draw_inventory_icons(
|
|
1270
|
+
screen: surface.Surface,
|
|
1271
|
+
assets: RenderAssets,
|
|
1272
|
+
*,
|
|
1273
|
+
has_fuel: bool,
|
|
1274
|
+
flashlight_count: int,
|
|
1275
|
+
shoes_count: int,
|
|
1276
|
+
) -> None:
|
|
1277
|
+
icons: list[surface.Surface] = []
|
|
1278
|
+
if has_fuel:
|
|
1279
|
+
icons.append(_get_hud_icon("fuel"))
|
|
1280
|
+
for _ in range(max(0, int(flashlight_count))):
|
|
1281
|
+
icons.append(_get_hud_icon("flashlight"))
|
|
1282
|
+
for _ in range(max(0, int(shoes_count))):
|
|
1283
|
+
icons.append(_get_hud_icon("shoes"))
|
|
1284
|
+
if not icons:
|
|
1285
|
+
return
|
|
1286
|
+
spacing = 3
|
|
1287
|
+
padding = 8
|
|
1288
|
+
total_width = sum(icon.get_width() for icon in icons)
|
|
1289
|
+
total_width += spacing * max(0, len(icons) - 1)
|
|
1290
|
+
start_x = assets.screen_width - padding - total_width
|
|
1291
|
+
y = 8
|
|
1292
|
+
x = max(padding, start_x)
|
|
1293
|
+
for icon in icons:
|
|
1294
|
+
screen.blit(icon, (x, y))
|
|
1295
|
+
x += icon.get_width() + spacing
|
|
1296
|
+
|
|
1297
|
+
|
|
998
1298
|
def _draw_endurance_timer(
|
|
999
1299
|
screen: surface.Surface,
|
|
1000
1300
|
assets: RenderAssets,
|
|
@@ -1183,6 +1483,7 @@ def draw(
|
|
|
1183
1483
|
hint_color: tuple[int, int, int] | None = None,
|
|
1184
1484
|
do_flip: bool = True,
|
|
1185
1485
|
present_fn: Callable[[surface.Surface], None] | None = None,
|
|
1486
|
+
fps: float | None = None,
|
|
1186
1487
|
) -> None:
|
|
1187
1488
|
hint_color = hint_color or YELLOW
|
|
1188
1489
|
state = game_data.state
|
|
@@ -1192,13 +1493,14 @@ def draw(
|
|
|
1192
1493
|
|
|
1193
1494
|
camera = game_data.camera
|
|
1194
1495
|
stage = game_data.stage
|
|
1195
|
-
|
|
1196
|
-
|
|
1496
|
+
field_rect = game_data.layout.field_rect
|
|
1497
|
+
outside_cells = game_data.layout.outside_cells
|
|
1197
1498
|
all_sprites = game_data.groups.all_sprites
|
|
1198
1499
|
fog_surfaces = game_data.fog
|
|
1199
1500
|
footprints = state.footprints
|
|
1200
1501
|
has_fuel = state.has_fuel
|
|
1201
1502
|
flashlight_count = state.flashlight_count
|
|
1503
|
+
shoes_count = state.shoes_count
|
|
1202
1504
|
elapsed_play_ms = state.elapsed_play_ms
|
|
1203
1505
|
fuel_message_until = state.fuel_message_until
|
|
1204
1506
|
buddy_onboard = state.buddy_onboard
|
|
@@ -1220,12 +1522,14 @@ def draw(
|
|
|
1220
1522
|
camera,
|
|
1221
1523
|
assets,
|
|
1222
1524
|
palette,
|
|
1223
|
-
|
|
1224
|
-
|
|
1525
|
+
field_rect,
|
|
1526
|
+
outside_cells,
|
|
1225
1527
|
game_data.layout.fall_spawn_cells,
|
|
1226
1528
|
)
|
|
1227
|
-
|
|
1228
|
-
|
|
1529
|
+
shadow_layer = _get_shadow_layer(screen.get_size())
|
|
1530
|
+
shadow_layer.fill((0, 0, 0, 0))
|
|
1531
|
+
drew_shadow = _draw_wall_shadows(
|
|
1532
|
+
shadow_layer,
|
|
1229
1533
|
camera,
|
|
1230
1534
|
wall_cells=game_data.layout.wall_cells,
|
|
1231
1535
|
wall_group=game_data.groups.wall_group,
|
|
@@ -1239,6 +1543,41 @@ def draw(
|
|
|
1239
1543
|
if fov_target
|
|
1240
1544
|
else None,
|
|
1241
1545
|
)
|
|
1546
|
+
drew_shadow |= _draw_entity_shadows(
|
|
1547
|
+
shadow_layer,
|
|
1548
|
+
camera,
|
|
1549
|
+
all_sprites,
|
|
1550
|
+
light_source_pos=fov_target.rect.center if fov_target else None,
|
|
1551
|
+
exclude_car=active_car if player.in_car else None,
|
|
1552
|
+
outside_cells=outside_cells,
|
|
1553
|
+
cell_size=game_data.cell_size,
|
|
1554
|
+
)
|
|
1555
|
+
player_shadow_alpha = max(1, int(ENTITY_SHADOW_ALPHA * PLAYER_SHADOW_ALPHA_MULT))
|
|
1556
|
+
player_shadow_radius = int(ZOMBIE_RADIUS * PLAYER_SHADOW_RADIUS_MULT)
|
|
1557
|
+
if player.in_car:
|
|
1558
|
+
drew_shadow |= _draw_single_entity_shadow(
|
|
1559
|
+
shadow_layer,
|
|
1560
|
+
camera,
|
|
1561
|
+
entity=active_car,
|
|
1562
|
+
light_source_pos=fov_target.rect.center if fov_target else None,
|
|
1563
|
+
outside_cells=outside_cells,
|
|
1564
|
+
cell_size=game_data.cell_size,
|
|
1565
|
+
shadow_radius=player_shadow_radius,
|
|
1566
|
+
alpha=player_shadow_alpha,
|
|
1567
|
+
)
|
|
1568
|
+
else:
|
|
1569
|
+
drew_shadow |= _draw_single_entity_shadow(
|
|
1570
|
+
shadow_layer,
|
|
1571
|
+
camera,
|
|
1572
|
+
entity=player,
|
|
1573
|
+
light_source_pos=fov_target.rect.center if fov_target else None,
|
|
1574
|
+
outside_cells=outside_cells,
|
|
1575
|
+
cell_size=game_data.cell_size,
|
|
1576
|
+
shadow_radius=player_shadow_radius,
|
|
1577
|
+
alpha=player_shadow_alpha,
|
|
1578
|
+
)
|
|
1579
|
+
if drew_shadow:
|
|
1580
|
+
screen.blit(shadow_layer, (0, 0))
|
|
1242
1581
|
_draw_footprints(
|
|
1243
1582
|
screen,
|
|
1244
1583
|
camera,
|
|
@@ -1302,6 +1641,13 @@ def draw(
|
|
|
1302
1641
|
)
|
|
1303
1642
|
if objective_lines:
|
|
1304
1643
|
_draw_objective(objective_lines, screen=screen)
|
|
1644
|
+
_draw_inventory_icons(
|
|
1645
|
+
screen,
|
|
1646
|
+
assets,
|
|
1647
|
+
has_fuel=has_fuel,
|
|
1648
|
+
flashlight_count=flashlight_count,
|
|
1649
|
+
shoes_count=shoes_count,
|
|
1650
|
+
)
|
|
1305
1651
|
_draw_survivor_messages(screen, assets, survivor_messages)
|
|
1306
1652
|
_draw_endurance_timer(screen, assets, stage=stage, state=state)
|
|
1307
1653
|
_draw_time_accel_indicator(screen, assets, stage=stage, state=state)
|
|
@@ -1314,6 +1660,7 @@ def draw(
|
|
|
1314
1660
|
debug_mode=state.debug_mode,
|
|
1315
1661
|
zombie_group=zombie_group,
|
|
1316
1662
|
falling_spawn_carry=state.falling_spawn_carry,
|
|
1663
|
+
fps=fps,
|
|
1317
1664
|
)
|
|
1318
1665
|
if do_flip:
|
|
1319
1666
|
if present_fn:
|