zombie-escape 1.1.4__tar.gz → 1.2.1__tar.gz
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-1.1.4 → zombie_escape-1.2.1}/PKG-INFO +11 -11
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/README.md +10 -10
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/__about__.py +1 -1
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/constants.py +4 -2
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/gameplay/logic.py +305 -111
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/models.py +4 -1
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/render.py +19 -1
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/screens/gameplay.py +29 -8
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/zombie_escape.py +9 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/.gitignore +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/LICENSE.txt +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/pyproject.toml +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/__init__.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/colors.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/config.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/entities.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/font_utils.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/gameplay/__init__.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/level_blueprints.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/locales/ui.en.json +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/locales/ui.ja.json +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/localization.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/render_assets.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/screens/__init__.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/screens/game_over.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/screens/settings.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/screens/title.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zombie-escape
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Top-down zombie survival game built with pygame.
|
|
5
5
|
Project-URL: Homepage, https://github.com/tos-kamiya/zombie-escape
|
|
6
6
|
Author-email: Toshihiro Kamiya <kamiya@mbj.nifty.com>
|
|
@@ -65,29 +65,29 @@ Open **Settings** from the title to toggle gameplay assists:
|
|
|
65
65
|
|
|
66
66
|
At the title screen you can pick a stage:
|
|
67
67
|
|
|
68
|
-
- **Stage 1: Find the Car** — locate the car and drive out.
|
|
69
|
-
- **Stage 2: Fuel Run** — find a fuel can first, pick it up, then find the car and escape.
|
|
70
|
-
- **Stage 3: Rescue Buddy** —
|
|
71
|
-
- **Stage 4: Evacuate Survivors** — find the car, gather nearby civilians, and escape before zombies reach them.
|
|
68
|
+
- **Stage 1: Find the Car** — locate the car and drive out (you already start with fuel).
|
|
69
|
+
- **Stage 2: Fuel Run** — you start with no fuel; find a fuel can first, pick it up, then find the car and escape.
|
|
70
|
+
- **Stage 3: Rescue Buddy** — same fuel hunt as Stage 2 (you begin empty) plus grab your buddy, pick them up with the car, then escape together.
|
|
71
|
+
- **Stage 4: Evacuate Survivors** — start fueled, find the car, gather nearby civilians, and escape before zombies reach them. Stage 4 sprinkles extra parked cars across the map; slamming into one while already driving fully repairs your current ride and adds five more safe seats.
|
|
72
72
|
|
|
73
73
|
An objective reminder is shown at the top-left during play.
|
|
74
74
|
|
|
75
75
|
### Characters/Items
|
|
76
76
|
|
|
77
|
-
- **Player:** A blue circle. Controlled with the WASD or arrow keys.
|
|
77
|
+
- **Player:** A blue circle. Controlled with the WASD or arrow keys. When carrying fuel a tiny yellow square appears near the sprite so you can immediately see whether you're ready to drive.
|
|
78
78
|
- **Zombie:** A red circle. Will chase the player (or car) once detected.
|
|
79
79
|
- When out of sight, the zombie's movement mode will randomly switch every certain time (moving horizontally/vertically only, side-to-side movement, random movement, etc.).
|
|
80
80
|
- **Car:** A yellow rectangle. The player can enter by making contact with it.
|
|
81
81
|
- The car has durability. Durability decreases when colliding with internal walls or hitting zombies.
|
|
82
|
-
- If durability reaches 0, the car is destroyed
|
|
83
|
-
- When
|
|
84
|
-
- After roughly 5 minutes of play, a small triangle near the player points toward the objective: fuel first (Stage 2 before pickup), car after fuel is collected (Stage 2), or car directly (Stage 1).
|
|
82
|
+
- If durability reaches 0, the car is destroyed and you are dumped on foot; you must track down another parked car hidden in the level.
|
|
83
|
+
- When you're already driving, ramming a parked car instantly restores your current car's health. On Stage 4 this also increases the safe passenger limit by five.
|
|
84
|
+
- After roughly 5 minutes of play, a small triangle near the player points toward the objective: fuel first (Stage 2 before pickup), car after fuel is collected (Stage 2/3), or car directly (Stage 1/4).
|
|
85
85
|
- **Walls:** Outer walls are gray; inner walls are beige.
|
|
86
86
|
- **Outer Walls:** Walls surrounding the stage that are nearly indestructible. Each side has at least three openings (exits).
|
|
87
87
|
- **Inner Walls:** Beige walls randomly placed inside the building. Inner wall segments each have durability. **The player can break these walls** by repeatedly colliding with a segment to reduce its durability; when it reaches 0, the segment is destroyed and disappears. The car cannot break walls.
|
|
88
88
|
- **Flashlight:** Picking one up boosts your visible radius by 35%.
|
|
89
89
|
- **Steel Beam (optional):** A square post with crossed diagonals; same collision as inner walls but with triple durability. Spawns independently of inner walls (may overlap them). If an inner wall covers a beam, the beam appears once the wall is destroyed.
|
|
90
|
-
- **Fuel Can (
|
|
90
|
+
- **Fuel Can (Stages 2 & 3):** A yellow jerrycan that only spawns on the fuel-run stages. Pick it up before driving the car; once collected the on-player indicator appears until you refuel the car.
|
|
91
91
|
- **Buddy (Stage 3):** A green circle survivor who spawns somewhere in the building and waits.
|
|
92
92
|
- Zombies only choose to pursue the buddy if they are on-screen; otherwise they ignore them.
|
|
93
93
|
- If a zombie tags the buddy off-screen, the buddy quietly respawns somewhere else instead of ending the run.
|
|
@@ -95,7 +95,7 @@ An objective reminder is shown at the top-left during play.
|
|
|
95
95
|
- **Survivors (Stage 4):** Light blue civilians scattered indoors.
|
|
96
96
|
- They stand still until you get close, then shuffle toward you at about one-third of player speed.
|
|
97
97
|
- Zombies can convert them if both are on-screen; the survivor shouts a line and turns instantly.
|
|
98
|
-
- They only board the car;
|
|
98
|
+
- They only board the car; your safe capacity starts at five but grows by five each time you sideswipe a parked car while already driving. Speed loss is based on how full the car is relative to that capacity, so extra slots mean quicker getaways.
|
|
99
99
|
|
|
100
100
|
### Win/Lose Conditions
|
|
101
101
|
|
|
@@ -44,29 +44,29 @@ Open **Settings** from the title to toggle gameplay assists:
|
|
|
44
44
|
|
|
45
45
|
At the title screen you can pick a stage:
|
|
46
46
|
|
|
47
|
-
- **Stage 1: Find the Car** — locate the car and drive out.
|
|
48
|
-
- **Stage 2: Fuel Run** — find a fuel can first, pick it up, then find the car and escape.
|
|
49
|
-
- **Stage 3: Rescue Buddy** —
|
|
50
|
-
- **Stage 4: Evacuate Survivors** — find the car, gather nearby civilians, and escape before zombies reach them.
|
|
47
|
+
- **Stage 1: Find the Car** — locate the car and drive out (you already start with fuel).
|
|
48
|
+
- **Stage 2: Fuel Run** — you start with no fuel; find a fuel can first, pick it up, then find the car and escape.
|
|
49
|
+
- **Stage 3: Rescue Buddy** — same fuel hunt as Stage 2 (you begin empty) plus grab your buddy, pick them up with the car, then escape together.
|
|
50
|
+
- **Stage 4: Evacuate Survivors** — start fueled, find the car, gather nearby civilians, and escape before zombies reach them. Stage 4 sprinkles extra parked cars across the map; slamming into one while already driving fully repairs your current ride and adds five more safe seats.
|
|
51
51
|
|
|
52
52
|
An objective reminder is shown at the top-left during play.
|
|
53
53
|
|
|
54
54
|
### Characters/Items
|
|
55
55
|
|
|
56
|
-
- **Player:** A blue circle. Controlled with the WASD or arrow keys.
|
|
56
|
+
- **Player:** A blue circle. Controlled with the WASD or arrow keys. When carrying fuel a tiny yellow square appears near the sprite so you can immediately see whether you're ready to drive.
|
|
57
57
|
- **Zombie:** A red circle. Will chase the player (or car) once detected.
|
|
58
58
|
- When out of sight, the zombie's movement mode will randomly switch every certain time (moving horizontally/vertically only, side-to-side movement, random movement, etc.).
|
|
59
59
|
- **Car:** A yellow rectangle. The player can enter by making contact with it.
|
|
60
60
|
- The car has durability. Durability decreases when colliding with internal walls or hitting zombies.
|
|
61
|
-
- If durability reaches 0, the car is destroyed
|
|
62
|
-
- When
|
|
63
|
-
- After roughly 5 minutes of play, a small triangle near the player points toward the objective: fuel first (Stage 2 before pickup), car after fuel is collected (Stage 2), or car directly (Stage 1).
|
|
61
|
+
- If durability reaches 0, the car is destroyed and you are dumped on foot; you must track down another parked car hidden in the level.
|
|
62
|
+
- When you're already driving, ramming a parked car instantly restores your current car's health. On Stage 4 this also increases the safe passenger limit by five.
|
|
63
|
+
- After roughly 5 minutes of play, a small triangle near the player points toward the objective: fuel first (Stage 2 before pickup), car after fuel is collected (Stage 2/3), or car directly (Stage 1/4).
|
|
64
64
|
- **Walls:** Outer walls are gray; inner walls are beige.
|
|
65
65
|
- **Outer Walls:** Walls surrounding the stage that are nearly indestructible. Each side has at least three openings (exits).
|
|
66
66
|
- **Inner Walls:** Beige walls randomly placed inside the building. Inner wall segments each have durability. **The player can break these walls** by repeatedly colliding with a segment to reduce its durability; when it reaches 0, the segment is destroyed and disappears. The car cannot break walls.
|
|
67
67
|
- **Flashlight:** Picking one up boosts your visible radius by 35%.
|
|
68
68
|
- **Steel Beam (optional):** A square post with crossed diagonals; same collision as inner walls but with triple durability. Spawns independently of inner walls (may overlap them). If an inner wall covers a beam, the beam appears once the wall is destroyed.
|
|
69
|
-
- **Fuel Can (
|
|
69
|
+
- **Fuel Can (Stages 2 & 3):** A yellow jerrycan that only spawns on the fuel-run stages. Pick it up before driving the car; once collected the on-player indicator appears until you refuel the car.
|
|
70
70
|
- **Buddy (Stage 3):** A green circle survivor who spawns somewhere in the building and waits.
|
|
71
71
|
- Zombies only choose to pursue the buddy if they are on-screen; otherwise they ignore them.
|
|
72
72
|
- If a zombie tags the buddy off-screen, the buddy quietly respawns somewhere else instead of ending the run.
|
|
@@ -74,7 +74,7 @@ An objective reminder is shown at the top-left during play.
|
|
|
74
74
|
- **Survivors (Stage 4):** Light blue civilians scattered indoors.
|
|
75
75
|
- They stand still until you get close, then shuffle toward you at about one-third of player speed.
|
|
76
76
|
- Zombies can convert them if both are on-screen; the survivor shouts a line and turns instantly.
|
|
77
|
-
- They only board the car;
|
|
77
|
+
- They only board the car; your safe capacity starts at five but grows by five each time you sideswipe a parked car while already driving. Speed loss is based on how full the car is relative to that capacity, so extra slots mean quicker getaways.
|
|
78
78
|
|
|
79
79
|
### Win/Lose Conditions
|
|
80
80
|
|
|
@@ -44,7 +44,7 @@ SURVIVOR_APPROACH_RADIUS = 48
|
|
|
44
44
|
SURVIVOR_APPROACH_SPEED = PLAYER_SPEED * 0.35
|
|
45
45
|
SURVIVOR_SPAWN_RATE = 0.07
|
|
46
46
|
SURVIVOR_MAX_SAFE_PASSENGERS = 5
|
|
47
|
-
SURVIVOR_SPEED_PENALTY_PER_PASSENGER = 0.
|
|
47
|
+
SURVIVOR_SPEED_PENALTY_PER_PASSENGER = 0.08
|
|
48
48
|
SURVIVOR_MIN_SPEED_FACTOR = 0.35
|
|
49
49
|
SURVIVOR_OVERLOAD_DAMAGE_RATIO = 0.2
|
|
50
50
|
SURVIVOR_MESSAGE_DURATION_MS = 2000
|
|
@@ -53,6 +53,7 @@ SURVIVOR_CONVERSION_LINE_KEYS = [
|
|
|
53
53
|
"stages.stage4.conversion_lines.line2",
|
|
54
54
|
"stages.stage4.conversion_lines.line3",
|
|
55
55
|
]
|
|
56
|
+
SURVIVOR_STAGE_WAITING_CAR_COUNT = 2
|
|
56
57
|
|
|
57
58
|
# --- Flashlight settings ---
|
|
58
59
|
DEFAULT_FLASHLIGHT_BONUS_SCALE = float(
|
|
@@ -83,7 +84,7 @@ ZOMBIE_INTERIOR_SPAWN_RATE = 0.015
|
|
|
83
84
|
ZOMBIE_SPAWN_PLAYER_BUFFER = 140
|
|
84
85
|
ZOMBIE_MODE_CHANGE_INTERVAL_MS = 5000
|
|
85
86
|
ZOMBIE_SIGHT_RANGE = FOV_RADIUS * 2.0
|
|
86
|
-
FAST_ZOMBIE_BASE_SPEED = PLAYER_SPEED * 0.
|
|
87
|
+
FAST_ZOMBIE_BASE_SPEED = PLAYER_SPEED * 0.77
|
|
87
88
|
FAST_ZOMBIE_SPEED_JITTER = 0.075
|
|
88
89
|
ZOMBIE_SEPARATION_DISTANCE = ZOMBIE_RADIUS * 2.2
|
|
89
90
|
|
|
@@ -170,6 +171,7 @@ __all__ = [
|
|
|
170
171
|
"SURVIVOR_OVERLOAD_DAMAGE_RATIO",
|
|
171
172
|
"SURVIVOR_MESSAGE_DURATION_MS",
|
|
172
173
|
"SURVIVOR_CONVERSION_LINE_KEYS",
|
|
174
|
+
"SURVIVOR_STAGE_WAITING_CAR_COUNT",
|
|
173
175
|
"DEFAULT_FLASHLIGHT_BONUS_SCALE",
|
|
174
176
|
"FLASHLIGHT_WIDTH",
|
|
175
177
|
"FLASHLIGHT_HEIGHT",
|
|
@@ -45,6 +45,7 @@ from ..constants import (
|
|
|
45
45
|
SURVIVOR_RADIUS,
|
|
46
46
|
SURVIVOR_SPAWN_RATE,
|
|
47
47
|
SURVIVOR_SPEED_PENALTY_PER_PASSENGER,
|
|
48
|
+
SURVIVOR_STAGE_WAITING_CAR_COUNT,
|
|
48
49
|
ZOMBIE_INTERIOR_SPAWN_RATE,
|
|
49
50
|
ZOMBIE_RADIUS,
|
|
50
51
|
ZOMBIE_SEPARATION_DISTANCE,
|
|
@@ -68,6 +69,8 @@ from ..entities import (
|
|
|
68
69
|
Zombie,
|
|
69
70
|
)
|
|
70
71
|
|
|
72
|
+
LOGICAL_SCREEN_RECT = pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
73
|
+
|
|
71
74
|
__all__ = [
|
|
72
75
|
"create_zombie",
|
|
73
76
|
"rect_for_cell",
|
|
@@ -80,8 +83,14 @@ __all__ = [
|
|
|
80
83
|
"scatter_positions_on_walkable",
|
|
81
84
|
"spawn_survivors",
|
|
82
85
|
"update_survivors",
|
|
86
|
+
"alive_waiting_cars",
|
|
87
|
+
"log_waiting_car_count",
|
|
88
|
+
"nearest_waiting_car",
|
|
83
89
|
"calculate_car_speed_for_passengers",
|
|
84
90
|
"apply_passenger_speed_penalty",
|
|
91
|
+
"waiting_car_target_count",
|
|
92
|
+
"spawn_waiting_car",
|
|
93
|
+
"maintain_waiting_car_supply",
|
|
85
94
|
"add_survivor_message",
|
|
86
95
|
"random_survivor_conversion_line",
|
|
87
96
|
"cleanup_survivor_messages",
|
|
@@ -91,7 +100,7 @@ __all__ = [
|
|
|
91
100
|
"get_shrunk_sprite",
|
|
92
101
|
"update_footprints",
|
|
93
102
|
"initialize_game_state",
|
|
94
|
-
"
|
|
103
|
+
"setup_player_and_cars",
|
|
95
104
|
"spawn_initial_zombies",
|
|
96
105
|
"process_player_input",
|
|
97
106
|
"update_entities",
|
|
@@ -246,6 +255,8 @@ def place_new_car(
|
|
|
246
255
|
wall_group: pygame.sprite.Group,
|
|
247
256
|
player: Player,
|
|
248
257
|
walkable_cells: list[pygame.Rect],
|
|
258
|
+
*,
|
|
259
|
+
existing_cars: Sequence[Car] | None = None,
|
|
249
260
|
) -> Car | None:
|
|
250
261
|
if not walkable_cells:
|
|
251
262
|
return None
|
|
@@ -268,13 +279,23 @@ def place_new_car(
|
|
|
268
279
|
temp_car, nearby_walls, collided=lambda s1, s2: s1.rect.colliderect(s2.rect)
|
|
269
280
|
)
|
|
270
281
|
collides_player = temp_rect.colliderect(player.rect.inflate(50, 50))
|
|
271
|
-
|
|
282
|
+
car_overlap = False
|
|
283
|
+
if existing_cars:
|
|
284
|
+
car_overlap = any(
|
|
285
|
+
temp_car.rect.colliderect(other.rect)
|
|
286
|
+
for other in existing_cars
|
|
287
|
+
if other and other.alive()
|
|
288
|
+
)
|
|
289
|
+
if not collides_wall and not collides_player and not car_overlap:
|
|
272
290
|
return temp_car
|
|
273
291
|
return None
|
|
274
292
|
|
|
275
293
|
|
|
276
294
|
def place_fuel_can(
|
|
277
|
-
walkable_cells: list[pygame.Rect],
|
|
295
|
+
walkable_cells: list[pygame.Rect],
|
|
296
|
+
player: Player,
|
|
297
|
+
*,
|
|
298
|
+
cars: Sequence[Car] | None = None,
|
|
278
299
|
) -> FuelCan | None:
|
|
279
300
|
"""Pick a spawn spot for the fuel can away from the player (and car if given)."""
|
|
280
301
|
if not walkable_cells:
|
|
@@ -290,14 +311,17 @@ def place_fuel_can(
|
|
|
290
311
|
< min_player_dist
|
|
291
312
|
):
|
|
292
313
|
continue
|
|
293
|
-
if
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
314
|
+
if cars:
|
|
315
|
+
too_close = False
|
|
316
|
+
for parked_car in cars:
|
|
317
|
+
if math.hypot(
|
|
318
|
+
cell.centerx - parked_car.rect.centerx,
|
|
319
|
+
cell.centery - parked_car.rect.centery,
|
|
320
|
+
) < min_car_dist:
|
|
321
|
+
too_close = True
|
|
322
|
+
break
|
|
323
|
+
if too_close:
|
|
324
|
+
continue
|
|
301
325
|
return FuelCan(cell.centerx, cell.centery)
|
|
302
326
|
|
|
303
327
|
# Fallback: drop near a random walkable cell
|
|
@@ -306,7 +330,10 @@ def place_fuel_can(
|
|
|
306
330
|
|
|
307
331
|
|
|
308
332
|
def place_flashlight(
|
|
309
|
-
walkable_cells: list[pygame.Rect],
|
|
333
|
+
walkable_cells: list[pygame.Rect],
|
|
334
|
+
player: Player,
|
|
335
|
+
*,
|
|
336
|
+
cars: Sequence[Car] | None = None,
|
|
310
337
|
) -> Flashlight | None:
|
|
311
338
|
"""Pick a spawn spot for the flashlight away from the player (and car if given)."""
|
|
312
339
|
if not walkable_cells:
|
|
@@ -322,14 +349,16 @@ def place_flashlight(
|
|
|
322
349
|
< min_player_dist
|
|
323
350
|
):
|
|
324
351
|
continue
|
|
325
|
-
if
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
352
|
+
if cars:
|
|
353
|
+
if any(
|
|
354
|
+
math.hypot(
|
|
355
|
+
cell.centerx - parked.rect.centerx,
|
|
356
|
+
cell.centery - parked.rect.centery,
|
|
357
|
+
)
|
|
358
|
+
< min_car_dist
|
|
359
|
+
for parked in cars
|
|
360
|
+
):
|
|
361
|
+
continue
|
|
333
362
|
return Flashlight(cell.centerx, cell.centery)
|
|
334
363
|
|
|
335
364
|
cell = random.choice(walkable_cells)
|
|
@@ -340,7 +369,7 @@ def place_flashlights(
|
|
|
340
369
|
walkable_cells: list[pygame.Rect],
|
|
341
370
|
player: Player,
|
|
342
371
|
*,
|
|
343
|
-
|
|
372
|
+
cars: Sequence[Car] | None = None,
|
|
344
373
|
count: int = DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
345
374
|
) -> list[Flashlight]:
|
|
346
375
|
"""Spawn multiple flashlights using the single-place helper to spread them out."""
|
|
@@ -349,7 +378,7 @@ def place_flashlights(
|
|
|
349
378
|
max_attempts = max(200, count * 80)
|
|
350
379
|
while len(placed) < count and attempts < max_attempts:
|
|
351
380
|
attempts += 1
|
|
352
|
-
fl = place_flashlight(walkable_cells, player,
|
|
381
|
+
fl = place_flashlight(walkable_cells, player, cars=cars)
|
|
353
382
|
if not fl:
|
|
354
383
|
break
|
|
355
384
|
# Avoid clustering too tightly
|
|
@@ -367,7 +396,10 @@ def place_flashlights(
|
|
|
367
396
|
|
|
368
397
|
|
|
369
398
|
def place_companion(
|
|
370
|
-
walkable_cells: list[pygame.Rect],
|
|
399
|
+
walkable_cells: list[pygame.Rect],
|
|
400
|
+
player: Player,
|
|
401
|
+
*,
|
|
402
|
+
cars: Sequence[Car] | None = None,
|
|
371
403
|
) -> Companion | None:
|
|
372
404
|
"""Spawn the stranded buddy somewhere on a walkable tile away from the player and car."""
|
|
373
405
|
if not walkable_cells:
|
|
@@ -383,14 +415,16 @@ def place_companion(
|
|
|
383
415
|
< min_player_dist
|
|
384
416
|
):
|
|
385
417
|
continue
|
|
386
|
-
if
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
418
|
+
if cars:
|
|
419
|
+
if any(
|
|
420
|
+
math.hypot(
|
|
421
|
+
cell.centerx - parked.rect.centerx,
|
|
422
|
+
cell.centery - parked.rect.centery,
|
|
423
|
+
)
|
|
424
|
+
< min_car_dist
|
|
425
|
+
for parked in cars
|
|
426
|
+
):
|
|
427
|
+
continue
|
|
394
428
|
return Companion(cell.centerx, cell.centery)
|
|
395
429
|
|
|
396
430
|
cell = random.choice(walkable_cells)
|
|
@@ -508,8 +542,12 @@ def update_survivors(game_data: GameData) -> None:
|
|
|
508
542
|
other.rect.center = (int(other.x), int(other.y))
|
|
509
543
|
|
|
510
544
|
|
|
511
|
-
def calculate_car_speed_for_passengers(
|
|
512
|
-
|
|
545
|
+
def calculate_car_speed_for_passengers(
|
|
546
|
+
passengers: int, *, capacity: int = SURVIVOR_MAX_SAFE_PASSENGERS
|
|
547
|
+
) -> float:
|
|
548
|
+
cap = max(1, capacity)
|
|
549
|
+
load_ratio = max(0.0, passengers / cap)
|
|
550
|
+
penalty = SURVIVOR_SPEED_PENALTY_PER_PASSENGER * load_ratio
|
|
513
551
|
penalty = min(0.95, max(0.0, penalty))
|
|
514
552
|
adjusted = CAR_SPEED * (1 - penalty)
|
|
515
553
|
return max(CAR_SPEED * SURVIVOR_MIN_SPEED_FACTOR, adjusted)
|
|
@@ -522,7 +560,98 @@ def apply_passenger_speed_penalty(game_data: GameData) -> None:
|
|
|
522
560
|
if not game_data.stage.survivor_stage:
|
|
523
561
|
car.speed = CAR_SPEED
|
|
524
562
|
return
|
|
525
|
-
car.speed = calculate_car_speed_for_passengers(
|
|
563
|
+
car.speed = calculate_car_speed_for_passengers(
|
|
564
|
+
game_data.state.survivors_onboard,
|
|
565
|
+
capacity=game_data.state.survivor_capacity,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def rect_visible_on_screen(camera: Camera | None, rect: pygame.Rect) -> bool:
|
|
570
|
+
if camera is None:
|
|
571
|
+
return False
|
|
572
|
+
return camera.apply_rect(rect).colliderect(LOGICAL_SCREEN_RECT)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def waiting_car_target_count(stage: Stage) -> int:
|
|
576
|
+
return SURVIVOR_STAGE_WAITING_CAR_COUNT if stage.survivor_stage else 1
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def spawn_waiting_car(game_data: GameData) -> Car | None:
|
|
580
|
+
"""Attempt to place an additional parked car on the map."""
|
|
581
|
+
player = game_data.player
|
|
582
|
+
if not player:
|
|
583
|
+
return None
|
|
584
|
+
walkable_cells = game_data.areas.walkable_cells
|
|
585
|
+
if not walkable_cells:
|
|
586
|
+
return None
|
|
587
|
+
wall_group = game_data.groups.wall_group
|
|
588
|
+
all_sprites = game_data.groups.all_sprites
|
|
589
|
+
active_car = game_data.car if game_data.car and game_data.car.alive() else None
|
|
590
|
+
waiting = alive_waiting_cars(game_data)
|
|
591
|
+
obstacles: list[Car] = list(waiting)
|
|
592
|
+
if active_car:
|
|
593
|
+
obstacles.append(active_car)
|
|
594
|
+
camera = game_data.camera
|
|
595
|
+
offscreen_attempts = 6
|
|
596
|
+
while offscreen_attempts > 0:
|
|
597
|
+
new_car = place_new_car(
|
|
598
|
+
wall_group,
|
|
599
|
+
player,
|
|
600
|
+
walkable_cells,
|
|
601
|
+
existing_cars=obstacles,
|
|
602
|
+
)
|
|
603
|
+
if not new_car:
|
|
604
|
+
return None
|
|
605
|
+
if rect_visible_on_screen(camera, new_car.rect):
|
|
606
|
+
offscreen_attempts -= 1
|
|
607
|
+
continue
|
|
608
|
+
game_data.waiting_cars.append(new_car)
|
|
609
|
+
all_sprites.add(new_car, layer=1)
|
|
610
|
+
return new_car
|
|
611
|
+
return None
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def maintain_waiting_car_supply(
|
|
615
|
+
game_data: GameData, *, minimum: int | None = None
|
|
616
|
+
) -> None:
|
|
617
|
+
"""Ensure a baseline count of parked cars exists."""
|
|
618
|
+
target = 1 if minimum is None else max(0, minimum)
|
|
619
|
+
current = len(alive_waiting_cars(game_data))
|
|
620
|
+
while current < target:
|
|
621
|
+
new_car = spawn_waiting_car(game_data)
|
|
622
|
+
if not new_car:
|
|
623
|
+
break
|
|
624
|
+
current += 1
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def alive_waiting_cars(game_data: GameData) -> list[Car]:
|
|
628
|
+
"""Return the list of parked cars that still exist, pruning any destroyed sprites."""
|
|
629
|
+
cars = [car for car in game_data.waiting_cars if car.alive()]
|
|
630
|
+
game_data.waiting_cars = cars
|
|
631
|
+
log_waiting_car_count(game_data)
|
|
632
|
+
return cars
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def log_waiting_car_count(game_data: GameData, *, force: bool = False) -> None:
|
|
636
|
+
"""Print the number of waiting cars when it changes."""
|
|
637
|
+
current = len(game_data.waiting_cars)
|
|
638
|
+
if not force and current == game_data.last_logged_waiting_cars:
|
|
639
|
+
return
|
|
640
|
+
stage_id = getattr(game_data.stage, "id", "unknown")
|
|
641
|
+
game_data.last_logged_waiting_cars = current
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def nearest_waiting_car(
|
|
645
|
+
game_data: GameData, origin: tuple[float, float]
|
|
646
|
+
) -> Car | None:
|
|
647
|
+
"""Find the closest waiting car to an origin point."""
|
|
648
|
+
cars = alive_waiting_cars(game_data)
|
|
649
|
+
if not cars:
|
|
650
|
+
return None
|
|
651
|
+
return min(
|
|
652
|
+
cars,
|
|
653
|
+
key=lambda car: math.hypot(car.rect.centerx - origin[0], car.rect.centery - origin[1]),
|
|
654
|
+
)
|
|
526
655
|
|
|
527
656
|
|
|
528
657
|
def add_survivor_message(game_data: GameData, text: str) -> None:
|
|
@@ -592,7 +721,6 @@ def handle_survivor_zombie_collisions(
|
|
|
592
721
|
zombies.sort(key=lambda s: s.rect.centerx)
|
|
593
722
|
zombie_xs = [z.rect.centerx for z in zombies]
|
|
594
723
|
camera = game_data.camera
|
|
595
|
-
screen_rect = pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
596
724
|
|
|
597
725
|
for survivor in list(survivor_group):
|
|
598
726
|
if not survivor.alive():
|
|
@@ -622,7 +750,7 @@ def handle_survivor_zombie_collisions(
|
|
|
622
750
|
|
|
623
751
|
if not collided:
|
|
624
752
|
continue
|
|
625
|
-
if not camera
|
|
753
|
+
if not rect_visible_on_screen(camera, survivor.rect):
|
|
626
754
|
continue
|
|
627
755
|
survivor.kill()
|
|
628
756
|
line = random_survivor_conversion_line()
|
|
@@ -749,6 +877,7 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
749
877
|
survivors_onboard=0,
|
|
750
878
|
survivors_rescued=0,
|
|
751
879
|
survivor_messages=[],
|
|
880
|
+
survivor_capacity=SURVIVOR_MAX_SAFE_PASSENGERS,
|
|
752
881
|
)
|
|
753
882
|
|
|
754
883
|
# Create sprite groups
|
|
@@ -796,10 +925,13 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
796
925
|
)
|
|
797
926
|
|
|
798
927
|
|
|
799
|
-
def
|
|
800
|
-
game_data: GameData,
|
|
801
|
-
|
|
802
|
-
|
|
928
|
+
def setup_player_and_cars(
|
|
929
|
+
game_data: GameData,
|
|
930
|
+
layout_data: Mapping[str, list[pygame.Rect]],
|
|
931
|
+
*,
|
|
932
|
+
car_count: int = 1,
|
|
933
|
+
) -> tuple[Player, list[Car]]:
|
|
934
|
+
"""Create the player plus one or more parked cars using blueprint candidates."""
|
|
803
935
|
all_sprites = game_data.groups.all_sprites
|
|
804
936
|
walkable_cells: list[pygame.Rect] = layout_data["walkable_cells"]
|
|
805
937
|
|
|
@@ -813,31 +945,38 @@ def setup_player_and_car(
|
|
|
813
945
|
player_pos = pick_center(layout_data["player_cells"] or walkable_cells)
|
|
814
946
|
player = Player(*player_pos)
|
|
815
947
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
948
|
+
car_candidates = list(layout_data["car_cells"] or walkable_cells)
|
|
949
|
+
waiting_cars: list[Car] = []
|
|
950
|
+
|
|
951
|
+
def _pick_car_position() -> tuple[int, int]:
|
|
952
|
+
"""Favor distant cells for the first car, otherwise fall back to random picks."""
|
|
953
|
+
if not car_candidates:
|
|
954
|
+
return (player_pos[0] + 200, player_pos[1])
|
|
955
|
+
random.shuffle(car_candidates)
|
|
956
|
+
for candidate in car_candidates:
|
|
957
|
+
if (
|
|
958
|
+
math.hypot(
|
|
959
|
+
candidate.centerx - player_pos[0],
|
|
960
|
+
candidate.centery - player_pos[1],
|
|
961
|
+
)
|
|
962
|
+
>= 400
|
|
963
|
+
):
|
|
964
|
+
car_candidates.remove(candidate)
|
|
965
|
+
return candidate.center
|
|
966
|
+
# No far-enough cells found; pick the first available
|
|
967
|
+
choice = car_candidates.pop()
|
|
968
|
+
return choice.center
|
|
969
|
+
|
|
970
|
+
for idx in range(max(1, car_count)):
|
|
971
|
+
car_pos = _pick_car_position()
|
|
972
|
+
car = Car(*car_pos)
|
|
973
|
+
waiting_cars.append(car)
|
|
974
|
+
all_sprites.add(car, layer=1)
|
|
975
|
+
if not car_candidates:
|
|
828
976
|
break
|
|
829
|
-
if car_pos is None and car_candidates:
|
|
830
|
-
car_pos = random.choice(car_candidates).center
|
|
831
|
-
elif car_pos is None:
|
|
832
|
-
car_pos = (player_pos[0] + 200, player_pos[1]) # Fallback
|
|
833
977
|
|
|
834
|
-
car = Car(*car_pos)
|
|
835
|
-
|
|
836
|
-
# Add to sprite groups
|
|
837
978
|
all_sprites.add(player, layer=2)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
return player, car
|
|
979
|
+
return player, waiting_cars
|
|
841
980
|
|
|
842
981
|
|
|
843
982
|
def spawn_initial_zombies(
|
|
@@ -878,7 +1017,7 @@ def spawn_initial_zombies(
|
|
|
878
1017
|
|
|
879
1018
|
|
|
880
1019
|
def process_player_input(
|
|
881
|
-
keys: Sequence[bool], player: Player, car: Car
|
|
1020
|
+
keys: Sequence[bool], player: Player, car: Car | None
|
|
882
1021
|
) -> tuple[float, float, float, float]:
|
|
883
1022
|
"""Process keyboard input and return movement deltas."""
|
|
884
1023
|
dx_input, dy_input = 0, 0
|
|
@@ -893,7 +1032,7 @@ def process_player_input(
|
|
|
893
1032
|
|
|
894
1033
|
player_dx, player_dy, car_dx, car_dy = 0, 0, 0, 0
|
|
895
1034
|
|
|
896
|
-
if player.in_car and car.alive():
|
|
1035
|
+
if player.in_car and car and car.alive():
|
|
897
1036
|
target_speed = getattr(car, "speed", CAR_SPEED)
|
|
898
1037
|
move_len = math.hypot(dx_input, dy_input)
|
|
899
1038
|
if move_len > 0:
|
|
@@ -925,32 +1064,35 @@ def update_entities(
|
|
|
925
1064
|
player = game_data.player
|
|
926
1065
|
assert player is not None
|
|
927
1066
|
car = game_data.car
|
|
928
|
-
assert car is not None
|
|
929
1067
|
companion = game_data.companion
|
|
930
1068
|
wall_group = game_data.groups.wall_group
|
|
931
1069
|
all_sprites = game_data.groups.all_sprites
|
|
932
1070
|
zombie_group = game_data.groups.zombie_group
|
|
933
1071
|
camera = game_data.camera
|
|
934
1072
|
stage = game_data.stage
|
|
1073
|
+
active_car = car if car and car.alive() else None
|
|
935
1074
|
|
|
936
1075
|
# Update player/car movement
|
|
937
|
-
if player.in_car and
|
|
938
|
-
|
|
939
|
-
player.rect.center =
|
|
940
|
-
player.x, player.y =
|
|
1076
|
+
if player.in_car and active_car:
|
|
1077
|
+
active_car.move(car_dx, car_dy, wall_group)
|
|
1078
|
+
player.rect.center = active_car.rect.center
|
|
1079
|
+
player.x, player.y = active_car.x, active_car.y
|
|
941
1080
|
elif not player.in_car:
|
|
942
1081
|
# Ensure player is in all_sprites if not in car
|
|
943
1082
|
if player not in all_sprites:
|
|
944
1083
|
all_sprites.add(player, layer=2)
|
|
945
1084
|
player.move(player_dx, player_dy, wall_group)
|
|
1085
|
+
else:
|
|
1086
|
+
# Player flagged as in-car but car is gone; drop them back to foot control
|
|
1087
|
+
player.in_car = False
|
|
946
1088
|
|
|
947
1089
|
# Update camera
|
|
948
|
-
target_for_camera =
|
|
1090
|
+
target_for_camera = active_car if player.in_car and active_car else player
|
|
949
1091
|
camera.update(target_for_camera)
|
|
950
1092
|
|
|
951
1093
|
# Update companion (Stage 3 follow logic)
|
|
952
1094
|
if companion and companion.alive() and not companion.rescued:
|
|
953
|
-
follow_target =
|
|
1095
|
+
follow_target = active_car if player.in_car and active_car else player
|
|
954
1096
|
companion.update_follow(follow_target.rect.center, wall_group)
|
|
955
1097
|
if companion not in all_sprites:
|
|
956
1098
|
all_sprites.add(companion, layer=2)
|
|
@@ -971,24 +1113,23 @@ def update_entities(
|
|
|
971
1113
|
|
|
972
1114
|
# Update zombies
|
|
973
1115
|
target_center = (
|
|
974
|
-
|
|
1116
|
+
active_car.rect.center if player.in_car and active_car else player.rect.center
|
|
975
1117
|
)
|
|
976
1118
|
companion_on_screen = False
|
|
977
|
-
screen_rect = pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
978
1119
|
if (
|
|
979
1120
|
game_data.stage.requires_companion
|
|
980
1121
|
and companion
|
|
981
1122
|
and companion.alive()
|
|
982
1123
|
and not companion.rescued
|
|
983
1124
|
):
|
|
984
|
-
companion_on_screen = camera
|
|
1125
|
+
companion_on_screen = rect_visible_on_screen(camera, companion.rect)
|
|
985
1126
|
|
|
986
1127
|
survivors_on_screen: list[Survivor] = []
|
|
987
1128
|
if stage.survivor_stage:
|
|
988
1129
|
survivor_group = game_data.groups.survivor_group
|
|
989
1130
|
for survivor in survivor_group:
|
|
990
1131
|
if getattr(survivor, "alive", lambda: False)():
|
|
991
|
-
if camera
|
|
1132
|
+
if rect_visible_on_screen(camera, survivor.rect):
|
|
992
1133
|
survivors_on_screen.append(survivor)
|
|
993
1134
|
|
|
994
1135
|
zombies_sorted: list[Zombie] = sorted(list(zombie_group), key=lambda z: z.x)
|
|
@@ -1024,7 +1165,7 @@ def update_entities(
|
|
|
1024
1165
|
target = companion.rect.center
|
|
1025
1166
|
|
|
1026
1167
|
if stage.survivor_stage:
|
|
1027
|
-
zombie_on_screen = camera
|
|
1168
|
+
zombie_on_screen = rect_visible_on_screen(camera, zombie.rect)
|
|
1028
1169
|
if zombie_on_screen:
|
|
1029
1170
|
candidate_positions: list[tuple[int, int]] = []
|
|
1030
1171
|
for survivor in survivors_on_screen:
|
|
@@ -1050,10 +1191,8 @@ def check_interactions(
|
|
|
1050
1191
|
player = game_data.player
|
|
1051
1192
|
assert player is not None
|
|
1052
1193
|
car = game_data.car
|
|
1053
|
-
assert car is not None
|
|
1054
1194
|
companion = game_data.companion
|
|
1055
1195
|
zombie_group = game_data.groups.zombie_group
|
|
1056
|
-
wall_group = game_data.groups.wall_group
|
|
1057
1196
|
all_sprites = game_data.groups.all_sprites
|
|
1058
1197
|
survivor_group = game_data.groups.survivor_group
|
|
1059
1198
|
state = game_data.state
|
|
@@ -1063,6 +1202,9 @@ def check_interactions(
|
|
|
1063
1202
|
flashlights = game_data.flashlights or []
|
|
1064
1203
|
camera = game_data.camera
|
|
1065
1204
|
stage = game_data.stage
|
|
1205
|
+
maintain_waiting_car_supply(game_data)
|
|
1206
|
+
active_car = car if car and car.alive() else None
|
|
1207
|
+
waiting_cars = game_data.waiting_cars
|
|
1066
1208
|
|
|
1067
1209
|
# Fuel pickup
|
|
1068
1210
|
if fuel and fuel.alive() and not state.has_fuel and not player.in_car:
|
|
@@ -1104,8 +1246,7 @@ def check_interactions(
|
|
|
1104
1246
|
)
|
|
1105
1247
|
if companion_active:
|
|
1106
1248
|
assert companion is not None
|
|
1107
|
-
|
|
1108
|
-
companion_on_screen = camera.apply_rect(companion.rect).colliderect(screen_rect)
|
|
1249
|
+
companion_on_screen = rect_visible_on_screen(camera, companion.rect)
|
|
1109
1250
|
|
|
1110
1251
|
# Companion interactions (Stage 3)
|
|
1111
1252
|
if companion_active and stage.requires_companion:
|
|
@@ -1113,10 +1254,10 @@ def check_interactions(
|
|
|
1113
1254
|
if not player.in_car:
|
|
1114
1255
|
if pygame.sprite.collide_circle(companion, player):
|
|
1115
1256
|
companion.set_following()
|
|
1116
|
-
elif player.in_car and
|
|
1257
|
+
elif player.in_car and active_car:
|
|
1117
1258
|
g = pygame.sprite.Group()
|
|
1118
1259
|
g.add(companion)
|
|
1119
|
-
if pygame.sprite.spritecollide(get_shrunk_sprite(
|
|
1260
|
+
if pygame.sprite.spritecollide(get_shrunk_sprite(active_car, 0.8), g, False):
|
|
1120
1261
|
state.companion_rescued = True
|
|
1121
1262
|
companion.mark_rescued()
|
|
1122
1263
|
companion.kill()
|
|
@@ -1140,13 +1281,18 @@ def check_interactions(
|
|
|
1140
1281
|
companion.teleport((LEVEL_WIDTH // 2, LEVEL_HEIGHT // 2))
|
|
1141
1282
|
companion.following = False
|
|
1142
1283
|
|
|
1143
|
-
|
|
1284
|
+
player_group = pygame.sprite.Group()
|
|
1285
|
+
player_group.add(player)
|
|
1144
1286
|
|
|
1145
|
-
# Player entering car
|
|
1146
|
-
if
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1287
|
+
# Player entering an active car already under control
|
|
1288
|
+
if (
|
|
1289
|
+
not player.in_car
|
|
1290
|
+
and active_car
|
|
1291
|
+
and active_car.alive()
|
|
1292
|
+
and active_car.health > 0
|
|
1293
|
+
):
|
|
1294
|
+
shrunk_active = get_shrunk_sprite(active_car, 0.8)
|
|
1295
|
+
if pygame.sprite.spritecollide(shrunk_active, player_group, False):
|
|
1150
1296
|
if state.has_fuel:
|
|
1151
1297
|
player.in_car = True
|
|
1152
1298
|
all_sprites.remove(player)
|
|
@@ -1156,19 +1302,72 @@ def check_interactions(
|
|
|
1156
1302
|
else:
|
|
1157
1303
|
now_ms = state.elapsed_play_ms
|
|
1158
1304
|
state.fuel_message_until = now_ms + FUEL_HINT_DURATION_MS
|
|
1159
|
-
# Keep hint timing unchanged so the car visit doesn't immediately reveal fuel
|
|
1160
1305
|
state.hint_target_type = "fuel"
|
|
1161
1306
|
|
|
1307
|
+
# Claim a waiting/parked car when the player finally reaches it
|
|
1308
|
+
if not player.in_car and not active_car and waiting_cars:
|
|
1309
|
+
claimed_car: Car | None = None
|
|
1310
|
+
for parked_car in waiting_cars:
|
|
1311
|
+
shrunk_waiting = get_shrunk_sprite(parked_car, 0.8)
|
|
1312
|
+
if pygame.sprite.spritecollide(shrunk_waiting, player_group, False):
|
|
1313
|
+
claimed_car = parked_car
|
|
1314
|
+
break
|
|
1315
|
+
if claimed_car:
|
|
1316
|
+
if state.has_fuel:
|
|
1317
|
+
try:
|
|
1318
|
+
game_data.waiting_cars.remove(claimed_car)
|
|
1319
|
+
except ValueError:
|
|
1320
|
+
pass
|
|
1321
|
+
game_data.car = claimed_car
|
|
1322
|
+
active_car = claimed_car
|
|
1323
|
+
player.in_car = True
|
|
1324
|
+
all_sprites.remove(player)
|
|
1325
|
+
state.hint_expires_at = 0
|
|
1326
|
+
state.hint_target_type = None
|
|
1327
|
+
apply_passenger_speed_penalty(game_data)
|
|
1328
|
+
maintain_waiting_car_supply(game_data)
|
|
1329
|
+
print("Player claimed a waiting car!")
|
|
1330
|
+
else:
|
|
1331
|
+
now_ms = state.elapsed_play_ms
|
|
1332
|
+
state.fuel_message_until = now_ms + FUEL_HINT_DURATION_MS
|
|
1333
|
+
state.hint_target_type = "fuel"
|
|
1334
|
+
|
|
1335
|
+
shrunk_car = get_shrunk_sprite(active_car, 0.8) if active_car else None
|
|
1336
|
+
|
|
1337
|
+
# Bonus: collide a parked car while driving to repair/extend capabilities
|
|
1338
|
+
if player.in_car and active_car and shrunk_car and waiting_cars:
|
|
1339
|
+
waiting_group = pygame.sprite.Group(waiting_cars)
|
|
1340
|
+
collided_waiters = pygame.sprite.spritecollide(
|
|
1341
|
+
shrunk_car, waiting_group, False, pygame.sprite.collide_rect
|
|
1342
|
+
)
|
|
1343
|
+
if collided_waiters:
|
|
1344
|
+
removed_any = False
|
|
1345
|
+
for parked in collided_waiters:
|
|
1346
|
+
if not parked.alive():
|
|
1347
|
+
continue
|
|
1348
|
+
parked.kill()
|
|
1349
|
+
try:
|
|
1350
|
+
game_data.waiting_cars.remove(parked)
|
|
1351
|
+
except ValueError:
|
|
1352
|
+
pass
|
|
1353
|
+
active_car.health = active_car.max_health
|
|
1354
|
+
active_car.update_color()
|
|
1355
|
+
removed_any = True
|
|
1356
|
+
if stage.survivor_stage:
|
|
1357
|
+
state.survivor_capacity += SURVIVOR_MAX_SAFE_PASSENGERS
|
|
1358
|
+
if removed_any:
|
|
1359
|
+
maintain_waiting_car_supply(game_data)
|
|
1360
|
+
|
|
1162
1361
|
# Car hitting zombies
|
|
1163
|
-
if player.in_car and
|
|
1362
|
+
if player.in_car and active_car and active_car.health > 0 and shrunk_car:
|
|
1164
1363
|
zombies_hit = pygame.sprite.spritecollide(shrunk_car, zombie_group, True)
|
|
1165
1364
|
if zombies_hit:
|
|
1166
|
-
|
|
1365
|
+
active_car.take_damage(CAR_ZOMBIE_DAMAGE * len(zombies_hit))
|
|
1167
1366
|
|
|
1168
1367
|
if (
|
|
1169
1368
|
stage.survivor_stage
|
|
1170
1369
|
and player.in_car
|
|
1171
|
-
and
|
|
1370
|
+
and active_car
|
|
1172
1371
|
and shrunk_car
|
|
1173
1372
|
and survivor_group
|
|
1174
1373
|
):
|
|
@@ -1178,18 +1377,20 @@ def check_interactions(
|
|
|
1178
1377
|
if boarded:
|
|
1179
1378
|
state.survivors_onboard += len(boarded)
|
|
1180
1379
|
apply_passenger_speed_penalty(game_data)
|
|
1181
|
-
|
|
1380
|
+
capacity_limit = state.survivor_capacity
|
|
1381
|
+
if state.survivors_onboard > capacity_limit:
|
|
1182
1382
|
overload_damage = max(
|
|
1183
|
-
1,
|
|
1383
|
+
1,
|
|
1384
|
+
int(active_car.max_health * SURVIVOR_OVERLOAD_DAMAGE_RATIO),
|
|
1184
1385
|
)
|
|
1185
1386
|
add_survivor_message(game_data, _("survivors.too_many_aboard"))
|
|
1186
|
-
|
|
1387
|
+
active_car.take_damage(overload_damage)
|
|
1187
1388
|
|
|
1188
1389
|
if stage.survivor_stage:
|
|
1189
1390
|
handle_survivor_zombie_collisions(game_data, config)
|
|
1190
1391
|
|
|
1191
1392
|
# Handle car destruction
|
|
1192
|
-
if car.alive() and car.health <= 0:
|
|
1393
|
+
if car and car.alive() and car.health <= 0:
|
|
1193
1394
|
car_destroyed_pos = car.rect.center
|
|
1194
1395
|
car.kill()
|
|
1195
1396
|
if stage.survivor_stage:
|
|
@@ -1202,21 +1403,14 @@ def check_interactions(
|
|
|
1202
1403
|
all_sprites.add(player, layer=2)
|
|
1203
1404
|
print("Car destroyed! Player ejected.")
|
|
1204
1405
|
|
|
1406
|
+
# Clear active car and let the player hunt for another waiting car.
|
|
1407
|
+
game_data.car = None
|
|
1408
|
+
state.survivor_capacity = SURVIVOR_MAX_SAFE_PASSENGERS
|
|
1409
|
+
apply_passenger_speed_penalty(game_data)
|
|
1410
|
+
|
|
1205
1411
|
# Bring back the rescued companion near the player after losing the car
|
|
1206
1412
|
respawn_rescued_companion_near_player(game_data)
|
|
1207
|
-
|
|
1208
|
-
# Respawn car
|
|
1209
|
-
new_car = place_new_car(wall_group, player, walkable_cells)
|
|
1210
|
-
if new_car is None:
|
|
1211
|
-
# Fallback: Try original car position or other strategies
|
|
1212
|
-
new_car = Car(car.rect.centerx, car.rect.centery)
|
|
1213
|
-
|
|
1214
|
-
if new_car is not None:
|
|
1215
|
-
game_data.car = new_car # Update car reference
|
|
1216
|
-
all_sprites.add(new_car, layer=1)
|
|
1217
|
-
apply_passenger_speed_penalty(game_data)
|
|
1218
|
-
else:
|
|
1219
|
-
print("Error: Failed to respawn car anywhere!")
|
|
1413
|
+
maintain_waiting_car_supply(game_data)
|
|
1220
1414
|
|
|
1221
1415
|
# Player getting caught by zombies
|
|
1222
1416
|
if not player.in_car and player in all_sprites:
|
|
@@ -1230,7 +1424,7 @@ def check_interactions(
|
|
|
1230
1424
|
state.game_over_message = _("game_over.scream")
|
|
1231
1425
|
|
|
1232
1426
|
# Player escaping the level
|
|
1233
|
-
if player.in_car and car.alive() and state.has_fuel:
|
|
1427
|
+
if player.in_car and car and car.alive() and state.has_fuel:
|
|
1234
1428
|
companion_ready = not stage.requires_companion or state.companion_rescued
|
|
1235
1429
|
if companion_ready and any(
|
|
1236
1430
|
outside.collidepoint(car.rect.center) for outside in outside_rects
|
|
@@ -1244,5 +1438,5 @@ def check_interactions(
|
|
|
1244
1438
|
|
|
1245
1439
|
# Return fog of view target
|
|
1246
1440
|
if not state.game_over and not state.game_won:
|
|
1247
|
-
return car if player.in_car and car.alive() else player
|
|
1441
|
+
return car if player.in_car and car and car.alive() else player
|
|
1248
1442
|
return None
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
import pygame
|
|
@@ -48,6 +48,7 @@ class ProgressState:
|
|
|
48
48
|
survivors_onboard: int
|
|
49
49
|
survivors_rescued: int
|
|
50
50
|
survivor_messages: list
|
|
51
|
+
survivor_capacity: int
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
@dataclass
|
|
@@ -74,7 +75,9 @@ class GameData:
|
|
|
74
75
|
flashlights: list[Flashlight] | None = None
|
|
75
76
|
player: Player | None = None
|
|
76
77
|
car: Car | None = None
|
|
78
|
+
waiting_cars: list[Car] = field(default_factory=list)
|
|
77
79
|
companion: Companion | None = None
|
|
80
|
+
last_logged_waiting_cars: int | None = None
|
|
78
81
|
|
|
79
82
|
|
|
80
83
|
@dataclass(frozen=True)
|
|
@@ -445,10 +445,28 @@ def draw(
|
|
|
445
445
|
pygame.draw.circle(screen, color, sr.center, assets.footprint_radius)
|
|
446
446
|
|
|
447
447
|
screen_rect_inflated = screen.get_rect().inflate(100, 100)
|
|
448
|
+
player_screen_rect: pygame.Rect | None = None
|
|
448
449
|
for entity in all_sprites:
|
|
449
450
|
sprite_screen_rect = camera.apply_rect(entity.rect)
|
|
450
451
|
if sprite_screen_rect.colliderect(screen_rect_inflated):
|
|
451
452
|
screen.blit(entity.image, sprite_screen_rect)
|
|
453
|
+
if entity is player:
|
|
454
|
+
player_screen_rect = sprite_screen_rect
|
|
455
|
+
|
|
456
|
+
if player_screen_rect is None:
|
|
457
|
+
player_screen_rect = camera.apply_rect(player.rect)
|
|
458
|
+
|
|
459
|
+
if has_fuel and player_screen_rect and not player.in_car:
|
|
460
|
+
indicator_size = max(6, (assets.player_radius // 2) * 2)
|
|
461
|
+
padding = max(2, indicator_size // 4)
|
|
462
|
+
indicator_rect = pygame.Rect(
|
|
463
|
+
player_screen_rect.right - indicator_size + padding,
|
|
464
|
+
player_screen_rect.bottom - indicator_size + padding,
|
|
465
|
+
indicator_size,
|
|
466
|
+
indicator_size,
|
|
467
|
+
)
|
|
468
|
+
pygame.draw.rect(screen, YELLOW, indicator_rect)
|
|
469
|
+
pygame.draw.rect(screen, BLACK, indicator_rect, width=1)
|
|
452
470
|
|
|
453
471
|
if hint_target and player:
|
|
454
472
|
current_fov_scale = get_fog_scale(
|
|
@@ -532,7 +550,7 @@ def draw(
|
|
|
532
550
|
and getattr(stage, "survivor_stage", False)
|
|
533
551
|
and (survivors_onboard is not None)
|
|
534
552
|
):
|
|
535
|
-
limit = SURVIVOR_MAX_SAFE_PASSENGERS
|
|
553
|
+
limit = getattr(state, "survivor_capacity", SURVIVOR_MAX_SAFE_PASSENGERS)
|
|
536
554
|
objective_lines.append(
|
|
537
555
|
_("objectives.survivors_onboard", count=survivors_onboard, limit=limit)
|
|
538
556
|
)
|
|
@@ -9,6 +9,7 @@ from ..colors import LIGHT_GRAY, RED, WHITE, YELLOW
|
|
|
9
9
|
from ..constants import (
|
|
10
10
|
CAR_HINT_DELAY_MS_DEFAULT,
|
|
11
11
|
DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
12
|
+
SURVIVOR_STAGE_WAITING_CAR_COUNT,
|
|
12
13
|
)
|
|
13
14
|
from ..gameplay import logic
|
|
14
15
|
from ..localization import translate as _
|
|
@@ -41,9 +42,19 @@ def gameplay_screen(
|
|
|
41
42
|
last_fov_target = None
|
|
42
43
|
|
|
43
44
|
layout_data = logic.generate_level_from_blueprint(game_data, config)
|
|
44
|
-
|
|
45
|
+
initial_waiting = (
|
|
46
|
+
SURVIVOR_STAGE_WAITING_CAR_COUNT if stage.survivor_stage else 1
|
|
47
|
+
)
|
|
48
|
+
player, waiting_cars = logic.setup_player_and_cars(
|
|
49
|
+
game_data, layout_data, car_count=initial_waiting
|
|
50
|
+
)
|
|
45
51
|
game_data.player = player
|
|
46
|
-
game_data.
|
|
52
|
+
game_data.waiting_cars = waiting_cars
|
|
53
|
+
game_data.car = None
|
|
54
|
+
# Only top up if initial placement spawned fewer than the intended baseline (shouldn't happen)
|
|
55
|
+
logic.maintain_waiting_car_supply(
|
|
56
|
+
game_data, minimum=logic.waiting_car_target_count(stage)
|
|
57
|
+
)
|
|
47
58
|
logic.apply_passenger_speed_penalty(game_data)
|
|
48
59
|
|
|
49
60
|
if stage.survivor_stage:
|
|
@@ -59,7 +70,7 @@ def gameplay_screen(
|
|
|
59
70
|
|
|
60
71
|
if stage.requires_fuel:
|
|
61
72
|
fuel_can = logic.place_fuel_can(
|
|
62
|
-
layout_data["walkable_cells"], player,
|
|
73
|
+
layout_data["walkable_cells"], player, cars=game_data.waiting_cars
|
|
63
74
|
)
|
|
64
75
|
if fuel_can:
|
|
65
76
|
game_data.fuel = fuel_can
|
|
@@ -68,7 +79,7 @@ def gameplay_screen(
|
|
|
68
79
|
flashlights = logic.place_flashlights(
|
|
69
80
|
layout_data["walkable_cells"],
|
|
70
81
|
player,
|
|
71
|
-
|
|
82
|
+
cars=game_data.waiting_cars,
|
|
72
83
|
count=max(1, flashlight_count),
|
|
73
84
|
)
|
|
74
85
|
game_data.flashlights = flashlights
|
|
@@ -76,7 +87,7 @@ def gameplay_screen(
|
|
|
76
87
|
|
|
77
88
|
if stage.requires_companion:
|
|
78
89
|
companion = logic.place_companion(
|
|
79
|
-
layout_data["walkable_cells"], player,
|
|
90
|
+
layout_data["walkable_cells"], player, cars=game_data.waiting_cars
|
|
80
91
|
)
|
|
81
92
|
if companion:
|
|
82
93
|
game_data.companion = companion
|
|
@@ -222,10 +233,13 @@ def gameplay_screen(
|
|
|
222
233
|
hint_expires_at = game_data.state.hint_expires_at
|
|
223
234
|
hint_target_type = game_data.state.hint_target_type
|
|
224
235
|
|
|
236
|
+
active_car = game_data.car if game_data.car and game_data.car.alive() else None
|
|
225
237
|
if hint_enabled:
|
|
226
238
|
if not has_fuel and game_data.fuel and game_data.fuel.alive():
|
|
227
239
|
target_type = "fuel"
|
|
228
|
-
elif not player.in_car and
|
|
240
|
+
elif not player.in_car and (
|
|
241
|
+
active_car or logic.alive_waiting_cars(game_data)
|
|
242
|
+
):
|
|
229
243
|
target_type = "car"
|
|
230
244
|
else:
|
|
231
245
|
target_type = None
|
|
@@ -246,8 +260,15 @@ def gameplay_screen(
|
|
|
246
260
|
):
|
|
247
261
|
if target_type == "fuel" and game_data.fuel and game_data.fuel.alive():
|
|
248
262
|
hint_target = game_data.fuel.rect.center
|
|
249
|
-
elif target_type == "car"
|
|
250
|
-
|
|
263
|
+
elif target_type == "car":
|
|
264
|
+
if active_car:
|
|
265
|
+
hint_target = active_car.rect.center
|
|
266
|
+
else:
|
|
267
|
+
waiting_target = logic.nearest_waiting_car(
|
|
268
|
+
game_data, (player.x, player.y)
|
|
269
|
+
)
|
|
270
|
+
if waiting_target:
|
|
271
|
+
hint_target = waiting_target.rect.center
|
|
251
272
|
|
|
252
273
|
draw(
|
|
253
274
|
render_assets,
|
|
@@ -29,6 +29,15 @@ from .screens.settings import settings_screen
|
|
|
29
29
|
from .screens.title import title_screen
|
|
30
30
|
from .gameplay.logic import calculate_car_speed_for_passengers
|
|
31
31
|
|
|
32
|
+
# Re-export the gameplay helpers constants for external callers/tests.
|
|
33
|
+
__all__ = [
|
|
34
|
+
"main",
|
|
35
|
+
"CAR_SPEED",
|
|
36
|
+
"SURVIVOR_MAX_SAFE_PASSENGERS",
|
|
37
|
+
"SURVIVOR_MIN_SPEED_FACTOR",
|
|
38
|
+
"calculate_car_speed_for_passengers",
|
|
39
|
+
]
|
|
40
|
+
|
|
32
41
|
|
|
33
42
|
# --- Main Entry Point ---
|
|
34
43
|
def main() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf
RENAMED
|
File without changes
|
{zombie_escape-1.1.4 → zombie_escape-1.2.1}/src/zombie_escape/assets/fonts/misaki_gothic.ttf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|