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/__about__.py
CHANGED
zombie_escape/colors.py
CHANGED
|
@@ -2,7 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
def _clamp(value: float) -> int:
|
|
7
|
+
return max(0, min(255, int(value)))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Basic palette.
|
|
6
11
|
WHITE: tuple[int, int, int] = (255, 255, 255)
|
|
7
12
|
BLACK: tuple[int, int, int] = (0, 0, 0)
|
|
8
13
|
RED: tuple[int, int, int] = (255, 0, 0)
|
|
@@ -13,8 +18,6 @@ LIGHT_GRAY: tuple[int, int, int] = (200, 200, 200)
|
|
|
13
18
|
YELLOW: tuple[int, int, int] = (255, 255, 0)
|
|
14
19
|
ORANGE: tuple[int, int, int] = (255, 165, 0)
|
|
15
20
|
DARK_RED: tuple[int, int, int] = (139, 0, 0)
|
|
16
|
-
TRACKER_OUTLINE_COLOR: tuple[int, int, int] = (170, 70, 220)
|
|
17
|
-
WALL_FOLLOWER_OUTLINE_COLOR: tuple[int, int, int] = (140, 140, 140)
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
@dataclass(frozen=True)
|
|
@@ -32,10 +35,6 @@ class EnvironmentPalette:
|
|
|
32
35
|
outer_wall_border: tuple[int, int, int]
|
|
33
36
|
|
|
34
37
|
|
|
35
|
-
def _clamp(value: float) -> int:
|
|
36
|
-
return max(0, min(255, int(value)))
|
|
37
|
-
|
|
38
|
-
|
|
39
38
|
def _adjust_color(
|
|
40
39
|
color: tuple[int, int, int], *, brightness: float = 1.0, saturation: float = 1.0
|
|
41
40
|
) -> tuple[int, int, int]:
|
|
@@ -56,6 +55,7 @@ DEFAULT_AMBIENT_PALETTE_KEY = "default"
|
|
|
56
55
|
NO_FLASHLIGHT_PALETTE_KEY = "no_flashlight"
|
|
57
56
|
DAWN_AMBIENT_PALETTE_KEY = "dawn"
|
|
58
57
|
|
|
58
|
+
|
|
59
59
|
# Base palette used throughout gameplay (matches the previous constants).
|
|
60
60
|
_DEFAULT_ENVIRONMENT_PALETTE = EnvironmentPalette(
|
|
61
61
|
floor_primary=(43, 57, 70),
|
|
@@ -72,10 +72,14 @@ _DEFAULT_ENVIRONMENT_PALETTE = EnvironmentPalette(
|
|
|
72
72
|
# Dark, desaturated palette that sells the "alone without a flashlight" vibe.
|
|
73
73
|
_GLOOM_ENVIRONMENT_PALETTE = EnvironmentPalette(
|
|
74
74
|
floor_primary=_adjust_color(
|
|
75
|
-
_DEFAULT_ENVIRONMENT_PALETTE.floor_primary,
|
|
75
|
+
_DEFAULT_ENVIRONMENT_PALETTE.floor_primary,
|
|
76
|
+
brightness=0.8,
|
|
77
|
+
saturation=0.75,
|
|
76
78
|
),
|
|
77
79
|
floor_secondary=_adjust_color(
|
|
78
|
-
_DEFAULT_ENVIRONMENT_PALETTE.floor_secondary,
|
|
80
|
+
_DEFAULT_ENVIRONMENT_PALETTE.floor_secondary,
|
|
81
|
+
brightness=0.8,
|
|
82
|
+
saturation=0.75,
|
|
79
83
|
),
|
|
80
84
|
fall_zone_primary=_adjust_color(
|
|
81
85
|
_DEFAULT_ENVIRONMENT_PALETTE.fall_zone_primary,
|
|
@@ -88,10 +92,14 @@ _GLOOM_ENVIRONMENT_PALETTE = EnvironmentPalette(
|
|
|
88
92
|
saturation=0.75,
|
|
89
93
|
),
|
|
90
94
|
outside=_adjust_color(
|
|
91
|
-
_DEFAULT_ENVIRONMENT_PALETTE.outside,
|
|
95
|
+
_DEFAULT_ENVIRONMENT_PALETTE.outside,
|
|
96
|
+
brightness=0.8,
|
|
97
|
+
saturation=0.75,
|
|
92
98
|
),
|
|
93
99
|
inner_wall=_adjust_color(
|
|
94
|
-
_DEFAULT_ENVIRONMENT_PALETTE.inner_wall,
|
|
100
|
+
_DEFAULT_ENVIRONMENT_PALETTE.inner_wall,
|
|
101
|
+
brightness=0.8,
|
|
102
|
+
saturation=0.75,
|
|
95
103
|
),
|
|
96
104
|
inner_wall_border=_adjust_color(
|
|
97
105
|
_DEFAULT_ENVIRONMENT_PALETTE.inner_wall_border,
|
|
@@ -99,7 +107,9 @@ _GLOOM_ENVIRONMENT_PALETTE = EnvironmentPalette(
|
|
|
99
107
|
saturation=0.75,
|
|
100
108
|
),
|
|
101
109
|
outer_wall=_adjust_color(
|
|
102
|
-
_DEFAULT_ENVIRONMENT_PALETTE.outer_wall,
|
|
110
|
+
_DEFAULT_ENVIRONMENT_PALETTE.outer_wall,
|
|
111
|
+
brightness=0.8,
|
|
112
|
+
saturation=0.75,
|
|
103
113
|
),
|
|
104
114
|
outer_wall_border=_adjust_color(
|
|
105
115
|
_DEFAULT_ENVIRONMENT_PALETTE.outer_wall_border,
|
|
@@ -181,8 +191,6 @@ __all__ = [
|
|
|
181
191
|
"YELLOW",
|
|
182
192
|
"ORANGE",
|
|
183
193
|
"DARK_RED",
|
|
184
|
-
"TRACKER_OUTLINE_COLOR",
|
|
185
|
-
"WALL_FOLLOWER_OUTLINE_COLOR",
|
|
186
194
|
"DAWN_AMBIENT_PALETTE_KEY",
|
|
187
195
|
"INTERNAL_WALL_COLOR",
|
|
188
196
|
"INTERNAL_WALL_BORDER_COLOR",
|
zombie_escape/entities.py
CHANGED
|
@@ -16,6 +16,7 @@ from pygame import rect
|
|
|
16
16
|
from .entities_constants import (
|
|
17
17
|
BUDDY_FOLLOW_SPEED,
|
|
18
18
|
BUDDY_RADIUS,
|
|
19
|
+
BUDDY_WALL_DAMAGE,
|
|
19
20
|
CAR_HEALTH,
|
|
20
21
|
CAR_HEIGHT,
|
|
21
22
|
CAR_SPEED,
|
|
@@ -26,11 +27,14 @@ from .entities_constants import (
|
|
|
26
27
|
FLASHLIGHT_WIDTH,
|
|
27
28
|
FUEL_CAN_HEIGHT,
|
|
28
29
|
FUEL_CAN_WIDTH,
|
|
30
|
+
HUMANOID_WALL_BUMP_FRAMES,
|
|
29
31
|
INTERNAL_WALL_BEVEL_DEPTH,
|
|
30
32
|
INTERNAL_WALL_HEALTH,
|
|
31
33
|
PLAYER_RADIUS,
|
|
32
34
|
PLAYER_SPEED,
|
|
33
35
|
PLAYER_WALL_DAMAGE,
|
|
36
|
+
SHOES_HEIGHT,
|
|
37
|
+
SHOES_WIDTH,
|
|
34
38
|
STEEL_BEAM_HEALTH,
|
|
35
39
|
SURVIVOR_APPROACH_RADIUS,
|
|
36
40
|
SURVIVOR_APPROACH_SPEED,
|
|
@@ -57,19 +61,24 @@ from .entities_constants import (
|
|
|
57
61
|
)
|
|
58
62
|
from .gameplay.constants import FOOTPRINT_STEP_DISTANCE
|
|
59
63
|
from .models import Footprint
|
|
64
|
+
from .render_constants import ANGLE_BINS, ZOMBIE_NOSE_COLOR
|
|
60
65
|
from .render_assets import (
|
|
61
66
|
EnvironmentPalette,
|
|
67
|
+
angle_bin_from_vector,
|
|
62
68
|
build_beveled_polygon,
|
|
69
|
+
build_car_directional_surfaces,
|
|
63
70
|
build_car_surface,
|
|
64
71
|
build_flashlight_surface,
|
|
65
72
|
build_fuel_can_surface,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
build_player_directional_surfaces,
|
|
74
|
+
build_shoes_surface,
|
|
75
|
+
build_survivor_directional_surfaces,
|
|
76
|
+
build_zombie_directional_surfaces,
|
|
77
|
+
draw_humanoid_hand,
|
|
78
|
+
draw_humanoid_nose,
|
|
69
79
|
paint_car_surface,
|
|
70
80
|
paint_steel_beam_surface,
|
|
71
81
|
paint_wall_surface,
|
|
72
|
-
paint_zombie_surface,
|
|
73
82
|
resolve_car_color,
|
|
74
83
|
resolve_steel_beam_colors,
|
|
75
84
|
resolve_wall_colors,
|
|
@@ -226,6 +235,14 @@ class SteelBeam(pygame.sprite.Sprite):
|
|
|
226
235
|
)
|
|
227
236
|
|
|
228
237
|
|
|
238
|
+
def _is_inner_wall(wall: pygame.sprite.Sprite) -> bool:
|
|
239
|
+
if isinstance(wall, SteelBeam):
|
|
240
|
+
return True
|
|
241
|
+
if isinstance(wall, Wall):
|
|
242
|
+
return wall.palette_category == "inner_wall"
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
229
246
|
MovementStrategy = Callable[
|
|
230
247
|
[
|
|
231
248
|
"Zombie",
|
|
@@ -239,6 +256,8 @@ MovementStrategy = Callable[
|
|
|
239
256
|
],
|
|
240
257
|
tuple[float, float],
|
|
241
258
|
]
|
|
259
|
+
|
|
260
|
+
|
|
242
261
|
def _sprite_center_and_radius(
|
|
243
262
|
sprite: pygame.sprite.Sprite,
|
|
244
263
|
) -> tuple[tuple[int, int], float]:
|
|
@@ -564,7 +583,14 @@ class Player(pygame.sprite.Sprite):
|
|
|
564
583
|
) -> None:
|
|
565
584
|
super().__init__()
|
|
566
585
|
self.radius = PLAYER_RADIUS
|
|
567
|
-
self.
|
|
586
|
+
self.facing_bin = 0
|
|
587
|
+
self.input_facing_bin = 0
|
|
588
|
+
self.wall_bump_counter = 0
|
|
589
|
+
self.wall_bump_flip = 1
|
|
590
|
+
self.inner_wall_hit = False
|
|
591
|
+
self.inner_wall_cell = None
|
|
592
|
+
self.directional_images = build_player_directional_surfaces(self.radius)
|
|
593
|
+
self.image = self.directional_images[self.facing_bin]
|
|
568
594
|
self.rect = self.image.get_rect(center=(x, y))
|
|
569
595
|
self.speed = PLAYER_SPEED
|
|
570
596
|
self.in_car = False
|
|
@@ -588,6 +614,9 @@ class Player(pygame.sprite.Sprite):
|
|
|
588
614
|
if level_width is None or level_height is None:
|
|
589
615
|
raise ValueError("level_width/level_height are required for movement")
|
|
590
616
|
|
|
617
|
+
inner_wall_hit = False
|
|
618
|
+
inner_wall_cell: tuple[int, int] | None = None
|
|
619
|
+
|
|
591
620
|
if dx != 0:
|
|
592
621
|
self.x += dx
|
|
593
622
|
self.x = min(level_width, max(0, self.x))
|
|
@@ -603,6 +632,13 @@ class Player(pygame.sprite.Sprite):
|
|
|
603
632
|
for wall in hit_list_x:
|
|
604
633
|
if wall.alive():
|
|
605
634
|
wall._take_damage(amount=damage)
|
|
635
|
+
if _is_inner_wall(wall):
|
|
636
|
+
inner_wall_hit = True
|
|
637
|
+
if inner_wall_cell is None and cell_size:
|
|
638
|
+
inner_wall_cell = (
|
|
639
|
+
int(wall.rect.centerx // cell_size),
|
|
640
|
+
int(wall.rect.centery // cell_size),
|
|
641
|
+
)
|
|
606
642
|
self.x -= dx * 1.5
|
|
607
643
|
self.rect.centerx = int(self.x)
|
|
608
644
|
|
|
@@ -621,10 +657,51 @@ class Player(pygame.sprite.Sprite):
|
|
|
621
657
|
for wall in hit_list_y:
|
|
622
658
|
if wall.alive():
|
|
623
659
|
wall._take_damage(amount=damage)
|
|
660
|
+
if _is_inner_wall(wall):
|
|
661
|
+
inner_wall_hit = True
|
|
662
|
+
if inner_wall_cell is None and cell_size:
|
|
663
|
+
inner_wall_cell = (
|
|
664
|
+
int(wall.rect.centerx // cell_size),
|
|
665
|
+
int(wall.rect.centery // cell_size),
|
|
666
|
+
)
|
|
624
667
|
self.y -= dy * 1.5
|
|
625
668
|
self.rect.centery = int(self.y)
|
|
626
669
|
|
|
627
670
|
self.rect.center = (int(self.x), int(self.y))
|
|
671
|
+
self.inner_wall_hit = inner_wall_hit
|
|
672
|
+
self.inner_wall_cell = inner_wall_cell
|
|
673
|
+
self._update_facing_for_bump(inner_wall_hit)
|
|
674
|
+
|
|
675
|
+
def update_facing_from_input(self: Self, dx: float, dy: float) -> None:
|
|
676
|
+
if self.in_car:
|
|
677
|
+
return
|
|
678
|
+
new_bin = angle_bin_from_vector(dx, dy)
|
|
679
|
+
if new_bin is None:
|
|
680
|
+
return
|
|
681
|
+
self.input_facing_bin = new_bin
|
|
682
|
+
|
|
683
|
+
def _update_facing_for_bump(self: Self, inner_wall_hit: bool) -> None:
|
|
684
|
+
if self.in_car:
|
|
685
|
+
return
|
|
686
|
+
if inner_wall_hit:
|
|
687
|
+
self.wall_bump_counter += 1
|
|
688
|
+
if self.wall_bump_counter % HUMANOID_WALL_BUMP_FRAMES == 0:
|
|
689
|
+
self.wall_bump_flip *= -1
|
|
690
|
+
bumped_bin = (self.input_facing_bin + self.wall_bump_flip) % ANGLE_BINS
|
|
691
|
+
self._set_facing_bin(bumped_bin)
|
|
692
|
+
return
|
|
693
|
+
if self.wall_bump_counter:
|
|
694
|
+
self.wall_bump_counter = 0
|
|
695
|
+
self.wall_bump_flip = 1
|
|
696
|
+
self._set_facing_bin(self.input_facing_bin)
|
|
697
|
+
|
|
698
|
+
def _set_facing_bin(self: Self, new_bin: int) -> None:
|
|
699
|
+
if new_bin == self.facing_bin:
|
|
700
|
+
return
|
|
701
|
+
center = self.rect.center
|
|
702
|
+
self.facing_bin = new_bin
|
|
703
|
+
self.image = self.directional_images[self.facing_bin]
|
|
704
|
+
self.rect = self.image.get_rect(center=center)
|
|
628
705
|
|
|
629
706
|
|
|
630
707
|
class Survivor(pygame.sprite.Sprite):
|
|
@@ -640,10 +717,16 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
640
717
|
super().__init__()
|
|
641
718
|
self.is_buddy = is_buddy
|
|
642
719
|
self.radius = BUDDY_RADIUS if is_buddy else SURVIVOR_RADIUS
|
|
643
|
-
self.
|
|
720
|
+
self.facing_bin = 0
|
|
721
|
+
self.input_facing_bin = 0
|
|
722
|
+
self.wall_bump_counter = 0
|
|
723
|
+
self.wall_bump_flip = 1
|
|
724
|
+
self.directional_images = build_survivor_directional_surfaces(
|
|
644
725
|
self.radius,
|
|
645
726
|
is_buddy=is_buddy,
|
|
727
|
+
draw_hands=is_buddy,
|
|
646
728
|
)
|
|
729
|
+
self.image = self.directional_images[self.facing_bin]
|
|
647
730
|
self.rect = self.image.get_rect(center=(int(x), int(y)))
|
|
648
731
|
self.x = float(self.rect.centerx)
|
|
649
732
|
self.y = float(self.rect.centery)
|
|
@@ -680,6 +763,7 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
680
763
|
grid_rows: int | None = None,
|
|
681
764
|
level_width: int | None = None,
|
|
682
765
|
level_height: int | None = None,
|
|
766
|
+
wall_target_cell: tuple[int, int] | None = None,
|
|
683
767
|
) -> None:
|
|
684
768
|
if level_width is None or level_height is None:
|
|
685
769
|
raise ValueError("level_width/level_height are required for movement")
|
|
@@ -688,11 +772,19 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
688
772
|
self.rect.center = (int(self.x), int(self.y))
|
|
689
773
|
return
|
|
690
774
|
|
|
691
|
-
|
|
692
|
-
|
|
775
|
+
target_pos = player_pos
|
|
776
|
+
if wall_target_cell is not None and cell_size is not None:
|
|
777
|
+
target_pos = (
|
|
778
|
+
wall_target_cell[0] * cell_size + cell_size // 2,
|
|
779
|
+
wall_target_cell[1] * cell_size + cell_size // 2,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
dx = target_pos[0] - self.x
|
|
783
|
+
dy = target_pos[1] - self.y
|
|
693
784
|
dist_sq = dx * dx + dy * dy
|
|
694
785
|
if dist_sq <= 0:
|
|
695
786
|
self.rect.center = (int(self.x), int(self.y))
|
|
787
|
+
self._update_facing_for_bump(False)
|
|
696
788
|
return
|
|
697
789
|
|
|
698
790
|
dist = math.sqrt(dist_sq)
|
|
@@ -717,32 +809,45 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
717
809
|
grid_rows=grid_rows,
|
|
718
810
|
)
|
|
719
811
|
|
|
812
|
+
self._update_input_facing(move_x, move_y)
|
|
813
|
+
inner_wall_hit = False
|
|
814
|
+
|
|
720
815
|
if move_x:
|
|
721
816
|
self.x += move_x
|
|
722
817
|
self.rect.centerx = int(self.x)
|
|
723
|
-
|
|
818
|
+
wall = spritecollideany_walls(
|
|
724
819
|
self,
|
|
725
820
|
walls,
|
|
726
821
|
wall_index=wall_index,
|
|
727
822
|
cell_size=cell_size,
|
|
728
|
-
)
|
|
823
|
+
)
|
|
824
|
+
if wall:
|
|
825
|
+
if wall.alive():
|
|
826
|
+
wall._take_damage(amount=max(1, BUDDY_WALL_DAMAGE))
|
|
827
|
+
if _is_inner_wall(wall):
|
|
828
|
+
inner_wall_hit = True
|
|
729
829
|
self.x -= move_x
|
|
730
830
|
self.rect.centerx = int(self.x)
|
|
731
831
|
if move_y:
|
|
732
832
|
self.y += move_y
|
|
733
833
|
self.rect.centery = int(self.y)
|
|
734
|
-
|
|
834
|
+
wall = spritecollideany_walls(
|
|
735
835
|
self,
|
|
736
836
|
walls,
|
|
737
837
|
wall_index=wall_index,
|
|
738
838
|
cell_size=cell_size,
|
|
739
|
-
)
|
|
839
|
+
)
|
|
840
|
+
if wall:
|
|
841
|
+
if wall.alive():
|
|
842
|
+
wall._take_damage(amount=max(1, BUDDY_WALL_DAMAGE))
|
|
843
|
+
if _is_inner_wall(wall):
|
|
844
|
+
inner_wall_hit = True
|
|
740
845
|
self.y -= move_y
|
|
741
846
|
self.rect.centery = int(self.y)
|
|
742
847
|
|
|
743
848
|
overlap_radius = (self.radius + PLAYER_RADIUS) * 1.05
|
|
744
|
-
dx_after =
|
|
745
|
-
dy_after =
|
|
849
|
+
dx_after = target_pos[0] - self.x
|
|
850
|
+
dy_after = target_pos[1] - self.y
|
|
746
851
|
dist_after_sq = dx_after * dx_after + dy_after * dy_after
|
|
747
852
|
if 0 < dist_after_sq < overlap_radius * overlap_radius:
|
|
748
853
|
dist_after = math.sqrt(dist_after_sq)
|
|
@@ -754,6 +859,7 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
754
859
|
self.x = min(level_width, max(0, self.x))
|
|
755
860
|
self.y = min(level_height, max(0, self.y))
|
|
756
861
|
self.rect.center = (int(self.x), int(self.y))
|
|
862
|
+
self._update_facing_for_bump(inner_wall_hit)
|
|
757
863
|
return
|
|
758
864
|
|
|
759
865
|
dx = player_pos[0] - self.x
|
|
@@ -769,6 +875,8 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
769
875
|
move_x = (dx / dist) * SURVIVOR_APPROACH_SPEED
|
|
770
876
|
move_y = (dy / dist) * SURVIVOR_APPROACH_SPEED
|
|
771
877
|
|
|
878
|
+
self._update_input_facing(move_x, move_y)
|
|
879
|
+
|
|
772
880
|
if (
|
|
773
881
|
cell_size is not None
|
|
774
882
|
and wall_cells is not None
|
|
@@ -811,6 +919,37 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
811
919
|
self.rect.centery = int(self.y)
|
|
812
920
|
|
|
813
921
|
self.rect.center = (int(self.x), int(self.y))
|
|
922
|
+
self._update_facing_for_bump(False)
|
|
923
|
+
|
|
924
|
+
def _update_input_facing(self: Self, dx: float, dy: float) -> None:
|
|
925
|
+
new_bin = angle_bin_from_vector(dx, dy)
|
|
926
|
+
if new_bin is None:
|
|
927
|
+
return
|
|
928
|
+
self.input_facing_bin = new_bin
|
|
929
|
+
|
|
930
|
+
def _update_facing_for_bump(self: Self, inner_wall_hit: bool) -> None:
|
|
931
|
+
if not self.is_buddy:
|
|
932
|
+
self._set_facing_bin(self.input_facing_bin)
|
|
933
|
+
return
|
|
934
|
+
if inner_wall_hit:
|
|
935
|
+
self.wall_bump_counter += 1
|
|
936
|
+
if self.wall_bump_counter % HUMANOID_WALL_BUMP_FRAMES == 0:
|
|
937
|
+
self.wall_bump_flip *= -1
|
|
938
|
+
bumped_bin = (self.input_facing_bin + self.wall_bump_flip) % ANGLE_BINS
|
|
939
|
+
self._set_facing_bin(bumped_bin)
|
|
940
|
+
return
|
|
941
|
+
if self.wall_bump_counter:
|
|
942
|
+
self.wall_bump_counter = 0
|
|
943
|
+
self.wall_bump_flip = 1
|
|
944
|
+
self._set_facing_bin(self.input_facing_bin)
|
|
945
|
+
|
|
946
|
+
def _set_facing_bin(self: Self, new_bin: int) -> None:
|
|
947
|
+
if new_bin == self.facing_bin:
|
|
948
|
+
return
|
|
949
|
+
center = self.rect.center
|
|
950
|
+
self.facing_bin = new_bin
|
|
951
|
+
self.image = self.directional_images[self.facing_bin]
|
|
952
|
+
self.rect = self.image.get_rect(center=center)
|
|
814
953
|
|
|
815
954
|
|
|
816
955
|
def random_position_outside_building(
|
|
@@ -1221,13 +1360,15 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1221
1360
|
) -> None:
|
|
1222
1361
|
super().__init__()
|
|
1223
1362
|
self.radius = ZOMBIE_RADIUS
|
|
1363
|
+
self.facing_bin = 0
|
|
1224
1364
|
self.tracker = tracker
|
|
1225
1365
|
self.wall_follower = wall_follower
|
|
1226
1366
|
self.carbonized = False
|
|
1227
|
-
self.
|
|
1228
|
-
self.radius,
|
|
1367
|
+
self.directional_images = build_zombie_directional_surfaces(
|
|
1368
|
+
self.radius,
|
|
1369
|
+
draw_hands=False,
|
|
1229
1370
|
)
|
|
1230
|
-
self.
|
|
1371
|
+
self.image = self.directional_images[self.facing_bin]
|
|
1231
1372
|
self.rect = self.image.get_rect(center=(x, y))
|
|
1232
1373
|
jitter_base = FAST_ZOMBIE_BASE_SPEED if speed > ZOMBIE_SPEED else ZOMBIE_SPEED
|
|
1233
1374
|
jitter = jitter_base * 0.2
|
|
@@ -1266,15 +1407,6 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1266
1407
|
0, self.wander_interval_ms + RNG.randint(-500, 500)
|
|
1267
1408
|
)
|
|
1268
1409
|
|
|
1269
|
-
def _redraw_image(self: Self, palm_angle: float | None = None) -> None:
|
|
1270
|
-
paint_zombie_surface(
|
|
1271
|
-
self.image,
|
|
1272
|
-
radius=self.radius,
|
|
1273
|
-
palm_angle=palm_angle,
|
|
1274
|
-
tracker=self.tracker,
|
|
1275
|
-
wall_follower=self.wall_follower,
|
|
1276
|
-
)
|
|
1277
|
-
|
|
1278
1410
|
def _update_mode(
|
|
1279
1411
|
self: Self, player_center: tuple[int, int], sight_range: float
|
|
1280
1412
|
) -> bool:
|
|
@@ -1381,6 +1513,53 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1381
1513
|
slowdown_ratio = 1.0 - progress * (1.0 - ZOMBIE_AGING_MIN_SPEED_RATIO)
|
|
1382
1514
|
self.speed = self.initial_speed * slowdown_ratio
|
|
1383
1515
|
|
|
1516
|
+
def _set_facing_bin(self: Self, new_bin: int) -> None:
|
|
1517
|
+
if new_bin == self.facing_bin:
|
|
1518
|
+
return
|
|
1519
|
+
center = self.rect.center
|
|
1520
|
+
self.facing_bin = new_bin
|
|
1521
|
+
self.image = self.directional_images[self.facing_bin]
|
|
1522
|
+
self.rect = self.image.get_rect(center=center)
|
|
1523
|
+
|
|
1524
|
+
def _update_facing_from_movement(self: Self, dx: float, dy: float) -> None:
|
|
1525
|
+
new_bin = angle_bin_from_vector(dx, dy)
|
|
1526
|
+
if new_bin is None:
|
|
1527
|
+
return
|
|
1528
|
+
self._set_facing_bin(new_bin)
|
|
1529
|
+
|
|
1530
|
+
def _apply_render_overlays(self: Self) -> None:
|
|
1531
|
+
base_surface = self.directional_images[self.facing_bin]
|
|
1532
|
+
needs_overlay = self.tracker or (
|
|
1533
|
+
self.wall_follower
|
|
1534
|
+
and self.wall_follow_side != 0
|
|
1535
|
+
and self.wall_follow_last_side_has_wall
|
|
1536
|
+
)
|
|
1537
|
+
if not needs_overlay:
|
|
1538
|
+
self.image = base_surface
|
|
1539
|
+
return
|
|
1540
|
+
self.image = base_surface.copy()
|
|
1541
|
+
angle_rad = (self.facing_bin % ANGLE_BINS) * (math.tau / ANGLE_BINS)
|
|
1542
|
+
if self.tracker:
|
|
1543
|
+
draw_humanoid_nose(
|
|
1544
|
+
self.image,
|
|
1545
|
+
radius=self.radius,
|
|
1546
|
+
angle_rad=angle_rad,
|
|
1547
|
+
color=ZOMBIE_NOSE_COLOR,
|
|
1548
|
+
)
|
|
1549
|
+
if (
|
|
1550
|
+
self.wall_follower
|
|
1551
|
+
and self.wall_follow_side != 0
|
|
1552
|
+
and self.wall_follow_last_side_has_wall
|
|
1553
|
+
):
|
|
1554
|
+
side_sign = 1.0 if self.wall_follow_side > 0 else -1.0
|
|
1555
|
+
hand_angle = angle_rad + side_sign * (math.pi / 2.0)
|
|
1556
|
+
draw_humanoid_hand(
|
|
1557
|
+
self.image,
|
|
1558
|
+
radius=self.radius,
|
|
1559
|
+
angle_rad=hand_angle,
|
|
1560
|
+
color=ZOMBIE_NOSE_COLOR,
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1384
1563
|
def _update_stuck_state(self: Self) -> None:
|
|
1385
1564
|
history = self.pos_history
|
|
1386
1565
|
history.append((self.x, self.y))
|
|
@@ -1450,18 +1629,8 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1450
1629
|
grid_cols=grid_cols,
|
|
1451
1630
|
grid_rows=grid_rows,
|
|
1452
1631
|
)
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
heading = math.atan2(move_y, move_x)
|
|
1456
|
-
elif self.wall_follow_angle is not None:
|
|
1457
|
-
heading = self.wall_follow_angle
|
|
1458
|
-
else:
|
|
1459
|
-
heading = self.wander_angle
|
|
1460
|
-
if self.wall_follow_side > 0:
|
|
1461
|
-
palm_angle = heading + (math.pi / 2.0)
|
|
1462
|
-
else:
|
|
1463
|
-
palm_angle = heading - (math.pi / 2.0)
|
|
1464
|
-
self._redraw_image(palm_angle)
|
|
1632
|
+
self._update_facing_from_movement(move_x, move_y)
|
|
1633
|
+
self._apply_render_overlays()
|
|
1465
1634
|
final_x, final_y = self._handle_wall_collision(
|
|
1466
1635
|
self.x + move_x, self.y + move_y, walls
|
|
1467
1636
|
)
|
|
@@ -1479,23 +1648,31 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1479
1648
|
return
|
|
1480
1649
|
self.carbonized = True
|
|
1481
1650
|
self.speed = 0
|
|
1651
|
+
self.image = self.directional_images[self.facing_bin].copy()
|
|
1482
1652
|
self.image.fill((0, 0, 0, 0))
|
|
1483
1653
|
color = (80, 80, 80)
|
|
1484
|
-
|
|
1654
|
+
center = self.image.get_rect().center
|
|
1655
|
+
pygame.draw.circle(self.image, color, center, self.radius)
|
|
1485
1656
|
pygame.draw.circle(
|
|
1486
|
-
self.image,
|
|
1657
|
+
self.image,
|
|
1658
|
+
(30, 30, 30),
|
|
1659
|
+
center,
|
|
1660
|
+
self.radius,
|
|
1661
|
+
width=1,
|
|
1487
1662
|
)
|
|
1488
1663
|
|
|
1489
1664
|
|
|
1490
1665
|
class Car(pygame.sprite.Sprite):
|
|
1491
1666
|
def __init__(self: Self, x: int, y: int, *, appearance: str = "default") -> None:
|
|
1492
1667
|
super().__init__()
|
|
1668
|
+
self.facing_bin = ANGLE_BINS * 3 // 4
|
|
1669
|
+
self.input_facing_bin = self.facing_bin
|
|
1493
1670
|
self.original_image = build_car_surface(CAR_WIDTH, CAR_HEIGHT)
|
|
1671
|
+
self.directional_images: list[pygame.Surface] = []
|
|
1494
1672
|
self.appearance = appearance
|
|
1495
1673
|
self.image = self.original_image.copy()
|
|
1496
1674
|
self.rect = self.image.get_rect(center=(x, y))
|
|
1497
1675
|
self.speed = CAR_SPEED
|
|
1498
|
-
self.angle = 0
|
|
1499
1676
|
self.x = float(self.rect.centerx)
|
|
1500
1677
|
self.y = float(self.rect.centery)
|
|
1501
1678
|
self.health = CAR_HEALTH
|
|
@@ -1517,10 +1694,28 @@ class Car(pygame.sprite.Sprite):
|
|
|
1517
1694
|
height=CAR_HEIGHT,
|
|
1518
1695
|
color=color,
|
|
1519
1696
|
)
|
|
1520
|
-
self.
|
|
1697
|
+
self.directional_images = build_car_directional_surfaces(self.original_image)
|
|
1698
|
+
self.image = self.directional_images[self.facing_bin]
|
|
1521
1699
|
old_center = self.rect.center
|
|
1522
1700
|
self.rect = self.image.get_rect(center=old_center)
|
|
1523
1701
|
|
|
1702
|
+
def update_facing_from_input(self: Self, dx: float, dy: float) -> None:
|
|
1703
|
+
new_bin = angle_bin_from_vector(dx, dy)
|
|
1704
|
+
if new_bin is None:
|
|
1705
|
+
return
|
|
1706
|
+
self.input_facing_bin = new_bin
|
|
1707
|
+
self._set_facing_bin(self.input_facing_bin)
|
|
1708
|
+
|
|
1709
|
+
def _set_facing_bin(self: Self, new_bin: int) -> None:
|
|
1710
|
+
if new_bin == self.facing_bin:
|
|
1711
|
+
return
|
|
1712
|
+
if not self.directional_images:
|
|
1713
|
+
return
|
|
1714
|
+
center = self.rect.center
|
|
1715
|
+
self.facing_bin = new_bin
|
|
1716
|
+
self.image = self.directional_images[self.facing_bin]
|
|
1717
|
+
self.rect = self.image.get_rect(center=center)
|
|
1718
|
+
|
|
1524
1719
|
def move(
|
|
1525
1720
|
self: Self,
|
|
1526
1721
|
dx: float,
|
|
@@ -1534,11 +1729,6 @@ class Car(pygame.sprite.Sprite):
|
|
|
1534
1729
|
if dx == 0 and dy == 0:
|
|
1535
1730
|
self.rect.center = (int(self.x), int(self.y))
|
|
1536
1731
|
return
|
|
1537
|
-
target_angle = math.degrees(math.atan2(-dy, dx)) - 90
|
|
1538
|
-
self.angle = target_angle
|
|
1539
|
-
self.image = pygame.transform.rotate(self.original_image, self.angle)
|
|
1540
|
-
old_center = (self.x, self.y)
|
|
1541
|
-
self.rect = self.image.get_rect(center=old_center)
|
|
1542
1732
|
new_x = self.x + dx
|
|
1543
1733
|
new_y = self.y + dy
|
|
1544
1734
|
|
|
@@ -1589,6 +1779,15 @@ class Flashlight(pygame.sprite.Sprite):
|
|
|
1589
1779
|
self.rect = self.image.get_rect(center=(x, y))
|
|
1590
1780
|
|
|
1591
1781
|
|
|
1782
|
+
class Shoes(pygame.sprite.Sprite):
|
|
1783
|
+
"""Shoes pickup that boosts the player's move speed when collected."""
|
|
1784
|
+
|
|
1785
|
+
def __init__(self: Self, x: int, y: int) -> None:
|
|
1786
|
+
super().__init__()
|
|
1787
|
+
self.image = build_shoes_surface(SHOES_WIDTH, SHOES_HEIGHT)
|
|
1788
|
+
self.rect = self.image.get_rect(center=(x, y))
|
|
1789
|
+
|
|
1790
|
+
|
|
1592
1791
|
def _car_body_radius(width: float, height: float) -> float:
|
|
1593
1792
|
"""Approximate car collision radius using only its own dimensions."""
|
|
1594
1793
|
return min(width, height) / 2
|
|
@@ -1604,5 +1803,6 @@ __all__ = [
|
|
|
1604
1803
|
"Car",
|
|
1605
1804
|
"FuelCan",
|
|
1606
1805
|
"Flashlight",
|
|
1806
|
+
"Shoes",
|
|
1607
1807
|
"random_position_outside_building",
|
|
1608
1808
|
]
|
|
@@ -11,6 +11,7 @@ PLAYER_SPEED = 1.4
|
|
|
11
11
|
FOV_RADIUS = 124 # approximate legacy FOV (80) * 1.55 cap
|
|
12
12
|
BUDDY_RADIUS = HUMANOID_RADIUS
|
|
13
13
|
BUDDY_FOLLOW_SPEED = PLAYER_SPEED * 0.7
|
|
14
|
+
HUMANOID_WALL_BUMP_FRAMES = 7
|
|
14
15
|
|
|
15
16
|
# --- Survivor settings (Stage 4) ---
|
|
16
17
|
SURVIVOR_RADIUS = HUMANOID_RADIUS
|
|
@@ -20,8 +21,12 @@ SURVIVOR_MAX_SAFE_PASSENGERS = 5
|
|
|
20
21
|
SURVIVOR_MIN_SPEED_FACTOR = 0.35
|
|
21
22
|
|
|
22
23
|
# --- Flashlight settings ---
|
|
23
|
-
FLASHLIGHT_WIDTH =
|
|
24
|
-
FLASHLIGHT_HEIGHT =
|
|
24
|
+
FLASHLIGHT_WIDTH = 12
|
|
25
|
+
FLASHLIGHT_HEIGHT = 10
|
|
26
|
+
|
|
27
|
+
# --- Shoes settings ---
|
|
28
|
+
SHOES_WIDTH = 14
|
|
29
|
+
SHOES_HEIGHT = 12
|
|
25
30
|
|
|
26
31
|
# --- Zombie settings ---
|
|
27
32
|
ZOMBIE_RADIUS = HUMANOID_RADIUS
|
|
@@ -45,12 +50,12 @@ ZOMBIE_WALL_FOLLOW_TARGET_GAP = 4.0
|
|
|
45
50
|
ZOMBIE_WALL_FOLLOW_LOST_WALL_MS = 2500
|
|
46
51
|
|
|
47
52
|
# --- Car and fuel settings ---
|
|
48
|
-
CAR_WIDTH =
|
|
49
|
-
CAR_HEIGHT =
|
|
53
|
+
CAR_WIDTH = 16
|
|
54
|
+
CAR_HEIGHT = 24
|
|
50
55
|
CAR_SPEED = 2
|
|
51
56
|
CAR_HEALTH = 20
|
|
52
57
|
CAR_WALL_DAMAGE = 1
|
|
53
|
-
FUEL_CAN_WIDTH =
|
|
58
|
+
FUEL_CAN_WIDTH = 12
|
|
54
59
|
FUEL_CAN_HEIGHT = 15
|
|
55
60
|
|
|
56
61
|
# --- Wall and beam settings ---
|
|
@@ -58,6 +63,7 @@ INTERNAL_WALL_HEALTH = 40 * 100
|
|
|
58
63
|
INTERNAL_WALL_BEVEL_DEPTH = 6
|
|
59
64
|
STEEL_BEAM_HEALTH = int(INTERNAL_WALL_HEALTH * 1.5)
|
|
60
65
|
PLAYER_WALL_DAMAGE = 100
|
|
66
|
+
BUDDY_WALL_DAMAGE = int(PLAYER_WALL_DAMAGE * 0.7)
|
|
61
67
|
ZOMBIE_WALL_DAMAGE = 1
|
|
62
68
|
|
|
63
69
|
__all__ = [
|
|
@@ -67,6 +73,7 @@ __all__ = [
|
|
|
67
73
|
"FOV_RADIUS",
|
|
68
74
|
"BUDDY_RADIUS",
|
|
69
75
|
"BUDDY_FOLLOW_SPEED",
|
|
76
|
+
"HUMANOID_WALL_BUMP_FRAMES",
|
|
70
77
|
"SURVIVOR_RADIUS",
|
|
71
78
|
"SURVIVOR_APPROACH_RADIUS",
|
|
72
79
|
"SURVIVOR_APPROACH_SPEED",
|
|
@@ -74,6 +81,8 @@ __all__ = [
|
|
|
74
81
|
"SURVIVOR_MIN_SPEED_FACTOR",
|
|
75
82
|
"FLASHLIGHT_WIDTH",
|
|
76
83
|
"FLASHLIGHT_HEIGHT",
|
|
84
|
+
"SHOES_WIDTH",
|
|
85
|
+
"SHOES_HEIGHT",
|
|
77
86
|
"ZOMBIE_RADIUS",
|
|
78
87
|
"ZOMBIE_SPEED",
|
|
79
88
|
"ZOMBIE_WANDER_INTERVAL_MS",
|
|
@@ -104,5 +113,6 @@ __all__ = [
|
|
|
104
113
|
"INTERNAL_WALL_BEVEL_DEPTH",
|
|
105
114
|
"STEEL_BEAM_HEALTH",
|
|
106
115
|
"PLAYER_WALL_DAMAGE",
|
|
116
|
+
"BUDDY_WALL_DAMAGE",
|
|
107
117
|
"ZOMBIE_WALL_DAMAGE",
|
|
108
118
|
]
|