zombie-escape 1.13.1__py3-none-any.whl → 1.14.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zombie_escape/__about__.py +1 -1
- zombie_escape/colors.py +7 -21
- zombie_escape/entities.py +100 -191
- zombie_escape/export_images.py +39 -33
- zombie_escape/gameplay/ambient.py +2 -6
- zombie_escape/gameplay/footprints.py +8 -11
- zombie_escape/gameplay/interactions.py +17 -58
- zombie_escape/gameplay/layout.py +20 -46
- zombie_escape/gameplay/movement.py +7 -21
- zombie_escape/gameplay/spawn.py +12 -40
- zombie_escape/gameplay/state.py +1 -0
- zombie_escape/gameplay/survivors.py +5 -16
- zombie_escape/gameplay/utils.py +4 -13
- zombie_escape/input_utils.py +8 -31
- zombie_escape/level_blueprints.py +112 -69
- zombie_escape/level_constants.py +8 -0
- zombie_escape/locales/ui.en.json +12 -0
- zombie_escape/locales/ui.ja.json +12 -0
- zombie_escape/localization.py +3 -11
- zombie_escape/models.py +26 -9
- zombie_escape/render/__init__.py +30 -0
- zombie_escape/render/core.py +992 -0
- zombie_escape/render/hud.py +444 -0
- zombie_escape/render/overview.py +218 -0
- zombie_escape/render/shadows.py +343 -0
- zombie_escape/render_assets.py +11 -33
- zombie_escape/rng.py +4 -8
- zombie_escape/screens/__init__.py +14 -30
- zombie_escape/screens/game_over.py +43 -15
- zombie_escape/screens/gameplay.py +41 -104
- zombie_escape/screens/settings.py +19 -104
- zombie_escape/screens/title.py +36 -176
- zombie_escape/stage_constants.py +192 -67
- zombie_escape/zombie_escape.py +1 -1
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/METADATA +100 -39
- zombie_escape-1.14.4.dist-info/RECORD +53 -0
- zombie_escape/render.py +0 -1746
- zombie_escape-1.13.1.dist-info/RECORD +0 -49
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/WHEEL +0 -0
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/export_images.py
CHANGED
|
@@ -41,7 +41,7 @@ from .render_assets import (
|
|
|
41
41
|
resolve_wall_colors,
|
|
42
42
|
RUBBLE_ROTATION_DEG,
|
|
43
43
|
)
|
|
44
|
-
from .colors import FALL_ZONE_FLOOR_PRIMARY
|
|
44
|
+
from .colors import FALL_ZONE_FLOOR_PRIMARY, FALL_ZONE_FLOOR_SECONDARY
|
|
45
45
|
from .render_constants import (
|
|
46
46
|
FALLING_ZOMBIE_COLOR,
|
|
47
47
|
PITFALL_ABYSS_COLOR,
|
|
@@ -67,14 +67,15 @@ def _ensure_pygame_ready() -> None:
|
|
|
67
67
|
pygame.display.set_mode((1, 1), flags=flags)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
def _save_surface(surface: pygame.Surface, path: Path) -> None:
|
|
70
|
+
def _save_surface(surface: pygame.Surface, path: Path, *, scale: int = 1) -> None:
|
|
71
|
+
if scale != 1:
|
|
72
|
+
width, height = surface.get_size()
|
|
73
|
+
surface = pygame.transform.scale(surface, (width * scale, height * scale))
|
|
71
74
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
75
|
pygame.image.save(surface, str(path))
|
|
73
76
|
|
|
74
77
|
|
|
75
|
-
def _pick_directional_surface(
|
|
76
|
-
surfaces: list[pygame.Surface], *, bin_index: int = 0
|
|
77
|
-
) -> pygame.Surface:
|
|
78
|
+
def _pick_directional_surface(surfaces: list[pygame.Surface], *, bin_index: int = 0) -> pygame.Surface:
|
|
78
79
|
if not surfaces:
|
|
79
80
|
return pygame.Surface((1, 1), pygame.SRCALPHA)
|
|
80
81
|
return surfaces[bin_index % len(surfaces)]
|
|
@@ -87,10 +88,7 @@ def _build_pitfall_tile(cell_size: int) -> pygame.Surface:
|
|
|
87
88
|
|
|
88
89
|
for i in range(PITFALL_SHADOW_WIDTH):
|
|
89
90
|
t = i / (PITFALL_SHADOW_WIDTH - 1.0)
|
|
90
|
-
color = tuple(
|
|
91
|
-
int(PITFALL_SHADOW_RIM_COLOR[j] * (1.0 - t) + PITFALL_ABYSS_COLOR[j] * t)
|
|
92
|
-
for j in range(3)
|
|
93
|
-
)
|
|
91
|
+
color = tuple(int(PITFALL_SHADOW_RIM_COLOR[j] * (1.0 - t) + PITFALL_ABYSS_COLOR[j] * t) for j in range(3))
|
|
94
92
|
pygame.draw.line(
|
|
95
93
|
surface,
|
|
96
94
|
color,
|
|
@@ -118,7 +116,7 @@ def _build_pitfall_tile(cell_size: int) -> pygame.Surface:
|
|
|
118
116
|
return surface
|
|
119
117
|
|
|
120
118
|
|
|
121
|
-
def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> list[Path]:
|
|
119
|
+
def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE, output_scale: int = 4) -> list[Path]:
|
|
122
120
|
_ensure_pygame_ready()
|
|
123
121
|
|
|
124
122
|
saved: list[Path] = []
|
|
@@ -129,7 +127,7 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
129
127
|
bin_index=0,
|
|
130
128
|
)
|
|
131
129
|
player_path = out / "player.png"
|
|
132
|
-
_save_surface(player, player_path)
|
|
130
|
+
_save_surface(player, player_path, scale=output_scale)
|
|
133
131
|
saved.append(player_path)
|
|
134
132
|
|
|
135
133
|
zombie_base = _pick_directional_surface(
|
|
@@ -137,7 +135,7 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
137
135
|
bin_index=0,
|
|
138
136
|
)
|
|
139
137
|
zombie_normal_path = out / "zombie-normal.png"
|
|
140
|
-
_save_surface(zombie_base, zombie_normal_path)
|
|
138
|
+
_save_surface(zombie_base, zombie_normal_path, scale=output_scale)
|
|
141
139
|
saved.append(zombie_normal_path)
|
|
142
140
|
|
|
143
141
|
tracker = zombie_base.copy()
|
|
@@ -148,7 +146,7 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
148
146
|
color=ZOMBIE_NOSE_COLOR,
|
|
149
147
|
)
|
|
150
148
|
tracker_path = out / "zombie-tracker.png"
|
|
151
|
-
_save_surface(tracker, tracker_path)
|
|
149
|
+
_save_surface(tracker, tracker_path, scale=output_scale)
|
|
152
150
|
saved.append(tracker_path)
|
|
153
151
|
|
|
154
152
|
wall_hugging = zombie_base.copy()
|
|
@@ -159,7 +157,7 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
159
157
|
color=ZOMBIE_NOSE_COLOR,
|
|
160
158
|
)
|
|
161
159
|
wall_path = out / "zombie-wall.png"
|
|
162
|
-
_save_surface(wall_hugging, wall_path)
|
|
160
|
+
_save_surface(wall_hugging, wall_path, scale=output_scale)
|
|
163
161
|
saved.append(wall_path)
|
|
164
162
|
|
|
165
163
|
buddy = _pick_directional_surface(
|
|
@@ -171,7 +169,7 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
171
169
|
bin_index=0,
|
|
172
170
|
)
|
|
173
171
|
buddy_path = out / "buddy.png"
|
|
174
|
-
_save_surface(buddy, buddy_path)
|
|
172
|
+
_save_surface(buddy, buddy_path, scale=output_scale)
|
|
175
173
|
saved.append(buddy_path)
|
|
176
174
|
|
|
177
175
|
survivor = _pick_directional_surface(
|
|
@@ -183,7 +181,7 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
183
181
|
bin_index=0,
|
|
184
182
|
)
|
|
185
183
|
survivor_path = out / "survivor.png"
|
|
186
|
-
_save_surface(survivor, survivor_path)
|
|
184
|
+
_save_surface(survivor, survivor_path, scale=output_scale)
|
|
187
185
|
saved.append(survivor_path)
|
|
188
186
|
|
|
189
187
|
car_surface = build_car_surface(CAR_WIDTH, CAR_HEIGHT)
|
|
@@ -196,27 +194,27 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
196
194
|
)
|
|
197
195
|
car = _pick_directional_surface(build_car_directional_surfaces(car_surface), bin_index=0)
|
|
198
196
|
car_path = out / "car.png"
|
|
199
|
-
_save_surface(car, car_path)
|
|
197
|
+
_save_surface(car, car_path, scale=output_scale)
|
|
200
198
|
saved.append(car_path)
|
|
201
199
|
|
|
202
200
|
fuel = build_fuel_can_surface(FUEL_CAN_WIDTH, FUEL_CAN_HEIGHT)
|
|
203
201
|
fuel_path = out / "fuel.png"
|
|
204
|
-
_save_surface(fuel, fuel_path)
|
|
202
|
+
_save_surface(fuel, fuel_path, scale=output_scale)
|
|
205
203
|
saved.append(fuel_path)
|
|
206
204
|
|
|
207
205
|
flashlight = build_flashlight_surface(FLASHLIGHT_WIDTH, FLASHLIGHT_HEIGHT)
|
|
208
206
|
flashlight_path = out / "flashlight.png"
|
|
209
|
-
_save_surface(flashlight, flashlight_path)
|
|
207
|
+
_save_surface(flashlight, flashlight_path, scale=output_scale)
|
|
210
208
|
saved.append(flashlight_path)
|
|
211
209
|
|
|
212
210
|
shoes = build_shoes_surface(SHOES_WIDTH, SHOES_HEIGHT)
|
|
213
211
|
shoes_path = out / "shoes.png"
|
|
214
|
-
_save_surface(shoes, shoes_path)
|
|
212
|
+
_save_surface(shoes, shoes_path, scale=output_scale)
|
|
215
213
|
saved.append(shoes_path)
|
|
216
214
|
|
|
217
215
|
beam = SteelBeam(0, 0, cell_size, health=STEEL_BEAM_HEALTH, palette=None)
|
|
218
216
|
beam_path = out / "steel-beam.png"
|
|
219
|
-
_save_surface(beam.image, beam_path)
|
|
217
|
+
_save_surface(beam.image, beam_path, scale=output_scale)
|
|
220
218
|
saved.append(beam_path)
|
|
221
219
|
|
|
222
220
|
inner_wall = pygame.Surface((cell_size, cell_size), pygame.SRCALPHA)
|
|
@@ -230,13 +228,13 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
230
228
|
fill_color=inner_fill,
|
|
231
229
|
border_color=inner_border,
|
|
232
230
|
bevel_depth=INTERNAL_WALL_BEVEL_DEPTH,
|
|
233
|
-
bevel_mask=(
|
|
234
|
-
draw_bottom_side=
|
|
231
|
+
bevel_mask=(True, True, True, True),
|
|
232
|
+
draw_bottom_side=True,
|
|
235
233
|
bottom_side_ratio=0.1,
|
|
236
234
|
side_shade_ratio=0.9,
|
|
237
235
|
)
|
|
238
236
|
inner_wall_path = out / "wall-inner.png"
|
|
239
|
-
_save_surface(inner_wall, inner_wall_path)
|
|
237
|
+
_save_surface(inner_wall, inner_wall_path, scale=output_scale)
|
|
240
238
|
saved.append(inner_wall_path)
|
|
241
239
|
|
|
242
240
|
rubble_wall = build_rubble_wall_surface(
|
|
@@ -246,7 +244,7 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
246
244
|
angle_deg=RUBBLE_ROTATION_DEG,
|
|
247
245
|
)
|
|
248
246
|
rubble_wall_path = out / "wall-rubble.png"
|
|
249
|
-
_save_surface(rubble_wall, rubble_wall_path)
|
|
247
|
+
_save_surface(rubble_wall, rubble_wall_path, scale=output_scale)
|
|
250
248
|
saved.append(rubble_wall_path)
|
|
251
249
|
|
|
252
250
|
outer_wall = pygame.Surface((cell_size, cell_size), pygame.SRCALPHA)
|
|
@@ -260,18 +258,18 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
260
258
|
fill_color=outer_fill,
|
|
261
259
|
border_color=outer_border,
|
|
262
260
|
bevel_depth=0,
|
|
263
|
-
bevel_mask=(
|
|
264
|
-
draw_bottom_side=
|
|
261
|
+
bevel_mask=(True, True, True, True),
|
|
262
|
+
draw_bottom_side=True,
|
|
265
263
|
bottom_side_ratio=0.1,
|
|
266
264
|
side_shade_ratio=0.9,
|
|
267
265
|
)
|
|
268
266
|
outer_wall_path = out / "wall-outer.png"
|
|
269
|
-
_save_surface(outer_wall, outer_wall_path)
|
|
267
|
+
_save_surface(outer_wall, outer_wall_path, scale=output_scale)
|
|
270
268
|
saved.append(outer_wall_path)
|
|
271
269
|
|
|
272
270
|
pitfall = _build_pitfall_tile(cell_size)
|
|
273
271
|
pitfall_path = out / "pitfall.png"
|
|
274
|
-
_save_surface(pitfall, pitfall_path)
|
|
272
|
+
_save_surface(pitfall, pitfall_path, scale=output_scale)
|
|
275
273
|
saved.append(pitfall_path)
|
|
276
274
|
|
|
277
275
|
fall_radius = max(1, int(ZOMBIE_RADIUS))
|
|
@@ -284,13 +282,21 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> li
|
|
|
284
282
|
fall_radius,
|
|
285
283
|
)
|
|
286
284
|
falling_path = out / "falling-zombie.png"
|
|
287
|
-
_save_surface(falling, falling_path)
|
|
285
|
+
_save_surface(falling, falling_path, scale=output_scale)
|
|
288
286
|
saved.append(falling_path)
|
|
289
287
|
|
|
290
|
-
|
|
291
|
-
fall_zone.
|
|
288
|
+
fall_zone_size = cell_size * 2
|
|
289
|
+
fall_zone = pygame.Surface((fall_zone_size, fall_zone_size), pygame.SRCALPHA)
|
|
290
|
+
for y in range(2):
|
|
291
|
+
for x in range(2):
|
|
292
|
+
color = FALL_ZONE_FLOOR_SECONDARY if (x + y) % 2 == 0 else FALL_ZONE_FLOOR_PRIMARY
|
|
293
|
+
pygame.draw.rect(
|
|
294
|
+
fall_zone,
|
|
295
|
+
color,
|
|
296
|
+
(x * cell_size, y * cell_size, cell_size, cell_size),
|
|
297
|
+
)
|
|
292
298
|
fall_zone_path = out / "fall-zone.png"
|
|
293
|
-
_save_surface(fall_zone, fall_zone_path)
|
|
299
|
+
_save_surface(fall_zone, fall_zone_path, scale=output_scale)
|
|
294
300
|
saved.append(fall_zone_path)
|
|
295
301
|
|
|
296
302
|
return saved
|
|
@@ -8,9 +8,7 @@ from ..colors import (
|
|
|
8
8
|
from ..models import GameData
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def _set_ambient_palette(
|
|
12
|
-
game_data: GameData, key: str, *, force: bool = False
|
|
13
|
-
) -> None:
|
|
11
|
+
def _set_ambient_palette(game_data: GameData, key: str, *, force: bool = False) -> None:
|
|
14
12
|
"""Apply a named ambient palette to all walls in the level."""
|
|
15
13
|
|
|
16
14
|
palette = get_environment_palette(key)
|
|
@@ -22,9 +20,7 @@ def _set_ambient_palette(
|
|
|
22
20
|
_apply_palette_to_walls(game_data, palette, force=True)
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
def sync_ambient_palette_with_flashlights(
|
|
26
|
-
game_data: GameData, *, force: bool = False
|
|
27
|
-
) -> None:
|
|
23
|
+
def sync_ambient_palette_with_flashlights(game_data: GameData, *, force: bool = False) -> None:
|
|
28
24
|
"""Sync the ambient palette with the player's flashlight inventory."""
|
|
29
25
|
|
|
30
26
|
state = game_data.state
|
|
@@ -46,17 +46,14 @@ def update_footprints(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
46
46
|
footprints = state.footprints
|
|
47
47
|
if not player.in_car:
|
|
48
48
|
last_pos = state.last_footprint_pos
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
):
|
|
58
|
-
footprints.append(Footprint(pos=(player.x, player.y), time=now))
|
|
59
|
-
state.last_footprint_pos = (player.x, player.y)
|
|
49
|
+
step_distance = FOOTPRINT_STEP_DISTANCE * 0.5
|
|
50
|
+
step_distance_sq = step_distance * step_distance
|
|
51
|
+
dist_sq = (player.x - last_pos[0]) ** 2 + (player.y - last_pos[1]) ** 2 if last_pos else None
|
|
52
|
+
if last_pos is None or (dist_sq is not None and dist_sq >= step_distance_sq):
|
|
53
|
+
pos = (int(player.x), int(player.y))
|
|
54
|
+
footprints.append(Footprint(pos=pos, time=now, visible=state.footprint_visible_toggle))
|
|
55
|
+
state.footprint_visible_toggle = not state.footprint_visible_toggle
|
|
56
|
+
state.last_footprint_pos = pos
|
|
60
57
|
|
|
61
58
|
if len(footprints) > FOOTPRINT_MAX:
|
|
62
59
|
footprints = footprints[-FOOTPRINT_MAX:]
|
|
@@ -44,6 +44,7 @@ def _interaction_radius(width: float, height: float) -> float:
|
|
|
44
44
|
"""Approximate interaction reach for a humanoid and an object."""
|
|
45
45
|
return HUMANOID_RADIUS + (width + height) / 4
|
|
46
46
|
|
|
47
|
+
|
|
47
48
|
RNG = get_rng()
|
|
48
49
|
|
|
49
50
|
|
|
@@ -71,9 +72,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
71
72
|
|
|
72
73
|
car_interaction_radius = _interaction_radius(CAR_WIDTH, CAR_HEIGHT)
|
|
73
74
|
fuel_interaction_radius = _interaction_radius(FUEL_CAN_WIDTH, FUEL_CAN_HEIGHT)
|
|
74
|
-
flashlight_interaction_radius = _interaction_radius(
|
|
75
|
-
FLASHLIGHT_WIDTH, FLASHLIGHT_HEIGHT
|
|
76
|
-
)
|
|
75
|
+
flashlight_interaction_radius = _interaction_radius(FLASHLIGHT_WIDTH, FLASHLIGHT_HEIGHT)
|
|
77
76
|
shoes_interaction_radius = _interaction_radius(SHOES_WIDTH, SHOES_HEIGHT)
|
|
78
77
|
|
|
79
78
|
def _rect_center_cell(rect: pygame.Rect) -> tuple[int, int] | None:
|
|
@@ -92,14 +91,8 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
92
91
|
dy = point[1] - player.y
|
|
93
92
|
return dx * dx + dy * dy <= radius * radius
|
|
94
93
|
|
|
95
|
-
def _player_near_sprite(
|
|
96
|
-
sprite_obj
|
|
97
|
-
) -> bool:
|
|
98
|
-
return bool(
|
|
99
|
-
sprite_obj
|
|
100
|
-
and sprite_obj.alive()
|
|
101
|
-
and _player_near_point(sprite_obj.rect.center, radius)
|
|
102
|
-
)
|
|
94
|
+
def _player_near_sprite(sprite_obj: pygame.sprite.Sprite | None, radius: float) -> bool:
|
|
95
|
+
return bool(sprite_obj and sprite_obj.alive() and _player_near_point(sprite_obj.rect.center, radius))
|
|
103
96
|
|
|
104
97
|
def _player_near_car(car_obj: Car | None) -> bool:
|
|
105
98
|
return _player_near_sprite(car_obj, car_interaction_radius)
|
|
@@ -120,9 +113,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
120
113
|
for flashlight in list(flashlights):
|
|
121
114
|
if not flashlight.alive():
|
|
122
115
|
continue
|
|
123
|
-
if _player_near_point(
|
|
124
|
-
flashlight.rect.center, flashlight_interaction_radius
|
|
125
|
-
):
|
|
116
|
+
if _player_near_point(flashlight.rect.center, flashlight_interaction_radius):
|
|
126
117
|
state.flashlight_count += 1
|
|
127
118
|
state.hint_expires_at = 0
|
|
128
119
|
state.hint_target_type = None
|
|
@@ -152,9 +143,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
152
143
|
sync_ambient_palette_with_flashlights(game_data)
|
|
153
144
|
|
|
154
145
|
buddies = [
|
|
155
|
-
survivor
|
|
156
|
-
for survivor in survivor_group
|
|
157
|
-
if survivor.alive() and survivor.is_buddy and not survivor.rescued
|
|
146
|
+
survivor for survivor in survivor_group if survivor.alive() and survivor.is_buddy and not survivor.rescued
|
|
158
147
|
]
|
|
159
148
|
|
|
160
149
|
# Buddy interactions (Stage 3)
|
|
@@ -164,20 +153,13 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
164
153
|
continue
|
|
165
154
|
buddy_on_screen = rect_visible_on_screen(camera, buddy.rect)
|
|
166
155
|
if not player.in_car:
|
|
167
|
-
dist_to_player_sq = (player.x - buddy.x) ** 2 + (
|
|
168
|
-
|
|
169
|
-
) ** 2
|
|
170
|
-
if (
|
|
171
|
-
dist_to_player_sq
|
|
172
|
-
<= SURVIVOR_APPROACH_RADIUS * SURVIVOR_APPROACH_RADIUS
|
|
173
|
-
):
|
|
156
|
+
dist_to_player_sq = (player.x - buddy.x) ** 2 + (player.y - buddy.y) ** 2
|
|
157
|
+
if dist_to_player_sq <= SURVIVOR_APPROACH_RADIUS * SURVIVOR_APPROACH_RADIUS:
|
|
174
158
|
buddy.set_following()
|
|
175
159
|
elif player.in_car and active_car and shrunk_car:
|
|
176
160
|
g = pygame.sprite.Group()
|
|
177
161
|
g.add(buddy)
|
|
178
|
-
if pygame.sprite.spritecollide(
|
|
179
|
-
shrunk_car, g, False, pygame.sprite.collide_circle
|
|
180
|
-
):
|
|
162
|
+
if pygame.sprite.spritecollide(shrunk_car, g, False, pygame.sprite.collide_circle):
|
|
181
163
|
prospective_passengers = state.survivors_onboard + 1
|
|
182
164
|
capacity_limit = state.survivor_capacity
|
|
183
165
|
if prospective_passengers > capacity_limit:
|
|
@@ -191,9 +173,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
191
173
|
buddy.kill()
|
|
192
174
|
continue
|
|
193
175
|
|
|
194
|
-
if buddy.alive() and pygame.sprite.spritecollide(
|
|
195
|
-
buddy, zombie_group, False, pygame.sprite.collide_circle
|
|
196
|
-
):
|
|
176
|
+
if buddy.alive() and pygame.sprite.spritecollide(buddy, zombie_group, False, pygame.sprite.collide_circle):
|
|
197
177
|
if buddy_on_screen:
|
|
198
178
|
state.game_over_message = tr("game_over.scream")
|
|
199
179
|
state.game_over = True
|
|
@@ -203,18 +183,11 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
203
183
|
new_cell = RNG.choice(walkable_cells)
|
|
204
184
|
buddy.teleport(_cell_center(new_cell))
|
|
205
185
|
else:
|
|
206
|
-
buddy.teleport(
|
|
207
|
-
(game_data.level_width // 2, game_data.level_height // 2)
|
|
208
|
-
)
|
|
186
|
+
buddy.teleport((game_data.level_width // 2, game_data.level_height // 2))
|
|
209
187
|
buddy.following = False
|
|
210
188
|
|
|
211
189
|
# Player entering an active car already under control
|
|
212
|
-
if (
|
|
213
|
-
not player.in_car
|
|
214
|
-
and _player_near_car(active_car)
|
|
215
|
-
and active_car
|
|
216
|
-
and active_car.health > 0
|
|
217
|
-
):
|
|
190
|
+
if not player.in_car and _player_near_car(active_car) and active_car and active_car.health > 0:
|
|
218
191
|
if state.has_fuel:
|
|
219
192
|
player.in_car = True
|
|
220
193
|
all_sprites.remove(player)
|
|
@@ -258,9 +231,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
258
231
|
# Bonus: collide a parked car while driving to repair/extend capabilities
|
|
259
232
|
if player.in_car and active_car and shrunk_car and waiting_cars:
|
|
260
233
|
waiting_group = pygame.sprite.Group(waiting_cars)
|
|
261
|
-
collided_waiters = pygame.sprite.spritecollide(
|
|
262
|
-
shrunk_car, waiting_group, False, pygame.sprite.collide_rect
|
|
263
|
-
)
|
|
234
|
+
collided_waiters = pygame.sprite.spritecollide(shrunk_car, waiting_group, False, pygame.sprite.collide_rect)
|
|
264
235
|
if collided_waiters:
|
|
265
236
|
removed_any = False
|
|
266
237
|
capacity_increments = 0
|
|
@@ -288,16 +259,8 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
288
259
|
if zombies_hit:
|
|
289
260
|
active_car._take_damage(CAR_ZOMBIE_DAMAGE * len(zombies_hit))
|
|
290
261
|
|
|
291
|
-
if
|
|
292
|
-
|
|
293
|
-
and player.in_car
|
|
294
|
-
and active_car
|
|
295
|
-
and shrunk_car
|
|
296
|
-
and survivor_group
|
|
297
|
-
):
|
|
298
|
-
boarded = pygame.sprite.spritecollide(
|
|
299
|
-
shrunk_car, survivor_group, True, pygame.sprite.collide_circle
|
|
300
|
-
)
|
|
262
|
+
if stage.rescue_stage and player.in_car and active_car and shrunk_car and survivor_group:
|
|
263
|
+
boarded = pygame.sprite.spritecollide(shrunk_car, survivor_group, True, pygame.sprite.collide_circle)
|
|
301
264
|
if boarded:
|
|
302
265
|
state.survivors_onboard += len(boarded)
|
|
303
266
|
apply_passenger_speed_penalty(game_data)
|
|
@@ -339,9 +302,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
339
302
|
# Player getting caught by zombies
|
|
340
303
|
if not player.in_car and player in all_sprites:
|
|
341
304
|
shrunk_player = get_shrunk_sprite(player, 0.8)
|
|
342
|
-
collisions = pygame.sprite.spritecollide(
|
|
343
|
-
shrunk_player, zombie_group, False, pygame.sprite.collide_circle
|
|
344
|
-
)
|
|
305
|
+
collisions = pygame.sprite.spritecollide(shrunk_player, zombie_group, False, pygame.sprite.collide_circle)
|
|
345
306
|
if any(not zombie.carbonized for zombie in collisions):
|
|
346
307
|
if not state.game_over:
|
|
347
308
|
state.game_over = True
|
|
@@ -367,9 +328,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
367
328
|
car_cell = _rect_center_cell(car.rect)
|
|
368
329
|
if buddy_ready and car_cell is not None and car_cell in outside_cells:
|
|
369
330
|
if stage.buddy_required_count > 0:
|
|
370
|
-
state.buddy_rescued = min(
|
|
371
|
-
stage.buddy_required_count, state.buddy_onboard
|
|
372
|
-
)
|
|
331
|
+
state.buddy_rescued = min(stage.buddy_required_count, state.buddy_onboard)
|
|
373
332
|
if stage.rescue_stage and state.survivors_onboard:
|
|
374
333
|
state.survivors_rescued += state.survivors_onboard
|
|
375
334
|
state.survivors_onboard = 0
|
zombie_escape/gameplay/layout.py
CHANGED
|
@@ -23,7 +23,6 @@ __all__ = ["generate_level_from_blueprint", "MapGenerationError"]
|
|
|
23
23
|
RNG = get_rng()
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
def _rect_for_cell(x_idx: int, y_idx: int, cell_size: int) -> pygame.Rect:
|
|
28
27
|
return pygame.Rect(
|
|
29
28
|
x_idx * cell_size,
|
|
@@ -53,9 +52,7 @@ def _expand_zone_cells(
|
|
|
53
52
|
return cells
|
|
54
53
|
|
|
55
54
|
|
|
56
|
-
def generate_level_from_blueprint(
|
|
57
|
-
game_data: GameData, config: dict[str, Any]
|
|
58
|
-
) -> dict[str, list[pygame.Rect]]:
|
|
55
|
+
def generate_level_from_blueprint(game_data: GameData, config: dict[str, Any]) -> dict[str, list[pygame.Rect]]:
|
|
59
56
|
"""Build walls/spawn candidates/outside area from a blueprint grid."""
|
|
60
57
|
wall_group = game_data.groups.wall_group
|
|
61
58
|
all_sprites = game_data.groups.all_sprites
|
|
@@ -69,7 +66,8 @@ def generate_level_from_blueprint(
|
|
|
69
66
|
cols=stage.grid_cols,
|
|
70
67
|
rows=stage.grid_rows,
|
|
71
68
|
wall_algo=stage.wall_algorithm,
|
|
72
|
-
pitfall_density=
|
|
69
|
+
pitfall_density=stage.pitfall_density,
|
|
70
|
+
pitfall_zones=stage.pitfall_zones,
|
|
73
71
|
base_seed=game_data.state.seed,
|
|
74
72
|
)
|
|
75
73
|
if isinstance(blueprint_data, dict):
|
|
@@ -81,23 +79,11 @@ def generate_level_from_blueprint(
|
|
|
81
79
|
steel_cells_raw = set()
|
|
82
80
|
car_reachable_cells = set()
|
|
83
81
|
|
|
84
|
-
steel_cells = (
|
|
85
|
-
{(int(x), int(y)) for x, y in steel_cells_raw} if steel_enabled else set()
|
|
86
|
-
)
|
|
82
|
+
steel_cells = {(int(x), int(y)) for x, y in steel_cells_raw} if steel_enabled else set()
|
|
87
83
|
game_data.layout.car_walkable_cells = car_reachable_cells
|
|
88
84
|
cell_size = game_data.cell_size
|
|
89
|
-
outer_wall_cells = {
|
|
90
|
-
|
|
91
|
-
for y, row in enumerate(blueprint)
|
|
92
|
-
for x, ch in enumerate(row)
|
|
93
|
-
if ch == "B"
|
|
94
|
-
}
|
|
95
|
-
wall_cells = {
|
|
96
|
-
(x, y)
|
|
97
|
-
for y, row in enumerate(blueprint)
|
|
98
|
-
for x, ch in enumerate(row)
|
|
99
|
-
if ch in {"B", "1"}
|
|
100
|
-
}
|
|
85
|
+
outer_wall_cells = {(x, y) for y, row in enumerate(blueprint) for x, ch in enumerate(row) if ch == "B"}
|
|
86
|
+
wall_cells = {(x, y) for y, row in enumerate(blueprint) for x, ch in enumerate(row) if ch in {"B", "1"}}
|
|
101
87
|
|
|
102
88
|
def _has_wall(nx: int, ny: int) -> bool:
|
|
103
89
|
if nx < 0 or ny < 0 or nx >= stage.grid_cols or ny >= stage.grid_rows:
|
|
@@ -119,7 +105,7 @@ def generate_level_from_blueprint(
|
|
|
119
105
|
interior_max_y = stage.grid_rows - 3
|
|
120
106
|
bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] = {}
|
|
121
107
|
palette = get_environment_palette(game_data.state.ambient_palette_key)
|
|
122
|
-
rubble_ratio = max(0.0, min(1.0,
|
|
108
|
+
rubble_ratio = max(0.0, min(1.0, stage.wall_rubble_ratio))
|
|
123
109
|
|
|
124
110
|
def add_beam_to_groups(beam: SteelBeam) -> None:
|
|
125
111
|
if beam._added_to_groups:
|
|
@@ -128,16 +114,16 @@ def generate_level_from_blueprint(
|
|
|
128
114
|
all_sprites.add(beam, layer=0)
|
|
129
115
|
beam._added_to_groups = True
|
|
130
116
|
|
|
131
|
-
def remove_wall_cell(cell: tuple[int, int]) -> None:
|
|
132
|
-
wall_cells
|
|
117
|
+
def remove_wall_cell(cell: tuple[int, int], *, allow_walkable: bool = True) -> None:
|
|
118
|
+
if cell in wall_cells:
|
|
119
|
+
wall_cells.discard(cell)
|
|
120
|
+
if allow_walkable and cell not in walkable_cells:
|
|
121
|
+
walkable_cells.append(cell)
|
|
133
122
|
outer_wall_cells.discard(cell)
|
|
134
123
|
|
|
135
124
|
for y, row in enumerate(blueprint):
|
|
136
125
|
if len(row) != stage.grid_cols:
|
|
137
|
-
raise ValueError(
|
|
138
|
-
"Blueprint width mismatch at row "
|
|
139
|
-
f"{y}: {len(row)} != {stage.grid_cols}"
|
|
140
|
-
)
|
|
126
|
+
raise ValueError(f"Blueprint width mismatch at row {y}: {len(row)} != {stage.grid_cols}")
|
|
141
127
|
for x, ch in enumerate(row):
|
|
142
128
|
cell_rect = _rect_for_cell(x, y, cell_size)
|
|
143
129
|
cell_has_beam = steel_enabled and (x, y) in steel_cells
|
|
@@ -180,29 +166,17 @@ def generate_level_from_blueprint(
|
|
|
180
166
|
)
|
|
181
167
|
draw_bottom_side = not _has_wall(x, y + 1)
|
|
182
168
|
bevel_mask = (
|
|
183
|
-
not _has_wall(x, y - 1)
|
|
184
|
-
and not _has_wall(x
|
|
185
|
-
and not _has_wall(x
|
|
186
|
-
not _has_wall(x, y - 1)
|
|
187
|
-
and not _has_wall(x + 1, y)
|
|
188
|
-
and not _has_wall(x + 1, y - 1),
|
|
189
|
-
not _has_wall(x, y + 1)
|
|
190
|
-
and not _has_wall(x + 1, y)
|
|
191
|
-
and not _has_wall(x + 1, y + 1),
|
|
192
|
-
not _has_wall(x, y + 1)
|
|
193
|
-
and not _has_wall(x - 1, y)
|
|
194
|
-
and not _has_wall(x - 1, y + 1),
|
|
169
|
+
not _has_wall(x, y - 1) and not _has_wall(x - 1, y) and not _has_wall(x - 1, y - 1),
|
|
170
|
+
not _has_wall(x, y - 1) and not _has_wall(x + 1, y) and not _has_wall(x + 1, y - 1),
|
|
171
|
+
not _has_wall(x, y + 1) and not _has_wall(x + 1, y) and not _has_wall(x + 1, y + 1),
|
|
172
|
+
not _has_wall(x, y + 1) and not _has_wall(x - 1, y) and not _has_wall(x - 1, y + 1),
|
|
195
173
|
)
|
|
196
174
|
if any(bevel_mask):
|
|
197
175
|
bevel_corners[(x, y)] = bevel_mask
|
|
198
176
|
wall_cell = (x, y)
|
|
199
177
|
use_rubble = rubble_ratio > 0 and random.random() < rubble_ratio
|
|
200
178
|
if use_rubble:
|
|
201
|
-
rotation_deg = (
|
|
202
|
-
RUBBLE_ROTATION_DEG
|
|
203
|
-
if random.random() < 0.5
|
|
204
|
-
else -RUBBLE_ROTATION_DEG
|
|
205
|
-
)
|
|
179
|
+
rotation_deg = RUBBLE_ROTATION_DEG if random.random() < 0.5 else -RUBBLE_ROTATION_DEG
|
|
206
180
|
wall = RubbleWall(
|
|
207
181
|
cell_rect.x,
|
|
208
182
|
cell_rect.y,
|
|
@@ -216,7 +190,7 @@ def generate_level_from_blueprint(
|
|
|
216
190
|
on_destroy=(
|
|
217
191
|
(
|
|
218
192
|
lambda _w, b=beam, cell=wall_cell: (
|
|
219
|
-
remove_wall_cell(cell),
|
|
193
|
+
remove_wall_cell(cell, allow_walkable=False),
|
|
220
194
|
add_beam_to_groups(b),
|
|
221
195
|
)
|
|
222
196
|
)
|
|
@@ -238,7 +212,7 @@ def generate_level_from_blueprint(
|
|
|
238
212
|
on_destroy=(
|
|
239
213
|
(
|
|
240
214
|
lambda _w, b=beam, cell=wall_cell: (
|
|
241
|
-
remove_wall_cell(cell),
|
|
215
|
+
remove_wall_cell(cell, allow_walkable=False),
|
|
242
216
|
add_beam_to_groups(b),
|
|
243
217
|
)
|
|
244
218
|
)
|
|
@@ -211,11 +211,7 @@ def update_entities(
|
|
|
211
211
|
if game_data.state.player_wall_target_ttl <= 0:
|
|
212
212
|
game_data.state.player_wall_target_cell = None
|
|
213
213
|
|
|
214
|
-
wall_target_cell =
|
|
215
|
-
game_data.state.player_wall_target_cell
|
|
216
|
-
if game_data.state.player_wall_target_ttl > 0
|
|
217
|
-
else None
|
|
218
|
-
)
|
|
214
|
+
wall_target_cell = game_data.state.player_wall_target_cell if game_data.state.player_wall_target_ttl > 0 else None
|
|
219
215
|
|
|
220
216
|
update_survivors(
|
|
221
217
|
game_data,
|
|
@@ -237,17 +233,11 @@ def update_entities(
|
|
|
237
233
|
game_data.state.last_zombie_spawn_time = current_time
|
|
238
234
|
|
|
239
235
|
# Update zombies
|
|
240
|
-
target_center =
|
|
241
|
-
active_car.rect.center if player.in_car and active_car else player.rect.center
|
|
242
|
-
)
|
|
236
|
+
target_center = active_car.rect.center if player.in_car and active_car else player.rect.center
|
|
243
237
|
buddies = [
|
|
244
|
-
survivor
|
|
245
|
-
for survivor in survivor_group
|
|
246
|
-
if survivor.alive() and survivor.is_buddy and not survivor.rescued
|
|
247
|
-
]
|
|
248
|
-
buddies_on_screen = [
|
|
249
|
-
buddy for buddy in buddies if rect_visible_on_screen(camera, buddy.rect)
|
|
238
|
+
survivor for survivor in survivor_group if survivor.alive() and survivor.is_buddy and not survivor.rescued
|
|
250
239
|
]
|
|
240
|
+
buddies_on_screen = [buddy for buddy in buddies if rect_visible_on_screen(camera, buddy.rect)]
|
|
251
241
|
|
|
252
242
|
survivors_on_screen: list[Survivor] = []
|
|
253
243
|
if stage.rescue_stage:
|
|
@@ -279,13 +269,10 @@ def update_entities(
|
|
|
279
269
|
for idx, zombie in enumerate(zombies_sorted):
|
|
280
270
|
target = target_center
|
|
281
271
|
if buddies_on_screen:
|
|
282
|
-
dist_to_target_sq = (target_center[0] - zombie.x) ** 2 + (
|
|
283
|
-
target_center[1] - zombie.y
|
|
284
|
-
) ** 2
|
|
272
|
+
dist_to_target_sq = (target_center[0] - zombie.x) ** 2 + (target_center[1] - zombie.y) ** 2
|
|
285
273
|
nearest_buddy = min(
|
|
286
274
|
buddies_on_screen,
|
|
287
|
-
key=lambda buddy: (buddy.rect.centerx - zombie.x) ** 2
|
|
288
|
-
+ (buddy.rect.centery - zombie.y) ** 2,
|
|
275
|
+
key=lambda buddy: (buddy.rect.centerx - zombie.x) ** 2 + (buddy.rect.centery - zombie.y) ** 2,
|
|
289
276
|
)
|
|
290
277
|
dist_to_buddy_sq = (nearest_buddy.rect.centerx - zombie.x) ** 2 + (
|
|
291
278
|
nearest_buddy.rect.centery - zombie.y
|
|
@@ -305,8 +292,7 @@ def update_entities(
|
|
|
305
292
|
if candidate_positions:
|
|
306
293
|
target = min(
|
|
307
294
|
candidate_positions,
|
|
308
|
-
key=lambda pos: (pos[0] - zombie.x) ** 2
|
|
309
|
-
+ (pos[1] - zombie.y) ** 2,
|
|
295
|
+
key=lambda pos: (pos[0] - zombie.x) ** 2 + (pos[1] - zombie.y) ** 2,
|
|
310
296
|
)
|
|
311
297
|
nearby_candidates = _nearby_zombies(idx)
|
|
312
298
|
zombie_search_radius = ZOMBIE_WALL_HUG_SENSOR_DISTANCE + zombie.radius + 120
|