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.
@@ -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[pygame.Rect],
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 cell in walkable_cells:
39
+ for cell_x, cell_y in walkable_cells:
39
40
  if RNG.random() >= clamped_rate:
40
41
  continue
41
- jitter_x = RNG.uniform(-cell.width * jitter_ratio, cell.width * jitter_ratio)
42
- jitter_y = RNG.uniform(-cell.height * jitter_ratio, cell.height * jitter_ratio)
43
- positions.append((int(cell.centerx + jitter_x), int(cell.centery + jitter_y)))
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[pygame.Rect],
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[pygame.Rect],
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
- cell = RNG.choice(walkable_cells)
108
- jitter_x = RNG.uniform(-cell.width * 0.35, cell.width * 0.35)
109
- jitter_y = RNG.uniform(-cell.height * 0.35, cell.height * 0.35)
110
- candidate = (int(cell.centerx + jitter_x), int(cell.centery + jitter_y))
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
- cell = RNG.choice(walkable_cells)
127
- center = (cell.centerx, cell.centery)
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
- fallback_x = RNG.uniform(-cell.width * 0.2, cell.width * 0.2)
138
- fallback_y = RNG.uniform(-cell.height * 0.2, cell.height * 0.2)
139
- return (int(cell.centerx + fallback_x), int(cell.centery + fallback_y))
140
- fallback_cell = RNG.choice(walkable_cells)
141
- fallback_x = RNG.uniform(-fallback_cell.width * 0.35, fallback_cell.width * 0.35)
142
- fallback_y = RNG.uniform(-fallback_cell.height * 0.35, fallback_cell.height * 0.35)
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(fallback_cell.centerx + fallback_x),
145
- int(fallback_cell.centery + fallback_y),
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(grid: list[list[str]]) -> None:
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: collect forbidden cells (exits + neighbors)
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(grid: list[list[str]]) -> None:
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
- pass
126
+ _ = (grid, forbidden_cells)
117
127
 
118
128
 
119
- def _place_walls_grid_wire(grid: list[list[str]]) -> None:
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 _place_walls_sparse(grid: list[list[str]]) -> None:
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() >= SPARSE_WALL_DENSITY:
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
- "sparse": _place_walls_sparse,
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]], *, chance: float = STEEL_BEAM_CHANCE
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] == "." and (x, y) not in forbidden:
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] == "." and (x, y) not in forbidden:
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
- # Select and run the wall placement algorithm
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
- algo_func(grid)
293
-
294
- steel_beams = _place_steel_beams(grid, chance=steel_chance)
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
- # Spawns: player, car, zombies
297
- px, py = _pick_empty_cell(grid, SPAWN_MARGIN, forbidden=steel_beams)
298
- grid[py][px] = "P"
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}
@@ -118,7 +118,15 @@
118
118
  },
119
119
  "stage13": {
120
120
  "name": "#13 Rescue Buddy 3",
121
- "description": "Rescue your buddy. Zombies may fall from above."
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": {
@@ -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
- outer_rect: tuple[int, int, int, int]
29
- inner_rect: tuple[int, int, int, int]
30
- outside_rects: list[pygame.Rect]
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