zombie-escape 1.12.0__py3-none-any.whl → 1.13.1__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/__main__.py +7 -0
- zombie_escape/colors.py +22 -14
- zombie_escape/entities.py +756 -147
- zombie_escape/entities_constants.py +35 -14
- zombie_escape/export_images.py +296 -0
- zombie_escape/gameplay/__init__.py +2 -1
- zombie_escape/gameplay/constants.py +6 -0
- zombie_escape/gameplay/footprints.py +4 -0
- zombie_escape/gameplay/interactions.py +19 -7
- zombie_escape/gameplay/layout.py +103 -34
- zombie_escape/gameplay/movement.py +85 -5
- zombie_escape/gameplay/spawn.py +139 -90
- zombie_escape/gameplay/state.py +18 -9
- zombie_escape/gameplay/survivors.py +13 -2
- zombie_escape/gameplay/utils.py +40 -21
- zombie_escape/level_blueprints.py +256 -19
- zombie_escape/locales/ui.en.json +12 -2
- zombie_escape/locales/ui.ja.json +12 -2
- zombie_escape/models.py +14 -7
- zombie_escape/render.py +149 -37
- zombie_escape/render_assets.py +419 -124
- zombie_escape/render_constants.py +27 -0
- zombie_escape/screens/game_over.py +14 -3
- zombie_escape/screens/gameplay.py +72 -14
- zombie_escape/screens/title.py +18 -7
- zombie_escape/stage_constants.py +51 -15
- zombie_escape/zombie_escape.py +24 -1
- {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/METADATA +41 -15
- zombie_escape-1.13.1.dist-info/RECORD +49 -0
- zombie_escape-1.12.0.dist-info/RECORD +0 -47
- {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/WHEEL +0 -0
- {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/gameplay/layout.py
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import random
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
import pygame
|
|
6
7
|
|
|
7
8
|
from ..colors import get_environment_palette
|
|
8
|
-
from ..entities import SteelBeam, Wall
|
|
9
|
-
from ..entities_constants import
|
|
9
|
+
from ..entities import RubbleWall, SteelBeam, Wall
|
|
10
|
+
from ..entities_constants import (
|
|
11
|
+
INTERNAL_WALL_BEVEL_DEPTH,
|
|
12
|
+
INTERNAL_WALL_HEALTH,
|
|
13
|
+
STEEL_BEAM_HEALTH,
|
|
14
|
+
)
|
|
15
|
+
from ..render_assets import RUBBLE_ROTATION_DEG
|
|
10
16
|
from .constants import OUTER_WALL_HEALTH
|
|
11
|
-
from ..level_blueprints import choose_blueprint
|
|
17
|
+
from ..level_blueprints import MapGenerationError, choose_blueprint
|
|
12
18
|
from ..models import GameData
|
|
13
19
|
from ..rng import get_rng
|
|
14
20
|
|
|
15
|
-
__all__ = ["generate_level_from_blueprint"]
|
|
21
|
+
__all__ = ["generate_level_from_blueprint", "MapGenerationError"]
|
|
16
22
|
|
|
17
23
|
RNG = get_rng()
|
|
18
24
|
|
|
19
25
|
|
|
26
|
+
|
|
20
27
|
def _rect_for_cell(x_idx: int, y_idx: int, cell_size: int) -> pygame.Rect:
|
|
21
28
|
return pygame.Rect(
|
|
22
29
|
x_idx * cell_size,
|
|
@@ -62,17 +69,22 @@ def generate_level_from_blueprint(
|
|
|
62
69
|
cols=stage.grid_cols,
|
|
63
70
|
rows=stage.grid_rows,
|
|
64
71
|
wall_algo=stage.wall_algorithm,
|
|
72
|
+
pitfall_density=getattr(stage, "pitfall_density", 0.0),
|
|
73
|
+
base_seed=game_data.state.seed,
|
|
65
74
|
)
|
|
66
75
|
if isinstance(blueprint_data, dict):
|
|
67
76
|
blueprint = blueprint_data.get("grid", [])
|
|
68
77
|
steel_cells_raw = blueprint_data.get("steel_cells", set())
|
|
78
|
+
car_reachable_cells = blueprint_data.get("car_reachable_cells", set())
|
|
69
79
|
else:
|
|
70
80
|
blueprint = blueprint_data
|
|
71
81
|
steel_cells_raw = set()
|
|
82
|
+
car_reachable_cells = set()
|
|
72
83
|
|
|
73
84
|
steel_cells = (
|
|
74
85
|
{(int(x), int(y)) for x, y in steel_cells_raw} if steel_enabled else set()
|
|
75
86
|
)
|
|
87
|
+
game_data.layout.car_walkable_cells = car_reachable_cells
|
|
76
88
|
cell_size = game_data.cell_size
|
|
77
89
|
outer_wall_cells = {
|
|
78
90
|
(x, y)
|
|
@@ -92,17 +104,22 @@ def generate_level_from_blueprint(
|
|
|
92
104
|
return True
|
|
93
105
|
return (nx, ny) in wall_cells
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
walkable_cells: list[
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
outside_cells: set[tuple[int, int]] = set()
|
|
108
|
+
walkable_cells: list[tuple[int, int]] = []
|
|
109
|
+
pitfall_cells: set[tuple[int, int]] = set()
|
|
110
|
+
player_cells: list[tuple[int, int]] = []
|
|
111
|
+
car_cells: list[tuple[int, int]] = []
|
|
112
|
+
zombie_cells: list[tuple[int, int]] = []
|
|
113
|
+
fuel_cells: list[tuple[int, int]] = []
|
|
114
|
+
flashlight_cells: list[tuple[int, int]] = []
|
|
115
|
+
shoes_cells: list[tuple[int, int]] = []
|
|
100
116
|
interior_min_x = 2
|
|
101
117
|
interior_max_x = stage.grid_cols - 3
|
|
102
118
|
interior_min_y = 2
|
|
103
119
|
interior_max_y = stage.grid_rows - 3
|
|
104
120
|
bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] = {}
|
|
105
121
|
palette = get_environment_palette(game_data.state.ambient_palette_key)
|
|
122
|
+
rubble_ratio = max(0.0, min(1.0, getattr(stage, "wall_rubble_ratio", 0.0)))
|
|
106
123
|
|
|
107
124
|
def add_beam_to_groups(beam: SteelBeam) -> None:
|
|
108
125
|
if beam._added_to_groups:
|
|
@@ -125,7 +142,7 @@ def generate_level_from_blueprint(
|
|
|
125
142
|
cell_rect = _rect_for_cell(x, y, cell_size)
|
|
126
143
|
cell_has_beam = steel_enabled and (x, y) in steel_cells
|
|
127
144
|
if ch == "O":
|
|
128
|
-
|
|
145
|
+
outside_cells.add((x, y))
|
|
129
146
|
continue
|
|
130
147
|
if ch == "B":
|
|
131
148
|
draw_bottom_side = not _has_wall(x, y + 1)
|
|
@@ -145,9 +162,12 @@ def generate_level_from_blueprint(
|
|
|
145
162
|
wall_group.add(wall)
|
|
146
163
|
all_sprites.add(wall, layer=0)
|
|
147
164
|
continue
|
|
165
|
+
if ch == "x":
|
|
166
|
+
pitfall_cells.add((x, y))
|
|
167
|
+
continue
|
|
148
168
|
if ch == "E":
|
|
149
169
|
if not cell_has_beam:
|
|
150
|
-
walkable_cells.append(
|
|
170
|
+
walkable_cells.append((x, y))
|
|
151
171
|
elif ch == "1":
|
|
152
172
|
beam = None
|
|
153
173
|
if cell_has_beam:
|
|
@@ -176,34 +196,74 @@ def generate_level_from_blueprint(
|
|
|
176
196
|
if any(bevel_mask):
|
|
177
197
|
bevel_corners[(x, y)] = bevel_mask
|
|
178
198
|
wall_cell = (x, y)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
use_rubble = rubble_ratio > 0 and random.random() < rubble_ratio
|
|
200
|
+
if use_rubble:
|
|
201
|
+
rotation_deg = (
|
|
202
|
+
RUBBLE_ROTATION_DEG
|
|
203
|
+
if random.random() < 0.5
|
|
204
|
+
else -RUBBLE_ROTATION_DEG
|
|
205
|
+
)
|
|
206
|
+
wall = RubbleWall(
|
|
207
|
+
cell_rect.x,
|
|
208
|
+
cell_rect.y,
|
|
209
|
+
cell_rect.width,
|
|
210
|
+
cell_rect.height,
|
|
211
|
+
health=INTERNAL_WALL_HEALTH,
|
|
212
|
+
palette=palette,
|
|
213
|
+
palette_category="inner_wall",
|
|
214
|
+
bevel_depth=INTERNAL_WALL_BEVEL_DEPTH,
|
|
215
|
+
rubble_rotation_deg=rotation_deg,
|
|
216
|
+
on_destroy=(
|
|
217
|
+
(
|
|
218
|
+
lambda _w, b=beam, cell=wall_cell: (
|
|
219
|
+
remove_wall_cell(cell),
|
|
220
|
+
add_beam_to_groups(b),
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
if beam
|
|
224
|
+
else (lambda _w, cell=wall_cell: remove_wall_cell(cell))
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
wall = Wall(
|
|
229
|
+
cell_rect.x,
|
|
230
|
+
cell_rect.y,
|
|
231
|
+
cell_rect.width,
|
|
232
|
+
cell_rect.height,
|
|
233
|
+
health=INTERNAL_WALL_HEALTH,
|
|
234
|
+
palette=palette,
|
|
235
|
+
palette_category="inner_wall",
|
|
236
|
+
bevel_mask=bevel_mask,
|
|
237
|
+
draw_bottom_side=draw_bottom_side,
|
|
238
|
+
on_destroy=(
|
|
239
|
+
(
|
|
240
|
+
lambda _w, b=beam, cell=wall_cell: (
|
|
241
|
+
remove_wall_cell(cell),
|
|
242
|
+
add_beam_to_groups(b),
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
if beam
|
|
246
|
+
else (lambda _w, cell=wall_cell: remove_wall_cell(cell))
|
|
247
|
+
),
|
|
248
|
+
)
|
|
195
249
|
wall_group.add(wall)
|
|
196
250
|
all_sprites.add(wall, layer=0)
|
|
197
251
|
else:
|
|
198
252
|
if not cell_has_beam:
|
|
199
|
-
walkable_cells.append(
|
|
253
|
+
walkable_cells.append((x, y))
|
|
200
254
|
|
|
201
255
|
if ch == "P":
|
|
202
|
-
player_cells.append(
|
|
256
|
+
player_cells.append((x, y))
|
|
203
257
|
if ch == "C":
|
|
204
|
-
car_cells.append(
|
|
258
|
+
car_cells.append((x, y))
|
|
205
259
|
if ch == "Z":
|
|
206
|
-
zombie_cells.append(
|
|
260
|
+
zombie_cells.append((x, y))
|
|
261
|
+
if ch == "f":
|
|
262
|
+
fuel_cells.append((x, y))
|
|
263
|
+
if ch == "l":
|
|
264
|
+
flashlight_cells.append((x, y))
|
|
265
|
+
if ch == "s":
|
|
266
|
+
shoes_cells.append((x, y))
|
|
207
267
|
|
|
208
268
|
if cell_has_beam and ch != "1":
|
|
209
269
|
beam = SteelBeam(
|
|
@@ -215,12 +275,17 @@ def generate_level_from_blueprint(
|
|
|
215
275
|
)
|
|
216
276
|
add_beam_to_groups(beam)
|
|
217
277
|
|
|
218
|
-
game_data.layout.
|
|
219
|
-
|
|
220
|
-
|
|
278
|
+
game_data.layout.field_rect = pygame.Rect(
|
|
279
|
+
0,
|
|
280
|
+
0,
|
|
281
|
+
game_data.level_width,
|
|
282
|
+
game_data.level_height,
|
|
283
|
+
)
|
|
284
|
+
game_data.layout.outside_cells = outside_cells
|
|
221
285
|
game_data.layout.walkable_cells = walkable_cells
|
|
222
286
|
game_data.layout.outer_wall_cells = outer_wall_cells
|
|
223
287
|
game_data.layout.wall_cells = wall_cells
|
|
288
|
+
game_data.layout.pitfall_cells = pitfall_cells
|
|
224
289
|
fall_spawn_cells = _expand_zone_cells(
|
|
225
290
|
stage.fall_spawn_zones,
|
|
226
291
|
grid_cols=stage.grid_cols,
|
|
@@ -246,5 +311,9 @@ def generate_level_from_blueprint(
|
|
|
246
311
|
"player_cells": player_cells,
|
|
247
312
|
"car_cells": car_cells,
|
|
248
313
|
"zombie_cells": zombie_cells,
|
|
314
|
+
"fuel_cells": fuel_cells,
|
|
315
|
+
"flashlight_cells": flashlight_cells,
|
|
316
|
+
"shoes_cells": shoes_cells,
|
|
249
317
|
"walkable_cells": walkable_cells,
|
|
318
|
+
"car_walkable_cells": list(car_reachable_cells),
|
|
250
319
|
}
|
|
@@ -13,15 +13,16 @@ from ..entities import (
|
|
|
13
13
|
Zombie,
|
|
14
14
|
)
|
|
15
15
|
from ..entities_constants import (
|
|
16
|
+
HUMANOID_WALL_BUMP_FRAMES,
|
|
16
17
|
PLAYER_SPEED,
|
|
17
18
|
ZOMBIE_SEPARATION_DISTANCE,
|
|
18
|
-
|
|
19
|
+
ZOMBIE_WALL_HUG_SENSOR_DISTANCE,
|
|
19
20
|
)
|
|
20
21
|
from ..gameplay_constants import (
|
|
21
22
|
SHOES_SPEED_MULTIPLIER_ONE,
|
|
22
23
|
SHOES_SPEED_MULTIPLIER_TWO,
|
|
23
24
|
)
|
|
24
|
-
from ..models import GameData
|
|
25
|
+
from ..models import FallingZombie, GameData
|
|
25
26
|
from ..world_grid import WallIndex, apply_tile_edge_nudge, walls_for_radius
|
|
26
27
|
from .constants import MAX_ZOMBIES
|
|
27
28
|
from .spawn import spawn_weighted_zombie, update_falling_zombies
|
|
@@ -49,9 +50,12 @@ def process_player_input(
|
|
|
49
50
|
dx_input += pad_input[0]
|
|
50
51
|
dy_input += pad_input[1]
|
|
51
52
|
|
|
53
|
+
player.update_facing_from_input(dx_input, dy_input)
|
|
54
|
+
|
|
52
55
|
player_dx, player_dy, car_dx, car_dy = 0, 0, 0, 0
|
|
53
56
|
|
|
54
57
|
if player.in_car and car and car.alive():
|
|
58
|
+
car.update_facing_from_input(dx_input, dy_input)
|
|
55
59
|
target_speed = car.speed
|
|
56
60
|
move_len = math.hypot(dx_input, dy_input)
|
|
57
61
|
if move_len > 0:
|
|
@@ -80,6 +84,28 @@ def _shoes_speed_multiplier(shoes_count: int) -> float:
|
|
|
80
84
|
return 1.0
|
|
81
85
|
|
|
82
86
|
|
|
87
|
+
def _handle_pitfall_detection(
|
|
88
|
+
x: float,
|
|
89
|
+
y: float,
|
|
90
|
+
cell_size: int,
|
|
91
|
+
pitfall_cells: set[tuple[int, int]],
|
|
92
|
+
pull_distance: float,
|
|
93
|
+
) -> tuple[int, int] | None:
|
|
94
|
+
"""Check if position is in pitfall and return pulled target coordinates if so."""
|
|
95
|
+
cx, cy = int(x // cell_size), int(y // cell_size)
|
|
96
|
+
if (cx, cy) not in pitfall_cells:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
cell_center_x = (cx * cell_size) + (cell_size // 2)
|
|
100
|
+
cell_center_y = (cy * cell_size) + (cell_size // 2)
|
|
101
|
+
dx, dy = cell_center_x - x, cell_center_y - y
|
|
102
|
+
dist = math.hypot(dx, dy)
|
|
103
|
+
if dist > 0:
|
|
104
|
+
move_factor = min(1.0, pull_distance / dist)
|
|
105
|
+
return int(x + dx * move_factor), int(y + dy * move_factor)
|
|
106
|
+
return int(x), int(y)
|
|
107
|
+
|
|
108
|
+
|
|
83
109
|
def update_entities(
|
|
84
110
|
game_data: GameData,
|
|
85
111
|
player_dx: float,
|
|
@@ -101,6 +127,8 @@ def update_entities(
|
|
|
101
127
|
stage = game_data.stage
|
|
102
128
|
active_car = car if car and car.alive() else None
|
|
103
129
|
wall_cells = game_data.layout.wall_cells
|
|
130
|
+
pitfall_cells = game_data.layout.pitfall_cells
|
|
131
|
+
walkable_cells = game_data.layout.walkable_cells
|
|
104
132
|
bevel_corners = game_data.layout.bevel_corners
|
|
105
133
|
|
|
106
134
|
all_walls = list(wall_group) if wall_index is None else None
|
|
@@ -131,7 +159,14 @@ def update_entities(
|
|
|
131
159
|
grid_rows=stage.grid_rows,
|
|
132
160
|
)
|
|
133
161
|
car_walls = _walls_near((active_car.x, active_car.y), 150.0)
|
|
134
|
-
active_car.move(
|
|
162
|
+
active_car.move(
|
|
163
|
+
car_dx,
|
|
164
|
+
car_dy,
|
|
165
|
+
car_walls,
|
|
166
|
+
walls_nearby=wall_index is not None,
|
|
167
|
+
cell_size=game_data.cell_size,
|
|
168
|
+
pitfall_cells=pitfall_cells,
|
|
169
|
+
)
|
|
135
170
|
player.rect.center = active_car.rect.center
|
|
136
171
|
player.x, player.y = active_car.x, active_car.y
|
|
137
172
|
elif not player.in_car:
|
|
@@ -157,6 +192,8 @@ def update_entities(
|
|
|
157
192
|
cell_size=game_data.cell_size,
|
|
158
193
|
level_width=game_data.level_width,
|
|
159
194
|
level_height=game_data.level_height,
|
|
195
|
+
pitfall_cells=pitfall_cells,
|
|
196
|
+
walkable_cells=walkable_cells,
|
|
160
197
|
)
|
|
161
198
|
else:
|
|
162
199
|
# Player flagged as in-car but car is gone; drop them back to foot control
|
|
@@ -166,7 +203,25 @@ def update_entities(
|
|
|
166
203
|
target_for_camera = active_car if player.in_car and active_car else player
|
|
167
204
|
camera.update(target_for_camera)
|
|
168
205
|
|
|
169
|
-
|
|
206
|
+
if player.inner_wall_hit and player.inner_wall_cell is not None:
|
|
207
|
+
game_data.state.player_wall_target_cell = player.inner_wall_cell
|
|
208
|
+
game_data.state.player_wall_target_ttl = HUMANOID_WALL_BUMP_FRAMES
|
|
209
|
+
elif game_data.state.player_wall_target_ttl > 0:
|
|
210
|
+
game_data.state.player_wall_target_ttl -= 1
|
|
211
|
+
if game_data.state.player_wall_target_ttl <= 0:
|
|
212
|
+
game_data.state.player_wall_target_cell = None
|
|
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
|
+
)
|
|
219
|
+
|
|
220
|
+
update_survivors(
|
|
221
|
+
game_data,
|
|
222
|
+
wall_index=wall_index,
|
|
223
|
+
wall_target_cell=wall_target_cell,
|
|
224
|
+
)
|
|
170
225
|
update_falling_zombies(game_data, config)
|
|
171
226
|
|
|
172
227
|
# Spawn new zombies if needed
|
|
@@ -254,7 +309,7 @@ def update_entities(
|
|
|
254
309
|
+ (pos[1] - zombie.y) ** 2,
|
|
255
310
|
)
|
|
256
311
|
nearby_candidates = _nearby_zombies(idx)
|
|
257
|
-
zombie_search_radius =
|
|
312
|
+
zombie_search_radius = ZOMBIE_WALL_HUG_SENSOR_DISTANCE + zombie.radius + 120
|
|
258
313
|
nearby_walls = _walls_near((zombie.x, zombie.y), zombie_search_radius)
|
|
259
314
|
zombie.update(
|
|
260
315
|
target,
|
|
@@ -268,5 +323,30 @@ def update_entities(
|
|
|
268
323
|
level_height=game_data.level_height,
|
|
269
324
|
outer_wall_cells=game_data.layout.outer_wall_cells,
|
|
270
325
|
wall_cells=game_data.layout.wall_cells,
|
|
326
|
+
pitfall_cells=game_data.layout.pitfall_cells,
|
|
271
327
|
bevel_corners=game_data.layout.bevel_corners,
|
|
272
328
|
)
|
|
329
|
+
|
|
330
|
+
# Check zombie pitfall
|
|
331
|
+
pull_dist = zombie.radius * 0.5
|
|
332
|
+
pitfall_target = _handle_pitfall_detection(
|
|
333
|
+
zombie.x,
|
|
334
|
+
zombie.y,
|
|
335
|
+
game_data.cell_size,
|
|
336
|
+
pitfall_cells,
|
|
337
|
+
pull_dist,
|
|
338
|
+
)
|
|
339
|
+
if pitfall_target is not None:
|
|
340
|
+
zombie.kill()
|
|
341
|
+
fall = FallingZombie(
|
|
342
|
+
start_pos=(int(zombie.x), int(zombie.y)),
|
|
343
|
+
target_pos=pitfall_target,
|
|
344
|
+
started_at_ms=pygame.time.get_ticks(),
|
|
345
|
+
pre_fx_ms=0,
|
|
346
|
+
fall_duration_ms=500,
|
|
347
|
+
dust_duration_ms=0,
|
|
348
|
+
tracker=zombie.tracker,
|
|
349
|
+
wall_hugging=zombie.wall_hugging,
|
|
350
|
+
mode="pitfall",
|
|
351
|
+
)
|
|
352
|
+
game_data.state.falling_zombies.append(fall)
|