zombie-escape 1.5.0__tar.gz → 1.5.4__tar.gz
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-1.5.0 → zombie_escape-1.5.4}/PKG-INFO +2 -6
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/README.md +1 -5
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/__about__.py +1 -1
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/colors.py +4 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/entities.py +223 -102
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/gameplay/logic.py +240 -131
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/gameplay_constants.py +13 -7
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/locales/ui.en.json +5 -2
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/locales/ui.ja.json +7 -4
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/models.py +10 -5
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/render.py +37 -86
- zombie_escape-1.5.4/src/zombie_escape/screens/__init__.py +222 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/screens/game_over.py +21 -8
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/screens/gameplay.py +25 -13
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/screens/settings.py +12 -1
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/screens/title.py +34 -2
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/stage_constants.py +5 -3
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/zombie_escape.py +46 -11
- zombie_escape-1.5.0/src/zombie_escape/screens/__init__.py +0 -109
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/.gitignore +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/LICENSE.txt +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/pyproject.toml +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/__init__.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/config.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/font_utils.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/gameplay/__init__.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/level_blueprints.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/level_constants.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/localization.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/progress.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/render_assets.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/render_constants.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/rng.py +0 -0
- {zombie_escape-1.5.0 → zombie_escape-1.5.4}/src/zombie_escape/screen_constants.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zombie-escape
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4
|
|
4
4
|
Summary: Top-down zombie survival game built with pygame.
|
|
5
5
|
Project-URL: Homepage, https://github.com/tos-kamiya/zombie-escape
|
|
6
6
|
Author-email: Toshihiro Kamiya <kamiya@mbj.nifty.com>
|
|
@@ -48,7 +48,7 @@ This game is a simple 2D top-down action game where the player aims to escape by
|
|
|
48
48
|
- **Enter Car:** Overlap the player with the car.
|
|
49
49
|
- **Quit Game:** `ESC` key
|
|
50
50
|
- **Restart:** `R` key (on Game Over/Clear screen)
|
|
51
|
-
- **Window
|
|
51
|
+
- **Window/Fullscreen (title/settings only):** `[` to shrink, `]` to enlarge, `F` to toggle fullscreen
|
|
52
52
|
- **Time Acceleration:** Hold either `Shift` key to run the entire world 4x faster; release to return to normal speed.
|
|
53
53
|
|
|
54
54
|
## Title Screen
|
|
@@ -142,10 +142,6 @@ Launch using the following command line:
|
|
|
142
142
|
zombie-escape
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
### Debug Mode
|
|
146
|
-
|
|
147
|
-
<!-- For Stage 5 balancing or capture needs, run `zombie-escape --debug`. This hides the pause overlay and starts survival stages with 3 minutes remaining so you can immediately test late-night behavior. -->
|
|
148
|
-
|
|
149
145
|
## License
|
|
150
146
|
|
|
151
147
|
This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details.
|
|
@@ -26,7 +26,7 @@ This game is a simple 2D top-down action game where the player aims to escape by
|
|
|
26
26
|
- **Enter Car:** Overlap the player with the car.
|
|
27
27
|
- **Quit Game:** `ESC` key
|
|
28
28
|
- **Restart:** `R` key (on Game Over/Clear screen)
|
|
29
|
-
- **Window
|
|
29
|
+
- **Window/Fullscreen (title/settings only):** `[` to shrink, `]` to enlarge, `F` to toggle fullscreen
|
|
30
30
|
- **Time Acceleration:** Hold either `Shift` key to run the entire world 4x faster; release to return to normal speed.
|
|
31
31
|
|
|
32
32
|
## Title Screen
|
|
@@ -120,10 +120,6 @@ Launch using the following command line:
|
|
|
120
120
|
zombie-escape
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
### Debug Mode
|
|
124
|
-
|
|
125
|
-
<!-- For Stage 5 balancing or capture needs, run `zombie-escape --debug`. This hides the pause overlay and starts survival stages with 3 minutes remaining so you can immediately test late-night behavior. -->
|
|
126
|
-
|
|
127
123
|
## License
|
|
128
124
|
|
|
129
125
|
This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details.
|
|
@@ -13,6 +13,8 @@ LIGHT_GRAY: tuple[int, int, int] = (200, 200, 200)
|
|
|
13
13
|
YELLOW: tuple[int, int, int] = (255, 255, 0)
|
|
14
14
|
ORANGE: tuple[int, int, int] = (255, 165, 0)
|
|
15
15
|
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)
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@dataclass(frozen=True)
|
|
@@ -146,6 +148,8 @@ __all__ = [
|
|
|
146
148
|
"YELLOW",
|
|
147
149
|
"ORANGE",
|
|
148
150
|
"DARK_RED",
|
|
151
|
+
"TRACKER_OUTLINE_COLOR",
|
|
152
|
+
"WALL_FOLLOWER_OUTLINE_COLOR",
|
|
149
153
|
"DAWN_AMBIENT_PALETTE_KEY",
|
|
150
154
|
"INTERNAL_WALL_COLOR",
|
|
151
155
|
"INTERNAL_WALL_BORDER_COLOR",
|
|
@@ -18,6 +18,8 @@ from .colors import (
|
|
|
18
18
|
RED,
|
|
19
19
|
STEEL_BEAM_COLOR,
|
|
20
20
|
STEEL_BEAM_LINE_COLOR,
|
|
21
|
+
TRACKER_OUTLINE_COLOR,
|
|
22
|
+
WALL_FOLLOWER_OUTLINE_COLOR,
|
|
21
23
|
YELLOW,
|
|
22
24
|
)
|
|
23
25
|
from .gameplay_constants import (
|
|
@@ -26,9 +28,9 @@ from .gameplay_constants import (
|
|
|
26
28
|
CAR_SPEED,
|
|
27
29
|
CAR_WALL_DAMAGE,
|
|
28
30
|
CAR_WIDTH,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
BUDDY_COLOR,
|
|
32
|
+
BUDDY_FOLLOW_SPEED,
|
|
33
|
+
BUDDY_RADIUS,
|
|
32
34
|
FAST_ZOMBIE_BASE_SPEED,
|
|
33
35
|
FLASHLIGHT_HEIGHT,
|
|
34
36
|
FLASHLIGHT_WIDTH,
|
|
@@ -57,7 +59,10 @@ from .gameplay_constants import (
|
|
|
57
59
|
ZOMBIE_TRACKER_WANDER_INTERVAL_MS,
|
|
58
60
|
ZOMBIE_WALL_DAMAGE,
|
|
59
61
|
ZOMBIE_WALL_FOLLOW_LOST_WALL_MS,
|
|
62
|
+
ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG,
|
|
63
|
+
ZOMBIE_WALL_FOLLOW_PROBE_STEP,
|
|
60
64
|
ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE,
|
|
65
|
+
ZOMBIE_WALL_FOLLOW_TARGET_GAP,
|
|
61
66
|
ZOMBIE_WANDER_INTERVAL_MS,
|
|
62
67
|
car_body_radius,
|
|
63
68
|
)
|
|
@@ -71,6 +76,50 @@ MovementStrategy = Callable[
|
|
|
71
76
|
["Zombie", tuple[int, int], list["Wall"], list[dict[str, object]]],
|
|
72
77
|
tuple[float, float],
|
|
73
78
|
]
|
|
79
|
+
WallIndex = dict[tuple[int, int], list["Wall"]]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_wall_index(walls: Iterable["Wall"]) -> WallIndex:
|
|
83
|
+
index: WallIndex = {}
|
|
84
|
+
for wall in walls:
|
|
85
|
+
if not wall.alive():
|
|
86
|
+
continue
|
|
87
|
+
cell_x = int(wall.rect.centerx // CELL_SIZE)
|
|
88
|
+
cell_y = int(wall.rect.centery // CELL_SIZE)
|
|
89
|
+
index.setdefault((cell_x, cell_y), []).append(wall)
|
|
90
|
+
return index
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _sprite_center_and_radius(
|
|
94
|
+
sprite: pygame.sprite.Sprite,
|
|
95
|
+
) -> tuple[tuple[int, int], float]:
|
|
96
|
+
center = sprite.rect.center
|
|
97
|
+
radius = float(
|
|
98
|
+
getattr(sprite, "radius", max(sprite.rect.width, sprite.rect.height) / 2)
|
|
99
|
+
)
|
|
100
|
+
return center, radius
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def walls_for_radius(
|
|
104
|
+
wall_index: WallIndex, center: tuple[float, float], radius: float
|
|
105
|
+
) -> list["Wall"]:
|
|
106
|
+
search_radius = radius + CELL_SIZE
|
|
107
|
+
min_x = max(0, int((center[0] - search_radius) // CELL_SIZE))
|
|
108
|
+
max_x = min(GRID_COLS - 1, int((center[0] + search_radius) // CELL_SIZE))
|
|
109
|
+
min_y = max(0, int((center[1] - search_radius) // CELL_SIZE))
|
|
110
|
+
max_y = min(GRID_ROWS - 1, int((center[1] + search_radius) // CELL_SIZE))
|
|
111
|
+
candidates: list[Wall] = []
|
|
112
|
+
for cy in range(min_y, max_y + 1):
|
|
113
|
+
for cx in range(min_x, max_x + 1):
|
|
114
|
+
candidates.extend(wall_index.get((cx, cy), []))
|
|
115
|
+
return candidates
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _walls_for_sprite(
|
|
119
|
+
sprite: pygame.sprite.Sprite, wall_index: WallIndex
|
|
120
|
+
) -> list["Wall"]:
|
|
121
|
+
center, radius = _sprite_center_and_radius(sprite)
|
|
122
|
+
return walls_for_radius(wall_index, center, radius)
|
|
74
123
|
|
|
75
124
|
|
|
76
125
|
def circle_rect_collision(
|
|
@@ -108,7 +157,7 @@ def _build_beveled_polygon(
|
|
|
108
157
|
if d == 0 or not any(bevels):
|
|
109
158
|
return [(0, 0), (width, 0), (width, height), (0, height)]
|
|
110
159
|
|
|
111
|
-
segments =
|
|
160
|
+
segments = 4
|
|
112
161
|
tl, tr, br, bl = bevels
|
|
113
162
|
points: list[tuple[int, int]] = []
|
|
114
163
|
|
|
@@ -311,17 +360,36 @@ def spritecollide_walls(
|
|
|
311
360
|
walls: pygame.sprite.Group,
|
|
312
361
|
*,
|
|
313
362
|
dokill: bool = False,
|
|
363
|
+
wall_index: WallIndex | None = None,
|
|
314
364
|
) -> list[pygame.sprite.Sprite]:
|
|
315
|
-
|
|
316
|
-
sprite
|
|
317
|
-
|
|
365
|
+
if wall_index is None:
|
|
366
|
+
return pygame.sprite.spritecollide(
|
|
367
|
+
sprite, walls, dokill, collided=collide_sprite_wall
|
|
368
|
+
)
|
|
369
|
+
candidates = _walls_for_sprite(sprite, wall_index)
|
|
370
|
+
if not candidates:
|
|
371
|
+
return []
|
|
372
|
+
hit_list = [wall for wall in candidates if collide_sprite_wall(sprite, wall)]
|
|
373
|
+
if dokill:
|
|
374
|
+
for wall in hit_list:
|
|
375
|
+
wall.kill()
|
|
376
|
+
return hit_list
|
|
318
377
|
|
|
319
378
|
|
|
320
379
|
def spritecollideany_walls(
|
|
321
380
|
sprite: pygame.sprite.Sprite,
|
|
322
381
|
walls: pygame.sprite.Group,
|
|
382
|
+
*,
|
|
383
|
+
wall_index: WallIndex | None = None,
|
|
323
384
|
) -> pygame.sprite.Sprite | None:
|
|
324
|
-
|
|
385
|
+
if wall_index is None:
|
|
386
|
+
return pygame.sprite.spritecollideany(
|
|
387
|
+
sprite, walls, collided=collide_sprite_wall
|
|
388
|
+
)
|
|
389
|
+
for wall in _walls_for_sprite(sprite, wall_index):
|
|
390
|
+
if collide_sprite_wall(sprite, wall):
|
|
391
|
+
return wall
|
|
392
|
+
return None
|
|
325
393
|
|
|
326
394
|
|
|
327
395
|
def circle_wall_collision(
|
|
@@ -490,8 +558,10 @@ class Wall(pygame.sprite.Sprite):
|
|
|
490
558
|
return rect_polygon_collision(rect_obj, self._collision_polygon)
|
|
491
559
|
|
|
492
560
|
def collides_circle(self: Self, center: tuple[float, float], radius: float) -> bool:
|
|
561
|
+
if not circle_rect_collision(center, radius, self.rect):
|
|
562
|
+
return False
|
|
493
563
|
if self._collision_polygon is None:
|
|
494
|
-
return
|
|
564
|
+
return True
|
|
495
565
|
return circle_polygon_collision(center, radius, self._collision_polygon)
|
|
496
566
|
|
|
497
567
|
def set_palette_colors(
|
|
@@ -621,7 +691,14 @@ class Player(pygame.sprite.Sprite):
|
|
|
621
691
|
self.x = float(self.rect.centerx)
|
|
622
692
|
self.y = float(self.rect.centery)
|
|
623
693
|
|
|
624
|
-
def move(
|
|
694
|
+
def move(
|
|
695
|
+
self: Self,
|
|
696
|
+
dx: float,
|
|
697
|
+
dy: float,
|
|
698
|
+
walls: pygame.sprite.Group,
|
|
699
|
+
*,
|
|
700
|
+
wall_index: WallIndex | None = None,
|
|
701
|
+
) -> None:
|
|
625
702
|
if self.in_car:
|
|
626
703
|
return
|
|
627
704
|
|
|
@@ -629,7 +706,7 @@ class Player(pygame.sprite.Sprite):
|
|
|
629
706
|
self.x += dx
|
|
630
707
|
self.x = min(LEVEL_WIDTH, max(0, self.x))
|
|
631
708
|
self.rect.centerx = int(self.x)
|
|
632
|
-
hit_list_x = spritecollide_walls(self, walls)
|
|
709
|
+
hit_list_x = spritecollide_walls(self, walls, wall_index=wall_index)
|
|
633
710
|
if hit_list_x:
|
|
634
711
|
damage = max(1, PLAYER_WALL_DAMAGE // len(hit_list_x))
|
|
635
712
|
for wall in hit_list_x:
|
|
@@ -642,7 +719,7 @@ class Player(pygame.sprite.Sprite):
|
|
|
642
719
|
self.y += dy
|
|
643
720
|
self.y = min(LEVEL_HEIGHT, max(0, self.y))
|
|
644
721
|
self.rect.centery = int(self.y)
|
|
645
|
-
hit_list_y = spritecollide_walls(self, walls)
|
|
722
|
+
hit_list_y = spritecollide_walls(self, walls, wall_index=wall_index)
|
|
646
723
|
if hit_list_y:
|
|
647
724
|
damage = max(1, PLAYER_WALL_DAMAGE // len(hit_list_y))
|
|
648
725
|
for wall in hit_list_y:
|
|
@@ -654,18 +731,20 @@ class Player(pygame.sprite.Sprite):
|
|
|
654
731
|
self.rect.center = (int(self.x), int(self.y))
|
|
655
732
|
|
|
656
733
|
|
|
657
|
-
class
|
|
658
|
-
"""
|
|
734
|
+
class Survivor(pygame.sprite.Sprite):
|
|
735
|
+
"""Civilians that gather near the player; optional buddy behavior."""
|
|
659
736
|
|
|
660
|
-
def __init__(self: Self, x: float, y: float) -> None:
|
|
737
|
+
def __init__(self: Self, x: float, y: float, *, is_buddy: bool = False) -> None:
|
|
661
738
|
super().__init__()
|
|
662
|
-
self.
|
|
739
|
+
self.is_buddy = is_buddy
|
|
740
|
+
self.radius = BUDDY_RADIUS if is_buddy else SURVIVOR_RADIUS
|
|
663
741
|
self.image = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA)
|
|
742
|
+
fill_color = BUDDY_COLOR if is_buddy else SURVIVOR_COLOR
|
|
664
743
|
_draw_outlined_circle(
|
|
665
744
|
self.image,
|
|
666
745
|
(self.radius, self.radius),
|
|
667
746
|
self.radius,
|
|
668
|
-
|
|
747
|
+
fill_color,
|
|
669
748
|
HUMANOID_OUTLINE_COLOR,
|
|
670
749
|
HUMANOID_OUTLINE_WIDTH,
|
|
671
750
|
)
|
|
@@ -676,88 +755,71 @@ class Companion(pygame.sprite.Sprite):
|
|
|
676
755
|
self.rescued = False
|
|
677
756
|
|
|
678
757
|
def set_following(self: Self) -> None:
|
|
679
|
-
if not self.rescued:
|
|
758
|
+
if self.is_buddy and not self.rescued:
|
|
680
759
|
self.following = True
|
|
681
760
|
|
|
682
761
|
def mark_rescued(self: Self) -> None:
|
|
683
|
-
self.
|
|
684
|
-
|
|
762
|
+
if self.is_buddy:
|
|
763
|
+
self.following = False
|
|
764
|
+
self.rescued = True
|
|
685
765
|
|
|
686
766
|
def teleport(self: Self, pos: tuple[int, int]) -> None:
|
|
687
|
-
"""Reposition the
|
|
767
|
+
"""Reposition the survivor (used for quiet respawns)."""
|
|
688
768
|
self.x, self.y = float(pos[0]), float(pos[1])
|
|
689
769
|
self.rect.center = (int(self.x), int(self.y))
|
|
690
|
-
self.
|
|
770
|
+
if self.is_buddy:
|
|
771
|
+
self.following = False
|
|
691
772
|
|
|
692
|
-
def
|
|
693
|
-
self: Self,
|
|
773
|
+
def update_behavior(
|
|
774
|
+
self: Self,
|
|
775
|
+
player_pos: tuple[int, int],
|
|
776
|
+
walls: pygame.sprite.Group,
|
|
777
|
+
*,
|
|
778
|
+
wall_index: WallIndex | None = None,
|
|
694
779
|
) -> None:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
780
|
+
if self.is_buddy:
|
|
781
|
+
if self.rescued or not self.following:
|
|
782
|
+
self.rect.center = (int(self.x), int(self.y))
|
|
783
|
+
return
|
|
699
784
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
785
|
+
dx = player_pos[0] - self.x
|
|
786
|
+
dy = player_pos[1] - self.y
|
|
787
|
+
dist = math.hypot(dx, dy)
|
|
788
|
+
if dist <= 0:
|
|
789
|
+
self.rect.center = (int(self.x), int(self.y))
|
|
790
|
+
return
|
|
706
791
|
|
|
707
|
-
|
|
708
|
-
|
|
792
|
+
move_x = (dx / dist) * BUDDY_FOLLOW_SPEED
|
|
793
|
+
move_y = (dy / dist) * BUDDY_FOLLOW_SPEED
|
|
709
794
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
self.rect.centerx = int(self.x)
|
|
713
|
-
if spritecollideany_walls(self, walls):
|
|
714
|
-
self.x -= move_x
|
|
795
|
+
if move_x:
|
|
796
|
+
self.x += move_x
|
|
715
797
|
self.rect.centerx = int(self.x)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
if
|
|
720
|
-
self.y
|
|
798
|
+
if spritecollideany_walls(self, walls, wall_index=wall_index):
|
|
799
|
+
self.x -= move_x
|
|
800
|
+
self.rect.centerx = int(self.x)
|
|
801
|
+
if move_y:
|
|
802
|
+
self.y += move_y
|
|
721
803
|
self.rect.centery = int(self.y)
|
|
804
|
+
if spritecollideany_walls(self, walls, wall_index=wall_index):
|
|
805
|
+
self.y -= move_y
|
|
806
|
+
self.rect.centery = int(self.y)
|
|
807
|
+
|
|
808
|
+
overlap_radius = (self.radius + PLAYER_RADIUS) * 1.05
|
|
809
|
+
dx_after = player_pos[0] - self.x
|
|
810
|
+
dy_after = player_pos[1] - self.y
|
|
811
|
+
dist_after = math.hypot(dx_after, dy_after)
|
|
812
|
+
if dist_after > 0 and dist_after < overlap_radius:
|
|
813
|
+
push_dist = overlap_radius - dist_after
|
|
814
|
+
self.x -= (dx_after / dist_after) * push_dist
|
|
815
|
+
self.y -= (dy_after / dist_after) * push_dist
|
|
816
|
+
self.rect.center = (int(self.x), int(self.y))
|
|
722
817
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
dx_after = target_pos[0] - self.x
|
|
726
|
-
dy_after = target_pos[1] - self.y
|
|
727
|
-
dist_after = math.hypot(dx_after, dy_after)
|
|
728
|
-
if dist_after > 0 and dist_after < overlap_radius:
|
|
729
|
-
push_dist = overlap_radius - dist_after
|
|
730
|
-
self.x -= (dx_after / dist_after) * push_dist
|
|
731
|
-
self.y -= (dy_after / dist_after) * push_dist
|
|
818
|
+
self.x = min(LEVEL_WIDTH, max(0, self.x))
|
|
819
|
+
self.y = min(LEVEL_HEIGHT, max(0, self.y))
|
|
732
820
|
self.rect.center = (int(self.x), int(self.y))
|
|
821
|
+
return
|
|
733
822
|
|
|
734
|
-
self.x = min(LEVEL_WIDTH, max(0, self.x))
|
|
735
|
-
self.y = min(LEVEL_HEIGHT, max(0, self.y))
|
|
736
|
-
self.rect.center = (int(self.x), int(self.y))
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
class Survivor(pygame.sprite.Sprite):
|
|
740
|
-
"""Civilians that gather near the player during Stage 4."""
|
|
741
|
-
|
|
742
|
-
def __init__(self: Self, x: float, y: float) -> None:
|
|
743
|
-
super().__init__()
|
|
744
|
-
self.radius = SURVIVOR_RADIUS
|
|
745
|
-
self.image = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA)
|
|
746
|
-
_draw_outlined_circle(
|
|
747
|
-
self.image,
|
|
748
|
-
(self.radius, self.radius),
|
|
749
|
-
self.radius,
|
|
750
|
-
SURVIVOR_COLOR,
|
|
751
|
-
HUMANOID_OUTLINE_COLOR,
|
|
752
|
-
HUMANOID_OUTLINE_WIDTH,
|
|
753
|
-
)
|
|
754
|
-
self.rect = self.image.get_rect(center=(int(x), int(y)))
|
|
755
|
-
self.x = float(self.rect.centerx)
|
|
756
|
-
self.y = float(self.rect.centery)
|
|
757
|
-
|
|
758
|
-
def update_behavior(
|
|
759
|
-
self: Self, player_pos: tuple[int, int], walls: pygame.sprite.Group
|
|
760
|
-
) -> None:
|
|
761
823
|
dx = player_pos[0] - self.x
|
|
762
824
|
dy = player_pos[1] - self.y
|
|
763
825
|
dist = math.hypot(dx, dy)
|
|
@@ -770,13 +832,13 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
770
832
|
if move_x:
|
|
771
833
|
self.x += move_x
|
|
772
834
|
self.rect.centerx = int(self.x)
|
|
773
|
-
if spritecollideany_walls(self, walls):
|
|
835
|
+
if spritecollideany_walls(self, walls, wall_index=wall_index):
|
|
774
836
|
self.x -= move_x
|
|
775
837
|
self.rect.centerx = int(self.x)
|
|
776
838
|
if move_y:
|
|
777
839
|
self.y += move_y
|
|
778
840
|
self.rect.centery = int(self.y)
|
|
779
|
-
if spritecollideany_walls(self, walls):
|
|
841
|
+
if spritecollideany_walls(self, walls, wall_index=wall_index):
|
|
780
842
|
self.y -= move_y
|
|
781
843
|
self.rect.centery = int(self.y)
|
|
782
844
|
|
|
@@ -832,6 +894,38 @@ def zombie_wall_follow_has_wall(
|
|
|
832
894
|
)
|
|
833
895
|
|
|
834
896
|
|
|
897
|
+
def zombie_wall_follow_wall_distance(
|
|
898
|
+
zombie: Zombie,
|
|
899
|
+
walls: list[Wall],
|
|
900
|
+
angle: float,
|
|
901
|
+
max_distance: float,
|
|
902
|
+
*,
|
|
903
|
+
step: float = ZOMBIE_WALL_FOLLOW_PROBE_STEP,
|
|
904
|
+
) -> float:
|
|
905
|
+
direction_x = math.cos(angle)
|
|
906
|
+
direction_y = math.sin(angle)
|
|
907
|
+
max_search = max_distance + 120
|
|
908
|
+
candidates = [
|
|
909
|
+
wall
|
|
910
|
+
for wall in walls
|
|
911
|
+
if abs(wall.rect.centerx - zombie.x) < max_search
|
|
912
|
+
and abs(wall.rect.centery - zombie.y) < max_search
|
|
913
|
+
]
|
|
914
|
+
if not candidates:
|
|
915
|
+
return max_distance
|
|
916
|
+
distance = step
|
|
917
|
+
while distance <= max_distance:
|
|
918
|
+
check_x = zombie.x + direction_x * distance
|
|
919
|
+
check_y = zombie.y + direction_y * distance
|
|
920
|
+
if any(
|
|
921
|
+
circle_wall_collision((check_x, check_y), zombie.radius, wall)
|
|
922
|
+
for wall in candidates
|
|
923
|
+
):
|
|
924
|
+
return distance
|
|
925
|
+
distance += step
|
|
926
|
+
return max_distance
|
|
927
|
+
|
|
928
|
+
|
|
835
929
|
def zombie_wall_follow_movement(
|
|
836
930
|
zombie: Zombie,
|
|
837
931
|
player_center: tuple[int, int],
|
|
@@ -844,22 +938,28 @@ def zombie_wall_follow_movement(
|
|
|
844
938
|
if zombie.wall_follow_side == 0:
|
|
845
939
|
sensor_distance = ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE + zombie.radius
|
|
846
940
|
forward_angle = zombie.wall_follow_angle
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
941
|
+
probe_offset = math.radians(ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG)
|
|
942
|
+
left_angle = forward_angle + probe_offset
|
|
943
|
+
right_angle = forward_angle - probe_offset
|
|
944
|
+
left_dist = zombie_wall_follow_wall_distance(
|
|
850
945
|
zombie, walls, left_angle, sensor_distance
|
|
851
946
|
)
|
|
852
|
-
|
|
947
|
+
right_dist = zombie_wall_follow_wall_distance(
|
|
853
948
|
zombie, walls, right_angle, sensor_distance
|
|
854
949
|
)
|
|
855
|
-
|
|
950
|
+
forward_dist = zombie_wall_follow_wall_distance(
|
|
856
951
|
zombie, walls, forward_angle, sensor_distance
|
|
857
952
|
)
|
|
953
|
+
left_wall = left_dist < sensor_distance
|
|
954
|
+
right_wall = right_dist < sensor_distance
|
|
955
|
+
forward_wall = forward_dist < sensor_distance
|
|
858
956
|
if left_wall or right_wall or forward_wall:
|
|
859
957
|
if left_wall and not right_wall:
|
|
860
958
|
zombie.wall_follow_side = 1.0
|
|
861
959
|
elif right_wall and not left_wall:
|
|
862
960
|
zombie.wall_follow_side = -1.0
|
|
961
|
+
elif left_wall and right_wall:
|
|
962
|
+
zombie.wall_follow_side = 1.0 if left_dist <= right_dist else -1.0
|
|
863
963
|
else:
|
|
864
964
|
zombie.wall_follow_side = RNG.choice([-1.0, 1.0])
|
|
865
965
|
zombie.wall_follow_last_wall_time = pygame.time.get_ticks()
|
|
@@ -870,13 +970,16 @@ def zombie_wall_follow_movement(
|
|
|
870
970
|
return zombie_wander_move(zombie, walls)
|
|
871
971
|
|
|
872
972
|
sensor_distance = ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE + zombie.radius
|
|
873
|
-
|
|
874
|
-
|
|
973
|
+
probe_offset = math.radians(ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG)
|
|
974
|
+
side_angle = zombie.wall_follow_angle + zombie.wall_follow_side * probe_offset
|
|
975
|
+
side_dist = zombie_wall_follow_wall_distance(
|
|
875
976
|
zombie, walls, side_angle, sensor_distance
|
|
876
977
|
)
|
|
877
|
-
|
|
978
|
+
forward_dist = zombie_wall_follow_wall_distance(
|
|
878
979
|
zombie, walls, zombie.wall_follow_angle, sensor_distance
|
|
879
980
|
)
|
|
981
|
+
side_has_wall = side_dist < sensor_distance
|
|
982
|
+
forward_has_wall = forward_dist < sensor_distance
|
|
880
983
|
now = pygame.time.get_ticks()
|
|
881
984
|
wall_recent = (
|
|
882
985
|
zombie.wall_follow_last_wall_time is not None
|
|
@@ -890,16 +993,22 @@ def zombie_wall_follow_movement(
|
|
|
890
993
|
zombie.wall_follow_last_wall_time = now
|
|
891
994
|
if side_has_wall:
|
|
892
995
|
zombie.wall_follow_last_side_has_wall = True
|
|
996
|
+
gap_error = ZOMBIE_WALL_FOLLOW_TARGET_GAP - side_dist
|
|
997
|
+
if abs(gap_error) > 0.1:
|
|
998
|
+
ratio = min(1.0, abs(gap_error) / ZOMBIE_WALL_FOLLOW_TARGET_GAP)
|
|
999
|
+
turn = turn_step * ratio
|
|
1000
|
+
if gap_error > 0:
|
|
1001
|
+
zombie.wall_follow_angle -= zombie.wall_follow_side * turn
|
|
1002
|
+
else:
|
|
1003
|
+
zombie.wall_follow_angle += zombie.wall_follow_side * turn
|
|
1004
|
+
if forward_dist < ZOMBIE_WALL_FOLLOW_TARGET_GAP:
|
|
1005
|
+
zombie.wall_follow_angle -= zombie.wall_follow_side * (turn_step * 1.5)
|
|
1006
|
+
else:
|
|
1007
|
+
zombie.wall_follow_last_side_has_wall = False
|
|
893
1008
|
if forward_has_wall:
|
|
894
1009
|
zombie.wall_follow_angle -= zombie.wall_follow_side * turn_step
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if zombie.wall_follow_last_side_has_wall and not forward_has_wall:
|
|
898
|
-
zombie.wall_follow_angle += zombie.wall_follow_side * (math.pi / 2.0)
|
|
899
|
-
zombie.wall_follow_last_side_has_wall = False
|
|
900
|
-
elif zombie.wall_follow_last_side_has_wall:
|
|
901
|
-
zombie.wall_follow_angle += zombie.wall_follow_side * turn_step
|
|
902
|
-
zombie.wall_follow_last_side_has_wall = False
|
|
1010
|
+
elif wall_recent:
|
|
1011
|
+
zombie.wall_follow_angle += zombie.wall_follow_side * (turn_step * 0.75)
|
|
903
1012
|
else:
|
|
904
1013
|
zombie.wall_follow_angle += zombie.wall_follow_side * (math.pi / 2.0)
|
|
905
1014
|
zombie.wall_follow_side = 0.0
|
|
@@ -1037,7 +1146,20 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1037
1146
|
super().__init__()
|
|
1038
1147
|
self.radius = ZOMBIE_RADIUS
|
|
1039
1148
|
self.image = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA)
|
|
1040
|
-
|
|
1149
|
+
if tracker:
|
|
1150
|
+
outline_color = TRACKER_OUTLINE_COLOR
|
|
1151
|
+
elif wall_follower:
|
|
1152
|
+
outline_color = WALL_FOLLOWER_OUTLINE_COLOR
|
|
1153
|
+
else:
|
|
1154
|
+
outline_color = DARK_RED
|
|
1155
|
+
_draw_outlined_circle(
|
|
1156
|
+
self.image,
|
|
1157
|
+
(self.radius, self.radius),
|
|
1158
|
+
self.radius,
|
|
1159
|
+
RED,
|
|
1160
|
+
outline_color,
|
|
1161
|
+
1,
|
|
1162
|
+
)
|
|
1041
1163
|
if start_pos:
|
|
1042
1164
|
x, y = start_pos
|
|
1043
1165
|
elif hint_pos:
|
|
@@ -1443,7 +1565,6 @@ __all__ = [
|
|
|
1443
1565
|
"SteelBeam",
|
|
1444
1566
|
"Camera",
|
|
1445
1567
|
"Player",
|
|
1446
|
-
"Companion",
|
|
1447
1568
|
"Survivor",
|
|
1448
1569
|
"Zombie",
|
|
1449
1570
|
"Car",
|