zombie-escape 1.3.5__tar.gz → 1.3.8__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.3.5 → zombie_escape-1.3.8}/.gitignore +4 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/PKG-INFO +19 -15
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/README.md +18 -14
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/__about__.py +1 -1
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/entities.py +339 -43
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/gameplay/logic.py +69 -39
- zombie_escape-1.3.5/src/zombie_escape/constants.py → zombie_escape-1.3.8/src/zombie_escape/gameplay_constants.py +17 -84
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/level_blueprints.py +1 -4
- zombie_escape-1.3.8/src/zombie_escape/level_constants.py +20 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/locales/ui.en.json +18 -3
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/locales/ui.ja.json +20 -5
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/models.py +13 -11
- zombie_escape-1.3.8/src/zombie_escape/progress.py +48 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/render.py +29 -23
- zombie_escape-1.3.8/src/zombie_escape/render_constants.py +52 -0
- zombie_escape-1.3.8/src/zombie_escape/screen_constants.py +21 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/screens/__init__.py +1 -1
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/screens/game_over.py +5 -6
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/screens/gameplay.py +7 -5
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/screens/settings.py +94 -21
- zombie_escape-1.3.8/src/zombie_escape/screens/title.py +379 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/zombie_escape.py +6 -4
- zombie_escape-1.3.5/src/zombie_escape/screens/title.py +0 -186
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/LICENSE.txt +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/pyproject.toml +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/__init__.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/colors.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/config.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/font_utils.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/gameplay/__init__.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/localization.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/render_assets.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.8}/src/zombie_escape/rng.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zombie-escape
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.8
|
|
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>
|
|
@@ -50,20 +50,7 @@ This game is a simple 2D top-down action game where the player aims to escape by
|
|
|
50
50
|
- **Window Scale (title/settings only):** `[` to shrink, `]` to enlarge
|
|
51
51
|
- **Time Acceleration:** Hold either `Shift` key to run the entire world 4x faster; release to return to normal speed.
|
|
52
52
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
Open **Settings** from the title to toggle gameplay assists:
|
|
56
|
-
|
|
57
|
-
- **Footprints:** Leave breadcrumb trails so you can backtrack in the dark.
|
|
58
|
-
- **Fast zombies:** Allow faster zombie variants; each zombie rolls a random speed between the normal and fast ranges.
|
|
59
|
-
- **Car hint:** After a delay, show a small triangle pointing toward the fuel (Stage 2 before pickup) or the car.
|
|
60
|
-
- **Steel beams:** Adds tougher single-cell obstacles (5% density) that block movement; hidden when stacked with an inner wall until that wall is destroyed.
|
|
61
|
-
|
|
62
|
-
### Shared Seeds
|
|
63
|
-
|
|
64
|
-
The title screen also lets you enter a numeric **seed**. Type digits (or pass `--seed <number>` on the CLI) to lock the procedural layout, wall placement, and pickups; share that seed with a friend and you will both play the exact same stage even on different machines. The current seed is shown at the bottom right of the title screen and in-game HUD. Backspace reverts to an automatically generated value so you can quickly roll a fresh challenge.
|
|
65
|
-
|
|
66
|
-
## Game Rules
|
|
53
|
+
## Title Screen
|
|
67
54
|
|
|
68
55
|
### Stages
|
|
69
56
|
|
|
@@ -75,8 +62,25 @@ At the title screen you can pick a stage:
|
|
|
75
62
|
- **Stage 4: Evacuate Survivors** — start fueled, find the car, gather nearby civilians, and escape before zombies reach them. Stage 4 sprinkles extra parked cars across the map; slamming into one while already driving fully repairs your current ride and adds five more safe seats.
|
|
76
63
|
- **Stage 5: Survive Until Dawn** — every car is bone-dry. Endure until the sun rises while the horde presses in from every direction. Once dawn hits, outdoor zombies carbonize and you must walk out through an existing exterior gap to win; cars remain unusable.
|
|
77
64
|
|
|
65
|
+
**Stage names are red until cleared** and turn white after at least one clear.
|
|
66
|
+
|
|
78
67
|
An objective reminder is shown at the top-left during play.
|
|
79
68
|
|
|
69
|
+
### Shared Seeds
|
|
70
|
+
|
|
71
|
+
The title screen also lets you enter a numeric **seed**. Type digits (or pass `--seed <number>` on the CLI) to lock the procedural layout, wall placement, and pickups; share that seed with a friend and you will both play the exact same stage even on different machines. The current seed is shown at the bottom right of the title screen and in-game HUD. Backspace reverts to an automatically generated value so you can quickly roll a fresh challenge.
|
|
72
|
+
|
|
73
|
+
## Settings Screen
|
|
74
|
+
|
|
75
|
+
Open **Settings** from the title to toggle gameplay assists:
|
|
76
|
+
|
|
77
|
+
- **Footprints:** Leave breadcrumb trails so you can backtrack in the dark.
|
|
78
|
+
- **Fast zombies:** Allow faster zombie variants; each zombie rolls a random speed between the normal and fast ranges.
|
|
79
|
+
- **Car hint:** After a delay, show a small triangle pointing toward the fuel (Stage 2 before pickup) or the car.
|
|
80
|
+
- **Steel beams:** Adds tougher single-cell obstacles (5% density) that block movement; hidden when stacked with an inner wall until that wall is destroyed.
|
|
81
|
+
|
|
82
|
+
## Game Rules
|
|
83
|
+
|
|
80
84
|
### Characters/Items
|
|
81
85
|
|
|
82
86
|
- **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.
|
|
@@ -29,20 +29,7 @@ This game is a simple 2D top-down action game where the player aims to escape by
|
|
|
29
29
|
- **Window Scale (title/settings only):** `[` to shrink, `]` to enlarge
|
|
30
30
|
- **Time Acceleration:** Hold either `Shift` key to run the entire world 4x faster; release to return to normal speed.
|
|
31
31
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
Open **Settings** from the title to toggle gameplay assists:
|
|
35
|
-
|
|
36
|
-
- **Footprints:** Leave breadcrumb trails so you can backtrack in the dark.
|
|
37
|
-
- **Fast zombies:** Allow faster zombie variants; each zombie rolls a random speed between the normal and fast ranges.
|
|
38
|
-
- **Car hint:** After a delay, show a small triangle pointing toward the fuel (Stage 2 before pickup) or the car.
|
|
39
|
-
- **Steel beams:** Adds tougher single-cell obstacles (5% density) that block movement; hidden when stacked with an inner wall until that wall is destroyed.
|
|
40
|
-
|
|
41
|
-
### Shared Seeds
|
|
42
|
-
|
|
43
|
-
The title screen also lets you enter a numeric **seed**. Type digits (or pass `--seed <number>` on the CLI) to lock the procedural layout, wall placement, and pickups; share that seed with a friend and you will both play the exact same stage even on different machines. The current seed is shown at the bottom right of the title screen and in-game HUD. Backspace reverts to an automatically generated value so you can quickly roll a fresh challenge.
|
|
44
|
-
|
|
45
|
-
## Game Rules
|
|
32
|
+
## Title Screen
|
|
46
33
|
|
|
47
34
|
### Stages
|
|
48
35
|
|
|
@@ -54,8 +41,25 @@ At the title screen you can pick a stage:
|
|
|
54
41
|
- **Stage 4: Evacuate Survivors** — start fueled, find the car, gather nearby civilians, and escape before zombies reach them. Stage 4 sprinkles extra parked cars across the map; slamming into one while already driving fully repairs your current ride and adds five more safe seats.
|
|
55
42
|
- **Stage 5: Survive Until Dawn** — every car is bone-dry. Endure until the sun rises while the horde presses in from every direction. Once dawn hits, outdoor zombies carbonize and you must walk out through an existing exterior gap to win; cars remain unusable.
|
|
56
43
|
|
|
44
|
+
**Stage names are red until cleared** and turn white after at least one clear.
|
|
45
|
+
|
|
57
46
|
An objective reminder is shown at the top-left during play.
|
|
58
47
|
|
|
48
|
+
### Shared Seeds
|
|
49
|
+
|
|
50
|
+
The title screen also lets you enter a numeric **seed**. Type digits (or pass `--seed <number>` on the CLI) to lock the procedural layout, wall placement, and pickups; share that seed with a friend and you will both play the exact same stage even on different machines. The current seed is shown at the bottom right of the title screen and in-game HUD. Backspace reverts to an automatically generated value so you can quickly roll a fresh challenge.
|
|
51
|
+
|
|
52
|
+
## Settings Screen
|
|
53
|
+
|
|
54
|
+
Open **Settings** from the title to toggle gameplay assists:
|
|
55
|
+
|
|
56
|
+
- **Footprints:** Leave breadcrumb trails so you can backtrack in the dark.
|
|
57
|
+
- **Fast zombies:** Allow faster zombie variants; each zombie rolls a random speed between the normal and fast ranges.
|
|
58
|
+
- **Car hint:** After a delay, show a small triangle pointing toward the fuel (Stage 2 before pickup) or the car.
|
|
59
|
+
- **Steel beams:** Adds tougher single-cell obstacles (5% density) that block movement; hidden when stacked with an inner wall until that wall is destroyed.
|
|
60
|
+
|
|
61
|
+
## Game Rules
|
|
62
|
+
|
|
59
63
|
### Characters/Items
|
|
60
64
|
|
|
61
65
|
- **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.
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Callable, Iterable, Self
|
|
7
|
+
from typing import Callable, Iterable, Self, Sequence
|
|
8
8
|
|
|
9
9
|
import pygame
|
|
10
10
|
from pygame import rect
|
|
@@ -21,50 +21,50 @@ from .colors import (
|
|
|
21
21
|
STEEL_BEAM_LINE_COLOR,
|
|
22
22
|
YELLOW,
|
|
23
23
|
)
|
|
24
|
-
from .
|
|
25
|
-
CAR_HEIGHT,
|
|
24
|
+
from .gameplay_constants import (
|
|
26
25
|
CAR_HEALTH,
|
|
26
|
+
CAR_HEIGHT,
|
|
27
27
|
CAR_SPEED,
|
|
28
|
-
CAR_WIDTH,
|
|
29
28
|
CAR_WALL_DAMAGE,
|
|
29
|
+
CAR_WIDTH,
|
|
30
30
|
COMPANION_COLOR,
|
|
31
31
|
COMPANION_FOLLOW_SPEED,
|
|
32
32
|
COMPANION_RADIUS,
|
|
33
|
-
|
|
33
|
+
FAST_ZOMBIE_BASE_SPEED,
|
|
34
34
|
FLASHLIGHT_HEIGHT,
|
|
35
35
|
FLASHLIGHT_WIDTH,
|
|
36
36
|
FUEL_CAN_HEIGHT,
|
|
37
37
|
FUEL_CAN_WIDTH,
|
|
38
|
+
INTERNAL_WALL_BEVEL_DEPTH,
|
|
38
39
|
INTERNAL_WALL_HEALTH,
|
|
39
|
-
LEVEL_HEIGHT,
|
|
40
|
-
LEVEL_WIDTH,
|
|
41
|
-
NORMAL_ZOMBIE_SPEED_JITTER,
|
|
42
40
|
PLAYER_RADIUS,
|
|
43
41
|
PLAYER_SPEED,
|
|
44
42
|
PLAYER_WALL_DAMAGE,
|
|
45
|
-
SCREEN_HEIGHT,
|
|
46
|
-
SCREEN_WIDTH,
|
|
47
43
|
STEEL_BEAM_HEALTH,
|
|
48
44
|
SURVIVOR_APPROACH_RADIUS,
|
|
49
45
|
SURVIVOR_APPROACH_SPEED,
|
|
50
46
|
SURVIVOR_COLOR,
|
|
51
47
|
SURVIVOR_RADIUS,
|
|
48
|
+
ZOMBIE_AGING_DURATION_FRAMES,
|
|
49
|
+
ZOMBIE_AGING_MIN_SPEED_RATIO,
|
|
52
50
|
ZOMBIE_MODE_CHANGE_INTERVAL_MS,
|
|
53
51
|
ZOMBIE_RADIUS,
|
|
54
52
|
ZOMBIE_SEPARATION_DISTANCE,
|
|
55
|
-
ZOMBIE_AGING_DURATION_FRAMES,
|
|
56
|
-
ZOMBIE_AGING_MIN_SPEED_RATIO,
|
|
57
53
|
ZOMBIE_SIGHT_RANGE,
|
|
58
54
|
ZOMBIE_SPEED,
|
|
59
55
|
ZOMBIE_WALL_DAMAGE,
|
|
60
56
|
car_body_radius,
|
|
61
57
|
)
|
|
58
|
+
from .level_constants import LEVEL_HEIGHT, LEVEL_WIDTH
|
|
59
|
+
from .screen_constants import SCREEN_HEIGHT, SCREEN_WIDTH
|
|
62
60
|
from .rng import get_rng
|
|
63
61
|
|
|
64
62
|
RNG = get_rng()
|
|
65
63
|
|
|
66
64
|
|
|
67
|
-
def circle_rect_collision(
|
|
65
|
+
def circle_rect_collision(
|
|
66
|
+
center: tuple[float, float], radius: float, rect_obj: rect.Rect
|
|
67
|
+
) -> bool:
|
|
68
68
|
"""Return True if a circle overlaps the provided rectangle."""
|
|
69
69
|
cx, cy = center
|
|
70
70
|
closest_x = max(rect_obj.left, min(cx, rect_obj.right))
|
|
@@ -74,6 +74,242 @@ def circle_rect_collision(center: tuple[float, float], radius: float, rect_obj:
|
|
|
74
74
|
return dx * dx + dy * dy <= radius * radius
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def _build_beveled_polygon(
|
|
78
|
+
width: int,
|
|
79
|
+
height: int,
|
|
80
|
+
depth: int,
|
|
81
|
+
bevels: tuple[bool, bool, bool, bool],
|
|
82
|
+
) -> list[tuple[int, int]]:
|
|
83
|
+
d = max(0, min(depth, width // 2, height // 2))
|
|
84
|
+
if d == 0 or not any(bevels):
|
|
85
|
+
return [(0, 0), (width, 0), (width, height), (0, height)]
|
|
86
|
+
|
|
87
|
+
segments = max(4, d // 2)
|
|
88
|
+
tl, tr, br, bl = bevels
|
|
89
|
+
points: list[tuple[int, int]] = []
|
|
90
|
+
|
|
91
|
+
def add_point(x: float, y: float) -> None:
|
|
92
|
+
point = (int(round(x)), int(round(y)))
|
|
93
|
+
if not points or points[-1] != point:
|
|
94
|
+
points.append(point)
|
|
95
|
+
|
|
96
|
+
def add_arc(
|
|
97
|
+
center_x: float,
|
|
98
|
+
center_y: float,
|
|
99
|
+
radius: float,
|
|
100
|
+
start_deg: float,
|
|
101
|
+
end_deg: float,
|
|
102
|
+
*,
|
|
103
|
+
skip_first: bool = False,
|
|
104
|
+
skip_last: bool = False,
|
|
105
|
+
) -> None:
|
|
106
|
+
for i in range(segments + 1):
|
|
107
|
+
if skip_first and i == 0:
|
|
108
|
+
continue
|
|
109
|
+
if skip_last and i == segments:
|
|
110
|
+
continue
|
|
111
|
+
t = i / segments
|
|
112
|
+
angle = math.radians(start_deg + (end_deg - start_deg) * t)
|
|
113
|
+
add_point(
|
|
114
|
+
center_x + radius * math.cos(angle),
|
|
115
|
+
center_y + radius * math.sin(angle),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
add_point(d if tl else 0, 0)
|
|
119
|
+
if tr:
|
|
120
|
+
add_point(width - d, 0)
|
|
121
|
+
add_arc(width - d, d, d, -90, 0, skip_first=True)
|
|
122
|
+
else:
|
|
123
|
+
add_point(width, 0)
|
|
124
|
+
if br:
|
|
125
|
+
add_point(width, height - d)
|
|
126
|
+
add_arc(width - d, height - d, d, 0, 90, skip_first=True)
|
|
127
|
+
else:
|
|
128
|
+
add_point(width, height)
|
|
129
|
+
if bl:
|
|
130
|
+
add_point(d, height)
|
|
131
|
+
add_arc(d, height - d, d, 90, 180, skip_first=True)
|
|
132
|
+
else:
|
|
133
|
+
add_point(0, height)
|
|
134
|
+
if tl:
|
|
135
|
+
add_point(0, d)
|
|
136
|
+
add_arc(d, d, d, 180, 270, skip_first=True, skip_last=True)
|
|
137
|
+
return points
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _point_in_polygon(
|
|
141
|
+
point: tuple[float, float], polygon: Sequence[tuple[float, float]]
|
|
142
|
+
) -> bool:
|
|
143
|
+
x, y = point
|
|
144
|
+
inside = False
|
|
145
|
+
count = len(polygon)
|
|
146
|
+
j = count - 1
|
|
147
|
+
for i in range(count):
|
|
148
|
+
xi, yi = polygon[i]
|
|
149
|
+
xj, yj = polygon[j]
|
|
150
|
+
intersects = (yi > y) != (yj > y) and (
|
|
151
|
+
x < (xj - xi) * (y - yi) / (yj - yi + 0.000001) + xi
|
|
152
|
+
)
|
|
153
|
+
if intersects:
|
|
154
|
+
inside = not inside
|
|
155
|
+
j = i
|
|
156
|
+
return inside
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _segments_intersect(
|
|
160
|
+
a1: tuple[float, float],
|
|
161
|
+
a2: tuple[float, float],
|
|
162
|
+
b1: tuple[float, float],
|
|
163
|
+
b2: tuple[float, float],
|
|
164
|
+
) -> bool:
|
|
165
|
+
def orient(
|
|
166
|
+
p: tuple[float, float], q: tuple[float, float], r: tuple[float, float]
|
|
167
|
+
) -> float:
|
|
168
|
+
return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])
|
|
169
|
+
|
|
170
|
+
def on_segment(
|
|
171
|
+
p: tuple[float, float], q: tuple[float, float], r: tuple[float, float]
|
|
172
|
+
) -> bool:
|
|
173
|
+
return min(p[0], r[0]) <= q[0] <= max(p[0], r[0]) and min(p[1], r[1]) <= q[
|
|
174
|
+
1
|
|
175
|
+
] <= max(p[1], r[1])
|
|
176
|
+
|
|
177
|
+
o1 = orient(a1, a2, b1)
|
|
178
|
+
o2 = orient(a1, a2, b2)
|
|
179
|
+
o3 = orient(b1, b2, a1)
|
|
180
|
+
o4 = orient(b1, b2, a2)
|
|
181
|
+
|
|
182
|
+
if (o1 > 0) != (o2 > 0) and (o3 > 0) != (o4 > 0):
|
|
183
|
+
return True
|
|
184
|
+
if o1 == 0 and on_segment(a1, b1, a2):
|
|
185
|
+
return True
|
|
186
|
+
if o2 == 0 and on_segment(a1, b2, a2):
|
|
187
|
+
return True
|
|
188
|
+
if o3 == 0 and on_segment(b1, a1, b2):
|
|
189
|
+
return True
|
|
190
|
+
if o4 == 0 and on_segment(b1, a2, b2):
|
|
191
|
+
return True
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _point_segment_distance_sq(
|
|
196
|
+
point: tuple[float, float],
|
|
197
|
+
seg_a: tuple[float, float],
|
|
198
|
+
seg_b: tuple[float, float],
|
|
199
|
+
) -> float:
|
|
200
|
+
px, py = point
|
|
201
|
+
ax, ay = seg_a
|
|
202
|
+
bx, by = seg_b
|
|
203
|
+
dx = bx - ax
|
|
204
|
+
dy = by - ay
|
|
205
|
+
if dx == 0 and dy == 0:
|
|
206
|
+
return (px - ax) ** 2 + (py - ay) ** 2
|
|
207
|
+
t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)
|
|
208
|
+
t = max(0.0, min(1.0, t))
|
|
209
|
+
nearest_x = ax + t * dx
|
|
210
|
+
nearest_y = ay + t * dy
|
|
211
|
+
return (px - nearest_x) ** 2 + (py - nearest_y) ** 2
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def rect_polygon_collision(
|
|
215
|
+
rect_obj: rect.Rect, polygon: Sequence[tuple[float, float]]
|
|
216
|
+
) -> bool:
|
|
217
|
+
min_x = min(p[0] for p in polygon)
|
|
218
|
+
max_x = max(p[0] for p in polygon)
|
|
219
|
+
min_y = min(p[1] for p in polygon)
|
|
220
|
+
max_y = max(p[1] for p in polygon)
|
|
221
|
+
if not rect_obj.colliderect(
|
|
222
|
+
pygame.Rect(min_x, min_y, max_x - min_x, max_y - min_y)
|
|
223
|
+
):
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
rect_points = [
|
|
227
|
+
(rect_obj.left, rect_obj.top),
|
|
228
|
+
(rect_obj.right, rect_obj.top),
|
|
229
|
+
(rect_obj.right, rect_obj.bottom),
|
|
230
|
+
(rect_obj.left, rect_obj.bottom),
|
|
231
|
+
]
|
|
232
|
+
if any(_point_in_polygon(p, polygon) for p in rect_points):
|
|
233
|
+
return True
|
|
234
|
+
if any(rect_obj.collidepoint(p) for p in polygon):
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
rect_edges = [
|
|
238
|
+
(rect_points[0], rect_points[1]),
|
|
239
|
+
(rect_points[1], rect_points[2]),
|
|
240
|
+
(rect_points[2], rect_points[3]),
|
|
241
|
+
(rect_points[3], rect_points[0]),
|
|
242
|
+
]
|
|
243
|
+
poly_edges = [
|
|
244
|
+
(polygon[i], polygon[(i + 1) % len(polygon)]) for i in range(len(polygon))
|
|
245
|
+
]
|
|
246
|
+
for edge_a in rect_edges:
|
|
247
|
+
for edge_b in poly_edges:
|
|
248
|
+
if _segments_intersect(edge_a[0], edge_a[1], edge_b[0], edge_b[1]):
|
|
249
|
+
return True
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def circle_polygon_collision(
|
|
254
|
+
center: tuple[float, float],
|
|
255
|
+
radius: float,
|
|
256
|
+
polygon: Sequence[tuple[float, float]],
|
|
257
|
+
) -> bool:
|
|
258
|
+
if _point_in_polygon(center, polygon):
|
|
259
|
+
return True
|
|
260
|
+
radius_sq = radius * radius
|
|
261
|
+
for i in range(len(polygon)):
|
|
262
|
+
a = polygon[i]
|
|
263
|
+
b = polygon[(i + 1) % len(polygon)]
|
|
264
|
+
if _point_segment_distance_sq(center, a, b) <= radius_sq:
|
|
265
|
+
return True
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def collide_sprite_wall(
|
|
270
|
+
sprite: pygame.sprite.Sprite, wall: pygame.sprite.Sprite
|
|
271
|
+
) -> bool:
|
|
272
|
+
if hasattr(sprite, "radius"):
|
|
273
|
+
center = sprite.rect.center
|
|
274
|
+
radius = float(getattr(sprite, "radius"))
|
|
275
|
+
if hasattr(wall, "collides_circle"):
|
|
276
|
+
return wall.collides_circle(center, radius)
|
|
277
|
+
return circle_rect_collision(center, radius, wall.rect)
|
|
278
|
+
if hasattr(wall, "collides_rect"):
|
|
279
|
+
return wall.collides_rect(sprite.rect)
|
|
280
|
+
if hasattr(sprite, "collides_rect"):
|
|
281
|
+
return sprite.collides_rect(wall.rect)
|
|
282
|
+
return sprite.rect.colliderect(wall.rect)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def spritecollide_walls(
|
|
286
|
+
sprite: pygame.sprite.Sprite,
|
|
287
|
+
walls: pygame.sprite.Group,
|
|
288
|
+
*,
|
|
289
|
+
dokill: bool = False,
|
|
290
|
+
) -> list[pygame.sprite.Sprite]:
|
|
291
|
+
return pygame.sprite.spritecollide(
|
|
292
|
+
sprite, walls, dokill, collided=collide_sprite_wall
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def spritecollideany_walls(
|
|
297
|
+
sprite: pygame.sprite.Sprite,
|
|
298
|
+
walls: pygame.sprite.Group,
|
|
299
|
+
) -> pygame.sprite.Sprite | None:
|
|
300
|
+
return pygame.sprite.spritecollideany(sprite, walls, collided=collide_sprite_wall)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def circle_wall_collision(
|
|
304
|
+
center: tuple[float, float],
|
|
305
|
+
radius: float,
|
|
306
|
+
wall: pygame.sprite.Sprite,
|
|
307
|
+
) -> bool:
|
|
308
|
+
if hasattr(wall, "collides_circle"):
|
|
309
|
+
return wall.collides_circle(center, radius)
|
|
310
|
+
return circle_rect_collision(center, radius, wall.rect)
|
|
311
|
+
|
|
312
|
+
|
|
77
313
|
# --- Camera Class ---
|
|
78
314
|
class Wall(pygame.sprite.Sprite):
|
|
79
315
|
def __init__(
|
|
@@ -87,20 +323,32 @@ class Wall(pygame.sprite.Sprite):
|
|
|
87
323
|
color: tuple[int, int, int] = INTERNAL_WALL_COLOR,
|
|
88
324
|
border_color: tuple[int, int, int] = INTERNAL_WALL_BORDER_COLOR,
|
|
89
325
|
palette_category: str = "inner_wall",
|
|
326
|
+
bevel_depth: int = INTERNAL_WALL_BEVEL_DEPTH,
|
|
327
|
+
bevel_mask: tuple[bool, bool, bool, bool] | None = None,
|
|
90
328
|
on_destroy: Callable[[Self], None] | None = None,
|
|
91
329
|
) -> None:
|
|
92
330
|
super().__init__()
|
|
93
331
|
safe_width = max(1, width)
|
|
94
332
|
safe_height = max(1, height)
|
|
95
|
-
self.image = pygame.Surface((safe_width, safe_height))
|
|
333
|
+
self.image = pygame.Surface((safe_width, safe_height), pygame.SRCALPHA)
|
|
96
334
|
self.base_color = color
|
|
97
335
|
self.border_base_color = border_color
|
|
98
336
|
self.palette_category = palette_category
|
|
99
337
|
self.health = health
|
|
100
338
|
self.max_health = max(1, health)
|
|
101
339
|
self.on_destroy = on_destroy
|
|
340
|
+
self.bevel_depth = max(0, bevel_depth)
|
|
341
|
+
self.bevel_mask = bevel_mask or (False, False, False, False)
|
|
342
|
+
self._local_polygon = _build_beveled_polygon(
|
|
343
|
+
safe_width, safe_height, self.bevel_depth, self.bevel_mask
|
|
344
|
+
)
|
|
102
345
|
self.update_color()
|
|
103
346
|
self.rect = self.image.get_rect(topleft=(x, y))
|
|
347
|
+
self._collision_polygon = (
|
|
348
|
+
[(px + self.rect.x, py + self.rect.y) for px, py in self._local_polygon]
|
|
349
|
+
if self.bevel_depth > 0 and any(self.bevel_mask)
|
|
350
|
+
else None
|
|
351
|
+
)
|
|
104
352
|
|
|
105
353
|
def take_damage(self: Self, *, amount: int = 1) -> None:
|
|
106
354
|
if self.health > 0:
|
|
@@ -115,9 +363,10 @@ class Wall(pygame.sprite.Sprite):
|
|
|
115
363
|
self.kill()
|
|
116
364
|
|
|
117
365
|
def update_color(self: Self) -> None:
|
|
366
|
+
self.image.fill((0, 0, 0, 0))
|
|
118
367
|
if self.health <= 0:
|
|
119
|
-
self.image.fill((40, 40, 40))
|
|
120
368
|
health_ratio = 0
|
|
369
|
+
fill_color = (40, 40, 40)
|
|
121
370
|
else:
|
|
122
371
|
health_ratio = max(0, self.health / self.max_health)
|
|
123
372
|
mix = (
|
|
@@ -126,12 +375,44 @@ class Wall(pygame.sprite.Sprite):
|
|
|
126
375
|
r = int(self.base_color[0] * mix)
|
|
127
376
|
g = int(self.base_color[1] * mix)
|
|
128
377
|
b = int(self.base_color[2] * mix)
|
|
129
|
-
|
|
378
|
+
fill_color = (r, g, b)
|
|
130
379
|
# Bright edge to separate walls from floor
|
|
131
380
|
br = int(self.border_base_color[0] * (0.6 + 0.4 * health_ratio))
|
|
132
381
|
bg = int(self.border_base_color[1] * (0.6 + 0.4 * health_ratio))
|
|
133
382
|
bb = int(self.border_base_color[2] * (0.6 + 0.4 * health_ratio))
|
|
134
|
-
|
|
383
|
+
border_color = (br, bg, bb)
|
|
384
|
+
|
|
385
|
+
if self.bevel_depth > 0 and any(self.bevel_mask):
|
|
386
|
+
pygame.draw.polygon(self.image, border_color, self._local_polygon)
|
|
387
|
+
else:
|
|
388
|
+
self.image.fill(border_color)
|
|
389
|
+
border_width = 18
|
|
390
|
+
inner_rect = self.image.get_rect().inflate(-border_width, -border_width)
|
|
391
|
+
if inner_rect.width > 0 and inner_rect.height > 0:
|
|
392
|
+
inner_depth = max(0, self.bevel_depth - border_width)
|
|
393
|
+
if inner_depth > 0 and any(self.bevel_mask):
|
|
394
|
+
inner_polygon = _build_beveled_polygon(
|
|
395
|
+
inner_rect.width,
|
|
396
|
+
inner_rect.height,
|
|
397
|
+
inner_depth,
|
|
398
|
+
self.bevel_mask,
|
|
399
|
+
)
|
|
400
|
+
inner_points = [
|
|
401
|
+
(px + inner_rect.x, py + inner_rect.y) for px, py in inner_polygon
|
|
402
|
+
]
|
|
403
|
+
pygame.draw.polygon(self.image, fill_color, inner_points)
|
|
404
|
+
else:
|
|
405
|
+
pygame.draw.rect(self.image, fill_color, inner_rect)
|
|
406
|
+
|
|
407
|
+
def collides_rect(self: Self, rect_obj: rect.Rect) -> bool:
|
|
408
|
+
if self._collision_polygon is None:
|
|
409
|
+
return self.rect.colliderect(rect_obj)
|
|
410
|
+
return rect_polygon_collision(rect_obj, self._collision_polygon)
|
|
411
|
+
|
|
412
|
+
def collides_circle(self: Self, center: tuple[float, float], radius: float) -> bool:
|
|
413
|
+
if self._collision_polygon is None:
|
|
414
|
+
return circle_rect_collision(center, radius, self.rect)
|
|
415
|
+
return circle_polygon_collision(center, radius, self._collision_polygon)
|
|
135
416
|
|
|
136
417
|
def set_palette_colors(
|
|
137
418
|
self: Self,
|
|
@@ -142,7 +423,11 @@ class Wall(pygame.sprite.Sprite):
|
|
|
142
423
|
) -> None:
|
|
143
424
|
"""Update the wall's base colors to match the current ambient palette."""
|
|
144
425
|
|
|
145
|
-
if
|
|
426
|
+
if (
|
|
427
|
+
not force
|
|
428
|
+
and self.base_color == color
|
|
429
|
+
and self.border_base_color == border_color
|
|
430
|
+
):
|
|
146
431
|
return
|
|
147
432
|
self.base_color = color
|
|
148
433
|
self.border_base_color = border_color
|
|
@@ -247,7 +532,7 @@ class Player(pygame.sprite.Sprite):
|
|
|
247
532
|
self.x += dx
|
|
248
533
|
self.x = min(LEVEL_WIDTH, max(0, self.x))
|
|
249
534
|
self.rect.centerx = int(self.x)
|
|
250
|
-
hit_list_x =
|
|
535
|
+
hit_list_x = spritecollide_walls(self, walls)
|
|
251
536
|
if hit_list_x:
|
|
252
537
|
damage = max(1, PLAYER_WALL_DAMAGE // len(hit_list_x))
|
|
253
538
|
for wall in hit_list_x:
|
|
@@ -260,7 +545,7 @@ class Player(pygame.sprite.Sprite):
|
|
|
260
545
|
self.y += dy
|
|
261
546
|
self.y = min(LEVEL_HEIGHT, max(0, self.y))
|
|
262
547
|
self.rect.centery = int(self.y)
|
|
263
|
-
hit_list_y =
|
|
548
|
+
hit_list_y = spritecollide_walls(self, walls)
|
|
264
549
|
if hit_list_y:
|
|
265
550
|
damage = max(1, PLAYER_WALL_DAMAGE // len(hit_list_y))
|
|
266
551
|
for wall in hit_list_y:
|
|
@@ -323,13 +608,13 @@ class Companion(pygame.sprite.Sprite):
|
|
|
323
608
|
if move_x != 0:
|
|
324
609
|
self.x += move_x
|
|
325
610
|
self.rect.centerx = int(self.x)
|
|
326
|
-
if
|
|
611
|
+
if spritecollideany_walls(self, walls):
|
|
327
612
|
self.x -= move_x
|
|
328
613
|
self.rect.centerx = int(self.x)
|
|
329
614
|
if move_y != 0:
|
|
330
615
|
self.y += move_y
|
|
331
616
|
self.rect.centery = int(self.y)
|
|
332
|
-
if
|
|
617
|
+
if spritecollideany_walls(self, walls):
|
|
333
618
|
self.y -= move_y
|
|
334
619
|
self.rect.centery = int(self.y)
|
|
335
620
|
|
|
@@ -378,13 +663,13 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
378
663
|
if move_x:
|
|
379
664
|
self.x += move_x
|
|
380
665
|
self.rect.centerx = int(self.x)
|
|
381
|
-
if
|
|
666
|
+
if spritecollideany_walls(self, walls):
|
|
382
667
|
self.x -= move_x
|
|
383
668
|
self.rect.centerx = int(self.x)
|
|
384
669
|
if move_y:
|
|
385
670
|
self.y += move_y
|
|
386
671
|
self.rect.centery = int(self.y)
|
|
387
|
-
if
|
|
672
|
+
if spritecollideany_walls(self, walls):
|
|
388
673
|
self.y -= move_y
|
|
389
674
|
self.rect.centery = int(self.y)
|
|
390
675
|
|
|
@@ -428,11 +713,8 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
428
713
|
else:
|
|
429
714
|
x, y = random_position_outside_building()
|
|
430
715
|
self.rect = self.image.get_rect(center=(x, y))
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if speed > ZOMBIE_SPEED
|
|
434
|
-
else NORMAL_ZOMBIE_SPEED_JITTER
|
|
435
|
-
)
|
|
716
|
+
jitter_base = FAST_ZOMBIE_BASE_SPEED if speed > ZOMBIE_SPEED else ZOMBIE_SPEED
|
|
717
|
+
jitter = jitter_base * 0.2
|
|
436
718
|
base_speed = speed + RNG.uniform(-jitter, jitter)
|
|
437
719
|
self.initial_speed = base_speed
|
|
438
720
|
self.speed = base_speed
|
|
@@ -500,21 +782,18 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
500
782
|
if abs(w.rect.centerx - self.x) < 100 and abs(w.rect.centery - self.y) < 100
|
|
501
783
|
]
|
|
502
784
|
|
|
503
|
-
temp_rect = self.rect.copy()
|
|
504
|
-
temp_rect.centerx = int(next_x)
|
|
505
|
-
temp_rect.centery = int(self.y)
|
|
506
785
|
for wall in possible_walls:
|
|
507
|
-
|
|
786
|
+
collides = circle_wall_collision((next_x, self.y), self.radius, wall)
|
|
787
|
+
if collides:
|
|
508
788
|
if wall.alive():
|
|
509
789
|
wall.take_damage(amount=ZOMBIE_WALL_DAMAGE)
|
|
510
790
|
if wall.alive():
|
|
511
791
|
final_x = self.x
|
|
512
792
|
break
|
|
513
793
|
|
|
514
|
-
temp_rect.centerx = int(final_x)
|
|
515
|
-
temp_rect.centery = int(next_y)
|
|
516
794
|
for wall in possible_walls:
|
|
517
|
-
|
|
795
|
+
collides = circle_wall_collision((final_x, next_y), self.radius, wall)
|
|
796
|
+
if collides:
|
|
518
797
|
if wall.alive():
|
|
519
798
|
wall.take_damage(amount=ZOMBIE_WALL_DAMAGE)
|
|
520
799
|
if wall.alive():
|
|
@@ -619,14 +898,30 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
619
898
|
self.image.fill((0, 0, 0, 0))
|
|
620
899
|
color = (80, 80, 80)
|
|
621
900
|
pygame.draw.circle(self.image, color, (self.radius, self.radius), self.radius)
|
|
622
|
-
pygame.draw.circle(
|
|
901
|
+
pygame.draw.circle(
|
|
902
|
+
self.image, (30, 30, 30), (self.radius, self.radius), self.radius, width=2
|
|
903
|
+
)
|
|
623
904
|
|
|
624
905
|
|
|
625
906
|
class Car(pygame.sprite.Sprite):
|
|
626
|
-
|
|
907
|
+
COLOR_SCHEMES: dict[str, dict[str, tuple[int, int, int]]] = {
|
|
908
|
+
"default": {
|
|
909
|
+
"healthy": YELLOW,
|
|
910
|
+
"damaged": ORANGE,
|
|
911
|
+
"critical": DARK_RED,
|
|
912
|
+
},
|
|
913
|
+
"disabled": {
|
|
914
|
+
"healthy": (185, 185, 185),
|
|
915
|
+
"damaged": (150, 150, 150),
|
|
916
|
+
"critical": (110, 110, 110),
|
|
917
|
+
},
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
def __init__(self: Self, x: int, y: int, *, appearance: str = "default") -> None:
|
|
627
921
|
super().__init__()
|
|
628
922
|
self.original_image = pygame.Surface((CAR_WIDTH, CAR_HEIGHT), pygame.SRCALPHA)
|
|
629
|
-
self.
|
|
923
|
+
self.appearance = appearance if appearance in self.COLOR_SCHEMES else "default"
|
|
924
|
+
self.base_color = self.COLOR_SCHEMES[self.appearance]["healthy"]
|
|
630
925
|
self.image = self.original_image.copy()
|
|
631
926
|
self.rect = self.image.get_rect(center=(x, y))
|
|
632
927
|
self.speed = CAR_SPEED
|
|
@@ -645,11 +940,12 @@ class Car(pygame.sprite.Sprite):
|
|
|
645
940
|
|
|
646
941
|
def update_color(self: Self) -> None:
|
|
647
942
|
health_ratio = max(0, self.health / self.max_health)
|
|
648
|
-
|
|
943
|
+
palette = self.COLOR_SCHEMES.get(self.appearance, self.COLOR_SCHEMES["default"])
|
|
944
|
+
color = palette["healthy"]
|
|
649
945
|
if health_ratio < 0.6:
|
|
650
|
-
color =
|
|
946
|
+
color = palette["damaged"]
|
|
651
947
|
if health_ratio < 0.3:
|
|
652
|
-
color =
|
|
948
|
+
color = palette["critical"]
|
|
653
949
|
self.original_image.fill((0, 0, 0, 0))
|
|
654
950
|
|
|
655
951
|
body_rect = pygame.Rect(1, 4, CAR_WIDTH - 2, CAR_HEIGHT - 8)
|
|
@@ -739,7 +1035,7 @@ class Car(pygame.sprite.Sprite):
|
|
|
739
1035
|
]
|
|
740
1036
|
car_center = (new_x, new_y)
|
|
741
1037
|
for wall in possible_walls:
|
|
742
|
-
if
|
|
1038
|
+
if circle_wall_collision(car_center, self.collision_radius, wall):
|
|
743
1039
|
hit_walls.append(wall)
|
|
744
1040
|
if hit_walls:
|
|
745
1041
|
self.take_damage(CAR_WALL_DAMAGE)
|