zombie-escape 1.12.0__tar.gz → 1.12.3__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.12.0 → zombie_escape-1.12.3}/PKG-INFO +4 -3
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/README.md +3 -2
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/__about__.py +1 -1
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/colors.py +22 -14
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/entities.py +236 -49
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/entities_constants.py +6 -2
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/footprints.py +4 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/interactions.py +19 -7
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/layout.py +18 -14
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/movement.py +23 -1
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/spawn.py +92 -50
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/state.py +15 -9
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/survivors.py +9 -1
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/utils.py +40 -21
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/level_blueprints.py +87 -4
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/models.py +5 -4
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/render.py +51 -16
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/render_assets.py +325 -124
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/render_constants.py +11 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/screens/game_over.py +3 -3
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/screens/gameplay.py +4 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/stage_constants.py +5 -5
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/zombie_escape.py +1 -1
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/.gitignore +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/LICENSE.txt +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/pyproject.toml +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/__init__.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/config.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/font_utils.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/__init__.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/ambient.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay/constants.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/gameplay_constants.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/input_utils.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/level_constants.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/locales/ui.en.json +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/locales/ui.ja.json +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/localization.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/progress.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/rng.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/screen_constants.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/screens/__init__.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/screens/settings.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/screens/title.py +0 -0
- {zombie_escape-1.12.0 → zombie_escape-1.12.3}/src/zombie_escape/world_grid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zombie-escape
|
|
3
|
-
Version: 1.12.
|
|
3
|
+
Version: 1.12.3
|
|
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>
|
|
@@ -93,7 +93,7 @@ Open **Settings** from the title to toggle gameplay assists:
|
|
|
93
93
|
|
|
94
94
|
### Characters/Items
|
|
95
95
|
|
|
96
|
-
- **Player:** A blue circle. Controlled with the WASD or arrow keys. When carrying fuel a tiny yellow square appears near the sprite so you can immediately see whether you're ready to drive.
|
|
96
|
+
- **Player:** A blue circle with small hands. Controlled with the WASD or arrow keys. When carrying fuel a tiny yellow square appears near the sprite so you can immediately see whether you're ready to drive.
|
|
97
97
|
- **Zombie:** A red circle. Will chase the player (or car) once detected.
|
|
98
98
|
- When out of sight, the zombie's movement mode will randomly switch every certain time (moving horizontally/vertically only, side-to-side movement, random movement, etc.).
|
|
99
99
|
- Variants with different behavior have been observed.
|
|
@@ -108,10 +108,11 @@ Open **Settings** from the title to toggle gameplay assists:
|
|
|
108
108
|
- **Flashlight:** Each pickup expands your visible radius by about 20% (grab two to reach the max boost).
|
|
109
109
|
- **Steel Beam (optional):** A square post with crossed diagonals; same collision as inner walls but with triple durability. Spawns independently of inner walls (may overlap them). If an inner wall covers a beam, the beam appears once the wall is destroyed.
|
|
110
110
|
- **Fuel Can (Stages 2 & 3):** A yellow jerrycan that only spawns on the fuel-run stages. Pick it up before driving the car; once collected the on-player indicator appears until you refuel the car.
|
|
111
|
-
- **Buddy (Stage 3):** A green circle survivor with a blue outline who spawns somewhere in the building and waits.
|
|
111
|
+
- **Buddy (Stage 3):** A green circle survivor with small hands and a blue outline who spawns somewhere in the building and waits.
|
|
112
112
|
- Zombies only choose to pursue the buddy if they are on-screen; otherwise they ignore them.
|
|
113
113
|
- If a zombie tags the buddy off-screen, the buddy quietly respawns somewhere else instead of ending the run.
|
|
114
114
|
- Touch the buddy on foot to make them follow you (at 70% of player speed). Touch them while driving to pick them up.
|
|
115
|
+
- If you bash an inner wall or steel beam, the buddy will drift toward that spot and help chip away at it.
|
|
115
116
|
- **Survivors (Stage 4):** Pale gray civilians with a blue outline, scattered indoors.
|
|
116
117
|
- They stand still until you get close, then shuffle toward you at about one-third of player speed.
|
|
117
118
|
- Zombies can convert them if both are on-screen; the survivor shouts a line and turns instantly.
|
|
@@ -64,7 +64,7 @@ Open **Settings** from the title to toggle gameplay assists:
|
|
|
64
64
|
|
|
65
65
|
### Characters/Items
|
|
66
66
|
|
|
67
|
-
- **Player:** A blue circle. Controlled with the WASD or arrow keys. When carrying fuel a tiny yellow square appears near the sprite so you can immediately see whether you're ready to drive.
|
|
67
|
+
- **Player:** A blue circle with small hands. Controlled with the WASD or arrow keys. When carrying fuel a tiny yellow square appears near the sprite so you can immediately see whether you're ready to drive.
|
|
68
68
|
- **Zombie:** A red circle. Will chase the player (or car) once detected.
|
|
69
69
|
- When out of sight, the zombie's movement mode will randomly switch every certain time (moving horizontally/vertically only, side-to-side movement, random movement, etc.).
|
|
70
70
|
- Variants with different behavior have been observed.
|
|
@@ -79,10 +79,11 @@ Open **Settings** from the title to toggle gameplay assists:
|
|
|
79
79
|
- **Flashlight:** Each pickup expands your visible radius by about 20% (grab two to reach the max boost).
|
|
80
80
|
- **Steel Beam (optional):** A square post with crossed diagonals; same collision as inner walls but with triple durability. Spawns independently of inner walls (may overlap them). If an inner wall covers a beam, the beam appears once the wall is destroyed.
|
|
81
81
|
- **Fuel Can (Stages 2 & 3):** A yellow jerrycan that only spawns on the fuel-run stages. Pick it up before driving the car; once collected the on-player indicator appears until you refuel the car.
|
|
82
|
-
- **Buddy (Stage 3):** A green circle survivor with a blue outline who spawns somewhere in the building and waits.
|
|
82
|
+
- **Buddy (Stage 3):** A green circle survivor with small hands and a blue outline who spawns somewhere in the building and waits.
|
|
83
83
|
- Zombies only choose to pursue the buddy if they are on-screen; otherwise they ignore them.
|
|
84
84
|
- If a zombie tags the buddy off-screen, the buddy quietly respawns somewhere else instead of ending the run.
|
|
85
85
|
- Touch the buddy on foot to make them follow you (at 70% of player speed). Touch them while driving to pick them up.
|
|
86
|
+
- If you bash an inner wall or steel beam, the buddy will drift toward that spot and help chip away at it.
|
|
86
87
|
- **Survivors (Stage 4):** Pale gray civilians with a blue outline, scattered indoors.
|
|
87
88
|
- They stand still until you get close, then shuffle toward you at about one-third of player speed.
|
|
88
89
|
- Zombies can convert them if both are on-screen; the survivor shouts a line and turns instantly.
|
|
@@ -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",
|
|
@@ -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,13 +27,14 @@ from .entities_constants import (
|
|
|
26
27
|
FLASHLIGHT_WIDTH,
|
|
27
28
|
FUEL_CAN_HEIGHT,
|
|
28
29
|
FUEL_CAN_WIDTH,
|
|
29
|
-
|
|
30
|
-
SHOES_WIDTH,
|
|
30
|
+
HUMANOID_WALL_BUMP_FRAMES,
|
|
31
31
|
INTERNAL_WALL_BEVEL_DEPTH,
|
|
32
32
|
INTERNAL_WALL_HEALTH,
|
|
33
33
|
PLAYER_RADIUS,
|
|
34
34
|
PLAYER_SPEED,
|
|
35
35
|
PLAYER_WALL_DAMAGE,
|
|
36
|
+
SHOES_HEIGHT,
|
|
37
|
+
SHOES_WIDTH,
|
|
36
38
|
STEEL_BEAM_HEALTH,
|
|
37
39
|
SURVIVOR_APPROACH_RADIUS,
|
|
38
40
|
SURVIVOR_APPROACH_SPEED,
|
|
@@ -59,20 +61,24 @@ from .entities_constants import (
|
|
|
59
61
|
)
|
|
60
62
|
from .gameplay.constants import FOOTPRINT_STEP_DISTANCE
|
|
61
63
|
from .models import Footprint
|
|
64
|
+
from .render_constants import ANGLE_BINS, ZOMBIE_NOSE_COLOR
|
|
62
65
|
from .render_assets import (
|
|
63
66
|
EnvironmentPalette,
|
|
67
|
+
angle_bin_from_vector,
|
|
64
68
|
build_beveled_polygon,
|
|
69
|
+
build_car_directional_surfaces,
|
|
65
70
|
build_car_surface,
|
|
66
71
|
build_flashlight_surface,
|
|
67
72
|
build_fuel_can_surface,
|
|
73
|
+
build_player_directional_surfaces,
|
|
68
74
|
build_shoes_surface,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
build_survivor_directional_surfaces,
|
|
76
|
+
build_zombie_directional_surfaces,
|
|
77
|
+
draw_humanoid_hand,
|
|
78
|
+
draw_humanoid_nose,
|
|
72
79
|
paint_car_surface,
|
|
73
80
|
paint_steel_beam_surface,
|
|
74
81
|
paint_wall_surface,
|
|
75
|
-
paint_zombie_surface,
|
|
76
82
|
resolve_car_color,
|
|
77
83
|
resolve_steel_beam_colors,
|
|
78
84
|
resolve_wall_colors,
|
|
@@ -229,6 +235,14 @@ class SteelBeam(pygame.sprite.Sprite):
|
|
|
229
235
|
)
|
|
230
236
|
|
|
231
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
|
+
|
|
232
246
|
MovementStrategy = Callable[
|
|
233
247
|
[
|
|
234
248
|
"Zombie",
|
|
@@ -242,6 +256,8 @@ MovementStrategy = Callable[
|
|
|
242
256
|
],
|
|
243
257
|
tuple[float, float],
|
|
244
258
|
]
|
|
259
|
+
|
|
260
|
+
|
|
245
261
|
def _sprite_center_and_radius(
|
|
246
262
|
sprite: pygame.sprite.Sprite,
|
|
247
263
|
) -> tuple[tuple[int, int], float]:
|
|
@@ -567,7 +583,14 @@ class Player(pygame.sprite.Sprite):
|
|
|
567
583
|
) -> None:
|
|
568
584
|
super().__init__()
|
|
569
585
|
self.radius = PLAYER_RADIUS
|
|
570
|
-
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]
|
|
571
594
|
self.rect = self.image.get_rect(center=(x, y))
|
|
572
595
|
self.speed = PLAYER_SPEED
|
|
573
596
|
self.in_car = False
|
|
@@ -591,6 +614,9 @@ class Player(pygame.sprite.Sprite):
|
|
|
591
614
|
if level_width is None or level_height is None:
|
|
592
615
|
raise ValueError("level_width/level_height are required for movement")
|
|
593
616
|
|
|
617
|
+
inner_wall_hit = False
|
|
618
|
+
inner_wall_cell: tuple[int, int] | None = None
|
|
619
|
+
|
|
594
620
|
if dx != 0:
|
|
595
621
|
self.x += dx
|
|
596
622
|
self.x = min(level_width, max(0, self.x))
|
|
@@ -606,6 +632,13 @@ class Player(pygame.sprite.Sprite):
|
|
|
606
632
|
for wall in hit_list_x:
|
|
607
633
|
if wall.alive():
|
|
608
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
|
+
)
|
|
609
642
|
self.x -= dx * 1.5
|
|
610
643
|
self.rect.centerx = int(self.x)
|
|
611
644
|
|
|
@@ -624,10 +657,51 @@ class Player(pygame.sprite.Sprite):
|
|
|
624
657
|
for wall in hit_list_y:
|
|
625
658
|
if wall.alive():
|
|
626
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
|
+
)
|
|
627
667
|
self.y -= dy * 1.5
|
|
628
668
|
self.rect.centery = int(self.y)
|
|
629
669
|
|
|
630
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)
|
|
631
705
|
|
|
632
706
|
|
|
633
707
|
class Survivor(pygame.sprite.Sprite):
|
|
@@ -643,10 +717,16 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
643
717
|
super().__init__()
|
|
644
718
|
self.is_buddy = is_buddy
|
|
645
719
|
self.radius = BUDDY_RADIUS if is_buddy else SURVIVOR_RADIUS
|
|
646
|
-
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(
|
|
647
725
|
self.radius,
|
|
648
726
|
is_buddy=is_buddy,
|
|
727
|
+
draw_hands=is_buddy,
|
|
649
728
|
)
|
|
729
|
+
self.image = self.directional_images[self.facing_bin]
|
|
650
730
|
self.rect = self.image.get_rect(center=(int(x), int(y)))
|
|
651
731
|
self.x = float(self.rect.centerx)
|
|
652
732
|
self.y = float(self.rect.centery)
|
|
@@ -683,6 +763,7 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
683
763
|
grid_rows: int | None = None,
|
|
684
764
|
level_width: int | None = None,
|
|
685
765
|
level_height: int | None = None,
|
|
766
|
+
wall_target_cell: tuple[int, int] | None = None,
|
|
686
767
|
) -> None:
|
|
687
768
|
if level_width is None or level_height is None:
|
|
688
769
|
raise ValueError("level_width/level_height are required for movement")
|
|
@@ -691,11 +772,19 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
691
772
|
self.rect.center = (int(self.x), int(self.y))
|
|
692
773
|
return
|
|
693
774
|
|
|
694
|
-
|
|
695
|
-
|
|
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
|
|
696
784
|
dist_sq = dx * dx + dy * dy
|
|
697
785
|
if dist_sq <= 0:
|
|
698
786
|
self.rect.center = (int(self.x), int(self.y))
|
|
787
|
+
self._update_facing_for_bump(False)
|
|
699
788
|
return
|
|
700
789
|
|
|
701
790
|
dist = math.sqrt(dist_sq)
|
|
@@ -720,32 +809,45 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
720
809
|
grid_rows=grid_rows,
|
|
721
810
|
)
|
|
722
811
|
|
|
812
|
+
self._update_input_facing(move_x, move_y)
|
|
813
|
+
inner_wall_hit = False
|
|
814
|
+
|
|
723
815
|
if move_x:
|
|
724
816
|
self.x += move_x
|
|
725
817
|
self.rect.centerx = int(self.x)
|
|
726
|
-
|
|
818
|
+
wall = spritecollideany_walls(
|
|
727
819
|
self,
|
|
728
820
|
walls,
|
|
729
821
|
wall_index=wall_index,
|
|
730
822
|
cell_size=cell_size,
|
|
731
|
-
)
|
|
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
|
|
732
829
|
self.x -= move_x
|
|
733
830
|
self.rect.centerx = int(self.x)
|
|
734
831
|
if move_y:
|
|
735
832
|
self.y += move_y
|
|
736
833
|
self.rect.centery = int(self.y)
|
|
737
|
-
|
|
834
|
+
wall = spritecollideany_walls(
|
|
738
835
|
self,
|
|
739
836
|
walls,
|
|
740
837
|
wall_index=wall_index,
|
|
741
838
|
cell_size=cell_size,
|
|
742
|
-
)
|
|
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
|
|
743
845
|
self.y -= move_y
|
|
744
846
|
self.rect.centery = int(self.y)
|
|
745
847
|
|
|
746
848
|
overlap_radius = (self.radius + PLAYER_RADIUS) * 1.05
|
|
747
|
-
dx_after =
|
|
748
|
-
dy_after =
|
|
849
|
+
dx_after = target_pos[0] - self.x
|
|
850
|
+
dy_after = target_pos[1] - self.y
|
|
749
851
|
dist_after_sq = dx_after * dx_after + dy_after * dy_after
|
|
750
852
|
if 0 < dist_after_sq < overlap_radius * overlap_radius:
|
|
751
853
|
dist_after = math.sqrt(dist_after_sq)
|
|
@@ -757,6 +859,7 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
757
859
|
self.x = min(level_width, max(0, self.x))
|
|
758
860
|
self.y = min(level_height, max(0, self.y))
|
|
759
861
|
self.rect.center = (int(self.x), int(self.y))
|
|
862
|
+
self._update_facing_for_bump(inner_wall_hit)
|
|
760
863
|
return
|
|
761
864
|
|
|
762
865
|
dx = player_pos[0] - self.x
|
|
@@ -772,6 +875,8 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
772
875
|
move_x = (dx / dist) * SURVIVOR_APPROACH_SPEED
|
|
773
876
|
move_y = (dy / dist) * SURVIVOR_APPROACH_SPEED
|
|
774
877
|
|
|
878
|
+
self._update_input_facing(move_x, move_y)
|
|
879
|
+
|
|
775
880
|
if (
|
|
776
881
|
cell_size is not None
|
|
777
882
|
and wall_cells is not None
|
|
@@ -814,6 +919,37 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
814
919
|
self.rect.centery = int(self.y)
|
|
815
920
|
|
|
816
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)
|
|
817
953
|
|
|
818
954
|
|
|
819
955
|
def random_position_outside_building(
|
|
@@ -1224,13 +1360,15 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1224
1360
|
) -> None:
|
|
1225
1361
|
super().__init__()
|
|
1226
1362
|
self.radius = ZOMBIE_RADIUS
|
|
1363
|
+
self.facing_bin = 0
|
|
1227
1364
|
self.tracker = tracker
|
|
1228
1365
|
self.wall_follower = wall_follower
|
|
1229
1366
|
self.carbonized = False
|
|
1230
|
-
self.
|
|
1231
|
-
self.radius,
|
|
1367
|
+
self.directional_images = build_zombie_directional_surfaces(
|
|
1368
|
+
self.radius,
|
|
1369
|
+
draw_hands=False,
|
|
1232
1370
|
)
|
|
1233
|
-
self.
|
|
1371
|
+
self.image = self.directional_images[self.facing_bin]
|
|
1234
1372
|
self.rect = self.image.get_rect(center=(x, y))
|
|
1235
1373
|
jitter_base = FAST_ZOMBIE_BASE_SPEED if speed > ZOMBIE_SPEED else ZOMBIE_SPEED
|
|
1236
1374
|
jitter = jitter_base * 0.2
|
|
@@ -1269,15 +1407,6 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1269
1407
|
0, self.wander_interval_ms + RNG.randint(-500, 500)
|
|
1270
1408
|
)
|
|
1271
1409
|
|
|
1272
|
-
def _redraw_image(self: Self, palm_angle: float | None = None) -> None:
|
|
1273
|
-
paint_zombie_surface(
|
|
1274
|
-
self.image,
|
|
1275
|
-
radius=self.radius,
|
|
1276
|
-
palm_angle=palm_angle,
|
|
1277
|
-
tracker=self.tracker,
|
|
1278
|
-
wall_follower=self.wall_follower,
|
|
1279
|
-
)
|
|
1280
|
-
|
|
1281
1410
|
def _update_mode(
|
|
1282
1411
|
self: Self, player_center: tuple[int, int], sight_range: float
|
|
1283
1412
|
) -> bool:
|
|
@@ -1384,6 +1513,53 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1384
1513
|
slowdown_ratio = 1.0 - progress * (1.0 - ZOMBIE_AGING_MIN_SPEED_RATIO)
|
|
1385
1514
|
self.speed = self.initial_speed * slowdown_ratio
|
|
1386
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
|
+
|
|
1387
1563
|
def _update_stuck_state(self: Self) -> None:
|
|
1388
1564
|
history = self.pos_history
|
|
1389
1565
|
history.append((self.x, self.y))
|
|
@@ -1453,18 +1629,8 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1453
1629
|
grid_cols=grid_cols,
|
|
1454
1630
|
grid_rows=grid_rows,
|
|
1455
1631
|
)
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
heading = math.atan2(move_y, move_x)
|
|
1459
|
-
elif self.wall_follow_angle is not None:
|
|
1460
|
-
heading = self.wall_follow_angle
|
|
1461
|
-
else:
|
|
1462
|
-
heading = self.wander_angle
|
|
1463
|
-
if self.wall_follow_side > 0:
|
|
1464
|
-
palm_angle = heading + (math.pi / 2.0)
|
|
1465
|
-
else:
|
|
1466
|
-
palm_angle = heading - (math.pi / 2.0)
|
|
1467
|
-
self._redraw_image(palm_angle)
|
|
1632
|
+
self._update_facing_from_movement(move_x, move_y)
|
|
1633
|
+
self._apply_render_overlays()
|
|
1468
1634
|
final_x, final_y = self._handle_wall_collision(
|
|
1469
1635
|
self.x + move_x, self.y + move_y, walls
|
|
1470
1636
|
)
|
|
@@ -1482,23 +1648,31 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1482
1648
|
return
|
|
1483
1649
|
self.carbonized = True
|
|
1484
1650
|
self.speed = 0
|
|
1651
|
+
self.image = self.directional_images[self.facing_bin].copy()
|
|
1485
1652
|
self.image.fill((0, 0, 0, 0))
|
|
1486
1653
|
color = (80, 80, 80)
|
|
1487
|
-
|
|
1654
|
+
center = self.image.get_rect().center
|
|
1655
|
+
pygame.draw.circle(self.image, color, center, self.radius)
|
|
1488
1656
|
pygame.draw.circle(
|
|
1489
|
-
self.image,
|
|
1657
|
+
self.image,
|
|
1658
|
+
(30, 30, 30),
|
|
1659
|
+
center,
|
|
1660
|
+
self.radius,
|
|
1661
|
+
width=1,
|
|
1490
1662
|
)
|
|
1491
1663
|
|
|
1492
1664
|
|
|
1493
1665
|
class Car(pygame.sprite.Sprite):
|
|
1494
1666
|
def __init__(self: Self, x: int, y: int, *, appearance: str = "default") -> None:
|
|
1495
1667
|
super().__init__()
|
|
1668
|
+
self.facing_bin = ANGLE_BINS * 3 // 4
|
|
1669
|
+
self.input_facing_bin = self.facing_bin
|
|
1496
1670
|
self.original_image = build_car_surface(CAR_WIDTH, CAR_HEIGHT)
|
|
1671
|
+
self.directional_images: list[pygame.Surface] = []
|
|
1497
1672
|
self.appearance = appearance
|
|
1498
1673
|
self.image = self.original_image.copy()
|
|
1499
1674
|
self.rect = self.image.get_rect(center=(x, y))
|
|
1500
1675
|
self.speed = CAR_SPEED
|
|
1501
|
-
self.angle = 0
|
|
1502
1676
|
self.x = float(self.rect.centerx)
|
|
1503
1677
|
self.y = float(self.rect.centery)
|
|
1504
1678
|
self.health = CAR_HEALTH
|
|
@@ -1520,10 +1694,28 @@ class Car(pygame.sprite.Sprite):
|
|
|
1520
1694
|
height=CAR_HEIGHT,
|
|
1521
1695
|
color=color,
|
|
1522
1696
|
)
|
|
1523
|
-
self.
|
|
1697
|
+
self.directional_images = build_car_directional_surfaces(self.original_image)
|
|
1698
|
+
self.image = self.directional_images[self.facing_bin]
|
|
1524
1699
|
old_center = self.rect.center
|
|
1525
1700
|
self.rect = self.image.get_rect(center=old_center)
|
|
1526
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
|
+
|
|
1527
1719
|
def move(
|
|
1528
1720
|
self: Self,
|
|
1529
1721
|
dx: float,
|
|
@@ -1537,11 +1729,6 @@ class Car(pygame.sprite.Sprite):
|
|
|
1537
1729
|
if dx == 0 and dy == 0:
|
|
1538
1730
|
self.rect.center = (int(self.x), int(self.y))
|
|
1539
1731
|
return
|
|
1540
|
-
target_angle = math.degrees(math.atan2(-dy, dx)) - 90
|
|
1541
|
-
self.angle = target_angle
|
|
1542
|
-
self.image = pygame.transform.rotate(self.original_image, self.angle)
|
|
1543
|
-
old_center = (self.x, self.y)
|
|
1544
|
-
self.rect = self.image.get_rect(center=old_center)
|
|
1545
1732
|
new_x = self.x + dx
|
|
1546
1733
|
new_y = self.y + dy
|
|
1547
1734
|
|