zombie-escape 1.12.0__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.
@@ -57,12 +57,13 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
57
57
  survivor_group = game_data.groups.survivor_group
58
58
  state = game_data.state
59
59
  walkable_cells = game_data.layout.walkable_cells
60
- outside_rects = game_data.layout.outside_rects
60
+ outside_cells = game_data.layout.outside_cells
61
61
  fuel = game_data.fuel
62
62
  flashlights = game_data.flashlights or []
63
63
  shoes_list = game_data.shoes or []
64
64
  camera = game_data.camera
65
65
  stage = game_data.stage
66
+ cell_size = game_data.cell_size
66
67
  maintain_waiting_car_supply(game_data)
67
68
  active_car = car if car and car.alive() else None
68
69
  waiting_cars = game_data.waiting_cars
@@ -75,6 +76,17 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
75
76
  )
76
77
  shoes_interaction_radius = _interaction_radius(SHOES_WIDTH, SHOES_HEIGHT)
77
78
 
79
+ def _rect_center_cell(rect: pygame.Rect) -> tuple[int, int] | None:
80
+ if cell_size <= 0:
81
+ return None
82
+ return (int(rect.centerx // cell_size), int(rect.centery // cell_size))
83
+
84
+ def _cell_center(cell: tuple[int, int]) -> tuple[int, int]:
85
+ return (
86
+ int((cell[0] * cell_size) + (cell_size / 2)),
87
+ int((cell[1] * cell_size) + (cell_size / 2)),
88
+ )
89
+
78
90
  def _player_near_point(point: tuple[float, float], radius: float) -> bool:
79
91
  dx = point[0] - player.x
80
92
  dy = point[1] - player.y
@@ -189,7 +201,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
189
201
  else:
190
202
  if walkable_cells:
191
203
  new_cell = RNG.choice(walkable_cells)
192
- buddy.teleport(new_cell.center)
204
+ buddy.teleport(_cell_center(new_cell))
193
205
  else:
194
206
  buddy.teleport(
195
207
  (game_data.level_width // 2, game_data.level_height // 2)
@@ -341,8 +353,9 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
341
353
  stage.endurance_stage
342
354
  and state.dawn_ready
343
355
  and not player.in_car
344
- and outside_rects
345
- and any(outside.collidepoint(player.rect.center) for outside in outside_rects)
356
+ and outside_cells
357
+ and (player_cell := _rect_center_cell(player.rect)) is not None
358
+ and player_cell in outside_cells
346
359
  ):
347
360
  state.game_won = True
348
361
 
@@ -351,9 +364,8 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
351
364
  buddy_ready = True
352
365
  if stage.buddy_required_count > 0:
353
366
  buddy_ready = state.buddy_onboard >= stage.buddy_required_count
354
- if buddy_ready and any(
355
- outside.collidepoint(car.rect.center) for outside in outside_rects
356
- ):
367
+ car_cell = _rect_center_cell(car.rect)
368
+ if buddy_ready and car_cell is not None and car_cell in outside_cells:
357
369
  if stage.buddy_required_count > 0:
358
370
  state.buddy_rescued = min(
359
371
  stage.buddy_required_count, state.buddy_onboard
@@ -92,11 +92,11 @@ def generate_level_from_blueprint(
92
92
  return True
93
93
  return (nx, ny) in wall_cells
94
94
 
95
- outside_rects: list[pygame.Rect] = []
96
- walkable_cells: list[pygame.Rect] = []
97
- player_cells: list[pygame.Rect] = []
98
- car_cells: list[pygame.Rect] = []
99
- zombie_cells: list[pygame.Rect] = []
95
+ outside_cells: set[tuple[int, int]] = set()
96
+ walkable_cells: list[tuple[int, int]] = []
97
+ player_cells: list[tuple[int, int]] = []
98
+ car_cells: list[tuple[int, int]] = []
99
+ zombie_cells: list[tuple[int, int]] = []
100
100
  interior_min_x = 2
101
101
  interior_max_x = stage.grid_cols - 3
102
102
  interior_min_y = 2
@@ -125,7 +125,7 @@ def generate_level_from_blueprint(
125
125
  cell_rect = _rect_for_cell(x, y, cell_size)
126
126
  cell_has_beam = steel_enabled and (x, y) in steel_cells
127
127
  if ch == "O":
128
- outside_rects.append(cell_rect)
128
+ outside_cells.add((x, y))
129
129
  continue
130
130
  if ch == "B":
131
131
  draw_bottom_side = not _has_wall(x, y + 1)
@@ -147,7 +147,7 @@ def generate_level_from_blueprint(
147
147
  continue
148
148
  if ch == "E":
149
149
  if not cell_has_beam:
150
- walkable_cells.append(cell_rect)
150
+ walkable_cells.append((x, y))
151
151
  elif ch == "1":
152
152
  beam = None
153
153
  if cell_has_beam:
@@ -196,14 +196,14 @@ def generate_level_from_blueprint(
196
196
  all_sprites.add(wall, layer=0)
197
197
  else:
198
198
  if not cell_has_beam:
199
- walkable_cells.append(cell_rect)
199
+ walkable_cells.append((x, y))
200
200
 
201
201
  if ch == "P":
202
- player_cells.append(cell_rect)
202
+ player_cells.append((x, y))
203
203
  if ch == "C":
204
- car_cells.append(cell_rect)
204
+ car_cells.append((x, y))
205
205
  if ch == "Z":
206
- zombie_cells.append(cell_rect)
206
+ zombie_cells.append((x, y))
207
207
 
208
208
  if cell_has_beam and ch != "1":
209
209
  beam = SteelBeam(
@@ -215,9 +215,13 @@ def generate_level_from_blueprint(
215
215
  )
216
216
  add_beam_to_groups(beam)
217
217
 
218
- game_data.layout.outer_rect = (0, 0, game_data.level_width, game_data.level_height)
219
- game_data.layout.inner_rect = (0, 0, game_data.level_width, game_data.level_height)
220
- game_data.layout.outside_rects = outside_rects
218
+ game_data.layout.field_rect = pygame.Rect(
219
+ 0,
220
+ 0,
221
+ game_data.level_width,
222
+ game_data.level_height,
223
+ )
224
+ game_data.layout.outside_cells = outside_cells
221
225
  game_data.layout.walkable_cells = walkable_cells
222
226
  game_data.layout.outer_wall_cells = outer_wall_cells
223
227
  game_data.layout.wall_cells = wall_cells
@@ -13,6 +13,7 @@ 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_FOLLOW_SENSOR_DISTANCE,
@@ -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:
@@ -166,7 +170,25 @@ def update_entities(
166
170
  target_for_camera = active_car if player.in_car and active_car else player
167
171
  camera.update(target_for_camera)
168
172
 
169
- update_survivors(game_data, wall_index=wall_index)
173
+ if player.inner_wall_hit and player.inner_wall_cell is not None:
174
+ game_data.state.player_wall_target_cell = player.inner_wall_cell
175
+ game_data.state.player_wall_target_ttl = HUMANOID_WALL_BUMP_FRAMES
176
+ elif game_data.state.player_wall_target_ttl > 0:
177
+ game_data.state.player_wall_target_ttl -= 1
178
+ if game_data.state.player_wall_target_ttl <= 0:
179
+ game_data.state.player_wall_target_cell = None
180
+
181
+ wall_target_cell = (
182
+ game_data.state.player_wall_target_cell
183
+ if game_data.state.player_wall_target_ttl > 0
184
+ else None
185
+ )
186
+
187
+ update_survivors(
188
+ game_data,
189
+ wall_index=wall_index,
190
+ wall_target_cell=wall_target_cell,
191
+ )
170
192
  update_falling_zombies(game_data, config)
171
193
 
172
194
  # Spawn new zombies if needed
@@ -68,6 +68,13 @@ __all__ = [
68
68
  ]
69
69
 
70
70
 
71
+ def _cell_center(cell: tuple[int, int], cell_size: int) -> tuple[int, int]:
72
+ return (
73
+ int((cell[0] * cell_size) + (cell_size / 2)),
74
+ int((cell[1] * cell_size) + (cell_size / 2)),
75
+ )
76
+
77
+
71
78
  def _car_appearance_for_stage(stage: Stage | None) -> str:
72
79
  return "disabled" if stage and stage.endurance_stage else "default"
73
80
 
@@ -311,7 +318,8 @@ def _create_zombie(
311
318
 
312
319
 
313
320
  def place_fuel_can(
314
- walkable_cells: list[pygame.Rect],
321
+ walkable_cells: list[tuple[int, int]],
322
+ cell_size: int,
315
323
  player: Player,
316
324
  *,
317
325
  cars: Sequence[Car] | None = None,
@@ -329,30 +337,33 @@ def place_fuel_can(
329
337
 
330
338
  for _ in range(200):
331
339
  cell = RNG.choice(walkable_cells)
332
- if reserved_centers and cell.center in reserved_centers:
340
+ center = _cell_center(cell, cell_size)
341
+ if reserved_centers and center in reserved_centers:
333
342
  continue
334
- dx = cell.centerx - player.x
335
- dy = cell.centery - player.y
343
+ dx = center[0] - player.x
344
+ dy = center[1] - player.y
336
345
  if dx * dx + dy * dy < min_player_dist_sq:
337
346
  continue
338
347
  if cars:
339
348
  too_close = False
340
349
  for parked_car in cars:
341
- dx = cell.centerx - parked_car.rect.centerx
342
- dy = cell.centery - parked_car.rect.centery
350
+ dx = center[0] - parked_car.rect.centerx
351
+ dy = center[1] - parked_car.rect.centery
343
352
  if dx * dx + dy * dy < min_car_dist_sq:
344
353
  too_close = True
345
354
  break
346
355
  if too_close:
347
356
  continue
348
- return FuelCan(cell.centerx, cell.centery)
357
+ return FuelCan(center[0], center[1])
349
358
 
350
359
  cell = RNG.choice(walkable_cells)
351
- return FuelCan(cell.centerx, cell.centery)
360
+ center = _cell_center(cell, cell_size)
361
+ return FuelCan(center[0], center[1])
352
362
 
353
363
 
354
364
  def _place_flashlight(
355
- walkable_cells: list[pygame.Rect],
365
+ walkable_cells: list[tuple[int, int]],
366
+ cell_size: int,
356
367
  player: Player,
357
368
  *,
358
369
  cars: Sequence[Car] | None = None,
@@ -369,28 +380,31 @@ def _place_flashlight(
369
380
 
370
381
  for _ in range(200):
371
382
  cell = RNG.choice(walkable_cells)
372
- if reserved_centers and cell.center in reserved_centers:
383
+ center = _cell_center(cell, cell_size)
384
+ if reserved_centers and center in reserved_centers:
373
385
  continue
374
- dx = cell.centerx - player.x
375
- dy = cell.centery - player.y
386
+ dx = center[0] - player.x
387
+ dy = center[1] - player.y
376
388
  if dx * dx + dy * dy < min_player_dist_sq:
377
389
  continue
378
390
  if cars:
379
391
  if any(
380
- (cell.centerx - parked.rect.centerx) ** 2
381
- + (cell.centery - parked.rect.centery) ** 2
392
+ (center[0] - parked.rect.centerx) ** 2
393
+ + (center[1] - parked.rect.centery) ** 2
382
394
  < min_car_dist_sq
383
395
  for parked in cars
384
396
  ):
385
397
  continue
386
- return Flashlight(cell.centerx, cell.centery)
398
+ return Flashlight(center[0], center[1])
387
399
 
388
400
  cell = RNG.choice(walkable_cells)
389
- return Flashlight(cell.centerx, cell.centery)
401
+ center = _cell_center(cell, cell_size)
402
+ return Flashlight(center[0], center[1])
390
403
 
391
404
 
392
405
  def place_flashlights(
393
- walkable_cells: list[pygame.Rect],
406
+ walkable_cells: list[tuple[int, int]],
407
+ cell_size: int,
394
408
  player: Player,
395
409
  *,
396
410
  cars: Sequence[Car] | None = None,
@@ -404,7 +418,11 @@ def place_flashlights(
404
418
  while len(placed) < count and attempts < max_attempts:
405
419
  attempts += 1
406
420
  fl = _place_flashlight(
407
- walkable_cells, player, cars=cars, reserved_centers=reserved_centers
421
+ walkable_cells,
422
+ cell_size,
423
+ player,
424
+ cars=cars,
425
+ reserved_centers=reserved_centers,
408
426
  )
409
427
  if not fl:
410
428
  break
@@ -421,7 +439,8 @@ def place_flashlights(
421
439
 
422
440
 
423
441
  def _place_shoes(
424
- walkable_cells: list[pygame.Rect],
442
+ walkable_cells: list[tuple[int, int]],
443
+ cell_size: int,
425
444
  player: Player,
426
445
  *,
427
446
  cars: Sequence[Car] | None = None,
@@ -438,28 +457,31 @@ def _place_shoes(
438
457
 
439
458
  for _ in range(200):
440
459
  cell = RNG.choice(walkable_cells)
441
- if reserved_centers and cell.center in reserved_centers:
460
+ center = _cell_center(cell, cell_size)
461
+ if reserved_centers and center in reserved_centers:
442
462
  continue
443
- dx = cell.centerx - player.x
444
- dy = cell.centery - player.y
463
+ dx = center[0] - player.x
464
+ dy = center[1] - player.y
445
465
  if dx * dx + dy * dy < min_player_dist_sq:
446
466
  continue
447
467
  if cars:
448
468
  if any(
449
- (cell.centerx - parked.rect.centerx) ** 2
450
- + (cell.centery - parked.rect.centery) ** 2
469
+ (center[0] - parked.rect.centerx) ** 2
470
+ + (center[1] - parked.rect.centery) ** 2
451
471
  < min_car_dist_sq
452
472
  for parked in cars
453
473
  ):
454
474
  continue
455
- return Shoes(cell.centerx, cell.centery)
475
+ return Shoes(center[0], center[1])
456
476
 
457
477
  cell = RNG.choice(walkable_cells)
458
- return Shoes(cell.centerx, cell.centery)
478
+ center = _cell_center(cell, cell_size)
479
+ return Shoes(center[0], center[1])
459
480
 
460
481
 
461
482
  def place_shoes(
462
- walkable_cells: list[pygame.Rect],
483
+ walkable_cells: list[tuple[int, int]],
484
+ cell_size: int,
463
485
  player: Player,
464
486
  *,
465
487
  cars: Sequence[Car] | None = None,
@@ -473,7 +495,11 @@ def place_shoes(
473
495
  while len(placed) < count and attempts < max_attempts:
474
496
  attempts += 1
475
497
  shoes = _place_shoes(
476
- walkable_cells, player, cars=cars, reserved_centers=reserved_centers
498
+ walkable_cells,
499
+ cell_size,
500
+ player,
501
+ cars=cars,
502
+ reserved_centers=reserved_centers,
477
503
  )
478
504
  if not shoes:
479
505
  break
@@ -489,7 +515,8 @@ def place_shoes(
489
515
 
490
516
 
491
517
  def place_buddies(
492
- walkable_cells: list[pygame.Rect],
518
+ walkable_cells: list[tuple[int, int]],
519
+ cell_size: int,
493
520
  player: Player,
494
521
  *,
495
522
  cars: Sequence[Car] | None = None,
@@ -501,6 +528,7 @@ def place_buddies(
501
528
  min_player_dist = 240
502
529
  positions = find_interior_spawn_positions(
503
530
  walkable_cells,
531
+ cell_size,
504
532
  1.0,
505
533
  player=player,
506
534
  min_player_dist=min_player_dist,
@@ -510,7 +538,10 @@ def place_buddies(
510
538
  placed.append(Survivor(pos[0], pos[1], is_buddy=True))
511
539
  remaining = count - len(placed)
512
540
  for _ in range(max(0, remaining)):
513
- spawn_pos = find_nearby_offscreen_spawn_position(walkable_cells)
541
+ spawn_pos = find_nearby_offscreen_spawn_position(
542
+ walkable_cells,
543
+ cell_size,
544
+ )
514
545
  placed.append(Survivor(spawn_pos[0], spawn_pos[1], is_buddy=True))
515
546
  return placed
516
547
 
@@ -518,7 +549,8 @@ def place_buddies(
518
549
  def place_new_car(
519
550
  wall_group: pygame.sprite.Group,
520
551
  player: Player,
521
- walkable_cells: list[pygame.Rect],
552
+ walkable_cells: list[tuple[int, int]],
553
+ cell_size: int,
522
554
  *,
523
555
  existing_cars: Sequence[Car] | None = None,
524
556
  appearance: str = "default",
@@ -529,7 +561,7 @@ def place_new_car(
529
561
  max_attempts = 150
530
562
  for _ in range(max_attempts):
531
563
  cell = RNG.choice(walkable_cells)
532
- c_x, c_y = cell.center
564
+ c_x, c_y = _cell_center(cell, cell_size)
533
565
  temp_car = Car(c_x, c_y, appearance=appearance)
534
566
  temp_rect = temp_car.rect.inflate(30, 30)
535
567
  nearby_walls = pygame.sprite.Group()
@@ -555,7 +587,7 @@ def place_new_car(
555
587
 
556
588
 
557
589
  def spawn_survivors(
558
- game_data: GameData, layout_data: Mapping[str, list[pygame.Rect]]
590
+ game_data: GameData, layout_data: Mapping[str, list[tuple[int, int]]]
559
591
  ) -> list[Survivor]:
560
592
  """Populate rescue-stage survivors and buddy-stage buddies."""
561
593
  survivors: list[Survivor] = []
@@ -566,10 +598,12 @@ def spawn_survivors(
566
598
  wall_group = game_data.groups.wall_group
567
599
  survivor_group = game_data.groups.survivor_group
568
600
  all_sprites = game_data.groups.all_sprites
601
+ cell_size = game_data.cell_size
569
602
 
570
603
  if game_data.stage.rescue_stage:
571
604
  positions = find_interior_spawn_positions(
572
605
  walkable,
606
+ cell_size,
573
607
  game_data.stage.survivor_spawn_rate,
574
608
  )
575
609
  for pos in positions:
@@ -586,6 +620,7 @@ def spawn_survivors(
586
620
  if game_data.player:
587
621
  buddies = place_buddies(
588
622
  walkable,
623
+ cell_size,
589
624
  game_data.player,
590
625
  cars=game_data.waiting_cars,
591
626
  count=buddy_count,
@@ -602,17 +637,18 @@ def spawn_survivors(
602
637
 
603
638
  def setup_player_and_cars(
604
639
  game_data: GameData,
605
- layout_data: Mapping[str, list[pygame.Rect]],
640
+ layout_data: Mapping[str, list[tuple[int, int]]],
606
641
  *,
607
642
  car_count: int = 1,
608
643
  ) -> tuple[Player, list[Car]]:
609
644
  """Create the player plus one or more parked cars using blueprint candidates."""
610
645
  all_sprites = game_data.groups.all_sprites
611
- walkable_cells: list[pygame.Rect] = layout_data["walkable_cells"]
646
+ walkable_cells: list[tuple[int, int]] = layout_data["walkable_cells"]
647
+ cell_size = game_data.cell_size
612
648
 
613
- def _pick_center(cells: list[pygame.Rect]) -> tuple[int, int]:
649
+ def _pick_center(cells: list[tuple[int, int]]) -> tuple[int, int]:
614
650
  return (
615
- RNG.choice(cells).center
651
+ _cell_center(RNG.choice(cells), cell_size)
616
652
  if cells
617
653
  else (game_data.level_width // 2, game_data.level_height // 2)
618
654
  )
@@ -630,13 +666,14 @@ def setup_player_and_cars(
630
666
  return (player_pos[0] + 200, player_pos[1])
631
667
  RNG.shuffle(car_candidates)
632
668
  for candidate in car_candidates:
633
- if (candidate.centerx - player_pos[0]) ** 2 + (
634
- candidate.centery - player_pos[1]
669
+ center = _cell_center(candidate, cell_size)
670
+ if (center[0] - player_pos[0]) ** 2 + (
671
+ center[1] - player_pos[1]
635
672
  ) ** 2 >= 400 * 400:
636
673
  car_candidates.remove(candidate)
637
- return candidate.center
674
+ return center
638
675
  choice = car_candidates.pop()
639
- return choice.center
676
+ return _cell_center(choice, cell_size)
640
677
 
641
678
  for _ in range(max(1, car_count)):
642
679
  car_pos = _pick_car_position()
@@ -653,7 +690,7 @@ def setup_player_and_cars(
653
690
  def spawn_initial_zombies(
654
691
  game_data: GameData,
655
692
  player: Player,
656
- layout_data: Mapping[str, list[pygame.Rect]],
693
+ layout_data: Mapping[str, list[tuple[int, int]]],
657
694
  config: dict[str, Any],
658
695
  ) -> None:
659
696
  """Spawn initial zombies using blueprint candidate cells."""
@@ -664,24 +701,25 @@ def spawn_initial_zombies(
664
701
  spawn_cells = layout_data["walkable_cells"]
665
702
  if not spawn_cells:
666
703
  return
704
+ cell_size = game_data.cell_size
667
705
 
668
706
  if game_data.stage.id == "debug_tracker":
669
707
  player_pos = player.rect.center
670
708
  min_dist_sq = 100 * 100
671
709
  max_dist_sq = 240 * 240
672
- candidates = [
673
- cell
674
- for cell in spawn_cells
675
- if min_dist_sq
676
- <= (cell.centerx - player_pos[0]) ** 2 + (cell.centery - player_pos[1]) ** 2
677
- <= max_dist_sq
678
- ]
710
+ candidates = []
711
+ for cell in spawn_cells:
712
+ center = _cell_center(cell, cell_size)
713
+ dist_sq = (center[0] - player_pos[0]) ** 2 + (center[1] - player_pos[1]) ** 2
714
+ if min_dist_sq <= dist_sq <= max_dist_sq:
715
+ candidates.append(cell)
679
716
  if not candidates:
680
717
  candidates = spawn_cells
681
718
  candidate = RNG.choice(candidates)
719
+ candidate_center = _cell_center(candidate, cell_size)
682
720
  tentative = _create_zombie(
683
721
  config,
684
- start_pos=candidate.center,
722
+ start_pos=candidate_center,
685
723
  stage=game_data.stage,
686
724
  tracker=True,
687
725
  wall_follower=False,
@@ -696,6 +734,7 @@ def spawn_initial_zombies(
696
734
  spawn_rate = max(0.0, game_data.stage.initial_interior_spawn_rate)
697
735
  positions = find_interior_spawn_positions(
698
736
  spawn_cells,
737
+ cell_size,
699
738
  spawn_rate,
700
739
  player=player,
701
740
  min_player_dist=ZOMBIE_SPAWN_PLAYER_BUFFER,
@@ -729,6 +768,7 @@ def spawn_waiting_car(game_data: GameData) -> Car | None:
729
768
  return None
730
769
  wall_group = game_data.groups.wall_group
731
770
  all_sprites = game_data.groups.all_sprites
771
+ cell_size = game_data.cell_size
732
772
  active_car = game_data.car if game_data.car and game_data.car.alive() else None
733
773
  waiting = _alive_waiting_cars(game_data)
734
774
  obstacles: list[Car] = list(waiting)
@@ -742,6 +782,7 @@ def spawn_waiting_car(game_data: GameData) -> Car | None:
742
782
  wall_group,
743
783
  player,
744
784
  walkable_cells,
785
+ cell_size,
745
786
  existing_cars=obstacles,
746
787
  appearance=appearance,
747
788
  )
@@ -813,6 +854,7 @@ def _spawn_nearby_zombie(
813
854
  all_sprites = game_data.groups.all_sprites
814
855
  spawn_pos = find_nearby_offscreen_spawn_position(
815
856
  game_data.layout.walkable_cells,
857
+ game_data.cell_size,
816
858
  player=player,
817
859
  camera=camera,
818
860
  min_player_dist=ZOMBIE_SPAWN_PLAYER_BUFFER,
@@ -54,6 +54,8 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
54
54
  falling_zombies=[],
55
55
  falling_spawn_carry=0,
56
56
  dust_rings=[],
57
+ player_wall_target_cell=None,
58
+ player_wall_target_ttl=0,
57
59
  )
58
60
 
59
61
  # Create sprite groups
@@ -69,8 +71,7 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
69
71
  camera = Camera(level_width, level_height)
70
72
 
71
73
  # Define level layout (will be filled by blueprint generation)
72
- outer_rect = 0, 0, level_width, level_height
73
- inner_rect = outer_rect
74
+ field_rect = pygame.Rect(0, 0, level_width, level_height)
74
75
 
75
76
  return GameData(
76
77
  state=game_state,
@@ -82,9 +83,8 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
82
83
  ),
83
84
  camera=camera,
84
85
  layout=LevelLayout(
85
- outer_rect=outer_rect,
86
- inner_rect=inner_rect,
87
- outside_rects=[],
86
+ field_rect=field_rect,
87
+ outside_cells=set(),
88
88
  walkable_cells=[],
89
89
  outer_wall_cells=set(),
90
90
  wall_cells=set(),
@@ -107,8 +107,11 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
107
107
 
108
108
  def carbonize_outdoor_zombies(game_data: GameData) -> None:
109
109
  """Petrify zombies that have already broken through to the exterior."""
110
- outside_rects = game_data.layout.outside_rects or []
111
- if not outside_rects:
110
+ outside_cells = game_data.layout.outside_cells
111
+ if not outside_cells:
112
+ return
113
+ cell_size = game_data.cell_size
114
+ if cell_size <= 0:
112
115
  return
113
116
  group = game_data.groups.zombie_group
114
117
  if not group:
@@ -116,8 +119,11 @@ def carbonize_outdoor_zombies(game_data: GameData) -> None:
116
119
  for zombie in list(group):
117
120
  if not zombie.alive():
118
121
  continue
119
- center = zombie.rect.center
120
- if any(rect_obj.collidepoint(center) for rect_obj in outside_rects):
122
+ cell = (
123
+ int(zombie.rect.centerx // cell_size),
124
+ int(zombie.rect.centery // cell_size),
125
+ )
126
+ if cell in outside_cells:
121
127
  zombie.carbonize()
122
128
 
123
129
 
@@ -29,7 +29,9 @@ RNG = get_rng()
29
29
 
30
30
 
31
31
  def update_survivors(
32
- game_data: GameData, wall_index: WallIndex | None = None
32
+ game_data: GameData,
33
+ wall_index: WallIndex | None = None,
34
+ wall_target_cell: tuple[int, int] | None = None,
33
35
  ) -> None:
34
36
  if not (game_data.stage.rescue_stage or game_data.stage.buddy_required_count > 0):
35
37
  return
@@ -54,6 +56,7 @@ def update_survivors(
54
56
  grid_rows=game_data.stage.grid_rows,
55
57
  level_width=game_data.level_width,
56
58
  level_height=game_data.level_height,
59
+ wall_target_cell=wall_target_cell,
57
60
  )
58
61
 
59
62
  # Gently prevent survivors from overlapping the player or each other
@@ -106,6 +109,7 @@ def update_survivors(
106
109
  other.rect.center = (int(other.x), int(other.y))
107
110
 
108
111
 
112
+
109
113
  def calculate_car_speed_for_passengers(
110
114
  passengers: int, *, capacity: int = SURVIVOR_MAX_SAFE_PASSENGERS
111
115
  ) -> float:
@@ -243,6 +247,7 @@ def handle_survivor_zombie_collisions(
243
247
  zombie_xs = [z.rect.centerx for z in zombies]
244
248
  camera = game_data.camera
245
249
  walkable_cells = game_data.layout.walkable_cells
250
+ cell_size = game_data.cell_size
246
251
 
247
252
  for survivor in list(survivor_group):
248
253
  if not survivor.alive():
@@ -275,6 +280,7 @@ def handle_survivor_zombie_collisions(
275
280
  if not rect_visible_on_screen(camera, survivor.rect):
276
281
  spawn_pos = find_nearby_offscreen_spawn_position(
277
282
  walkable_cells,
283
+ cell_size,
278
284
  camera=camera,
279
285
  )
280
286
  survivor.teleport(spawn_pos)
@@ -310,6 +316,7 @@ def respawn_buddies_near_player(game_data: GameData) -> None:
310
316
  wall_group = game_data.groups.wall_group
311
317
  camera = game_data.camera
312
318
  walkable_cells = game_data.layout.walkable_cells
319
+ cell_size = game_data.cell_size
313
320
  offsets = [
314
321
  (BUDDY_RADIUS * 3, 0),
315
322
  (-BUDDY_RADIUS * 3, 0),
@@ -321,6 +328,7 @@ def respawn_buddies_near_player(game_data: GameData) -> None:
321
328
  if walkable_cells:
322
329
  spawn_pos = find_nearby_offscreen_spawn_position(
323
330
  walkable_cells,
331
+ cell_size,
324
332
  camera=camera,
325
333
  )
326
334
  else: