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