zombie-escape 1.10.1__py3-none-any.whl → 1.12.3__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 +22 -14
- zombie_escape/entities.py +247 -47
- zombie_escape/entities_constants.py +15 -5
- zombie_escape/gameplay/__init__.py +2 -0
- zombie_escape/gameplay/footprints.py +4 -0
- zombie_escape/gameplay/interactions.py +38 -7
- zombie_escape/gameplay/layout.py +40 -15
- zombie_escape/gameplay/movement.py +38 -2
- zombie_escape/gameplay/spawn.py +174 -41
- zombie_escape/gameplay/state.py +17 -9
- zombie_escape/gameplay/survivors.py +9 -1
- zombie_escape/gameplay/utils.py +40 -21
- zombie_escape/gameplay_constants.py +8 -0
- zombie_escape/level_blueprints.py +139 -24
- zombie_escape/locales/ui.en.json +9 -1
- zombie_escape/locales/ui.ja.json +8 -0
- zombie_escape/models.py +11 -5
- zombie_escape/render.py +390 -43
- zombie_escape/render_assets.py +427 -174
- zombie_escape/render_constants.py +25 -4
- zombie_escape/screens/game_over.py +4 -4
- zombie_escape/screens/gameplay.py +31 -1
- zombie_escape/stage_constants.py +33 -16
- zombie_escape/zombie_escape.py +1 -1
- {zombie_escape-1.10.1.dist-info → zombie_escape-1.12.3.dist-info}/METADATA +7 -4
- zombie_escape-1.12.3.dist-info/RECORD +47 -0
- zombie_escape-1.10.1.dist-info/RECORD +0 -47
- {zombie_escape-1.10.1.dist-info → zombie_escape-1.12.3.dist-info}/WHEEL +0 -0
- {zombie_escape-1.10.1.dist-info → zombie_escape-1.12.3.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.10.1.dist-info → zombie_escape-1.12.3.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/gameplay/utils.py
CHANGED
|
@@ -25,7 +25,8 @@ def rect_visible_on_screen(camera: Camera | None, rect: pygame.Rect) -> bool:
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _scatter_positions_on_walkable(
|
|
28
|
-
walkable_cells: list[
|
|
28
|
+
walkable_cells: list[tuple[int, int]],
|
|
29
|
+
cell_size: int,
|
|
29
30
|
spawn_rate: float,
|
|
30
31
|
*,
|
|
31
32
|
jitter_ratio: float = 0.35,
|
|
@@ -35,17 +36,21 @@ def _scatter_positions_on_walkable(
|
|
|
35
36
|
return positions
|
|
36
37
|
|
|
37
38
|
clamped_rate = max(0.0, min(1.0, spawn_rate))
|
|
38
|
-
for
|
|
39
|
+
for cell_x, cell_y in walkable_cells:
|
|
39
40
|
if RNG.random() >= clamped_rate:
|
|
40
41
|
continue
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
jitter_extent = cell_size * jitter_ratio
|
|
43
|
+
jitter_x = RNG.uniform(-jitter_extent, jitter_extent)
|
|
44
|
+
jitter_y = RNG.uniform(-jitter_extent, jitter_extent)
|
|
45
|
+
base_x = (cell_x * cell_size) + (cell_size / 2)
|
|
46
|
+
base_y = (cell_y * cell_size) + (cell_size / 2)
|
|
47
|
+
positions.append((int(base_x + jitter_x), int(base_y + jitter_y)))
|
|
44
48
|
return positions
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
def find_interior_spawn_positions(
|
|
48
|
-
walkable_cells: list[
|
|
52
|
+
walkable_cells: list[tuple[int, int]],
|
|
53
|
+
cell_size: int,
|
|
49
54
|
spawn_rate: float,
|
|
50
55
|
*,
|
|
51
56
|
player: Player | None = None,
|
|
@@ -53,12 +58,14 @@ def find_interior_spawn_positions(
|
|
|
53
58
|
) -> list[tuple[int, int]]:
|
|
54
59
|
positions = _scatter_positions_on_walkable(
|
|
55
60
|
walkable_cells,
|
|
61
|
+
cell_size,
|
|
56
62
|
spawn_rate,
|
|
57
63
|
jitter_ratio=0.35,
|
|
58
64
|
)
|
|
59
65
|
if not positions and spawn_rate > 0:
|
|
60
66
|
positions = _scatter_positions_on_walkable(
|
|
61
67
|
walkable_cells,
|
|
68
|
+
cell_size,
|
|
62
69
|
spawn_rate * 1.5,
|
|
63
70
|
jitter_ratio=0.35,
|
|
64
71
|
)
|
|
@@ -78,7 +85,8 @@ def find_interior_spawn_positions(
|
|
|
78
85
|
|
|
79
86
|
|
|
80
87
|
def find_nearby_offscreen_spawn_position(
|
|
81
|
-
walkable_cells: list[
|
|
88
|
+
walkable_cells: list[tuple[int, int]],
|
|
89
|
+
cell_size: int,
|
|
82
90
|
*,
|
|
83
91
|
player: Player | None = None,
|
|
84
92
|
camera: Camera | None = None,
|
|
@@ -104,10 +112,14 @@ def find_nearby_offscreen_spawn_position(
|
|
|
104
112
|
None if max_player_dist is None else max_player_dist * max_player_dist
|
|
105
113
|
)
|
|
106
114
|
for _ in range(max(1, attempts)):
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
cell_x, cell_y = RNG.choice(walkable_cells)
|
|
116
|
+
jitter_extent = cell_size * 0.35
|
|
117
|
+
jitter_x = RNG.uniform(-jitter_extent, jitter_extent)
|
|
118
|
+
jitter_y = RNG.uniform(-jitter_extent, jitter_extent)
|
|
119
|
+
candidate = (
|
|
120
|
+
int((cell_x * cell_size) + (cell_size / 2) + jitter_x),
|
|
121
|
+
int((cell_y * cell_size) + (cell_size / 2) + jitter_y),
|
|
122
|
+
)
|
|
111
123
|
if player is not None and (
|
|
112
124
|
min_distance_sq is not None or max_distance_sq is not None
|
|
113
125
|
):
|
|
@@ -123,8 +135,11 @@ def find_nearby_offscreen_spawn_position(
|
|
|
123
135
|
return candidate
|
|
124
136
|
if player is not None and (min_distance_sq is not None or max_distance_sq is not None):
|
|
125
137
|
for _ in range(20):
|
|
126
|
-
|
|
127
|
-
center = (
|
|
138
|
+
cell_x, cell_y = RNG.choice(walkable_cells)
|
|
139
|
+
center = (
|
|
140
|
+
(cell_x * cell_size) + (cell_size / 2),
|
|
141
|
+
(cell_y * cell_size) + (cell_size / 2),
|
|
142
|
+
)
|
|
128
143
|
if view_rect is not None and view_rect.collidepoint(center):
|
|
129
144
|
continue
|
|
130
145
|
dx = center[0] - player.x
|
|
@@ -134,15 +149,19 @@ def find_nearby_offscreen_spawn_position(
|
|
|
134
149
|
continue
|
|
135
150
|
if max_distance_sq is not None and dist_sq > max_distance_sq:
|
|
136
151
|
continue
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
fallback_extent = cell_size * 0.2
|
|
153
|
+
fallback_x = RNG.uniform(-fallback_extent, fallback_extent)
|
|
154
|
+
fallback_y = RNG.uniform(-fallback_extent, fallback_extent)
|
|
155
|
+
return (int(center[0] + fallback_x), int(center[1] + fallback_y))
|
|
156
|
+
fallback_cell_x, fallback_cell_y = RNG.choice(walkable_cells)
|
|
157
|
+
fallback_center_x = (fallback_cell_x * cell_size) + (cell_size / 2)
|
|
158
|
+
fallback_center_y = (fallback_cell_y * cell_size) + (cell_size / 2)
|
|
159
|
+
fallback_extent = cell_size * 0.35
|
|
160
|
+
fallback_x = RNG.uniform(-fallback_extent, fallback_extent)
|
|
161
|
+
fallback_y = RNG.uniform(-fallback_extent, fallback_extent)
|
|
143
162
|
return (
|
|
144
|
-
int(
|
|
145
|
-
int(
|
|
163
|
+
int(fallback_center_x + fallback_x),
|
|
164
|
+
int(fallback_center_y + fallback_y),
|
|
146
165
|
)
|
|
147
166
|
|
|
148
167
|
|
|
@@ -13,6 +13,11 @@ SURVIVOR_SPAWN_RATE = 0.07
|
|
|
13
13
|
# --- Flashlight settings ---
|
|
14
14
|
DEFAULT_FLASHLIGHT_SPAWN_COUNT = 2
|
|
15
15
|
|
|
16
|
+
# --- Shoes settings ---
|
|
17
|
+
DEFAULT_SHOES_SPAWN_COUNT = 0
|
|
18
|
+
SHOES_SPEED_MULTIPLIER_ONE = 1.176
|
|
19
|
+
SHOES_SPEED_MULTIPLIER_TWO = 1.25
|
|
20
|
+
|
|
16
21
|
# --- Zombie settings ---
|
|
17
22
|
ZOMBIE_SPAWN_DELAY_MS = 4000
|
|
18
23
|
|
|
@@ -25,6 +30,9 @@ __all__ = [
|
|
|
25
30
|
"SURVIVAL_FAKE_CLOCK_RATIO",
|
|
26
31
|
"SURVIVOR_SPAWN_RATE",
|
|
27
32
|
"DEFAULT_FLASHLIGHT_SPAWN_COUNT",
|
|
33
|
+
"DEFAULT_SHOES_SPAWN_COUNT",
|
|
34
|
+
"SHOES_SPEED_MULTIPLIER_ONE",
|
|
35
|
+
"SHOES_SPEED_MULTIPLIER_TWO",
|
|
28
36
|
"ZOMBIE_SPAWN_DELAY_MS",
|
|
29
37
|
"CAR_HINT_DELAY_MS_DEFAULT",
|
|
30
38
|
]
|
|
@@ -84,11 +84,17 @@ def _place_exits(grid: list[list[str]], exits_per_side: int) -> None:
|
|
|
84
84
|
grid[y][x] = "E"
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
def _place_walls_default(
|
|
87
|
+
def _place_walls_default(
|
|
88
|
+
grid: list[list[str]],
|
|
89
|
+
*,
|
|
90
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
91
|
+
) -> None:
|
|
88
92
|
cols, rows = len(grid[0]), len(grid)
|
|
89
93
|
rng = RNG.randint
|
|
90
|
-
# Avoid placing walls adjacent to exits
|
|
94
|
+
# Avoid placing walls adjacent to exits and on reserved cells.
|
|
91
95
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
96
|
+
if forbidden_cells:
|
|
97
|
+
forbidden |= forbidden_cells
|
|
92
98
|
|
|
93
99
|
for _ in range(NUM_WALL_LINES):
|
|
94
100
|
length = rng(WALL_MIN_LEN, WALL_MAX_LEN)
|
|
@@ -111,12 +117,20 @@ def _place_walls_default(grid: list[list[str]]) -> None:
|
|
|
111
117
|
grid[y + i][x] = "1"
|
|
112
118
|
|
|
113
119
|
|
|
114
|
-
def _place_walls_empty(
|
|
120
|
+
def _place_walls_empty(
|
|
121
|
+
grid: list[list[str]],
|
|
122
|
+
*,
|
|
123
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
124
|
+
) -> None:
|
|
115
125
|
"""Place no internal walls (open floor plan)."""
|
|
116
|
-
|
|
126
|
+
_ = (grid, forbidden_cells)
|
|
117
127
|
|
|
118
128
|
|
|
119
|
-
def _place_walls_grid_wire(
|
|
129
|
+
def _place_walls_grid_wire(
|
|
130
|
+
grid: list[list[str]],
|
|
131
|
+
*,
|
|
132
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
133
|
+
) -> None:
|
|
120
134
|
"""
|
|
121
135
|
Place walls using a 2-pass approach with independent layers.
|
|
122
136
|
Vertical and horizontal walls are generated on separate grids to ensure
|
|
@@ -127,6 +141,8 @@ def _place_walls_grid_wire(grid: list[list[str]]) -> None:
|
|
|
127
141
|
cols, rows = len(grid[0]), len(grid)
|
|
128
142
|
rng = RNG.randint
|
|
129
143
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
144
|
+
if forbidden_cells:
|
|
145
|
+
forbidden |= forbidden_cells
|
|
130
146
|
|
|
131
147
|
# Temporary grids for independent generation
|
|
132
148
|
# They only track the internal walls ("1").
|
|
@@ -202,17 +218,24 @@ def _place_walls_grid_wire(grid: list[list[str]]) -> None:
|
|
|
202
218
|
grid[y][x] = "1"
|
|
203
219
|
|
|
204
220
|
|
|
205
|
-
def
|
|
221
|
+
def _place_walls_sparse_moore(
|
|
222
|
+
grid: list[list[str]],
|
|
223
|
+
*,
|
|
224
|
+
density: float = SPARSE_WALL_DENSITY,
|
|
225
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
226
|
+
) -> None:
|
|
206
227
|
"""Place isolated wall tiles at a low density, avoiding adjacency."""
|
|
207
228
|
cols, rows = len(grid[0]), len(grid)
|
|
208
229
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
230
|
+
if forbidden_cells:
|
|
231
|
+
forbidden |= forbidden_cells
|
|
209
232
|
for y in range(2, rows - 2):
|
|
210
233
|
for x in range(2, cols - 2):
|
|
211
234
|
if (x, y) in forbidden:
|
|
212
235
|
continue
|
|
213
236
|
if grid[y][x] != ".":
|
|
214
237
|
continue
|
|
215
|
-
if RNG.random() >=
|
|
238
|
+
if RNG.random() >= density:
|
|
216
239
|
continue
|
|
217
240
|
if (
|
|
218
241
|
grid[y - 1][x] == "1"
|
|
@@ -228,20 +251,54 @@ def _place_walls_sparse(grid: list[list[str]]) -> None:
|
|
|
228
251
|
grid[y][x] = "1"
|
|
229
252
|
|
|
230
253
|
|
|
254
|
+
def _place_walls_sparse_ortho(
|
|
255
|
+
grid: list[list[str]],
|
|
256
|
+
*,
|
|
257
|
+
density: float = SPARSE_WALL_DENSITY,
|
|
258
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Place isolated wall tiles at a low density, avoiding orthogonal adjacency."""
|
|
261
|
+
cols, rows = len(grid[0]), len(grid)
|
|
262
|
+
forbidden = _collect_exit_adjacent_cells(grid)
|
|
263
|
+
if forbidden_cells:
|
|
264
|
+
forbidden |= forbidden_cells
|
|
265
|
+
for y in range(2, rows - 2):
|
|
266
|
+
for x in range(2, cols - 2):
|
|
267
|
+
if (x, y) in forbidden:
|
|
268
|
+
continue
|
|
269
|
+
if grid[y][x] != ".":
|
|
270
|
+
continue
|
|
271
|
+
if RNG.random() >= density:
|
|
272
|
+
continue
|
|
273
|
+
if (
|
|
274
|
+
grid[y - 1][x] == "1"
|
|
275
|
+
or grid[y + 1][x] == "1"
|
|
276
|
+
or grid[y][x - 1] == "1"
|
|
277
|
+
or grid[y][x + 1] == "1"
|
|
278
|
+
):
|
|
279
|
+
continue
|
|
280
|
+
grid[y][x] = "1"
|
|
281
|
+
|
|
231
282
|
WALL_ALGORITHMS = {
|
|
232
283
|
"default": _place_walls_default,
|
|
233
284
|
"empty": _place_walls_empty,
|
|
234
285
|
"grid_wire": _place_walls_grid_wire,
|
|
235
|
-
"
|
|
286
|
+
"sparse_moore": _place_walls_sparse_moore,
|
|
287
|
+
"sparse_ortho": _place_walls_sparse_ortho,
|
|
236
288
|
}
|
|
237
289
|
|
|
238
290
|
|
|
239
291
|
def _place_steel_beams(
|
|
240
|
-
grid: list[list[str]],
|
|
292
|
+
grid: list[list[str]],
|
|
293
|
+
*,
|
|
294
|
+
chance: float = STEEL_BEAM_CHANCE,
|
|
295
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
241
296
|
) -> set[tuple[int, int]]:
|
|
242
297
|
"""Pick individual cells for steel beams, avoiding exits and their neighbors."""
|
|
243
298
|
cols, rows = len(grid[0]), len(grid)
|
|
244
299
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
300
|
+
if forbidden_cells:
|
|
301
|
+
forbidden |= forbidden_cells
|
|
245
302
|
beams: set[tuple[int, int]] = set()
|
|
246
303
|
for y in range(2, rows - 2):
|
|
247
304
|
for x in range(2, cols - 2):
|
|
@@ -257,7 +314,6 @@ def _place_steel_beams(
|
|
|
257
314
|
def _pick_empty_cell(
|
|
258
315
|
grid: list[list[str]],
|
|
259
316
|
margin: int,
|
|
260
|
-
forbidden: set[tuple[int, int]],
|
|
261
317
|
) -> tuple[int, int]:
|
|
262
318
|
cols, rows = len(grid[0]), len(grid)
|
|
263
319
|
attempts = 0
|
|
@@ -265,12 +321,12 @@ def _pick_empty_cell(
|
|
|
265
321
|
attempts += 1
|
|
266
322
|
x = RNG.randint(margin, cols - margin - 1)
|
|
267
323
|
y = RNG.randint(margin, rows - margin - 1)
|
|
268
|
-
if grid[y][x] == "."
|
|
324
|
+
if grid[y][x] == ".":
|
|
269
325
|
return x, y
|
|
270
326
|
# Fallback: scan for any acceptable cell
|
|
271
327
|
for y in range(margin, rows - margin):
|
|
272
328
|
for x in range(margin, cols - margin):
|
|
273
|
-
if grid[y][x] == "."
|
|
329
|
+
if grid[y][x] == ".":
|
|
274
330
|
return x, y
|
|
275
331
|
return cols // 2, rows // 2
|
|
276
332
|
|
|
@@ -281,7 +337,70 @@ def _generate_random_blueprint(
|
|
|
281
337
|
grid = _init_grid(cols, rows)
|
|
282
338
|
_place_exits(grid, EXITS_PER_SIDE)
|
|
283
339
|
|
|
284
|
-
#
|
|
340
|
+
# Spawns: player, car, zombies
|
|
341
|
+
reserved_cells: set[tuple[int, int]] = set()
|
|
342
|
+
px, py = _pick_empty_cell(grid, SPAWN_MARGIN)
|
|
343
|
+
grid[py][px] = "P"
|
|
344
|
+
reserved_cells.add((px, py))
|
|
345
|
+
cx, cy = _pick_empty_cell(grid, SPAWN_MARGIN)
|
|
346
|
+
grid[cy][cx] = "C"
|
|
347
|
+
reserved_cells.add((cx, cy))
|
|
348
|
+
for _ in range(SPAWN_ZOMBIES):
|
|
349
|
+
zx, zy = _pick_empty_cell(grid, SPAWN_MARGIN)
|
|
350
|
+
grid[zy][zx] = "Z"
|
|
351
|
+
reserved_cells.add((zx, zy))
|
|
352
|
+
|
|
353
|
+
# Select and run the wall placement algorithm (after reserving spawns)
|
|
354
|
+
sparse_density = SPARSE_WALL_DENSITY
|
|
355
|
+
original_wall_algo = wall_algo
|
|
356
|
+
if wall_algo == "sparse":
|
|
357
|
+
print(
|
|
358
|
+
"WARNING: 'sparse' is deprecated. Use 'sparse_moore' instead."
|
|
359
|
+
)
|
|
360
|
+
wall_algo = "sparse_moore"
|
|
361
|
+
elif wall_algo.startswith("sparse."):
|
|
362
|
+
print(
|
|
363
|
+
"WARNING: 'sparse.<int>%' is deprecated. Use "
|
|
364
|
+
"'sparse_moore.<int>%' instead."
|
|
365
|
+
)
|
|
366
|
+
suffix = wall_algo[len("sparse.") :]
|
|
367
|
+
wall_algo = "sparse_moore"
|
|
368
|
+
if suffix.endswith("%") and suffix[:-1].isdigit():
|
|
369
|
+
percent = int(suffix[:-1])
|
|
370
|
+
if 0 <= percent <= 100:
|
|
371
|
+
sparse_density = percent / 100.0
|
|
372
|
+
else:
|
|
373
|
+
print(
|
|
374
|
+
"WARNING: Sparse wall density must be 0-100%. "
|
|
375
|
+
f"Got '{suffix}'. Falling back to default sparse density."
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
print(
|
|
379
|
+
"WARNING: Invalid sparse wall format. Use "
|
|
380
|
+
"'sparse_moore.<int>%' or 'sparse_ortho.<int>%'. "
|
|
381
|
+
f"Got '{original_wall_algo}'. Falling back to default sparse density."
|
|
382
|
+
)
|
|
383
|
+
if wall_algo.startswith("sparse_moore.") or wall_algo.startswith("sparse_ortho."):
|
|
384
|
+
base, suffix = wall_algo.split(".", 1)
|
|
385
|
+
if suffix.endswith("%") and suffix[:-1].isdigit():
|
|
386
|
+
percent = int(suffix[:-1])
|
|
387
|
+
if 0 <= percent <= 100:
|
|
388
|
+
sparse_density = percent / 100.0
|
|
389
|
+
wall_algo = base
|
|
390
|
+
else:
|
|
391
|
+
print(
|
|
392
|
+
"WARNING: Sparse wall density must be 0-100%. "
|
|
393
|
+
f"Got '{suffix}'. Falling back to default sparse density."
|
|
394
|
+
)
|
|
395
|
+
wall_algo = base
|
|
396
|
+
else:
|
|
397
|
+
print(
|
|
398
|
+
"WARNING: Invalid sparse wall format. Use "
|
|
399
|
+
"'sparse_moore.<int>%' or 'sparse_ortho.<int>%'. "
|
|
400
|
+
f"Got '{wall_algo}'. Falling back to default sparse density."
|
|
401
|
+
)
|
|
402
|
+
wall_algo = base
|
|
403
|
+
|
|
285
404
|
if wall_algo not in WALL_ALGORITHMS:
|
|
286
405
|
print(
|
|
287
406
|
f"WARNING: Unknown wall algorithm '{wall_algo}'. Falling back to 'default'."
|
|
@@ -289,18 +408,14 @@ def _generate_random_blueprint(
|
|
|
289
408
|
wall_algo = "default"
|
|
290
409
|
|
|
291
410
|
algo_func = WALL_ALGORITHMS[wall_algo]
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
411
|
+
if wall_algo in {"sparse_moore", "sparse_ortho"}:
|
|
412
|
+
algo_func(grid, density=sparse_density, forbidden_cells=reserved_cells)
|
|
413
|
+
else:
|
|
414
|
+
algo_func(grid, forbidden_cells=reserved_cells)
|
|
295
415
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
cx, cy = _pick_empty_cell(grid, SPAWN_MARGIN, forbidden=steel_beams)
|
|
300
|
-
grid[cy][cx] = "C"
|
|
301
|
-
for _ in range(SPAWN_ZOMBIES):
|
|
302
|
-
zx, zy = _pick_empty_cell(grid, SPAWN_MARGIN, forbidden=steel_beams)
|
|
303
|
-
grid[zy][zx] = "Z"
|
|
416
|
+
steel_beams = _place_steel_beams(
|
|
417
|
+
grid, chance=steel_chance, forbidden_cells=reserved_cells
|
|
418
|
+
)
|
|
304
419
|
|
|
305
420
|
blueprint_rows = ["".join(row) for row in grid]
|
|
306
421
|
return {"grid": blueprint_rows, "steel_cells": steel_beams}
|
zombie_escape/locales/ui.en.json
CHANGED
|
@@ -118,7 +118,15 @@
|
|
|
118
118
|
},
|
|
119
119
|
"stage13": {
|
|
120
120
|
"name": "#13 Rescue Buddy 3",
|
|
121
|
-
"description": "Rescue your buddy.
|
|
121
|
+
"description": "Rescue your buddy. Falling zombies."
|
|
122
|
+
},
|
|
123
|
+
"stage14": {
|
|
124
|
+
"name": "#14 Falling Factory",
|
|
125
|
+
"description": "A factory with collapsed upper floors. Break through obstacles."
|
|
126
|
+
},
|
|
127
|
+
"stage15": {
|
|
128
|
+
"name": "#15 The Divide",
|
|
129
|
+
"description": "A central hazard splits the building. Cross with care."
|
|
122
130
|
}
|
|
123
131
|
},
|
|
124
132
|
"status": {
|
zombie_escape/locales/ui.ja.json
CHANGED
|
@@ -119,6 +119,14 @@
|
|
|
119
119
|
"stage13": {
|
|
120
120
|
"name": "#13 相棒を救え 3",
|
|
121
121
|
"description": "はぐれた相棒を救え。ゾンビの落下あり。"
|
|
122
|
+
},
|
|
123
|
+
"stage14": {
|
|
124
|
+
"name": "#14 崩落工場",
|
|
125
|
+
"description": "上階の床が崩れた工場。破壊しながら進め。"
|
|
126
|
+
},
|
|
127
|
+
"stage15": {
|
|
128
|
+
"name": "#15 分断ライン",
|
|
129
|
+
"description": "建物中央を分断する危険地帯。横断には注意。"
|
|
122
130
|
}
|
|
123
131
|
},
|
|
124
132
|
"status": {
|
zombie_escape/models.py
CHANGED
|
@@ -11,6 +11,7 @@ from pygame import sprite, surface
|
|
|
11
11
|
from .entities_constants import ZOMBIE_AGING_DURATION_FRAMES
|
|
12
12
|
from .gameplay_constants import (
|
|
13
13
|
DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
14
|
+
DEFAULT_SHOES_SPAWN_COUNT,
|
|
14
15
|
SURVIVOR_SPAWN_RATE,
|
|
15
16
|
ZOMBIE_SPAWN_DELAY_MS,
|
|
16
17
|
)
|
|
@@ -18,17 +19,16 @@ from .level_constants import DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS
|
|
|
18
19
|
from .localization import translate as tr
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING: # pragma: no cover - typing-only imports
|
|
21
|
-
from .entities import Camera, Car, Flashlight, FuelCan, Player
|
|
22
|
+
from .entities import Camera, Car, Flashlight, FuelCan, Player, Shoes
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@dataclass
|
|
25
26
|
class LevelLayout:
|
|
26
27
|
"""Container for level layout rectangles and cell sets."""
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
walkable_cells: list[pygame.Rect]
|
|
29
|
+
field_rect: pygame.Rect
|
|
30
|
+
outside_cells: set[tuple[int, int]]
|
|
31
|
+
walkable_cells: list[tuple[int, int]]
|
|
32
32
|
outer_wall_cells: set[tuple[int, int]]
|
|
33
33
|
wall_cells: set[tuple[int, int]]
|
|
34
34
|
fall_spawn_cells: set[tuple[int, int]]
|
|
@@ -82,6 +82,7 @@ class ProgressState:
|
|
|
82
82
|
elapsed_play_ms: int
|
|
83
83
|
has_fuel: bool
|
|
84
84
|
flashlight_count: int
|
|
85
|
+
shoes_count: int
|
|
85
86
|
ambient_palette_key: str
|
|
86
87
|
hint_expires_at: int
|
|
87
88
|
hint_target_type: str | None
|
|
@@ -104,6 +105,8 @@ class ProgressState:
|
|
|
104
105
|
falling_zombies: list[FallingZombie]
|
|
105
106
|
falling_spawn_carry: int
|
|
106
107
|
dust_rings: list[DustRing]
|
|
108
|
+
player_wall_target_cell: tuple[int, int] | None
|
|
109
|
+
player_wall_target_ttl: int
|
|
107
110
|
|
|
108
111
|
|
|
109
112
|
@dataclass
|
|
@@ -131,6 +134,7 @@ class GameData:
|
|
|
131
134
|
level_height: int
|
|
132
135
|
fuel: FuelCan | None = None
|
|
133
136
|
flashlights: list[Flashlight] | None = None
|
|
137
|
+
shoes: list[Shoes] | None = None
|
|
134
138
|
player: Player | None = None
|
|
135
139
|
car: Car | None = None
|
|
136
140
|
waiting_cars: list[Car] = field(default_factory=list)
|
|
@@ -153,6 +157,7 @@ class Stage:
|
|
|
153
157
|
endurance_goal_ms: int = 0
|
|
154
158
|
fuel_spawn_count: int = 1
|
|
155
159
|
initial_flashlight_count: int = DEFAULT_FLASHLIGHT_SPAWN_COUNT
|
|
160
|
+
initial_shoes_count: int = DEFAULT_SHOES_SPAWN_COUNT
|
|
156
161
|
survivor_spawn_rate: float = SURVIVOR_SPAWN_RATE
|
|
157
162
|
spawn_interval_ms: int = ZOMBIE_SPAWN_DELAY_MS
|
|
158
163
|
initial_interior_spawn_rate: float = 0.015
|
|
@@ -160,6 +165,7 @@ class Stage:
|
|
|
160
165
|
interior_spawn_weight: float = 0.0
|
|
161
166
|
interior_fall_spawn_weight: float = 0.0
|
|
162
167
|
fall_spawn_zones: list[tuple[int, int, int, int]] = field(default_factory=list)
|
|
168
|
+
fall_spawn_floor_ratio: float = 0.0
|
|
163
169
|
zombie_tracker_ratio: float = 0.0
|
|
164
170
|
zombie_wall_follower_ratio: float = 0.0
|
|
165
171
|
zombie_normal_ratio: float = 1.0
|