zombie-escape 1.1.4__tar.gz → 1.2.0__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.0}/PKG-INFO +11 -11
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/README.md +10 -10
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/__about__.py +1 -1
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/constants.py +1 -1
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/gameplay/logic.py +291 -111
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/models.py +3 -1
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/render.py +19 -1
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/screens/gameplay.py +24 -8
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/zombie_escape.py +9 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/.gitignore +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/LICENSE.txt +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/pyproject.toml +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/__init__.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/colors.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/config.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/entities.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/font_utils.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/gameplay/__init__.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/level_blueprints.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/locales/ui.en.json +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/locales/ui.ja.json +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/localization.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/render_assets.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/screens/__init__.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/screens/game_over.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/src/zombie_escape/screens/settings.py +0 -0
- {zombie_escape-1.1.4 → zombie_escape-1.2.0}/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.
|
|
3
|
+
Version: 1.2.0
|
|
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
|
|
@@ -68,6 +68,8 @@ from ..entities import (
|
|
|
68
68
|
Zombie,
|
|
69
69
|
)
|
|
70
70
|
|
|
71
|
+
LOGICAL_SCREEN_RECT = pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
72
|
+
|
|
71
73
|
__all__ = [
|
|
72
74
|
"create_zombie",
|
|
73
75
|
"rect_for_cell",
|
|
@@ -80,8 +82,13 @@ __all__ = [
|
|
|
80
82
|
"scatter_positions_on_walkable",
|
|
81
83
|
"spawn_survivors",
|
|
82
84
|
"update_survivors",
|
|
85
|
+
"alive_waiting_cars",
|
|
86
|
+
"nearest_waiting_car",
|
|
83
87
|
"calculate_car_speed_for_passengers",
|
|
84
88
|
"apply_passenger_speed_penalty",
|
|
89
|
+
"waiting_car_target_count",
|
|
90
|
+
"spawn_waiting_car",
|
|
91
|
+
"maintain_waiting_car_supply",
|
|
85
92
|
"add_survivor_message",
|
|
86
93
|
"random_survivor_conversion_line",
|
|
87
94
|
"cleanup_survivor_messages",
|
|
@@ -91,7 +98,7 @@ __all__ = [
|
|
|
91
98
|
"get_shrunk_sprite",
|
|
92
99
|
"update_footprints",
|
|
93
100
|
"initialize_game_state",
|
|
94
|
-
"
|
|
101
|
+
"setup_player_and_cars",
|
|
95
102
|
"spawn_initial_zombies",
|
|
96
103
|
"process_player_input",
|
|
97
104
|
"update_entities",
|
|
@@ -246,6 +253,8 @@ def place_new_car(
|
|
|
246
253
|
wall_group: pygame.sprite.Group,
|
|
247
254
|
player: Player,
|
|
248
255
|
walkable_cells: list[pygame.Rect],
|
|
256
|
+
*,
|
|
257
|
+
existing_cars: Sequence[Car] | None = None,
|
|
249
258
|
) -> Car | None:
|
|
250
259
|
if not walkable_cells:
|
|
251
260
|
return None
|
|
@@ -268,13 +277,23 @@ def place_new_car(
|
|
|
268
277
|
temp_car, nearby_walls, collided=lambda s1, s2: s1.rect.colliderect(s2.rect)
|
|
269
278
|
)
|
|
270
279
|
collides_player = temp_rect.colliderect(player.rect.inflate(50, 50))
|
|
271
|
-
|
|
280
|
+
car_overlap = False
|
|
281
|
+
if existing_cars:
|
|
282
|
+
car_overlap = any(
|
|
283
|
+
temp_car.rect.colliderect(other.rect)
|
|
284
|
+
for other in existing_cars
|
|
285
|
+
if other and other.alive()
|
|
286
|
+
)
|
|
287
|
+
if not collides_wall and not collides_player and not car_overlap:
|
|
272
288
|
return temp_car
|
|
273
289
|
return None
|
|
274
290
|
|
|
275
291
|
|
|
276
292
|
def place_fuel_can(
|
|
277
|
-
walkable_cells: list[pygame.Rect],
|
|
293
|
+
walkable_cells: list[pygame.Rect],
|
|
294
|
+
player: Player,
|
|
295
|
+
*,
|
|
296
|
+
cars: Sequence[Car] | None = None,
|
|
278
297
|
) -> FuelCan | None:
|
|
279
298
|
"""Pick a spawn spot for the fuel can away from the player (and car if given)."""
|
|
280
299
|
if not walkable_cells:
|
|
@@ -290,14 +309,17 @@ def place_fuel_can(
|
|
|
290
309
|
< min_player_dist
|
|
291
310
|
):
|
|
292
311
|
continue
|
|
293
|
-
if
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
312
|
+
if cars:
|
|
313
|
+
too_close = False
|
|
314
|
+
for parked_car in cars:
|
|
315
|
+
if math.hypot(
|
|
316
|
+
cell.centerx - parked_car.rect.centerx,
|
|
317
|
+
cell.centery - parked_car.rect.centery,
|
|
318
|
+
) < min_car_dist:
|
|
319
|
+
too_close = True
|
|
320
|
+
break
|
|
321
|
+
if too_close:
|
|
322
|
+
continue
|
|
301
323
|
return FuelCan(cell.centerx, cell.centery)
|
|
302
324
|
|
|
303
325
|
# Fallback: drop near a random walkable cell
|
|
@@ -306,7 +328,10 @@ def place_fuel_can(
|
|
|
306
328
|
|
|
307
329
|
|
|
308
330
|
def place_flashlight(
|
|
309
|
-
walkable_cells: list[pygame.Rect],
|
|
331
|
+
walkable_cells: list[pygame.Rect],
|
|
332
|
+
player: Player,
|
|
333
|
+
*,
|
|
334
|
+
cars: Sequence[Car] | None = None,
|
|
310
335
|
) -> Flashlight | None:
|
|
311
336
|
"""Pick a spawn spot for the flashlight away from the player (and car if given)."""
|
|
312
337
|
if not walkable_cells:
|
|
@@ -322,14 +347,16 @@ def place_flashlight(
|
|
|
322
347
|
< min_player_dist
|
|
323
348
|
):
|
|
324
349
|
continue
|
|
325
|
-
if
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
350
|
+
if cars:
|
|
351
|
+
if any(
|
|
352
|
+
math.hypot(
|
|
353
|
+
cell.centerx - parked.rect.centerx,
|
|
354
|
+
cell.centery - parked.rect.centery,
|
|
355
|
+
)
|
|
356
|
+
< min_car_dist
|
|
357
|
+
for parked in cars
|
|
358
|
+
):
|
|
359
|
+
continue
|
|
333
360
|
return Flashlight(cell.centerx, cell.centery)
|
|
334
361
|
|
|
335
362
|
cell = random.choice(walkable_cells)
|
|
@@ -340,7 +367,7 @@ def place_flashlights(
|
|
|
340
367
|
walkable_cells: list[pygame.Rect],
|
|
341
368
|
player: Player,
|
|
342
369
|
*,
|
|
343
|
-
|
|
370
|
+
cars: Sequence[Car] | None = None,
|
|
344
371
|
count: int = DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
345
372
|
) -> list[Flashlight]:
|
|
346
373
|
"""Spawn multiple flashlights using the single-place helper to spread them out."""
|
|
@@ -349,7 +376,7 @@ def place_flashlights(
|
|
|
349
376
|
max_attempts = max(200, count * 80)
|
|
350
377
|
while len(placed) < count and attempts < max_attempts:
|
|
351
378
|
attempts += 1
|
|
352
|
-
fl = place_flashlight(walkable_cells, player,
|
|
379
|
+
fl = place_flashlight(walkable_cells, player, cars=cars)
|
|
353
380
|
if not fl:
|
|
354
381
|
break
|
|
355
382
|
# Avoid clustering too tightly
|
|
@@ -367,7 +394,10 @@ def place_flashlights(
|
|
|
367
394
|
|
|
368
395
|
|
|
369
396
|
def place_companion(
|
|
370
|
-
walkable_cells: list[pygame.Rect],
|
|
397
|
+
walkable_cells: list[pygame.Rect],
|
|
398
|
+
player: Player,
|
|
399
|
+
*,
|
|
400
|
+
cars: Sequence[Car] | None = None,
|
|
371
401
|
) -> Companion | None:
|
|
372
402
|
"""Spawn the stranded buddy somewhere on a walkable tile away from the player and car."""
|
|
373
403
|
if not walkable_cells:
|
|
@@ -383,14 +413,16 @@ def place_companion(
|
|
|
383
413
|
< min_player_dist
|
|
384
414
|
):
|
|
385
415
|
continue
|
|
386
|
-
if
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
416
|
+
if cars:
|
|
417
|
+
if any(
|
|
418
|
+
math.hypot(
|
|
419
|
+
cell.centerx - parked.rect.centerx,
|
|
420
|
+
cell.centery - parked.rect.centery,
|
|
421
|
+
)
|
|
422
|
+
< min_car_dist
|
|
423
|
+
for parked in cars
|
|
424
|
+
):
|
|
425
|
+
continue
|
|
394
426
|
return Companion(cell.centerx, cell.centery)
|
|
395
427
|
|
|
396
428
|
cell = random.choice(walkable_cells)
|
|
@@ -508,8 +540,12 @@ def update_survivors(game_data: GameData) -> None:
|
|
|
508
540
|
other.rect.center = (int(other.x), int(other.y))
|
|
509
541
|
|
|
510
542
|
|
|
511
|
-
def calculate_car_speed_for_passengers(
|
|
512
|
-
|
|
543
|
+
def calculate_car_speed_for_passengers(
|
|
544
|
+
passengers: int, *, capacity: int = SURVIVOR_MAX_SAFE_PASSENGERS
|
|
545
|
+
) -> float:
|
|
546
|
+
cap = max(1, capacity)
|
|
547
|
+
load_ratio = max(0.0, passengers / cap)
|
|
548
|
+
penalty = SURVIVOR_SPEED_PENALTY_PER_PASSENGER * load_ratio
|
|
513
549
|
penalty = min(0.95, max(0.0, penalty))
|
|
514
550
|
adjusted = CAR_SPEED * (1 - penalty)
|
|
515
551
|
return max(CAR_SPEED * SURVIVOR_MIN_SPEED_FACTOR, adjusted)
|
|
@@ -522,7 +558,86 @@ def apply_passenger_speed_penalty(game_data: GameData) -> None:
|
|
|
522
558
|
if not game_data.stage.survivor_stage:
|
|
523
559
|
car.speed = CAR_SPEED
|
|
524
560
|
return
|
|
525
|
-
car.speed = calculate_car_speed_for_passengers(
|
|
561
|
+
car.speed = calculate_car_speed_for_passengers(
|
|
562
|
+
game_data.state.survivors_onboard,
|
|
563
|
+
capacity=game_data.state.survivor_capacity,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def rect_visible_on_screen(camera: Camera | None, rect: pygame.Rect) -> bool:
|
|
568
|
+
if camera is None:
|
|
569
|
+
return False
|
|
570
|
+
return camera.apply_rect(rect).colliderect(LOGICAL_SCREEN_RECT)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def waiting_car_target_count(stage: Stage) -> int:
|
|
574
|
+
return 3 if stage.survivor_stage else 1
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def spawn_waiting_car(game_data: GameData) -> Car | None:
|
|
578
|
+
"""Attempt to place an additional parked car on the map."""
|
|
579
|
+
player = game_data.player
|
|
580
|
+
if not player:
|
|
581
|
+
return None
|
|
582
|
+
walkable_cells = game_data.areas.walkable_cells
|
|
583
|
+
if not walkable_cells:
|
|
584
|
+
return None
|
|
585
|
+
wall_group = game_data.groups.wall_group
|
|
586
|
+
all_sprites = game_data.groups.all_sprites
|
|
587
|
+
active_car = game_data.car if game_data.car and game_data.car.alive() else None
|
|
588
|
+
waiting = alive_waiting_cars(game_data)
|
|
589
|
+
obstacles: list[Car] = list(waiting)
|
|
590
|
+
if active_car:
|
|
591
|
+
obstacles.append(active_car)
|
|
592
|
+
camera = game_data.camera
|
|
593
|
+
offscreen_attempts = 6
|
|
594
|
+
while offscreen_attempts > 0:
|
|
595
|
+
new_car = place_new_car(
|
|
596
|
+
wall_group,
|
|
597
|
+
player,
|
|
598
|
+
walkable_cells,
|
|
599
|
+
existing_cars=obstacles,
|
|
600
|
+
)
|
|
601
|
+
if not new_car:
|
|
602
|
+
return None
|
|
603
|
+
if rect_visible_on_screen(camera, new_car.rect):
|
|
604
|
+
offscreen_attempts -= 1
|
|
605
|
+
continue
|
|
606
|
+
game_data.waiting_cars.append(new_car)
|
|
607
|
+
all_sprites.add(new_car, layer=1)
|
|
608
|
+
return new_car
|
|
609
|
+
return None
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def maintain_waiting_car_supply(game_data: GameData) -> None:
|
|
613
|
+
"""Ensure at least one parked car remains on the field."""
|
|
614
|
+
minimum = 1
|
|
615
|
+
current = len(alive_waiting_cars(game_data))
|
|
616
|
+
while current < minimum:
|
|
617
|
+
new_car = spawn_waiting_car(game_data)
|
|
618
|
+
if not new_car:
|
|
619
|
+
break
|
|
620
|
+
current += 1
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def alive_waiting_cars(game_data: GameData) -> list[Car]:
|
|
624
|
+
"""Return the list of parked cars that still exist, pruning any destroyed sprites."""
|
|
625
|
+
cars = [car for car in game_data.waiting_cars if car.alive()]
|
|
626
|
+
game_data.waiting_cars = cars
|
|
627
|
+
return cars
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def nearest_waiting_car(
|
|
631
|
+
game_data: GameData, origin: tuple[float, float]
|
|
632
|
+
) -> Car | None:
|
|
633
|
+
"""Find the closest waiting car to an origin point."""
|
|
634
|
+
cars = alive_waiting_cars(game_data)
|
|
635
|
+
if not cars:
|
|
636
|
+
return None
|
|
637
|
+
return min(
|
|
638
|
+
cars,
|
|
639
|
+
key=lambda car: math.hypot(car.rect.centerx - origin[0], car.rect.centery - origin[1]),
|
|
640
|
+
)
|
|
526
641
|
|
|
527
642
|
|
|
528
643
|
def add_survivor_message(game_data: GameData, text: str) -> None:
|
|
@@ -592,7 +707,6 @@ def handle_survivor_zombie_collisions(
|
|
|
592
707
|
zombies.sort(key=lambda s: s.rect.centerx)
|
|
593
708
|
zombie_xs = [z.rect.centerx for z in zombies]
|
|
594
709
|
camera = game_data.camera
|
|
595
|
-
screen_rect = pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
596
710
|
|
|
597
711
|
for survivor in list(survivor_group):
|
|
598
712
|
if not survivor.alive():
|
|
@@ -622,7 +736,7 @@ def handle_survivor_zombie_collisions(
|
|
|
622
736
|
|
|
623
737
|
if not collided:
|
|
624
738
|
continue
|
|
625
|
-
if not camera
|
|
739
|
+
if not rect_visible_on_screen(camera, survivor.rect):
|
|
626
740
|
continue
|
|
627
741
|
survivor.kill()
|
|
628
742
|
line = random_survivor_conversion_line()
|
|
@@ -749,6 +863,7 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
749
863
|
survivors_onboard=0,
|
|
750
864
|
survivors_rescued=0,
|
|
751
865
|
survivor_messages=[],
|
|
866
|
+
survivor_capacity=SURVIVOR_MAX_SAFE_PASSENGERS,
|
|
752
867
|
)
|
|
753
868
|
|
|
754
869
|
# Create sprite groups
|
|
@@ -796,10 +911,13 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
796
911
|
)
|
|
797
912
|
|
|
798
913
|
|
|
799
|
-
def
|
|
800
|
-
game_data: GameData,
|
|
801
|
-
|
|
802
|
-
|
|
914
|
+
def setup_player_and_cars(
|
|
915
|
+
game_data: GameData,
|
|
916
|
+
layout_data: Mapping[str, list[pygame.Rect]],
|
|
917
|
+
*,
|
|
918
|
+
car_count: int = 1,
|
|
919
|
+
) -> tuple[Player, list[Car]]:
|
|
920
|
+
"""Create the player plus one or more parked cars using blueprint candidates."""
|
|
803
921
|
all_sprites = game_data.groups.all_sprites
|
|
804
922
|
walkable_cells: list[pygame.Rect] = layout_data["walkable_cells"]
|
|
805
923
|
|
|
@@ -813,31 +931,38 @@ def setup_player_and_car(
|
|
|
813
931
|
player_pos = pick_center(layout_data["player_cells"] or walkable_cells)
|
|
814
932
|
player = Player(*player_pos)
|
|
815
933
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
934
|
+
car_candidates = list(layout_data["car_cells"] or walkable_cells)
|
|
935
|
+
waiting_cars: list[Car] = []
|
|
936
|
+
|
|
937
|
+
def _pick_car_position() -> tuple[int, int]:
|
|
938
|
+
"""Favor distant cells for the first car, otherwise fall back to random picks."""
|
|
939
|
+
if not car_candidates:
|
|
940
|
+
return (player_pos[0] + 200, player_pos[1])
|
|
941
|
+
random.shuffle(car_candidates)
|
|
942
|
+
for candidate in car_candidates:
|
|
943
|
+
if (
|
|
944
|
+
math.hypot(
|
|
945
|
+
candidate.centerx - player_pos[0],
|
|
946
|
+
candidate.centery - player_pos[1],
|
|
947
|
+
)
|
|
948
|
+
>= 400
|
|
949
|
+
):
|
|
950
|
+
car_candidates.remove(candidate)
|
|
951
|
+
return candidate.center
|
|
952
|
+
# No far-enough cells found; pick the first available
|
|
953
|
+
choice = car_candidates.pop()
|
|
954
|
+
return choice.center
|
|
955
|
+
|
|
956
|
+
for idx in range(max(1, car_count)):
|
|
957
|
+
car_pos = _pick_car_position()
|
|
958
|
+
car = Car(*car_pos)
|
|
959
|
+
waiting_cars.append(car)
|
|
960
|
+
all_sprites.add(car, layer=1)
|
|
961
|
+
if not car_candidates:
|
|
828
962
|
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
|
-
|
|
834
|
-
car = Car(*car_pos)
|
|
835
963
|
|
|
836
|
-
# Add to sprite groups
|
|
837
964
|
all_sprites.add(player, layer=2)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
return player, car
|
|
965
|
+
return player, waiting_cars
|
|
841
966
|
|
|
842
967
|
|
|
843
968
|
def spawn_initial_zombies(
|
|
@@ -878,7 +1003,7 @@ def spawn_initial_zombies(
|
|
|
878
1003
|
|
|
879
1004
|
|
|
880
1005
|
def process_player_input(
|
|
881
|
-
keys: Sequence[bool], player: Player, car: Car
|
|
1006
|
+
keys: Sequence[bool], player: Player, car: Car | None
|
|
882
1007
|
) -> tuple[float, float, float, float]:
|
|
883
1008
|
"""Process keyboard input and return movement deltas."""
|
|
884
1009
|
dx_input, dy_input = 0, 0
|
|
@@ -893,7 +1018,7 @@ def process_player_input(
|
|
|
893
1018
|
|
|
894
1019
|
player_dx, player_dy, car_dx, car_dy = 0, 0, 0, 0
|
|
895
1020
|
|
|
896
|
-
if player.in_car and car.alive():
|
|
1021
|
+
if player.in_car and car and car.alive():
|
|
897
1022
|
target_speed = getattr(car, "speed", CAR_SPEED)
|
|
898
1023
|
move_len = math.hypot(dx_input, dy_input)
|
|
899
1024
|
if move_len > 0:
|
|
@@ -925,32 +1050,35 @@ def update_entities(
|
|
|
925
1050
|
player = game_data.player
|
|
926
1051
|
assert player is not None
|
|
927
1052
|
car = game_data.car
|
|
928
|
-
assert car is not None
|
|
929
1053
|
companion = game_data.companion
|
|
930
1054
|
wall_group = game_data.groups.wall_group
|
|
931
1055
|
all_sprites = game_data.groups.all_sprites
|
|
932
1056
|
zombie_group = game_data.groups.zombie_group
|
|
933
1057
|
camera = game_data.camera
|
|
934
1058
|
stage = game_data.stage
|
|
1059
|
+
active_car = car if car and car.alive() else None
|
|
935
1060
|
|
|
936
1061
|
# Update player/car movement
|
|
937
|
-
if player.in_car and
|
|
938
|
-
|
|
939
|
-
player.rect.center =
|
|
940
|
-
player.x, player.y =
|
|
1062
|
+
if player.in_car and active_car:
|
|
1063
|
+
active_car.move(car_dx, car_dy, wall_group)
|
|
1064
|
+
player.rect.center = active_car.rect.center
|
|
1065
|
+
player.x, player.y = active_car.x, active_car.y
|
|
941
1066
|
elif not player.in_car:
|
|
942
1067
|
# Ensure player is in all_sprites if not in car
|
|
943
1068
|
if player not in all_sprites:
|
|
944
1069
|
all_sprites.add(player, layer=2)
|
|
945
1070
|
player.move(player_dx, player_dy, wall_group)
|
|
1071
|
+
else:
|
|
1072
|
+
# Player flagged as in-car but car is gone; drop them back to foot control
|
|
1073
|
+
player.in_car = False
|
|
946
1074
|
|
|
947
1075
|
# Update camera
|
|
948
|
-
target_for_camera =
|
|
1076
|
+
target_for_camera = active_car if player.in_car and active_car else player
|
|
949
1077
|
camera.update(target_for_camera)
|
|
950
1078
|
|
|
951
1079
|
# Update companion (Stage 3 follow logic)
|
|
952
1080
|
if companion and companion.alive() and not companion.rescued:
|
|
953
|
-
follow_target =
|
|
1081
|
+
follow_target = active_car if player.in_car and active_car else player
|
|
954
1082
|
companion.update_follow(follow_target.rect.center, wall_group)
|
|
955
1083
|
if companion not in all_sprites:
|
|
956
1084
|
all_sprites.add(companion, layer=2)
|
|
@@ -971,24 +1099,23 @@ def update_entities(
|
|
|
971
1099
|
|
|
972
1100
|
# Update zombies
|
|
973
1101
|
target_center = (
|
|
974
|
-
|
|
1102
|
+
active_car.rect.center if player.in_car and active_car else player.rect.center
|
|
975
1103
|
)
|
|
976
1104
|
companion_on_screen = False
|
|
977
|
-
screen_rect = pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
978
1105
|
if (
|
|
979
1106
|
game_data.stage.requires_companion
|
|
980
1107
|
and companion
|
|
981
1108
|
and companion.alive()
|
|
982
1109
|
and not companion.rescued
|
|
983
1110
|
):
|
|
984
|
-
companion_on_screen = camera
|
|
1111
|
+
companion_on_screen = rect_visible_on_screen(camera, companion.rect)
|
|
985
1112
|
|
|
986
1113
|
survivors_on_screen: list[Survivor] = []
|
|
987
1114
|
if stage.survivor_stage:
|
|
988
1115
|
survivor_group = game_data.groups.survivor_group
|
|
989
1116
|
for survivor in survivor_group:
|
|
990
1117
|
if getattr(survivor, "alive", lambda: False)():
|
|
991
|
-
if camera
|
|
1118
|
+
if rect_visible_on_screen(camera, survivor.rect):
|
|
992
1119
|
survivors_on_screen.append(survivor)
|
|
993
1120
|
|
|
994
1121
|
zombies_sorted: list[Zombie] = sorted(list(zombie_group), key=lambda z: z.x)
|
|
@@ -1024,7 +1151,7 @@ def update_entities(
|
|
|
1024
1151
|
target = companion.rect.center
|
|
1025
1152
|
|
|
1026
1153
|
if stage.survivor_stage:
|
|
1027
|
-
zombie_on_screen = camera
|
|
1154
|
+
zombie_on_screen = rect_visible_on_screen(camera, zombie.rect)
|
|
1028
1155
|
if zombie_on_screen:
|
|
1029
1156
|
candidate_positions: list[tuple[int, int]] = []
|
|
1030
1157
|
for survivor in survivors_on_screen:
|
|
@@ -1050,10 +1177,8 @@ def check_interactions(
|
|
|
1050
1177
|
player = game_data.player
|
|
1051
1178
|
assert player is not None
|
|
1052
1179
|
car = game_data.car
|
|
1053
|
-
assert car is not None
|
|
1054
1180
|
companion = game_data.companion
|
|
1055
1181
|
zombie_group = game_data.groups.zombie_group
|
|
1056
|
-
wall_group = game_data.groups.wall_group
|
|
1057
1182
|
all_sprites = game_data.groups.all_sprites
|
|
1058
1183
|
survivor_group = game_data.groups.survivor_group
|
|
1059
1184
|
state = game_data.state
|
|
@@ -1063,6 +1188,9 @@ def check_interactions(
|
|
|
1063
1188
|
flashlights = game_data.flashlights or []
|
|
1064
1189
|
camera = game_data.camera
|
|
1065
1190
|
stage = game_data.stage
|
|
1191
|
+
maintain_waiting_car_supply(game_data)
|
|
1192
|
+
active_car = car if car and car.alive() else None
|
|
1193
|
+
waiting_cars = game_data.waiting_cars
|
|
1066
1194
|
|
|
1067
1195
|
# Fuel pickup
|
|
1068
1196
|
if fuel and fuel.alive() and not state.has_fuel and not player.in_car:
|
|
@@ -1104,8 +1232,7 @@ def check_interactions(
|
|
|
1104
1232
|
)
|
|
1105
1233
|
if companion_active:
|
|
1106
1234
|
assert companion is not None
|
|
1107
|
-
|
|
1108
|
-
companion_on_screen = camera.apply_rect(companion.rect).colliderect(screen_rect)
|
|
1235
|
+
companion_on_screen = rect_visible_on_screen(camera, companion.rect)
|
|
1109
1236
|
|
|
1110
1237
|
# Companion interactions (Stage 3)
|
|
1111
1238
|
if companion_active and stage.requires_companion:
|
|
@@ -1113,10 +1240,10 @@ def check_interactions(
|
|
|
1113
1240
|
if not player.in_car:
|
|
1114
1241
|
if pygame.sprite.collide_circle(companion, player):
|
|
1115
1242
|
companion.set_following()
|
|
1116
|
-
elif player.in_car and
|
|
1243
|
+
elif player.in_car and active_car:
|
|
1117
1244
|
g = pygame.sprite.Group()
|
|
1118
1245
|
g.add(companion)
|
|
1119
|
-
if pygame.sprite.spritecollide(get_shrunk_sprite(
|
|
1246
|
+
if pygame.sprite.spritecollide(get_shrunk_sprite(active_car, 0.8), g, False):
|
|
1120
1247
|
state.companion_rescued = True
|
|
1121
1248
|
companion.mark_rescued()
|
|
1122
1249
|
companion.kill()
|
|
@@ -1140,13 +1267,18 @@ def check_interactions(
|
|
|
1140
1267
|
companion.teleport((LEVEL_WIDTH // 2, LEVEL_HEIGHT // 2))
|
|
1141
1268
|
companion.following = False
|
|
1142
1269
|
|
|
1143
|
-
|
|
1270
|
+
player_group = pygame.sprite.Group()
|
|
1271
|
+
player_group.add(player)
|
|
1144
1272
|
|
|
1145
|
-
# Player entering car
|
|
1146
|
-
if
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1273
|
+
# Player entering an active car already under control
|
|
1274
|
+
if (
|
|
1275
|
+
not player.in_car
|
|
1276
|
+
and active_car
|
|
1277
|
+
and active_car.alive()
|
|
1278
|
+
and active_car.health > 0
|
|
1279
|
+
):
|
|
1280
|
+
shrunk_active = get_shrunk_sprite(active_car, 0.8)
|
|
1281
|
+
if pygame.sprite.spritecollide(shrunk_active, player_group, False):
|
|
1150
1282
|
if state.has_fuel:
|
|
1151
1283
|
player.in_car = True
|
|
1152
1284
|
all_sprites.remove(player)
|
|
@@ -1156,19 +1288,72 @@ def check_interactions(
|
|
|
1156
1288
|
else:
|
|
1157
1289
|
now_ms = state.elapsed_play_ms
|
|
1158
1290
|
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
1291
|
state.hint_target_type = "fuel"
|
|
1161
1292
|
|
|
1293
|
+
# Claim a waiting/parked car when the player finally reaches it
|
|
1294
|
+
if not player.in_car and not active_car and waiting_cars:
|
|
1295
|
+
claimed_car: Car | None = None
|
|
1296
|
+
for parked_car in waiting_cars:
|
|
1297
|
+
shrunk_waiting = get_shrunk_sprite(parked_car, 0.8)
|
|
1298
|
+
if pygame.sprite.spritecollide(shrunk_waiting, player_group, False):
|
|
1299
|
+
claimed_car = parked_car
|
|
1300
|
+
break
|
|
1301
|
+
if claimed_car:
|
|
1302
|
+
if state.has_fuel:
|
|
1303
|
+
try:
|
|
1304
|
+
game_data.waiting_cars.remove(claimed_car)
|
|
1305
|
+
except ValueError:
|
|
1306
|
+
pass
|
|
1307
|
+
game_data.car = claimed_car
|
|
1308
|
+
active_car = claimed_car
|
|
1309
|
+
player.in_car = True
|
|
1310
|
+
all_sprites.remove(player)
|
|
1311
|
+
state.hint_expires_at = 0
|
|
1312
|
+
state.hint_target_type = None
|
|
1313
|
+
apply_passenger_speed_penalty(game_data)
|
|
1314
|
+
maintain_waiting_car_supply(game_data)
|
|
1315
|
+
print("Player claimed a waiting car!")
|
|
1316
|
+
else:
|
|
1317
|
+
now_ms = state.elapsed_play_ms
|
|
1318
|
+
state.fuel_message_until = now_ms + FUEL_HINT_DURATION_MS
|
|
1319
|
+
state.hint_target_type = "fuel"
|
|
1320
|
+
|
|
1321
|
+
shrunk_car = get_shrunk_sprite(active_car, 0.8) if active_car else None
|
|
1322
|
+
|
|
1323
|
+
# Bonus: collide a parked car while driving to repair/extend capabilities
|
|
1324
|
+
if player.in_car and active_car and shrunk_car and waiting_cars:
|
|
1325
|
+
waiting_group = pygame.sprite.Group(waiting_cars)
|
|
1326
|
+
collided_waiters = pygame.sprite.spritecollide(
|
|
1327
|
+
shrunk_car, waiting_group, False, pygame.sprite.collide_rect
|
|
1328
|
+
)
|
|
1329
|
+
if collided_waiters:
|
|
1330
|
+
removed_any = False
|
|
1331
|
+
for parked in collided_waiters:
|
|
1332
|
+
if not parked.alive():
|
|
1333
|
+
continue
|
|
1334
|
+
parked.kill()
|
|
1335
|
+
try:
|
|
1336
|
+
game_data.waiting_cars.remove(parked)
|
|
1337
|
+
except ValueError:
|
|
1338
|
+
pass
|
|
1339
|
+
active_car.health = active_car.max_health
|
|
1340
|
+
active_car.update_color()
|
|
1341
|
+
removed_any = True
|
|
1342
|
+
if stage.survivor_stage:
|
|
1343
|
+
state.survivor_capacity += SURVIVOR_MAX_SAFE_PASSENGERS
|
|
1344
|
+
if removed_any:
|
|
1345
|
+
maintain_waiting_car_supply(game_data)
|
|
1346
|
+
|
|
1162
1347
|
# Car hitting zombies
|
|
1163
|
-
if player.in_car and
|
|
1348
|
+
if player.in_car and active_car and active_car.health > 0 and shrunk_car:
|
|
1164
1349
|
zombies_hit = pygame.sprite.spritecollide(shrunk_car, zombie_group, True)
|
|
1165
1350
|
if zombies_hit:
|
|
1166
|
-
|
|
1351
|
+
active_car.take_damage(CAR_ZOMBIE_DAMAGE * len(zombies_hit))
|
|
1167
1352
|
|
|
1168
1353
|
if (
|
|
1169
1354
|
stage.survivor_stage
|
|
1170
1355
|
and player.in_car
|
|
1171
|
-
and
|
|
1356
|
+
and active_car
|
|
1172
1357
|
and shrunk_car
|
|
1173
1358
|
and survivor_group
|
|
1174
1359
|
):
|
|
@@ -1178,18 +1363,20 @@ def check_interactions(
|
|
|
1178
1363
|
if boarded:
|
|
1179
1364
|
state.survivors_onboard += len(boarded)
|
|
1180
1365
|
apply_passenger_speed_penalty(game_data)
|
|
1181
|
-
|
|
1366
|
+
capacity_limit = state.survivor_capacity
|
|
1367
|
+
if state.survivors_onboard > capacity_limit:
|
|
1182
1368
|
overload_damage = max(
|
|
1183
|
-
1,
|
|
1369
|
+
1,
|
|
1370
|
+
int(active_car.max_health * SURVIVOR_OVERLOAD_DAMAGE_RATIO),
|
|
1184
1371
|
)
|
|
1185
1372
|
add_survivor_message(game_data, _("survivors.too_many_aboard"))
|
|
1186
|
-
|
|
1373
|
+
active_car.take_damage(overload_damage)
|
|
1187
1374
|
|
|
1188
1375
|
if stage.survivor_stage:
|
|
1189
1376
|
handle_survivor_zombie_collisions(game_data, config)
|
|
1190
1377
|
|
|
1191
1378
|
# Handle car destruction
|
|
1192
|
-
if car.alive() and car.health <= 0:
|
|
1379
|
+
if car and car.alive() and car.health <= 0:
|
|
1193
1380
|
car_destroyed_pos = car.rect.center
|
|
1194
1381
|
car.kill()
|
|
1195
1382
|
if stage.survivor_stage:
|
|
@@ -1202,21 +1389,14 @@ def check_interactions(
|
|
|
1202
1389
|
all_sprites.add(player, layer=2)
|
|
1203
1390
|
print("Car destroyed! Player ejected.")
|
|
1204
1391
|
|
|
1392
|
+
# Clear active car and let the player hunt for another waiting car.
|
|
1393
|
+
game_data.car = None
|
|
1394
|
+
state.survivor_capacity = SURVIVOR_MAX_SAFE_PASSENGERS
|
|
1395
|
+
apply_passenger_speed_penalty(game_data)
|
|
1396
|
+
|
|
1205
1397
|
# Bring back the rescued companion near the player after losing the car
|
|
1206
1398
|
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!")
|
|
1399
|
+
maintain_waiting_car_supply(game_data)
|
|
1220
1400
|
|
|
1221
1401
|
# Player getting caught by zombies
|
|
1222
1402
|
if not player.in_car and player in all_sprites:
|
|
@@ -1230,7 +1410,7 @@ def check_interactions(
|
|
|
1230
1410
|
state.game_over_message = _("game_over.scream")
|
|
1231
1411
|
|
|
1232
1412
|
# Player escaping the level
|
|
1233
|
-
if player.in_car and car.alive() and state.has_fuel:
|
|
1413
|
+
if player.in_car and car and car.alive() and state.has_fuel:
|
|
1234
1414
|
companion_ready = not stage.requires_companion or state.companion_rescued
|
|
1235
1415
|
if companion_ready and any(
|
|
1236
1416
|
outside.collidepoint(car.rect.center) for outside in outside_rects
|
|
@@ -1244,5 +1424,5 @@ def check_interactions(
|
|
|
1244
1424
|
|
|
1245
1425
|
# Return fog of view target
|
|
1246
1426
|
if not state.game_over and not state.game_won:
|
|
1247
|
-
return car if player.in_car and car.alive() else player
|
|
1427
|
+
return car if player.in_car and car and car.alive() else player
|
|
1248
1428
|
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,6 +75,7 @@ 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
|
|
78
80
|
|
|
79
81
|
|
|
@@ -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
|
)
|
|
@@ -41,9 +41,15 @@ def gameplay_screen(
|
|
|
41
41
|
last_fov_target = None
|
|
42
42
|
|
|
43
43
|
layout_data = logic.generate_level_from_blueprint(game_data, config)
|
|
44
|
-
|
|
44
|
+
initial_waiting = 3 if stage.survivor_stage else 1
|
|
45
|
+
player, waiting_cars = logic.setup_player_and_cars(
|
|
46
|
+
game_data, layout_data, car_count=initial_waiting
|
|
47
|
+
)
|
|
45
48
|
game_data.player = player
|
|
46
|
-
game_data.
|
|
49
|
+
game_data.waiting_cars = waiting_cars
|
|
50
|
+
game_data.car = None
|
|
51
|
+
# Only top up if initial placement spawned fewer than the intended baseline (shouldn't happen)
|
|
52
|
+
logic.maintain_waiting_car_supply(game_data)
|
|
47
53
|
logic.apply_passenger_speed_penalty(game_data)
|
|
48
54
|
|
|
49
55
|
if stage.survivor_stage:
|
|
@@ -59,7 +65,7 @@ def gameplay_screen(
|
|
|
59
65
|
|
|
60
66
|
if stage.requires_fuel:
|
|
61
67
|
fuel_can = logic.place_fuel_can(
|
|
62
|
-
layout_data["walkable_cells"], player,
|
|
68
|
+
layout_data["walkable_cells"], player, cars=game_data.waiting_cars
|
|
63
69
|
)
|
|
64
70
|
if fuel_can:
|
|
65
71
|
game_data.fuel = fuel_can
|
|
@@ -68,7 +74,7 @@ def gameplay_screen(
|
|
|
68
74
|
flashlights = logic.place_flashlights(
|
|
69
75
|
layout_data["walkable_cells"],
|
|
70
76
|
player,
|
|
71
|
-
|
|
77
|
+
cars=game_data.waiting_cars,
|
|
72
78
|
count=max(1, flashlight_count),
|
|
73
79
|
)
|
|
74
80
|
game_data.flashlights = flashlights
|
|
@@ -76,7 +82,7 @@ def gameplay_screen(
|
|
|
76
82
|
|
|
77
83
|
if stage.requires_companion:
|
|
78
84
|
companion = logic.place_companion(
|
|
79
|
-
layout_data["walkable_cells"], player,
|
|
85
|
+
layout_data["walkable_cells"], player, cars=game_data.waiting_cars
|
|
80
86
|
)
|
|
81
87
|
if companion:
|
|
82
88
|
game_data.companion = companion
|
|
@@ -222,10 +228,13 @@ def gameplay_screen(
|
|
|
222
228
|
hint_expires_at = game_data.state.hint_expires_at
|
|
223
229
|
hint_target_type = game_data.state.hint_target_type
|
|
224
230
|
|
|
231
|
+
active_car = game_data.car if game_data.car and game_data.car.alive() else None
|
|
225
232
|
if hint_enabled:
|
|
226
233
|
if not has_fuel and game_data.fuel and game_data.fuel.alive():
|
|
227
234
|
target_type = "fuel"
|
|
228
|
-
elif not player.in_car and
|
|
235
|
+
elif not player.in_car and (
|
|
236
|
+
active_car or logic.alive_waiting_cars(game_data)
|
|
237
|
+
):
|
|
229
238
|
target_type = "car"
|
|
230
239
|
else:
|
|
231
240
|
target_type = None
|
|
@@ -246,8 +255,15 @@ def gameplay_screen(
|
|
|
246
255
|
):
|
|
247
256
|
if target_type == "fuel" and game_data.fuel and game_data.fuel.alive():
|
|
248
257
|
hint_target = game_data.fuel.rect.center
|
|
249
|
-
elif target_type == "car"
|
|
250
|
-
|
|
258
|
+
elif target_type == "car":
|
|
259
|
+
if active_car:
|
|
260
|
+
hint_target = active_car.rect.center
|
|
261
|
+
else:
|
|
262
|
+
waiting_target = logic.nearest_waiting_car(
|
|
263
|
+
game_data, (player.x, player.y)
|
|
264
|
+
)
|
|
265
|
+
if waiting_target:
|
|
266
|
+
hint_target = waiting_target.rect.center
|
|
251
267
|
|
|
252
268
|
draw(
|
|
253
269
|
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.0}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf
RENAMED
|
File without changes
|
{zombie_escape-1.1.4 → zombie_escape-1.2.0}/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
|