zombie-escape 1.5.4__py3-none-any.whl → 1.7.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 (37) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/entities.py +501 -537
  3. zombie_escape/entities_constants.py +102 -0
  4. zombie_escape/gameplay/__init__.py +75 -2
  5. zombie_escape/gameplay/ambient.py +50 -0
  6. zombie_escape/gameplay/constants.py +46 -0
  7. zombie_escape/gameplay/footprints.py +60 -0
  8. zombie_escape/gameplay/interactions.py +354 -0
  9. zombie_escape/gameplay/layout.py +190 -0
  10. zombie_escape/gameplay/movement.py +220 -0
  11. zombie_escape/gameplay/spawn.py +618 -0
  12. zombie_escape/gameplay/state.py +137 -0
  13. zombie_escape/gameplay/survivors.py +306 -0
  14. zombie_escape/gameplay/utils.py +147 -0
  15. zombie_escape/gameplay_constants.py +0 -148
  16. zombie_escape/level_blueprints.py +123 -10
  17. zombie_escape/level_constants.py +6 -13
  18. zombie_escape/locales/ui.en.json +10 -1
  19. zombie_escape/locales/ui.ja.json +10 -1
  20. zombie_escape/models.py +15 -9
  21. zombie_escape/render.py +42 -27
  22. zombie_escape/render_assets.py +533 -23
  23. zombie_escape/render_constants.py +57 -22
  24. zombie_escape/rng.py +9 -9
  25. zombie_escape/screens/__init__.py +59 -29
  26. zombie_escape/screens/game_over.py +3 -3
  27. zombie_escape/screens/gameplay.py +45 -27
  28. zombie_escape/screens/title.py +5 -2
  29. zombie_escape/stage_constants.py +34 -1
  30. zombie_escape/zombie_escape.py +30 -12
  31. {zombie_escape-1.5.4.dist-info → zombie_escape-1.7.1.dist-info}/METADATA +1 -1
  32. zombie_escape-1.7.1.dist-info/RECORD +45 -0
  33. zombie_escape/gameplay/logic.py +0 -1917
  34. zombie_escape-1.5.4.dist-info/RECORD +0 -35
  35. {zombie_escape-1.5.4.dist-info → zombie_escape-1.7.1.dist-info}/WHEEL +0 -0
  36. {zombie_escape-1.5.4.dist-info → zombie_escape-1.7.1.dist-info}/entry_points.txt +0 -0
  37. {zombie_escape-1.5.4.dist-info → zombie_escape-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,102 @@
1
+ """Entity-related constants and helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .screen_constants import FPS
6
+
7
+ # --- Player and buddy settings ---
8
+ HUMANOID_RADIUS = 6
9
+ PLAYER_RADIUS = HUMANOID_RADIUS
10
+ PLAYER_SPEED = 1.4
11
+ FOV_RADIUS = 124 # approximate legacy FOV (80) * 1.55 cap
12
+ BUDDY_RADIUS = HUMANOID_RADIUS
13
+ BUDDY_FOLLOW_SPEED = PLAYER_SPEED * 0.7
14
+
15
+ # --- Survivor settings (Stage 4) ---
16
+ SURVIVOR_RADIUS = HUMANOID_RADIUS
17
+ SURVIVOR_APPROACH_RADIUS = 48
18
+ SURVIVOR_APPROACH_SPEED = PLAYER_SPEED * 0.35
19
+ SURVIVOR_MAX_SAFE_PASSENGERS = 5
20
+ SURVIVOR_MIN_SPEED_FACTOR = 0.35
21
+
22
+ # --- Flashlight settings ---
23
+ FLASHLIGHT_WIDTH = 10
24
+ FLASHLIGHT_HEIGHT = 8
25
+
26
+ # --- Zombie settings ---
27
+ ZOMBIE_RADIUS = HUMANOID_RADIUS
28
+ ZOMBIE_SPEED = PLAYER_SPEED * 0.45
29
+ ZOMBIE_WANDER_INTERVAL_MS = 5000
30
+ ZOMBIE_SIGHT_RANGE = FOV_RADIUS * 1.2
31
+ ZOMBIE_TRACKER_SIGHT_RANGE = ZOMBIE_SIGHT_RANGE * 0.2
32
+ FAST_ZOMBIE_BASE_SPEED = PLAYER_SPEED * 0.77
33
+ ZOMBIE_SEPARATION_DISTANCE = ZOMBIE_RADIUS * 2.2
34
+ ZOMBIE_AGING_DURATION_FRAMES = FPS * 60 * 6 # ~6 minutes at target framerate
35
+ ZOMBIE_AGING_MIN_SPEED_RATIO = 0.3
36
+ ZOMBIE_TRACKER_SCENT_RADIUS = 70
37
+ ZOMBIE_TRACKER_WANDER_INTERVAL_MS = 2500
38
+ ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE = 24
39
+ ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG = 45
40
+ ZOMBIE_WALL_FOLLOW_PROBE_STEP = 2.0
41
+ ZOMBIE_WALL_FOLLOW_TARGET_GAP = 4.0
42
+ ZOMBIE_WALL_FOLLOW_LOST_WALL_MS = 2500
43
+
44
+ # --- Car and fuel settings ---
45
+ CAR_WIDTH = 15
46
+ CAR_HEIGHT = 25
47
+ CAR_SPEED = 2
48
+ CAR_HEALTH = 20
49
+ CAR_WALL_DAMAGE = 1
50
+ FUEL_CAN_WIDTH = 11
51
+ FUEL_CAN_HEIGHT = 15
52
+
53
+ # --- Wall and beam settings ---
54
+ INTERNAL_WALL_HEALTH = 40 * 100
55
+ INTERNAL_WALL_BEVEL_DEPTH = 6
56
+ STEEL_BEAM_HEALTH = int(INTERNAL_WALL_HEALTH * 1.5)
57
+ PLAYER_WALL_DAMAGE = 100
58
+ ZOMBIE_WALL_DAMAGE = 1
59
+
60
+ __all__ = [
61
+ "HUMANOID_RADIUS",
62
+ "PLAYER_RADIUS",
63
+ "PLAYER_SPEED",
64
+ "FOV_RADIUS",
65
+ "BUDDY_RADIUS",
66
+ "BUDDY_FOLLOW_SPEED",
67
+ "SURVIVOR_RADIUS",
68
+ "SURVIVOR_APPROACH_RADIUS",
69
+ "SURVIVOR_APPROACH_SPEED",
70
+ "SURVIVOR_MAX_SAFE_PASSENGERS",
71
+ "SURVIVOR_MIN_SPEED_FACTOR",
72
+ "FLASHLIGHT_WIDTH",
73
+ "FLASHLIGHT_HEIGHT",
74
+ "ZOMBIE_RADIUS",
75
+ "ZOMBIE_SPEED",
76
+ "ZOMBIE_WANDER_INTERVAL_MS",
77
+ "ZOMBIE_SIGHT_RANGE",
78
+ "ZOMBIE_TRACKER_SIGHT_RANGE",
79
+ "FAST_ZOMBIE_BASE_SPEED",
80
+ "ZOMBIE_SEPARATION_DISTANCE",
81
+ "ZOMBIE_AGING_DURATION_FRAMES",
82
+ "ZOMBIE_AGING_MIN_SPEED_RATIO",
83
+ "ZOMBIE_TRACKER_SCENT_RADIUS",
84
+ "ZOMBIE_TRACKER_WANDER_INTERVAL_MS",
85
+ "ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE",
86
+ "ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG",
87
+ "ZOMBIE_WALL_FOLLOW_PROBE_STEP",
88
+ "ZOMBIE_WALL_FOLLOW_TARGET_GAP",
89
+ "ZOMBIE_WALL_FOLLOW_LOST_WALL_MS",
90
+ "CAR_WIDTH",
91
+ "CAR_HEIGHT",
92
+ "CAR_SPEED",
93
+ "CAR_HEALTH",
94
+ "CAR_WALL_DAMAGE",
95
+ "FUEL_CAN_WIDTH",
96
+ "FUEL_CAN_HEIGHT",
97
+ "INTERNAL_WALL_HEALTH",
98
+ "INTERNAL_WALL_BEVEL_DEPTH",
99
+ "STEEL_BEAM_HEALTH",
100
+ "PLAYER_WALL_DAMAGE",
101
+ "ZOMBIE_WALL_DAMAGE",
102
+ ]
@@ -1,7 +1,80 @@
1
1
  """Gameplay helpers and logic utilities."""
2
2
 
3
- from . import logic
3
+ # ruff: noqa: F401
4
+
5
+ from .ambient import sync_ambient_palette_with_flashlights
6
+ from .footprints import get_shrunk_sprite, update_footprints
7
+ from .interactions import check_interactions
8
+ from .layout import generate_level_from_blueprint
9
+ from .movement import process_player_input, update_entities
10
+ from .spawn import (
11
+ maintain_waiting_car_supply,
12
+ nearest_waiting_car,
13
+ place_buddies,
14
+ place_flashlights,
15
+ place_fuel_can,
16
+ place_new_car,
17
+ setup_player_and_cars,
18
+ spawn_exterior_zombie,
19
+ spawn_initial_zombies,
20
+ spawn_survivors,
21
+ spawn_waiting_car,
22
+ spawn_weighted_zombie,
23
+ )
24
+ from .state import carbonize_outdoor_zombies, initialize_game_state, update_survival_timer
25
+ from .survivors import (
26
+ add_survivor_message,
27
+ apply_passenger_speed_penalty,
28
+ calculate_car_speed_for_passengers,
29
+ cleanup_survivor_messages,
30
+ drop_survivors_from_car,
31
+ handle_survivor_zombie_collisions,
32
+ increase_survivor_capacity,
33
+ random_survivor_conversion_line,
34
+ respawn_buddies_near_player,
35
+ update_survivors,
36
+ )
37
+ from .utils import (
38
+ find_exterior_spawn_position,
39
+ find_interior_spawn_positions,
40
+ find_nearby_offscreen_spawn_position,
41
+ rect_visible_on_screen,
42
+ )
4
43
 
5
44
  __all__ = [
6
- "logic",
45
+ "generate_level_from_blueprint",
46
+ "place_new_car",
47
+ "place_fuel_can",
48
+ "place_flashlights",
49
+ "place_buddies",
50
+ "find_interior_spawn_positions",
51
+ "find_nearby_offscreen_spawn_position",
52
+ "find_exterior_spawn_position",
53
+ "spawn_survivors",
54
+ "spawn_exterior_zombie",
55
+ "spawn_weighted_zombie",
56
+ "update_survivors",
57
+ "nearest_waiting_car",
58
+ "calculate_car_speed_for_passengers",
59
+ "apply_passenger_speed_penalty",
60
+ "increase_survivor_capacity",
61
+ "spawn_waiting_car",
62
+ "maintain_waiting_car_supply",
63
+ "add_survivor_message",
64
+ "random_survivor_conversion_line",
65
+ "cleanup_survivor_messages",
66
+ "drop_survivors_from_car",
67
+ "handle_survivor_zombie_collisions",
68
+ "respawn_buddies_near_player",
69
+ "get_shrunk_sprite",
70
+ "update_footprints",
71
+ "initialize_game_state",
72
+ "setup_player_and_cars",
73
+ "spawn_initial_zombies",
74
+ "update_survival_timer",
75
+ "carbonize_outdoor_zombies",
76
+ "process_player_input",
77
+ "update_entities",
78
+ "check_interactions",
79
+ "sync_ambient_palette_with_flashlights",
7
80
  ]
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from ..colors import (
4
+ DAWN_AMBIENT_PALETTE_KEY,
5
+ ambient_palette_key_for_flashlights,
6
+ get_environment_palette,
7
+ )
8
+ from ..models import GameData
9
+
10
+
11
+ def _set_ambient_palette(
12
+ game_data: GameData, key: str, *, force: bool = False
13
+ ) -> None:
14
+ """Apply a named ambient palette to all walls in the level."""
15
+
16
+ palette = get_environment_palette(key)
17
+ state = game_data.state
18
+ if not force and state.ambient_palette_key == key:
19
+ return
20
+
21
+ state.ambient_palette_key = key
22
+ _apply_palette_to_walls(game_data, palette, force=True)
23
+
24
+
25
+ def sync_ambient_palette_with_flashlights(
26
+ game_data: GameData, *, force: bool = False
27
+ ) -> None:
28
+ """Sync the ambient palette with the player's flashlight inventory."""
29
+
30
+ state = game_data.state
31
+ if state.dawn_ready:
32
+ _set_ambient_palette(game_data, DAWN_AMBIENT_PALETTE_KEY, force=force)
33
+ return
34
+ key = ambient_palette_key_for_flashlights(state.flashlight_count)
35
+ _set_ambient_palette(game_data, key, force=force)
36
+
37
+
38
+ def _apply_palette_to_walls(
39
+ game_data: GameData,
40
+ palette,
41
+ *,
42
+ force: bool = False,
43
+ ) -> None:
44
+ if not hasattr(game_data, "groups") or not hasattr(game_data.groups, "wall_group"):
45
+ return
46
+ wall_group = game_data.groups.wall_group
47
+ for wall in wall_group:
48
+ if not hasattr(wall, "set_palette"):
49
+ continue
50
+ wall.set_palette(palette, force=force)
@@ -0,0 +1,46 @@
1
+ """Gameplay-only constants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..entities_constants import ZOMBIE_AGING_DURATION_FRAMES
6
+
7
+ # --- Survivor settings ---
8
+ SURVIVOR_SPEED_PENALTY_PER_PASSENGER = 0.08
9
+ SURVIVOR_OVERLOAD_DAMAGE_RATIO = 0.2
10
+ SURVIVOR_MESSAGE_DURATION_MS = 2000
11
+ SURVIVOR_CONVERSION_LINE_KEYS = [
12
+ "stages.stage4.conversion_lines.line1",
13
+ "stages.stage4.conversion_lines.line2",
14
+ "stages.stage4.conversion_lines.line3",
15
+ ]
16
+
17
+ # --- Footprint settings (gameplay) ---
18
+ FOOTPRINT_STEP_DISTANCE = 40
19
+ FOOTPRINT_MAX = 320
20
+
21
+ # --- Zombie settings ---
22
+ MAX_ZOMBIES = 400
23
+ ZOMBIE_SPAWN_PLAYER_BUFFER = 140
24
+ ZOMBIE_TRACKER_AGING_DURATION_FRAMES = ZOMBIE_AGING_DURATION_FRAMES
25
+
26
+ # --- Car and fuel settings ---
27
+ CAR_ZOMBIE_DAMAGE = 1
28
+ FUEL_HINT_DURATION_MS = 1600
29
+
30
+ # --- Wall settings ---
31
+ OUTER_WALL_HEALTH = 999999
32
+
33
+ __all__ = [
34
+ "SURVIVOR_SPEED_PENALTY_PER_PASSENGER",
35
+ "SURVIVOR_OVERLOAD_DAMAGE_RATIO",
36
+ "SURVIVOR_MESSAGE_DURATION_MS",
37
+ "SURVIVOR_CONVERSION_LINE_KEYS",
38
+ "FOOTPRINT_STEP_DISTANCE",
39
+ "FOOTPRINT_MAX",
40
+ "MAX_ZOMBIES",
41
+ "ZOMBIE_SPAWN_PLAYER_BUFFER",
42
+ "ZOMBIE_TRACKER_AGING_DURATION_FRAMES",
43
+ "CAR_ZOMBIE_DAMAGE",
44
+ "FUEL_HINT_DURATION_MS",
45
+ "OUTER_WALL_HEALTH",
46
+ ]
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pygame
6
+
7
+ from .constants import FOOTPRINT_MAX, FOOTPRINT_STEP_DISTANCE
8
+ from ..models import GameData
9
+
10
+
11
+ def get_shrunk_sprite(
12
+ sprite_obj: pygame.sprite.Sprite, scale_x: float, *, scale_y: float | None = None
13
+ ) -> pygame.sprite.Sprite:
14
+ if scale_y is None:
15
+ scale_y = scale_x
16
+
17
+ original_rect = sprite_obj.rect
18
+ shrunk_width = int(original_rect.width * scale_x)
19
+ shrunk_height = int(original_rect.height * scale_y)
20
+
21
+ shrunk_width = max(1, shrunk_width)
22
+ shrunk_height = max(1, shrunk_height)
23
+
24
+ rect = pygame.Rect(0, 0, shrunk_width, shrunk_height)
25
+ rect.center = original_rect.center
26
+
27
+ new_sprite = pygame.sprite.Sprite()
28
+ new_sprite.rect = rect
29
+
30
+ return new_sprite
31
+
32
+
33
+ def update_footprints(game_data: GameData, config: dict[str, Any]) -> None:
34
+ """Record player steps and clean up old footprints."""
35
+ _ = config # Footprints are always tracked; config only affects rendering.
36
+ state = game_data.state
37
+ player = game_data.player
38
+ assert player is not None
39
+
40
+ now = pygame.time.get_ticks()
41
+
42
+ footprints = state.footprints
43
+ if not player.in_car:
44
+ last_pos = state.last_footprint_pos
45
+ dist_sq = (
46
+ (player.x - last_pos[0]) ** 2 + (player.y - last_pos[1]) ** 2
47
+ if last_pos
48
+ else None
49
+ )
50
+ if last_pos is None or (
51
+ dist_sq is not None
52
+ and dist_sq >= FOOTPRINT_STEP_DISTANCE * FOOTPRINT_STEP_DISTANCE
53
+ ):
54
+ footprints.append({"pos": (player.x, player.y), "time": now})
55
+ state.last_footprint_pos = (player.x, player.y)
56
+
57
+ if len(footprints) > FOOTPRINT_MAX:
58
+ footprints = footprints[-FOOTPRINT_MAX:]
59
+
60
+ state.footprints = footprints
@@ -0,0 +1,354 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pygame
6
+
7
+ from ..entities_constants import (
8
+ CAR_HEIGHT,
9
+ CAR_WIDTH,
10
+ FLASHLIGHT_HEIGHT,
11
+ FLASHLIGHT_WIDTH,
12
+ FUEL_CAN_HEIGHT,
13
+ FUEL_CAN_WIDTH,
14
+ HUMANOID_RADIUS,
15
+ SURVIVOR_APPROACH_RADIUS,
16
+ SURVIVOR_MAX_SAFE_PASSENGERS,
17
+ )
18
+ from .constants import (
19
+ CAR_ZOMBIE_DAMAGE,
20
+ FUEL_HINT_DURATION_MS,
21
+ SURVIVOR_OVERLOAD_DAMAGE_RATIO,
22
+ )
23
+ from ..localization import translate as tr
24
+ from ..models import GameData
25
+ from ..rng import get_rng
26
+ from ..entities import Car
27
+ from .footprints import get_shrunk_sprite
28
+ from .spawn import maintain_waiting_car_supply
29
+ from .survivors import (
30
+ add_survivor_message,
31
+ apply_passenger_speed_penalty,
32
+ drop_survivors_from_car,
33
+ handle_survivor_zombie_collisions,
34
+ increase_survivor_capacity,
35
+ respawn_buddies_near_player,
36
+ )
37
+ from .utils import rect_visible_on_screen
38
+ from .ambient import sync_ambient_palette_with_flashlights
39
+
40
+
41
+ def _interaction_radius(width: float, height: float) -> float:
42
+ """Approximate interaction reach for a humanoid and an object."""
43
+ return HUMANOID_RADIUS + (width + height) / 4
44
+
45
+ RNG = get_rng()
46
+
47
+
48
+ def check_interactions(
49
+ game_data: GameData, config: dict[str, Any]
50
+ ) -> pygame.sprite.Sprite | None:
51
+ """Check and handle interactions between entities."""
52
+ player = game_data.player
53
+ assert player is not None
54
+ car = game_data.car
55
+ zombie_group = game_data.groups.zombie_group
56
+ all_sprites = game_data.groups.all_sprites
57
+ survivor_group = game_data.groups.survivor_group
58
+ state = game_data.state
59
+ walkable_cells = game_data.layout.walkable_cells
60
+ outside_rects = game_data.layout.outside_rects
61
+ fuel = game_data.fuel
62
+ flashlights = game_data.flashlights or []
63
+ camera = game_data.camera
64
+ stage = game_data.stage
65
+ maintain_waiting_car_supply(game_data)
66
+ active_car = car if car and car.alive() else None
67
+ waiting_cars = game_data.waiting_cars
68
+ shrunk_car = get_shrunk_sprite(active_car, 0.8) if active_car else None
69
+
70
+ car_interaction_radius = _interaction_radius(CAR_WIDTH, CAR_HEIGHT)
71
+ fuel_interaction_radius = _interaction_radius(FUEL_CAN_WIDTH, FUEL_CAN_HEIGHT)
72
+ flashlight_interaction_radius = _interaction_radius(
73
+ FLASHLIGHT_WIDTH, FLASHLIGHT_HEIGHT
74
+ )
75
+
76
+ def _player_near_point(point: tuple[float, float], radius: float) -> bool:
77
+ dx = point[0] - player.x
78
+ dy = point[1] - player.y
79
+ return dx * dx + dy * dy <= radius * radius
80
+
81
+ def _player_near_sprite(
82
+ sprite_obj: pygame.sprite.Sprite | None, radius: float
83
+ ) -> bool:
84
+ return bool(
85
+ sprite_obj
86
+ and sprite_obj.alive()
87
+ and _player_near_point(sprite_obj.rect.center, radius)
88
+ )
89
+
90
+ def _player_near_car(car_obj: Car | None) -> bool:
91
+ return _player_near_sprite(car_obj, car_interaction_radius)
92
+
93
+ # Fuel pickup
94
+ if fuel and fuel.alive() and not state.has_fuel and not player.in_car:
95
+ if _player_near_point(fuel.rect.center, fuel_interaction_radius):
96
+ state.has_fuel = True
97
+ state.fuel_message_until = 0
98
+ state.hint_expires_at = 0
99
+ state.hint_target_type = None
100
+ fuel.kill()
101
+ game_data.fuel = None
102
+ print("Fuel acquired!")
103
+
104
+ # Flashlight pickup
105
+ if not player.in_car:
106
+ for flashlight in list(flashlights):
107
+ if not flashlight.alive():
108
+ continue
109
+ if _player_near_point(
110
+ flashlight.rect.center, flashlight_interaction_radius
111
+ ):
112
+ state.flashlight_count += 1
113
+ state.hint_expires_at = 0
114
+ state.hint_target_type = None
115
+ flashlight.kill()
116
+ try:
117
+ flashlights.remove(flashlight)
118
+ except ValueError:
119
+ pass
120
+ print("Flashlight acquired!")
121
+ break
122
+
123
+ sync_ambient_palette_with_flashlights(game_data)
124
+
125
+ buddies = [
126
+ survivor
127
+ for survivor in survivor_group
128
+ if survivor.alive() and survivor.is_buddy and not survivor.rescued
129
+ ]
130
+
131
+ # Buddy interactions (Stage 3)
132
+ if stage.buddy_required_count > 0 and buddies:
133
+ for buddy in list(buddies):
134
+ if not buddy.alive():
135
+ continue
136
+ buddy_on_screen = rect_visible_on_screen(camera, buddy.rect)
137
+ if not player.in_car:
138
+ dist_to_player_sq = (player.x - buddy.x) ** 2 + (
139
+ player.y - buddy.y
140
+ ) ** 2
141
+ if (
142
+ dist_to_player_sq
143
+ <= SURVIVOR_APPROACH_RADIUS * SURVIVOR_APPROACH_RADIUS
144
+ ):
145
+ buddy.set_following()
146
+ elif player.in_car and active_car and shrunk_car:
147
+ g = pygame.sprite.Group()
148
+ g.add(buddy)
149
+ if pygame.sprite.spritecollide(
150
+ shrunk_car, g, False, pygame.sprite.collide_circle
151
+ ):
152
+ prospective_passengers = state.survivors_onboard + 1
153
+ capacity_limit = state.survivor_capacity
154
+ if prospective_passengers > capacity_limit:
155
+ overload_damage = max(
156
+ 1,
157
+ int(active_car.max_health * SURVIVOR_OVERLOAD_DAMAGE_RATIO),
158
+ )
159
+ add_survivor_message(game_data, tr("survivors.too_many_aboard"))
160
+ active_car._take_damage(overload_damage)
161
+ state.buddy_onboard += 1
162
+ buddy.kill()
163
+ continue
164
+
165
+ if buddy.alive() and pygame.sprite.spritecollide(
166
+ buddy, zombie_group, False, pygame.sprite.collide_circle
167
+ ):
168
+ if buddy_on_screen:
169
+ state.game_over_message = tr("game_over.scream")
170
+ state.game_over = True
171
+ state.game_over_at = state.game_over_at or pygame.time.get_ticks()
172
+ else:
173
+ if walkable_cells:
174
+ new_cell = RNG.choice(walkable_cells)
175
+ buddy.teleport(new_cell.center)
176
+ else:
177
+ buddy.teleport(
178
+ (game_data.level_width // 2, game_data.level_height // 2)
179
+ )
180
+ buddy.following = False
181
+
182
+ # Player entering an active car already under control
183
+ if (
184
+ not player.in_car
185
+ and _player_near_car(active_car)
186
+ and active_car
187
+ and active_car.health > 0
188
+ ):
189
+ if state.has_fuel:
190
+ player.in_car = True
191
+ all_sprites.remove(player)
192
+ state.hint_expires_at = 0
193
+ state.hint_target_type = None
194
+ print("Player entered car!")
195
+ else:
196
+ if not stage.survival_stage:
197
+ now_ms = state.elapsed_play_ms
198
+ state.fuel_message_until = now_ms + FUEL_HINT_DURATION_MS
199
+ state.hint_target_type = "fuel"
200
+
201
+ # Claim a waiting/parked car when the player finally reaches it
202
+ if not player.in_car and not active_car and waiting_cars:
203
+ claimed_car: Car | None = None
204
+ for parked_car in waiting_cars:
205
+ if _player_near_car(parked_car):
206
+ claimed_car = parked_car
207
+ break
208
+ if claimed_car:
209
+ if state.has_fuel:
210
+ try:
211
+ game_data.waiting_cars.remove(claimed_car)
212
+ except ValueError:
213
+ pass
214
+ game_data.car = claimed_car
215
+ active_car = claimed_car
216
+ player.in_car = True
217
+ all_sprites.remove(player)
218
+ state.hint_expires_at = 0
219
+ state.hint_target_type = None
220
+ apply_passenger_speed_penalty(game_data)
221
+ maintain_waiting_car_supply(game_data)
222
+ print("Player claimed a waiting car!")
223
+ else:
224
+ if not stage.survival_stage:
225
+ now_ms = state.elapsed_play_ms
226
+ state.fuel_message_until = now_ms + FUEL_HINT_DURATION_MS
227
+ state.hint_target_type = "fuel"
228
+
229
+ # Bonus: collide a parked car while driving to repair/extend capabilities
230
+ if player.in_car and active_car and shrunk_car and waiting_cars:
231
+ waiting_group = pygame.sprite.Group(waiting_cars)
232
+ collided_waiters = pygame.sprite.spritecollide(
233
+ shrunk_car, waiting_group, False, pygame.sprite.collide_rect
234
+ )
235
+ if collided_waiters:
236
+ removed_any = False
237
+ capacity_increments = 0
238
+ for parked in collided_waiters:
239
+ if not parked.alive():
240
+ continue
241
+ parked.kill()
242
+ try:
243
+ game_data.waiting_cars.remove(parked)
244
+ except ValueError:
245
+ pass
246
+ active_car.health = active_car.max_health
247
+ active_car._update_color()
248
+ removed_any = True
249
+ if stage.rescue_stage:
250
+ capacity_increments += 1
251
+ if removed_any:
252
+ if capacity_increments:
253
+ increase_survivor_capacity(game_data, capacity_increments)
254
+ maintain_waiting_car_supply(game_data)
255
+
256
+ # Car hitting zombies
257
+ if player.in_car and active_car and active_car.health > 0 and shrunk_car:
258
+ zombies_hit = pygame.sprite.spritecollide(shrunk_car, zombie_group, True)
259
+ if zombies_hit:
260
+ active_car._take_damage(CAR_ZOMBIE_DAMAGE * len(zombies_hit))
261
+
262
+ if (
263
+ stage.rescue_stage
264
+ and player.in_car
265
+ and active_car
266
+ and shrunk_car
267
+ and survivor_group
268
+ ):
269
+ boarded = pygame.sprite.spritecollide(
270
+ shrunk_car, survivor_group, True, pygame.sprite.collide_circle
271
+ )
272
+ if boarded:
273
+ state.survivors_onboard += len(boarded)
274
+ apply_passenger_speed_penalty(game_data)
275
+ capacity_limit = state.survivor_capacity
276
+ if state.survivors_onboard > capacity_limit:
277
+ overload_damage = max(
278
+ 1,
279
+ int(active_car.max_health * SURVIVOR_OVERLOAD_DAMAGE_RATIO),
280
+ )
281
+ add_survivor_message(game_data, tr("survivors.too_many_aboard"))
282
+ active_car._take_damage(overload_damage)
283
+
284
+ if stage.rescue_stage:
285
+ handle_survivor_zombie_collisions(game_data, config)
286
+
287
+ # Handle car destruction
288
+ if car and car.alive() and car.health <= 0:
289
+ car_destroyed_pos = car.rect.center
290
+ car.kill()
291
+ if stage.rescue_stage:
292
+ drop_survivors_from_car(game_data, car_destroyed_pos)
293
+ if player.in_car:
294
+ player.in_car = False
295
+ player.x, player.y = car_destroyed_pos[0], car_destroyed_pos[1]
296
+ player.rect.center = (int(player.x), int(player.y))
297
+ if player not in all_sprites:
298
+ all_sprites.add(player, layer=2)
299
+ print("Car destroyed! Player ejected.")
300
+
301
+ # Clear active car and let the player hunt for another waiting car.
302
+ game_data.car = None
303
+ state.survivor_capacity = SURVIVOR_MAX_SAFE_PASSENGERS
304
+ apply_passenger_speed_penalty(game_data)
305
+
306
+ # Bring back the buddies near the player after losing the car
307
+ respawn_buddies_near_player(game_data)
308
+ maintain_waiting_car_supply(game_data)
309
+
310
+ # Player getting caught by zombies
311
+ if not player.in_car and player in all_sprites:
312
+ shrunk_player = get_shrunk_sprite(player, 0.8)
313
+ collisions = pygame.sprite.spritecollide(
314
+ shrunk_player, zombie_group, False, pygame.sprite.collide_circle
315
+ )
316
+ if any(not zombie.carbonized for zombie in collisions):
317
+ if not state.game_over:
318
+ state.game_over = True
319
+ state.game_over_at = pygame.time.get_ticks()
320
+ state.game_over_message = tr("game_over.scream")
321
+
322
+ # Player escaping on foot after dawn (Stage 5)
323
+ if (
324
+ stage.survival_stage
325
+ and state.dawn_ready
326
+ and not player.in_car
327
+ and outside_rects
328
+ and any(outside.collidepoint(player.rect.center) for outside in outside_rects)
329
+ ):
330
+ state.game_won = True
331
+
332
+ # Player escaping the level
333
+ if player.in_car and car and car.alive() and state.has_fuel:
334
+ buddy_ready = True
335
+ if stage.buddy_required_count > 0:
336
+ buddy_ready = state.buddy_onboard >= stage.buddy_required_count
337
+ if buddy_ready and any(
338
+ outside.collidepoint(car.rect.center) for outside in outside_rects
339
+ ):
340
+ if stage.buddy_required_count > 0:
341
+ state.buddy_rescued = min(
342
+ stage.buddy_required_count, state.buddy_onboard
343
+ )
344
+ if stage.rescue_stage and state.survivors_onboard:
345
+ state.survivors_rescued += state.survivors_onboard
346
+ state.survivors_onboard = 0
347
+ state.next_overload_check_ms = 0
348
+ apply_passenger_speed_penalty(game_data)
349
+ state.game_won = True
350
+
351
+ # Return fog of view target
352
+ if not state.game_over and not state.game_won:
353
+ return car if player.in_car and car and car.alive() else player
354
+ return None