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.
Files changed (34) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/__main__.py +7 -0
  3. zombie_escape/colors.py +22 -14
  4. zombie_escape/entities.py +756 -147
  5. zombie_escape/entities_constants.py +35 -14
  6. zombie_escape/export_images.py +296 -0
  7. zombie_escape/gameplay/__init__.py +2 -1
  8. zombie_escape/gameplay/constants.py +6 -0
  9. zombie_escape/gameplay/footprints.py +4 -0
  10. zombie_escape/gameplay/interactions.py +19 -7
  11. zombie_escape/gameplay/layout.py +103 -34
  12. zombie_escape/gameplay/movement.py +85 -5
  13. zombie_escape/gameplay/spawn.py +139 -90
  14. zombie_escape/gameplay/state.py +18 -9
  15. zombie_escape/gameplay/survivors.py +13 -2
  16. zombie_escape/gameplay/utils.py +40 -21
  17. zombie_escape/level_blueprints.py +256 -19
  18. zombie_escape/locales/ui.en.json +12 -2
  19. zombie_escape/locales/ui.ja.json +12 -2
  20. zombie_escape/models.py +14 -7
  21. zombie_escape/render.py +149 -37
  22. zombie_escape/render_assets.py +419 -124
  23. zombie_escape/render_constants.py +27 -0
  24. zombie_escape/screens/game_over.py +14 -3
  25. zombie_escape/screens/gameplay.py +72 -14
  26. zombie_escape/screens/title.py +18 -7
  27. zombie_escape/stage_constants.py +51 -15
  28. zombie_escape/zombie_escape.py +24 -1
  29. {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/METADATA +41 -15
  30. zombie_escape-1.13.1.dist-info/RECORD +49 -0
  31. zombie_escape-1.12.0.dist-info/RECORD +0 -47
  32. {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/WHEEL +0 -0
  33. {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/entry_points.txt +0 -0
  34. {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -11,6 +11,14 @@ PLAYER_SPEED = 1.4
11
11
  FOV_RADIUS = 124 # approximate legacy FOV (80) * 1.55 cap
12
12
  BUDDY_RADIUS = HUMANOID_RADIUS
13
13
  BUDDY_FOLLOW_SPEED = PLAYER_SPEED * 0.7
14
+ HUMANOID_WALL_BUMP_FRAMES = 7
15
+
16
+ # --- Jump settings ---
17
+ PLAYER_JUMP_RANGE = 15 # px (enough to clear one 50px tile)
18
+ SURVIVOR_JUMP_RANGE = int(PLAYER_JUMP_RANGE * 0.7) # px
19
+ JUMP_DURATION_MS = 200
20
+ JUMP_SCALE_MAX = 0.15
21
+ JUMP_SHADOW_OFFSET = 5
14
22
 
15
23
  # --- Survivor settings (Stage 4) ---
16
24
  SURVIVOR_RADIUS = HUMANOID_RADIUS
@@ -38,19 +46,24 @@ ZOMBIE_SEPARATION_DISTANCE = ZOMBIE_RADIUS * 2.2
38
46
  ZOMBIE_AGING_DURATION_FRAMES = FPS * 60 * 6 # ~6 minutes at target framerate
39
47
  ZOMBIE_AGING_MIN_SPEED_RATIO = 0.3
40
48
  ZOMBIE_TRACKER_SCENT_RADIUS = 70
41
- ZOMBIE_TRACKER_SCAN_RADIUS_MULTIPLIER = 2
49
+ ZOMBIE_TRACKER_FAR_SCENT_RADIUS = 140
50
+ ZOMBIE_TRACKER_NEWER_FOOTPRINT_MS = 5000
42
51
  ZOMBIE_TRACKER_SCENT_TOP_K = 3
43
52
  ZOMBIE_TRACKER_SCAN_INTERVAL_MS = int(1000 * 30 / FPS)
44
53
  ZOMBIE_TRACKER_WANDER_INTERVAL_MS = 2500
45
- ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE = 24
46
- ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG = 45
47
- ZOMBIE_WALL_FOLLOW_PROBE_STEP = 2.0
48
- ZOMBIE_WALL_FOLLOW_TARGET_GAP = 4.0
49
- ZOMBIE_WALL_FOLLOW_LOST_WALL_MS = 2500
54
+ ZOMBIE_TRACKER_CROWD_BAND_WIDTH = 50
55
+ ZOMBIE_TRACKER_CROWD_BAND_LENGTH = 50
56
+ ZOMBIE_TRACKER_CROWD_COUNT = 5
57
+ ZOMBIE_TRACKER_RELOCK_DELAY_MS = 3000
58
+ ZOMBIE_WALL_HUG_SENSOR_DISTANCE = 24
59
+ ZOMBIE_WALL_HUG_PROBE_ANGLE_DEG = 45
60
+ ZOMBIE_WALL_HUG_PROBE_STEP = 2.0
61
+ ZOMBIE_WALL_HUG_TARGET_GAP = 4.0
62
+ ZOMBIE_WALL_HUG_LOST_WALL_MS = 2500
50
63
 
51
64
  # --- Car and fuel settings ---
52
- CAR_WIDTH = 15
53
- CAR_HEIGHT = 25
65
+ CAR_WIDTH = 16
66
+ CAR_HEIGHT = 24
54
67
  CAR_SPEED = 2
55
68
  CAR_HEALTH = 20
56
69
  CAR_WALL_DAMAGE = 1
@@ -62,6 +75,7 @@ INTERNAL_WALL_HEALTH = 40 * 100
62
75
  INTERNAL_WALL_BEVEL_DEPTH = 6
63
76
  STEEL_BEAM_HEALTH = int(INTERNAL_WALL_HEALTH * 1.5)
64
77
  PLAYER_WALL_DAMAGE = 100
78
+ BUDDY_WALL_DAMAGE = int(PLAYER_WALL_DAMAGE * 0.7)
65
79
  ZOMBIE_WALL_DAMAGE = 1
66
80
 
67
81
  __all__ = [
@@ -71,6 +85,7 @@ __all__ = [
71
85
  "FOV_RADIUS",
72
86
  "BUDDY_RADIUS",
73
87
  "BUDDY_FOLLOW_SPEED",
88
+ "HUMANOID_WALL_BUMP_FRAMES",
74
89
  "SURVIVOR_RADIUS",
75
90
  "SURVIVOR_APPROACH_RADIUS",
76
91
  "SURVIVOR_APPROACH_SPEED",
@@ -90,15 +105,20 @@ __all__ = [
90
105
  "ZOMBIE_AGING_DURATION_FRAMES",
91
106
  "ZOMBIE_AGING_MIN_SPEED_RATIO",
92
107
  "ZOMBIE_TRACKER_SCENT_RADIUS",
93
- "ZOMBIE_TRACKER_SCAN_RADIUS_MULTIPLIER",
108
+ "ZOMBIE_TRACKER_FAR_SCENT_RADIUS",
109
+ "ZOMBIE_TRACKER_NEWER_FOOTPRINT_MS",
94
110
  "ZOMBIE_TRACKER_SCENT_TOP_K",
95
111
  "ZOMBIE_TRACKER_SCAN_INTERVAL_MS",
96
112
  "ZOMBIE_TRACKER_WANDER_INTERVAL_MS",
97
- "ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE",
98
- "ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG",
99
- "ZOMBIE_WALL_FOLLOW_PROBE_STEP",
100
- "ZOMBIE_WALL_FOLLOW_TARGET_GAP",
101
- "ZOMBIE_WALL_FOLLOW_LOST_WALL_MS",
113
+ "ZOMBIE_TRACKER_CROWD_BAND_WIDTH",
114
+ "ZOMBIE_TRACKER_CROWD_BAND_LENGTH",
115
+ "ZOMBIE_TRACKER_CROWD_COUNT",
116
+ "ZOMBIE_TRACKER_RELOCK_DELAY_MS",
117
+ "ZOMBIE_WALL_HUG_SENSOR_DISTANCE",
118
+ "ZOMBIE_WALL_HUG_PROBE_ANGLE_DEG",
119
+ "ZOMBIE_WALL_HUG_PROBE_STEP",
120
+ "ZOMBIE_WALL_HUG_TARGET_GAP",
121
+ "ZOMBIE_WALL_HUG_LOST_WALL_MS",
102
122
  "CAR_WIDTH",
103
123
  "CAR_HEIGHT",
104
124
  "CAR_SPEED",
@@ -110,5 +130,6 @@ __all__ = [
110
130
  "INTERNAL_WALL_BEVEL_DEPTH",
111
131
  "STEEL_BEAM_HEALTH",
112
132
  "PLAYER_WALL_DAMAGE",
133
+ "BUDDY_WALL_DAMAGE",
113
134
  "ZOMBIE_WALL_DAMAGE",
114
135
  ]
@@ -0,0 +1,296 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from pathlib import Path
5
+
6
+ import pygame
7
+
8
+ from .entities import SteelBeam
9
+ from .entities_constants import (
10
+ BUDDY_RADIUS,
11
+ CAR_HEIGHT,
12
+ CAR_WIDTH,
13
+ FLASHLIGHT_HEIGHT,
14
+ FLASHLIGHT_WIDTH,
15
+ FUEL_CAN_HEIGHT,
16
+ FUEL_CAN_WIDTH,
17
+ INTERNAL_WALL_BEVEL_DEPTH,
18
+ PLAYER_RADIUS,
19
+ SHOES_HEIGHT,
20
+ SHOES_WIDTH,
21
+ STEEL_BEAM_HEALTH,
22
+ SURVIVOR_RADIUS,
23
+ ZOMBIE_RADIUS,
24
+ )
25
+ from .level_constants import DEFAULT_TILE_SIZE
26
+ from .render_assets import (
27
+ build_car_directional_surfaces,
28
+ build_car_surface,
29
+ build_flashlight_surface,
30
+ build_fuel_can_surface,
31
+ build_player_directional_surfaces,
32
+ build_rubble_wall_surface,
33
+ build_shoes_surface,
34
+ build_survivor_directional_surfaces,
35
+ build_zombie_directional_surfaces,
36
+ draw_humanoid_hand,
37
+ draw_humanoid_nose,
38
+ paint_car_surface,
39
+ paint_wall_surface,
40
+ resolve_car_color,
41
+ resolve_wall_colors,
42
+ RUBBLE_ROTATION_DEG,
43
+ )
44
+ from .colors import FALL_ZONE_FLOOR_PRIMARY
45
+ from .render_constants import (
46
+ FALLING_ZOMBIE_COLOR,
47
+ PITFALL_ABYSS_COLOR,
48
+ PITFALL_EDGE_DEPTH_OFFSET,
49
+ PITFALL_EDGE_METAL_COLOR,
50
+ PITFALL_EDGE_STRIPE_COLOR,
51
+ PITFALL_EDGE_STRIPE_SPACING,
52
+ PITFALL_SHADOW_RIM_COLOR,
53
+ PITFALL_SHADOW_WIDTH,
54
+ ZOMBIE_NOSE_COLOR,
55
+ )
56
+
57
+ __all__ = ["export_images"]
58
+
59
+
60
+ def _ensure_pygame_ready() -> None:
61
+ if not pygame.get_init():
62
+ pygame.init()
63
+ if not pygame.display.get_init():
64
+ pygame.display.init()
65
+ if pygame.display.get_surface() is None:
66
+ flags = pygame.HIDDEN if hasattr(pygame, "HIDDEN") else 0
67
+ pygame.display.set_mode((1, 1), flags=flags)
68
+
69
+
70
+ def _save_surface(surface: pygame.Surface, path: Path) -> None:
71
+ path.parent.mkdir(parents=True, exist_ok=True)
72
+ pygame.image.save(surface, str(path))
73
+
74
+
75
+ def _pick_directional_surface(
76
+ surfaces: list[pygame.Surface], *, bin_index: int = 0
77
+ ) -> pygame.Surface:
78
+ if not surfaces:
79
+ return pygame.Surface((1, 1), pygame.SRCALPHA)
80
+ return surfaces[bin_index % len(surfaces)]
81
+
82
+
83
+ def _build_pitfall_tile(cell_size: int) -> pygame.Surface:
84
+ surface = pygame.Surface((cell_size, cell_size), pygame.SRCALPHA)
85
+ rect = surface.get_rect()
86
+ pygame.draw.rect(surface, PITFALL_ABYSS_COLOR, rect)
87
+
88
+ for i in range(PITFALL_SHADOW_WIDTH):
89
+ 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
+ )
94
+ pygame.draw.line(
95
+ surface,
96
+ color,
97
+ (rect.x + i, rect.y),
98
+ (rect.x + i, rect.bottom - 1),
99
+ )
100
+ pygame.draw.line(
101
+ surface,
102
+ color,
103
+ (rect.right - 1 - i, rect.y),
104
+ (rect.right - 1 - i, rect.bottom - 1),
105
+ )
106
+
107
+ edge_height = max(1, INTERNAL_WALL_BEVEL_DEPTH - PITFALL_EDGE_DEPTH_OFFSET)
108
+ pygame.draw.rect(surface, PITFALL_EDGE_METAL_COLOR, (rect.x, rect.y, rect.w, edge_height))
109
+ for sx in range(rect.x - edge_height, rect.right, PITFALL_EDGE_STRIPE_SPACING):
110
+ pygame.draw.line(
111
+ surface,
112
+ PITFALL_EDGE_STRIPE_COLOR,
113
+ (max(rect.x, sx), rect.y),
114
+ (min(rect.right - 1, sx + edge_height), rect.y + edge_height - 1),
115
+ width=2,
116
+ )
117
+
118
+ return surface
119
+
120
+
121
+ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE) -> list[Path]:
122
+ _ensure_pygame_ready()
123
+
124
+ saved: list[Path] = []
125
+ out = Path(output_dir)
126
+
127
+ player = _pick_directional_surface(
128
+ build_player_directional_surfaces(radius=PLAYER_RADIUS),
129
+ bin_index=0,
130
+ )
131
+ player_path = out / "player.png"
132
+ _save_surface(player, player_path)
133
+ saved.append(player_path)
134
+
135
+ zombie_base = _pick_directional_surface(
136
+ build_zombie_directional_surfaces(radius=ZOMBIE_RADIUS, draw_hands=False),
137
+ bin_index=0,
138
+ )
139
+ zombie_normal_path = out / "zombie-normal.png"
140
+ _save_surface(zombie_base, zombie_normal_path)
141
+ saved.append(zombie_normal_path)
142
+
143
+ tracker = zombie_base.copy()
144
+ draw_humanoid_nose(
145
+ tracker,
146
+ radius=ZOMBIE_RADIUS,
147
+ angle_rad=0.0,
148
+ color=ZOMBIE_NOSE_COLOR,
149
+ )
150
+ tracker_path = out / "zombie-tracker.png"
151
+ _save_surface(tracker, tracker_path)
152
+ saved.append(tracker_path)
153
+
154
+ wall_hugging = zombie_base.copy()
155
+ draw_humanoid_hand(
156
+ wall_hugging,
157
+ radius=ZOMBIE_RADIUS,
158
+ angle_rad=math.pi / 2.0,
159
+ color=ZOMBIE_NOSE_COLOR,
160
+ )
161
+ wall_path = out / "zombie-wall.png"
162
+ _save_surface(wall_hugging, wall_path)
163
+ saved.append(wall_path)
164
+
165
+ buddy = _pick_directional_surface(
166
+ build_survivor_directional_surfaces(
167
+ radius=BUDDY_RADIUS,
168
+ is_buddy=True,
169
+ draw_hands=True,
170
+ ),
171
+ bin_index=0,
172
+ )
173
+ buddy_path = out / "buddy.png"
174
+ _save_surface(buddy, buddy_path)
175
+ saved.append(buddy_path)
176
+
177
+ survivor = _pick_directional_surface(
178
+ build_survivor_directional_surfaces(
179
+ radius=SURVIVOR_RADIUS,
180
+ is_buddy=False,
181
+ draw_hands=False,
182
+ ),
183
+ bin_index=0,
184
+ )
185
+ survivor_path = out / "survivor.png"
186
+ _save_surface(survivor, survivor_path)
187
+ saved.append(survivor_path)
188
+
189
+ car_surface = build_car_surface(CAR_WIDTH, CAR_HEIGHT)
190
+ car_color = resolve_car_color(health_ratio=1.0, appearance="default")
191
+ paint_car_surface(
192
+ car_surface,
193
+ width=CAR_WIDTH,
194
+ height=CAR_HEIGHT,
195
+ color=car_color,
196
+ )
197
+ car = _pick_directional_surface(build_car_directional_surfaces(car_surface), bin_index=0)
198
+ car_path = out / "car.png"
199
+ _save_surface(car, car_path)
200
+ saved.append(car_path)
201
+
202
+ fuel = build_fuel_can_surface(FUEL_CAN_WIDTH, FUEL_CAN_HEIGHT)
203
+ fuel_path = out / "fuel.png"
204
+ _save_surface(fuel, fuel_path)
205
+ saved.append(fuel_path)
206
+
207
+ flashlight = build_flashlight_surface(FLASHLIGHT_WIDTH, FLASHLIGHT_HEIGHT)
208
+ flashlight_path = out / "flashlight.png"
209
+ _save_surface(flashlight, flashlight_path)
210
+ saved.append(flashlight_path)
211
+
212
+ shoes = build_shoes_surface(SHOES_WIDTH, SHOES_HEIGHT)
213
+ shoes_path = out / "shoes.png"
214
+ _save_surface(shoes, shoes_path)
215
+ saved.append(shoes_path)
216
+
217
+ beam = SteelBeam(0, 0, cell_size, health=STEEL_BEAM_HEALTH, palette=None)
218
+ beam_path = out / "steel-beam.png"
219
+ _save_surface(beam.image, beam_path)
220
+ saved.append(beam_path)
221
+
222
+ inner_wall = pygame.Surface((cell_size, cell_size), pygame.SRCALPHA)
223
+ inner_fill, inner_border = resolve_wall_colors(
224
+ health_ratio=1.0,
225
+ palette_category="inner_wall",
226
+ palette=None,
227
+ )
228
+ paint_wall_surface(
229
+ inner_wall,
230
+ fill_color=inner_fill,
231
+ border_color=inner_border,
232
+ bevel_depth=INTERNAL_WALL_BEVEL_DEPTH,
233
+ bevel_mask=(False, False, False, False),
234
+ draw_bottom_side=False,
235
+ bottom_side_ratio=0.1,
236
+ side_shade_ratio=0.9,
237
+ )
238
+ inner_wall_path = out / "wall-inner.png"
239
+ _save_surface(inner_wall, inner_wall_path)
240
+ saved.append(inner_wall_path)
241
+
242
+ rubble_wall = build_rubble_wall_surface(
243
+ cell_size,
244
+ fill_color=inner_fill,
245
+ border_color=inner_border,
246
+ angle_deg=RUBBLE_ROTATION_DEG,
247
+ )
248
+ rubble_wall_path = out / "wall-rubble.png"
249
+ _save_surface(rubble_wall, rubble_wall_path)
250
+ saved.append(rubble_wall_path)
251
+
252
+ outer_wall = pygame.Surface((cell_size, cell_size), pygame.SRCALPHA)
253
+ outer_fill, outer_border = resolve_wall_colors(
254
+ health_ratio=1.0,
255
+ palette_category="outer_wall",
256
+ palette=None,
257
+ )
258
+ paint_wall_surface(
259
+ outer_wall,
260
+ fill_color=outer_fill,
261
+ border_color=outer_border,
262
+ bevel_depth=0,
263
+ bevel_mask=(False, False, False, False),
264
+ draw_bottom_side=False,
265
+ bottom_side_ratio=0.1,
266
+ side_shade_ratio=0.9,
267
+ )
268
+ outer_wall_path = out / "wall-outer.png"
269
+ _save_surface(outer_wall, outer_wall_path)
270
+ saved.append(outer_wall_path)
271
+
272
+ pitfall = _build_pitfall_tile(cell_size)
273
+ pitfall_path = out / "pitfall.png"
274
+ _save_surface(pitfall, pitfall_path)
275
+ saved.append(pitfall_path)
276
+
277
+ fall_radius = max(1, int(ZOMBIE_RADIUS))
278
+ fall_size = fall_radius * 2
279
+ falling = pygame.Surface((fall_size, fall_size), pygame.SRCALPHA)
280
+ pygame.draw.circle(
281
+ falling,
282
+ FALLING_ZOMBIE_COLOR,
283
+ (fall_radius, fall_radius),
284
+ fall_radius,
285
+ )
286
+ falling_path = out / "falling-zombie.png"
287
+ _save_surface(falling, falling_path)
288
+ saved.append(falling_path)
289
+
290
+ fall_zone = pygame.Surface((cell_size, cell_size), pygame.SRCALPHA)
291
+ fall_zone.fill(FALL_ZONE_FLOOR_PRIMARY)
292
+ fall_zone_path = out / "fall-zone.png"
293
+ _save_surface(fall_zone, fall_zone_path)
294
+ saved.append(fall_zone_path)
295
+
296
+ return saved
@@ -5,7 +5,7 @@
5
5
  from .ambient import sync_ambient_palette_with_flashlights
6
6
  from .footprints import get_shrunk_sprite, update_footprints
7
7
  from .interactions import check_interactions
8
- from .layout import generate_level_from_blueprint
8
+ from .layout import MapGenerationError, generate_level_from_blueprint
9
9
  from .movement import process_player_input, update_entities
10
10
  from .spawn import (
11
11
  maintain_waiting_car_supply,
@@ -43,6 +43,7 @@ from .utils import (
43
43
  )
44
44
 
45
45
  __all__ = [
46
+ "MapGenerationError",
46
47
  "generate_level_from_blueprint",
47
48
  "place_new_car",
48
49
  "place_fuel_can",
@@ -17,6 +17,9 @@ FOOTPRINT_MAX = 320
17
17
  MAX_ZOMBIES = 400
18
18
  ZOMBIE_SPAWN_PLAYER_BUFFER = 230
19
19
  ZOMBIE_TRACKER_AGING_DURATION_FRAMES = ZOMBIE_AGING_DURATION_FRAMES
20
+ FALLING_ZOMBIE_PRE_FX_MS = 600
21
+ FALLING_ZOMBIE_DURATION_MS = 450
22
+ FALLING_ZOMBIE_DUST_DURATION_MS = 220
20
23
 
21
24
  # --- Car and fuel settings ---
22
25
  CAR_ZOMBIE_DAMAGE = 1
@@ -34,6 +37,9 @@ __all__ = [
34
37
  "MAX_ZOMBIES",
35
38
  "ZOMBIE_SPAWN_PLAYER_BUFFER",
36
39
  "ZOMBIE_TRACKER_AGING_DURATION_FRAMES",
40
+ "FALLING_ZOMBIE_PRE_FX_MS",
41
+ "FALLING_ZOMBIE_DURATION_MS",
42
+ "FALLING_ZOMBIE_DUST_DURATION_MS",
37
43
  "CAR_ZOMBIE_DAMAGE",
38
44
  "FUEL_HINT_DURATION_MS",
39
45
  "OUTER_WALL_HEALTH",
@@ -26,6 +26,10 @@ def get_shrunk_sprite(
26
26
 
27
27
  new_sprite = pygame.sprite.Sprite()
28
28
  new_sprite.rect = rect
29
+ if hasattr(sprite_obj, "radius"):
30
+ base_radius = getattr(sprite_obj, "radius", None)
31
+ if base_radius is not None:
32
+ new_sprite.radius = base_radius * min(scale_x, scale_y)
29
33
 
30
34
  return new_sprite
31
35
 
@@ -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