zombie-escape 1.14.4__py3-none-any.whl → 1.15.2__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/config.py +1 -0
- zombie_escape/entities.py +126 -199
- zombie_escape/entities_constants.py +11 -1
- zombie_escape/export_images.py +4 -4
- zombie_escape/font_utils.py +47 -0
- zombie_escape/gameplay/__init__.py +2 -1
- zombie_escape/gameplay/constants.py +4 -0
- zombie_escape/gameplay/interactions.py +83 -16
- zombie_escape/gameplay/layout.py +9 -15
- zombie_escape/gameplay/movement.py +45 -29
- zombie_escape/gameplay/spawn.py +15 -29
- zombie_escape/gameplay/state.py +62 -7
- zombie_escape/gameplay/survivors.py +61 -10
- zombie_escape/gameplay/utils.py +33 -0
- zombie_escape/level_blueprints.py +35 -31
- zombie_escape/level_constants.py +2 -2
- zombie_escape/locales/ui.en.json +19 -8
- zombie_escape/locales/ui.ja.json +19 -8
- zombie_escape/localization.py +7 -1
- zombie_escape/models.py +21 -6
- zombie_escape/render/__init__.py +2 -2
- zombie_escape/render/core.py +113 -81
- zombie_escape/render/hud.py +112 -40
- zombie_escape/render/overview.py +93 -2
- zombie_escape/render/shadows.py +2 -2
- zombie_escape/render_constants.py +12 -0
- zombie_escape/screens/__init__.py +6 -189
- zombie_escape/screens/game_over.py +8 -21
- zombie_escape/screens/gameplay.py +71 -26
- zombie_escape/screens/settings.py +114 -43
- zombie_escape/screens/title.py +128 -47
- zombie_escape/stage_constants.py +37 -8
- zombie_escape/windowing.py +508 -0
- zombie_escape/world_grid.py +7 -5
- zombie_escape/zombie_escape.py +26 -13
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/METADATA +24 -24
- zombie_escape-1.15.2.dist-info/RECORD +54 -0
- zombie_escape-1.14.4.dist-info/RECORD +0 -53
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/WHEEL +0 -0
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/font_utils.py
CHANGED
|
@@ -41,5 +41,52 @@ def load_font(resource: str | None, size: int) -> pygame.font.Font:
|
|
|
41
41
|
return font
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def render_text_scaled(
|
|
45
|
+
resource: str | None,
|
|
46
|
+
size: int,
|
|
47
|
+
text: str,
|
|
48
|
+
color: tuple[int, int, int],
|
|
49
|
+
*,
|
|
50
|
+
scale_factor: int = 1,
|
|
51
|
+
antialias: bool = False,
|
|
52
|
+
) -> pygame.Surface:
|
|
53
|
+
"""Render text, optionally supersampling then downscaling."""
|
|
54
|
+
normalized_size = max(1, int(size))
|
|
55
|
+
if scale_factor <= 1:
|
|
56
|
+
font = load_font(resource, normalized_size)
|
|
57
|
+
return font.render(text, antialias, color)
|
|
58
|
+
high_size = max(1, int(round(normalized_size * scale_factor)))
|
|
59
|
+
font_high = load_font(resource, high_size)
|
|
60
|
+
high_surface = font_high.render(text, antialias, color)
|
|
61
|
+
target_width = max(1, int(round(high_surface.get_width() / scale_factor)))
|
|
62
|
+
target_height = max(1, int(round(high_surface.get_height() / scale_factor)))
|
|
63
|
+
return pygame.transform.scale(high_surface, (target_width, target_height))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def blit_text_scaled(
|
|
67
|
+
target: pygame.Surface,
|
|
68
|
+
resource: str | None,
|
|
69
|
+
size: int,
|
|
70
|
+
text: str,
|
|
71
|
+
color: tuple[int, int, int],
|
|
72
|
+
*,
|
|
73
|
+
scale_factor: int = 1,
|
|
74
|
+
antialias: bool = False,
|
|
75
|
+
**rect_kwargs: int | tuple[int, int],
|
|
76
|
+
) -> pygame.Rect:
|
|
77
|
+
"""Render scaled text and blit it to target using rect kwargs."""
|
|
78
|
+
surface = render_text_scaled(
|
|
79
|
+
resource,
|
|
80
|
+
size,
|
|
81
|
+
text,
|
|
82
|
+
color,
|
|
83
|
+
scale_factor=scale_factor,
|
|
84
|
+
antialias=antialias,
|
|
85
|
+
)
|
|
86
|
+
rect = surface.get_rect(**rect_kwargs)
|
|
87
|
+
target.blit(surface, rect)
|
|
88
|
+
return rect
|
|
89
|
+
|
|
90
|
+
|
|
44
91
|
def clear_font_cache() -> None:
|
|
45
92
|
_FONT_CACHE.clear()
|
|
@@ -22,7 +22,7 @@ from .spawn import (
|
|
|
22
22
|
spawn_waiting_car,
|
|
23
23
|
spawn_weighted_zombie,
|
|
24
24
|
)
|
|
25
|
-
from .state import carbonize_outdoor_zombies, initialize_game_state, update_endurance_timer
|
|
25
|
+
from .state import carbonize_outdoor_zombies, initialize_game_state, schedule_timed_message, update_endurance_timer
|
|
26
26
|
from .survivors import (
|
|
27
27
|
add_survivor_message,
|
|
28
28
|
apply_passenger_speed_penalty,
|
|
@@ -72,6 +72,7 @@ __all__ = [
|
|
|
72
72
|
"get_shrunk_sprite",
|
|
73
73
|
"update_footprints",
|
|
74
74
|
"initialize_game_state",
|
|
75
|
+
"schedule_timed_message",
|
|
75
76
|
"setup_player_and_cars",
|
|
76
77
|
"spawn_initial_zombies",
|
|
77
78
|
"update_endurance_timer",
|
|
@@ -8,6 +8,8 @@ from ..entities_constants import ZOMBIE_AGING_DURATION_FRAMES
|
|
|
8
8
|
SURVIVOR_SPEED_PENALTY_PER_PASSENGER = 0.08
|
|
9
9
|
SURVIVOR_OVERLOAD_DAMAGE_RATIO = 0.2
|
|
10
10
|
SURVIVOR_MESSAGE_DURATION_MS = 2000
|
|
11
|
+
INTRO_MESSAGE_DISPLAY_FRAMES = 180
|
|
12
|
+
SCREAM_MESSAGE_DISPLAY_FRAMES = 60
|
|
11
13
|
|
|
12
14
|
# --- Footprint settings (gameplay) ---
|
|
13
15
|
FOOTPRINT_STEP_DISTANCE = 40
|
|
@@ -32,6 +34,8 @@ __all__ = [
|
|
|
32
34
|
"SURVIVOR_SPEED_PENALTY_PER_PASSENGER",
|
|
33
35
|
"SURVIVOR_OVERLOAD_DAMAGE_RATIO",
|
|
34
36
|
"SURVIVOR_MESSAGE_DURATION_MS",
|
|
37
|
+
"INTRO_MESSAGE_DISPLAY_FRAMES",
|
|
38
|
+
"SCREAM_MESSAGE_DISPLAY_FRAMES",
|
|
35
39
|
"FOOTPRINT_STEP_DISTANCE",
|
|
36
40
|
"FOOTPRINT_MAX",
|
|
37
41
|
"MAX_ZOMBIES",
|
|
@@ -5,6 +5,9 @@ from typing import Any
|
|
|
5
5
|
import pygame
|
|
6
6
|
|
|
7
7
|
from ..entities_constants import (
|
|
8
|
+
BUDDY_FOLLOW_START_DISTANCE,
|
|
9
|
+
BUDDY_FOLLOW_STOP_DISTANCE,
|
|
10
|
+
BUDDY_MERGE_DISTANCE,
|
|
8
11
|
CAR_HEIGHT,
|
|
9
12
|
CAR_WIDTH,
|
|
10
13
|
FLASHLIGHT_HEIGHT,
|
|
@@ -14,7 +17,6 @@ from ..entities_constants import (
|
|
|
14
17
|
HUMANOID_RADIUS,
|
|
15
18
|
SHOES_HEIGHT,
|
|
16
19
|
SHOES_WIDTH,
|
|
17
|
-
SURVIVOR_APPROACH_RADIUS,
|
|
18
20
|
SURVIVOR_MAX_SAFE_PASSENGERS,
|
|
19
21
|
)
|
|
20
22
|
from .constants import (
|
|
@@ -22,9 +24,12 @@ from .constants import (
|
|
|
22
24
|
FUEL_HINT_DURATION_MS,
|
|
23
25
|
SURVIVOR_OVERLOAD_DAMAGE_RATIO,
|
|
24
26
|
)
|
|
27
|
+
from ..colors import BLUE, YELLOW
|
|
25
28
|
from ..localization import translate as tr
|
|
26
29
|
from ..models import GameData
|
|
27
30
|
from ..rng import get_rng
|
|
31
|
+
from ..render_constants import BUDDY_COLOR
|
|
32
|
+
from ..screen_constants import FPS
|
|
28
33
|
from ..entities import Car
|
|
29
34
|
from .footprints import get_shrunk_sprite
|
|
30
35
|
from .spawn import maintain_waiting_car_supply
|
|
@@ -36,8 +41,10 @@ from .survivors import (
|
|
|
36
41
|
increase_survivor_capacity,
|
|
37
42
|
respawn_buddies_near_player,
|
|
38
43
|
)
|
|
39
|
-
from .utils import rect_visible_on_screen
|
|
44
|
+
from .utils import is_entity_in_fov, rect_visible_on_screen
|
|
40
45
|
from .ambient import sync_ambient_palette_with_flashlights
|
|
46
|
+
from .constants import SCREAM_MESSAGE_DISPLAY_FRAMES
|
|
47
|
+
from .state import schedule_timed_message
|
|
41
48
|
|
|
42
49
|
|
|
43
50
|
def _interaction_radius(width: float, height: float) -> float:
|
|
@@ -45,6 +52,12 @@ def _interaction_radius(width: float, height: float) -> float:
|
|
|
45
52
|
return HUMANOID_RADIUS + (width + height) / 4
|
|
46
53
|
|
|
47
54
|
|
|
55
|
+
def _ms_to_frames(ms: int) -> int:
|
|
56
|
+
if ms <= 0:
|
|
57
|
+
return 0
|
|
58
|
+
return max(1, int(round(ms / (1000 / max(1, FPS)))))
|
|
59
|
+
|
|
60
|
+
|
|
48
61
|
RNG = get_rng()
|
|
49
62
|
|
|
50
63
|
|
|
@@ -65,6 +78,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
65
78
|
camera = game_data.camera
|
|
66
79
|
stage = game_data.stage
|
|
67
80
|
cell_size = game_data.cell_size
|
|
81
|
+
need_fuel_text = tr("hud.need_fuel")
|
|
68
82
|
maintain_waiting_car_supply(game_data)
|
|
69
83
|
active_car = car if car and car.alive() else None
|
|
70
84
|
waiting_cars = game_data.waiting_cars
|
|
@@ -101,7 +115,8 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
101
115
|
if fuel and fuel.alive() and not state.has_fuel and not player.in_car:
|
|
102
116
|
if _player_near_point(fuel.rect.center, fuel_interaction_radius):
|
|
103
117
|
state.has_fuel = True
|
|
104
|
-
state.
|
|
118
|
+
if state.timed_message == need_fuel_text:
|
|
119
|
+
schedule_timed_message(state, None, duration_frames=0)
|
|
105
120
|
state.hint_expires_at = 0
|
|
106
121
|
state.hint_target_type = None
|
|
107
122
|
fuel.kill()
|
|
@@ -145,7 +160,6 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
145
160
|
buddies = [
|
|
146
161
|
survivor for survivor in survivor_group if survivor.alive() and survivor.is_buddy and not survivor.rescued
|
|
147
162
|
]
|
|
148
|
-
|
|
149
163
|
# Buddy interactions (Stage 3)
|
|
150
164
|
if stage.buddy_required_count > 0 and buddies:
|
|
151
165
|
for buddy in list(buddies):
|
|
@@ -154,7 +168,10 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
154
168
|
buddy_on_screen = rect_visible_on_screen(camera, buddy.rect)
|
|
155
169
|
if not player.in_car:
|
|
156
170
|
dist_to_player_sq = (player.x - buddy.x) ** 2 + (player.y - buddy.y) ** 2
|
|
157
|
-
if
|
|
171
|
+
if buddy.following:
|
|
172
|
+
if dist_to_player_sq >= BUDDY_FOLLOW_STOP_DISTANCE * BUDDY_FOLLOW_STOP_DISTANCE:
|
|
173
|
+
buddy.following = False
|
|
174
|
+
elif dist_to_player_sq <= BUDDY_FOLLOW_START_DISTANCE * BUDDY_FOLLOW_START_DISTANCE:
|
|
158
175
|
buddy.set_following()
|
|
159
176
|
elif player.in_car and active_car and shrunk_car:
|
|
160
177
|
g = pygame.sprite.Group()
|
|
@@ -174,8 +191,24 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
174
191
|
continue
|
|
175
192
|
|
|
176
193
|
if buddy.alive() and pygame.sprite.spritecollide(buddy, zombie_group, False, pygame.sprite.collide_circle):
|
|
177
|
-
|
|
178
|
-
|
|
194
|
+
fov_target = None
|
|
195
|
+
if player.in_car and active_car:
|
|
196
|
+
fov_target = active_car
|
|
197
|
+
else:
|
|
198
|
+
fov_target = player
|
|
199
|
+
buddy_in_fov = is_entity_in_fov(
|
|
200
|
+
buddy.rect,
|
|
201
|
+
fov_target=fov_target,
|
|
202
|
+
flashlight_count=state.flashlight_count,
|
|
203
|
+
)
|
|
204
|
+
if buddy_on_screen and buddy_in_fov:
|
|
205
|
+
schedule_timed_message(
|
|
206
|
+
state,
|
|
207
|
+
tr("game_over.scream"),
|
|
208
|
+
duration_frames=SCREAM_MESSAGE_DISPLAY_FRAMES,
|
|
209
|
+
clear_on_input=False,
|
|
210
|
+
color=BUDDY_COLOR,
|
|
211
|
+
)
|
|
179
212
|
state.game_over = True
|
|
180
213
|
state.game_over_at = state.game_over_at or pygame.time.get_ticks()
|
|
181
214
|
else:
|
|
@@ -183,9 +216,23 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
183
216
|
new_cell = RNG.choice(walkable_cells)
|
|
184
217
|
buddy.teleport(_cell_center(new_cell))
|
|
185
218
|
else:
|
|
186
|
-
buddy.teleport(
|
|
219
|
+
buddy.teleport(game_data.layout.field_rect.center)
|
|
187
220
|
buddy.following = False
|
|
188
221
|
|
|
222
|
+
if stage.buddy_required_count > 0:
|
|
223
|
+
near_following_count = 0
|
|
224
|
+
max_dist_sq = BUDDY_MERGE_DISTANCE * BUDDY_MERGE_DISTANCE
|
|
225
|
+
for buddy in buddies:
|
|
226
|
+
if not buddy.following:
|
|
227
|
+
continue
|
|
228
|
+
dx = player.x - buddy.x
|
|
229
|
+
dy = player.y - buddy.y
|
|
230
|
+
if (dx * dx + dy * dy) <= max_dist_sq:
|
|
231
|
+
near_following_count += 1
|
|
232
|
+
state.buddy_merged_count = state.buddy_onboard + near_following_count
|
|
233
|
+
else:
|
|
234
|
+
state.buddy_merged_count = 0
|
|
235
|
+
|
|
189
236
|
# Player entering an active car already under control
|
|
190
237
|
if not player.in_car and _player_near_car(active_car) and active_car and active_car.health > 0:
|
|
191
238
|
if state.has_fuel:
|
|
@@ -196,8 +243,13 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
196
243
|
print("Player entered car!")
|
|
197
244
|
else:
|
|
198
245
|
if not stage.endurance_stage:
|
|
199
|
-
|
|
200
|
-
|
|
246
|
+
schedule_timed_message(
|
|
247
|
+
state,
|
|
248
|
+
need_fuel_text,
|
|
249
|
+
duration_frames=_ms_to_frames(FUEL_HINT_DURATION_MS),
|
|
250
|
+
clear_on_input=False,
|
|
251
|
+
color=YELLOW,
|
|
252
|
+
)
|
|
201
253
|
state.hint_target_type = "fuel"
|
|
202
254
|
|
|
203
255
|
# Claim a waiting/parked car when the player finally reaches it
|
|
@@ -224,8 +276,13 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
224
276
|
print("Player claimed a waiting car!")
|
|
225
277
|
else:
|
|
226
278
|
if not stage.endurance_stage:
|
|
227
|
-
|
|
228
|
-
|
|
279
|
+
schedule_timed_message(
|
|
280
|
+
state,
|
|
281
|
+
need_fuel_text,
|
|
282
|
+
duration_frames=_ms_to_frames(FUEL_HINT_DURATION_MS),
|
|
283
|
+
clear_on_input=False,
|
|
284
|
+
color=YELLOW,
|
|
285
|
+
)
|
|
229
286
|
state.hint_target_type = "fuel"
|
|
230
287
|
|
|
231
288
|
# Bonus: collide a parked car while driving to repair/extend capabilities
|
|
@@ -307,7 +364,13 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
307
364
|
if not state.game_over:
|
|
308
365
|
state.game_over = True
|
|
309
366
|
state.game_over_at = pygame.time.get_ticks()
|
|
310
|
-
|
|
367
|
+
schedule_timed_message(
|
|
368
|
+
state,
|
|
369
|
+
tr("game_over.scream"),
|
|
370
|
+
duration_frames=SCREAM_MESSAGE_DISPLAY_FRAMES,
|
|
371
|
+
clear_on_input=False,
|
|
372
|
+
color=BLUE,
|
|
373
|
+
)
|
|
311
374
|
|
|
312
375
|
# Player escaping on foot after dawn (Stage 5)
|
|
313
376
|
if (
|
|
@@ -318,17 +381,21 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
318
381
|
and (player_cell := _rect_center_cell(player.rect)) is not None
|
|
319
382
|
and player_cell in outside_cells
|
|
320
383
|
):
|
|
321
|
-
|
|
384
|
+
buddy_ready = True
|
|
385
|
+
if stage.buddy_required_count > 0:
|
|
386
|
+
buddy_ready = state.buddy_merged_count >= stage.buddy_required_count
|
|
387
|
+
if buddy_ready:
|
|
388
|
+
state.game_won = True
|
|
322
389
|
|
|
323
390
|
# Player escaping the level
|
|
324
391
|
if player.in_car and car and car.alive() and state.has_fuel:
|
|
325
392
|
buddy_ready = True
|
|
326
393
|
if stage.buddy_required_count > 0:
|
|
327
|
-
buddy_ready = state.
|
|
394
|
+
buddy_ready = state.buddy_merged_count >= stage.buddy_required_count
|
|
328
395
|
car_cell = _rect_center_cell(car.rect)
|
|
329
396
|
if buddy_ready and car_cell is not None and car_cell in outside_cells:
|
|
330
397
|
if stage.buddy_required_count > 0:
|
|
331
|
-
state.buddy_rescued = min(stage.buddy_required_count, state.
|
|
398
|
+
state.buddy_rescued = min(stage.buddy_required_count, state.buddy_merged_count)
|
|
332
399
|
if stage.rescue_stage and state.survivors_onboard:
|
|
333
400
|
state.survivors_rescued += state.survivors_onboard
|
|
334
401
|
state.survivors_onboard = 0
|
zombie_escape/gameplay/layout.py
CHANGED
|
@@ -70,17 +70,12 @@ def generate_level_from_blueprint(game_data: GameData, config: dict[str, Any]) -
|
|
|
70
70
|
pitfall_zones=stage.pitfall_zones,
|
|
71
71
|
base_seed=game_data.state.seed,
|
|
72
72
|
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
else:
|
|
78
|
-
blueprint = blueprint_data
|
|
79
|
-
steel_cells_raw = set()
|
|
80
|
-
car_reachable_cells = set()
|
|
73
|
+
game_data.blueprint = blueprint_data
|
|
74
|
+
blueprint = blueprint_data.grid
|
|
75
|
+
steel_cells_raw = blueprint_data.steel_cells
|
|
76
|
+
car_reachable_cells = blueprint_data.car_reachable_cells
|
|
81
77
|
|
|
82
78
|
steel_cells = {(int(x), int(y)) for x, y in steel_cells_raw} if steel_enabled else set()
|
|
83
|
-
game_data.layout.car_walkable_cells = car_reachable_cells
|
|
84
79
|
cell_size = game_data.cell_size
|
|
85
80
|
outer_wall_cells = {(x, y) for y, row in enumerate(blueprint) for x, ch in enumerate(row) if ch == "B"}
|
|
86
81
|
wall_cells = {(x, y) for y, row in enumerate(blueprint) for x, ch in enumerate(row) if ch in {"B", "1"}}
|
|
@@ -95,7 +90,6 @@ def generate_level_from_blueprint(game_data: GameData, config: dict[str, Any]) -
|
|
|
95
90
|
pitfall_cells: set[tuple[int, int]] = set()
|
|
96
91
|
player_cells: list[tuple[int, int]] = []
|
|
97
92
|
car_cells: list[tuple[int, int]] = []
|
|
98
|
-
zombie_cells: list[tuple[int, int]] = []
|
|
99
93
|
fuel_cells: list[tuple[int, int]] = []
|
|
100
94
|
flashlight_cells: list[tuple[int, int]] = []
|
|
101
95
|
shoes_cells: list[tuple[int, int]] = []
|
|
@@ -230,8 +224,6 @@ def generate_level_from_blueprint(game_data: GameData, config: dict[str, Any]) -
|
|
|
230
224
|
player_cells.append((x, y))
|
|
231
225
|
if ch == "C":
|
|
232
226
|
car_cells.append((x, y))
|
|
233
|
-
if ch == "Z":
|
|
234
|
-
zombie_cells.append((x, y))
|
|
235
227
|
if ch == "f":
|
|
236
228
|
fuel_cells.append((x, y))
|
|
237
229
|
if ch == "l":
|
|
@@ -252,14 +244,17 @@ def generate_level_from_blueprint(game_data: GameData, config: dict[str, Any]) -
|
|
|
252
244
|
game_data.layout.field_rect = pygame.Rect(
|
|
253
245
|
0,
|
|
254
246
|
0,
|
|
255
|
-
game_data.
|
|
256
|
-
game_data.
|
|
247
|
+
stage.grid_cols * game_data.cell_size,
|
|
248
|
+
stage.grid_rows * game_data.cell_size,
|
|
257
249
|
)
|
|
250
|
+
game_data.layout.grid_cols = stage.grid_cols
|
|
251
|
+
game_data.layout.grid_rows = stage.grid_rows
|
|
258
252
|
game_data.layout.outside_cells = outside_cells
|
|
259
253
|
game_data.layout.walkable_cells = walkable_cells
|
|
260
254
|
game_data.layout.outer_wall_cells = outer_wall_cells
|
|
261
255
|
game_data.layout.wall_cells = wall_cells
|
|
262
256
|
game_data.layout.pitfall_cells = pitfall_cells
|
|
257
|
+
game_data.layout.car_walkable_cells = car_reachable_cells
|
|
263
258
|
fall_spawn_cells = _expand_zone_cells(
|
|
264
259
|
stage.fall_spawn_zones,
|
|
265
260
|
grid_cols=stage.grid_cols,
|
|
@@ -284,7 +279,6 @@ def generate_level_from_blueprint(game_data: GameData, config: dict[str, Any]) -
|
|
|
284
279
|
return {
|
|
285
280
|
"player_cells": player_cells,
|
|
286
281
|
"car_cells": car_cells,
|
|
287
|
-
"zombie_cells": zombie_cells,
|
|
288
282
|
"fuel_cells": fuel_cells,
|
|
289
283
|
"flashlight_cells": flashlight_cells,
|
|
290
284
|
"shoes_cells": shoes_cells,
|
|
@@ -16,6 +16,8 @@ from ..entities_constants import (
|
|
|
16
16
|
HUMANOID_WALL_BUMP_FRAMES,
|
|
17
17
|
PLAYER_SPEED,
|
|
18
18
|
ZOMBIE_SEPARATION_DISTANCE,
|
|
19
|
+
ZOMBIE_TRACKER_CROWD_BAND_WIDTH,
|
|
20
|
+
ZOMBIE_TRACKER_GRID_CROWD_COUNT,
|
|
19
21
|
ZOMBIE_WALL_HUG_SENSOR_DISTANCE,
|
|
20
22
|
)
|
|
21
23
|
from ..gameplay_constants import (
|
|
@@ -23,12 +25,15 @@ from ..gameplay_constants import (
|
|
|
23
25
|
SHOES_SPEED_MULTIPLIER_TWO,
|
|
24
26
|
)
|
|
25
27
|
from ..models import FallingZombie, GameData
|
|
26
|
-
from ..
|
|
28
|
+
from ..rng import get_rng
|
|
29
|
+
from ..world_grid import WallIndex, apply_cell_edge_nudge, walls_for_radius
|
|
27
30
|
from .constants import MAX_ZOMBIES
|
|
28
31
|
from .spawn import spawn_weighted_zombie, update_falling_zombies
|
|
29
32
|
from .survivors import update_survivors
|
|
30
33
|
from .utils import rect_visible_on_screen
|
|
31
34
|
|
|
35
|
+
RNG = get_rng()
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
def process_player_input(
|
|
34
39
|
keys: Sequence[bool],
|
|
@@ -126,10 +131,8 @@ def update_entities(
|
|
|
126
131
|
camera = game_data.camera
|
|
127
132
|
stage = game_data.stage
|
|
128
133
|
active_car = car if car and car.alive() else None
|
|
129
|
-
wall_cells = game_data.layout.wall_cells
|
|
130
134
|
pitfall_cells = game_data.layout.pitfall_cells
|
|
131
|
-
|
|
132
|
-
bevel_corners = game_data.layout.bevel_corners
|
|
135
|
+
field_rect = game_data.layout.field_rect
|
|
133
136
|
|
|
134
137
|
all_walls = list(wall_group) if wall_index is None else None
|
|
135
138
|
|
|
@@ -141,22 +144,19 @@ def update_entities(
|
|
|
141
144
|
center,
|
|
142
145
|
radius,
|
|
143
146
|
cell_size=game_data.cell_size,
|
|
144
|
-
grid_cols=
|
|
145
|
-
grid_rows=
|
|
147
|
+
grid_cols=game_data.layout.grid_cols,
|
|
148
|
+
grid_rows=game_data.layout.grid_rows,
|
|
146
149
|
)
|
|
147
150
|
|
|
148
151
|
# Update player/car movement
|
|
149
152
|
if player.in_car and active_car:
|
|
150
|
-
car_dx, car_dy =
|
|
153
|
+
car_dx, car_dy = apply_cell_edge_nudge(
|
|
151
154
|
active_car.x,
|
|
152
155
|
active_car.y,
|
|
153
156
|
car_dx,
|
|
154
157
|
car_dy,
|
|
158
|
+
layout=game_data.layout,
|
|
155
159
|
cell_size=game_data.cell_size,
|
|
156
|
-
wall_cells=wall_cells,
|
|
157
|
-
bevel_corners=bevel_corners,
|
|
158
|
-
grid_cols=stage.grid_cols,
|
|
159
|
-
grid_rows=stage.grid_rows,
|
|
160
160
|
)
|
|
161
161
|
car_walls = _walls_near((active_car.x, active_car.y), 150.0)
|
|
162
162
|
active_car.move(
|
|
@@ -167,22 +167,26 @@ def update_entities(
|
|
|
167
167
|
cell_size=game_data.cell_size,
|
|
168
168
|
pitfall_cells=pitfall_cells,
|
|
169
169
|
)
|
|
170
|
+
if field_rect is not None:
|
|
171
|
+
car_allow_rect = field_rect.inflate(active_car.rect.width, active_car.rect.height)
|
|
172
|
+
clamped_rect = active_car.rect.clamp(car_allow_rect)
|
|
173
|
+
if clamped_rect.topleft != active_car.rect.topleft:
|
|
174
|
+
active_car.rect = clamped_rect
|
|
175
|
+
active_car.x = float(active_car.rect.centerx)
|
|
176
|
+
active_car.y = float(active_car.rect.centery)
|
|
170
177
|
player.rect.center = active_car.rect.center
|
|
171
178
|
player.x, player.y = active_car.x, active_car.y
|
|
172
179
|
elif not player.in_car:
|
|
173
180
|
# Ensure player is in all_sprites if not in car
|
|
174
181
|
if player not in all_sprites:
|
|
175
182
|
all_sprites.add(player, layer=2)
|
|
176
|
-
player_dx, player_dy =
|
|
183
|
+
player_dx, player_dy = apply_cell_edge_nudge(
|
|
177
184
|
player.x,
|
|
178
185
|
player.y,
|
|
179
186
|
player_dx,
|
|
180
187
|
player_dy,
|
|
188
|
+
layout=game_data.layout,
|
|
181
189
|
cell_size=game_data.cell_size,
|
|
182
|
-
wall_cells=wall_cells,
|
|
183
|
-
bevel_corners=bevel_corners,
|
|
184
|
-
grid_cols=stage.grid_cols,
|
|
185
|
-
grid_rows=stage.grid_rows,
|
|
186
190
|
)
|
|
187
191
|
player.move(
|
|
188
192
|
player_dx,
|
|
@@ -190,10 +194,7 @@ def update_entities(
|
|
|
190
194
|
wall_group,
|
|
191
195
|
wall_index=wall_index,
|
|
192
196
|
cell_size=game_data.cell_size,
|
|
193
|
-
|
|
194
|
-
level_height=game_data.level_height,
|
|
195
|
-
pitfall_cells=pitfall_cells,
|
|
196
|
-
walkable_cells=walkable_cells,
|
|
197
|
+
layout=game_data.layout,
|
|
197
198
|
)
|
|
198
199
|
else:
|
|
199
200
|
# Player flagged as in-car but car is gone; drop them back to foot control
|
|
@@ -248,6 +249,28 @@ def update_entities(
|
|
|
248
249
|
|
|
249
250
|
zombies_sorted: list[Zombie] = sorted(list(zombie_group), key=lambda z: z.x)
|
|
250
251
|
|
|
252
|
+
tracker_buckets: dict[tuple[int, int, int], list[Zombie]] = {}
|
|
253
|
+
tracker_cell_size = ZOMBIE_TRACKER_CROWD_BAND_WIDTH
|
|
254
|
+
angle_step = math.pi / 4.0
|
|
255
|
+
for zombie in zombies_sorted:
|
|
256
|
+
if not zombie.alive() or not zombie.tracker:
|
|
257
|
+
continue
|
|
258
|
+
zombie.tracker_force_wander = False
|
|
259
|
+
dx = zombie.last_move_dx
|
|
260
|
+
dy = zombie.last_move_dy
|
|
261
|
+
if abs(dx) <= 0.001 and abs(dy) <= 0.001:
|
|
262
|
+
continue
|
|
263
|
+
angle = math.atan2(dy, dx)
|
|
264
|
+
angle_bin = int(round(angle / angle_step)) % 8
|
|
265
|
+
cell_x = int(zombie.x // tracker_cell_size)
|
|
266
|
+
cell_y = int(zombie.y // tracker_cell_size)
|
|
267
|
+
tracker_buckets.setdefault((cell_x, cell_y, angle_bin), []).append(zombie)
|
|
268
|
+
|
|
269
|
+
for bucket in tracker_buckets.values():
|
|
270
|
+
if len(bucket) < ZOMBIE_TRACKER_GRID_CROWD_COUNT:
|
|
271
|
+
continue
|
|
272
|
+
RNG.choice(bucket).tracker_force_wander = True
|
|
273
|
+
|
|
251
274
|
def _nearby_zombies(index: int) -> list[Zombie]:
|
|
252
275
|
center = zombies_sorted[index]
|
|
253
276
|
neighbors: list[Zombie] = []
|
|
@@ -303,14 +326,7 @@ def update_entities(
|
|
|
303
326
|
nearby_candidates,
|
|
304
327
|
footprints=game_data.state.footprints,
|
|
305
328
|
cell_size=game_data.cell_size,
|
|
306
|
-
|
|
307
|
-
grid_rows=stage.grid_rows,
|
|
308
|
-
level_width=game_data.level_width,
|
|
309
|
-
level_height=game_data.level_height,
|
|
310
|
-
outer_wall_cells=game_data.layout.outer_wall_cells,
|
|
311
|
-
wall_cells=game_data.layout.wall_cells,
|
|
312
|
-
pitfall_cells=game_data.layout.pitfall_cells,
|
|
313
|
-
bevel_corners=game_data.layout.bevel_corners,
|
|
329
|
+
layout=game_data.layout,
|
|
314
330
|
)
|
|
315
331
|
|
|
316
332
|
# Check zombie pitfall
|
|
@@ -327,7 +343,7 @@ def update_entities(
|
|
|
327
343
|
fall = FallingZombie(
|
|
328
344
|
start_pos=(int(zombie.x), int(zombie.y)),
|
|
329
345
|
target_pos=pitfall_target,
|
|
330
|
-
started_at_ms=
|
|
346
|
+
started_at_ms=game_data.state.elapsed_play_ms,
|
|
331
347
|
pre_fx_ms=0,
|
|
332
348
|
fall_duration_ms=500,
|
|
333
349
|
dust_duration_ms=0,
|
zombie_escape/gameplay/spawn.py
CHANGED
|
@@ -17,7 +17,6 @@ from ..entities import (
|
|
|
17
17
|
)
|
|
18
18
|
from ..entities_constants import (
|
|
19
19
|
FAST_ZOMBIE_BASE_SPEED,
|
|
20
|
-
FOV_RADIUS,
|
|
21
20
|
PLAYER_SPEED,
|
|
22
21
|
ZOMBIE_AGING_DURATION_FRAMES,
|
|
23
22
|
ZOMBIE_SPEED,
|
|
@@ -26,13 +25,8 @@ from ..gameplay_constants import (
|
|
|
26
25
|
DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
27
26
|
DEFAULT_SHOES_SPAWN_COUNT,
|
|
28
27
|
)
|
|
29
|
-
from ..level_constants import DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS
|
|
28
|
+
from ..level_constants import DEFAULT_CELL_SIZE, DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS
|
|
30
29
|
from ..models import DustRing, FallingZombie, GameData, Stage
|
|
31
|
-
from ..render_constants import (
|
|
32
|
-
FLASHLIGHT_FOG_SCALE_ONE,
|
|
33
|
-
FLASHLIGHT_FOG_SCALE_TWO,
|
|
34
|
-
FOG_RADIUS_SCALE,
|
|
35
|
-
)
|
|
36
30
|
from ..rng import get_rng
|
|
37
31
|
from .constants import (
|
|
38
32
|
FALLING_ZOMBIE_DUST_DURATION_MS,
|
|
@@ -43,6 +37,7 @@ from .constants import (
|
|
|
43
37
|
ZOMBIE_TRACKER_AGING_DURATION_FRAMES,
|
|
44
38
|
)
|
|
45
39
|
from .utils import (
|
|
40
|
+
fov_radius_for_flashlights,
|
|
46
41
|
find_exterior_spawn_position,
|
|
47
42
|
find_interior_spawn_positions,
|
|
48
43
|
find_nearby_offscreen_spawn_position,
|
|
@@ -111,17 +106,6 @@ def _pick_zombie_variant(stage: Stage | None) -> tuple[bool, bool]:
|
|
|
111
106
|
return False, True
|
|
112
107
|
|
|
113
108
|
|
|
114
|
-
def _fov_radius_for_flashlights(flashlight_count: int) -> float:
|
|
115
|
-
count = max(0, int(flashlight_count))
|
|
116
|
-
if count <= 0:
|
|
117
|
-
scale = FOG_RADIUS_SCALE
|
|
118
|
-
elif count == 1:
|
|
119
|
-
scale = FLASHLIGHT_FOG_SCALE_ONE
|
|
120
|
-
else:
|
|
121
|
-
scale = FLASHLIGHT_FOG_SCALE_TWO
|
|
122
|
-
return FOV_RADIUS * scale
|
|
123
|
-
|
|
124
|
-
|
|
125
109
|
def _is_spawn_position_clear(
|
|
126
110
|
game_data: GameData,
|
|
127
111
|
candidate: Zombie,
|
|
@@ -168,7 +152,7 @@ def _pick_fall_spawn_position(
|
|
|
168
152
|
target_sprite = car if player.in_car and car and car.alive() else player
|
|
169
153
|
target_center = target_sprite.rect.center
|
|
170
154
|
cell_size = game_data.cell_size
|
|
171
|
-
fov_radius =
|
|
155
|
+
fov_radius = fov_radius_for_flashlights(game_data.state.flashlight_count)
|
|
172
156
|
min_dist_sq = min_distance * min_distance
|
|
173
157
|
max_dist_sq = fov_radius * fov_radius
|
|
174
158
|
wall_cells = game_data.layout.wall_cells
|
|
@@ -212,7 +196,7 @@ def _schedule_falling_zombie(
|
|
|
212
196
|
zombie_group = game_data.groups.zombie_group
|
|
213
197
|
if len(zombie_group) + len(state.falling_zombies) >= MAX_ZOMBIES:
|
|
214
198
|
return "blocked"
|
|
215
|
-
min_distance = game_data.stage.
|
|
199
|
+
min_distance = game_data.stage.cell_size * 0.5
|
|
216
200
|
tracker, wall_hugging = _pick_zombie_variant(game_data.stage)
|
|
217
201
|
|
|
218
202
|
def _candidate_clear(pos: tuple[int, int]) -> bool:
|
|
@@ -239,7 +223,7 @@ def _schedule_falling_zombie(
|
|
|
239
223
|
fall = FallingZombie(
|
|
240
224
|
start_pos=start_pos,
|
|
241
225
|
target_pos=(int(spawn_pos[0]), int(spawn_pos[1])),
|
|
242
|
-
started_at_ms=
|
|
226
|
+
started_at_ms=game_data.state.elapsed_play_ms,
|
|
243
227
|
pre_fx_ms=FALLING_ZOMBIE_PRE_FX_MS,
|
|
244
228
|
fall_duration_ms=FALLING_ZOMBIE_DURATION_MS,
|
|
245
229
|
dust_duration_ms=FALLING_ZOMBIE_DUST_DURATION_MS,
|
|
@@ -290,15 +274,15 @@ def _create_zombie(
|
|
|
290
274
|
)
|
|
291
275
|
aging_duration_frames = max(1.0, aging_duration_frames * ratio)
|
|
292
276
|
if start_pos is None:
|
|
293
|
-
|
|
277
|
+
cell_size = stage.cell_size if stage else DEFAULT_CELL_SIZE
|
|
294
278
|
if stage is None:
|
|
295
279
|
grid_cols = DEFAULT_GRID_COLS
|
|
296
280
|
grid_rows = DEFAULT_GRID_ROWS
|
|
297
281
|
else:
|
|
298
282
|
grid_cols = stage.grid_cols
|
|
299
283
|
grid_rows = stage.grid_rows
|
|
300
|
-
level_width = grid_cols *
|
|
301
|
-
level_height = grid_rows *
|
|
284
|
+
level_width = grid_cols * cell_size
|
|
285
|
+
level_height = grid_rows * cell_size
|
|
302
286
|
if hint_pos is not None:
|
|
303
287
|
points = [random_position_outside_building(level_width, level_height) for _ in range(5)]
|
|
304
288
|
points.sort(key=lambda p: (p[0] - hint_pos[0]) ** 2 + (p[1] - hint_pos[1]) ** 2)
|
|
@@ -625,12 +609,13 @@ def setup_player_and_cars(
|
|
|
625
609
|
all_sprites = game_data.groups.all_sprites
|
|
626
610
|
walkable_cells: list[tuple[int, int]] = layout_data["walkable_cells"]
|
|
627
611
|
cell_size = game_data.cell_size
|
|
612
|
+
level_rect = game_data.layout.field_rect
|
|
628
613
|
|
|
629
614
|
def _pick_center(cells: list[tuple[int, int]]) -> tuple[int, int]:
|
|
630
615
|
return (
|
|
631
616
|
_cell_center(RNG.choice(cells), cell_size)
|
|
632
617
|
if cells
|
|
633
|
-
else
|
|
618
|
+
else level_rect.center
|
|
634
619
|
)
|
|
635
620
|
|
|
636
621
|
player_pos = _pick_center(layout_data["player_cells"] or walkable_cells)
|
|
@@ -677,10 +662,10 @@ def spawn_initial_zombies(
|
|
|
677
662
|
zombie_group = game_data.groups.zombie_group
|
|
678
663
|
all_sprites = game_data.groups.all_sprites
|
|
679
664
|
|
|
665
|
+
cell_size = game_data.cell_size
|
|
680
666
|
spawn_cells = layout_data["walkable_cells"]
|
|
681
667
|
if not spawn_cells:
|
|
682
668
|
return
|
|
683
|
-
cell_size = game_data.cell_size
|
|
684
669
|
|
|
685
670
|
if game_data.stage.id == "debug_tracker":
|
|
686
671
|
player_pos = player.rect.center
|
|
@@ -860,9 +845,10 @@ def spawn_exterior_zombie(
|
|
|
860
845
|
return None
|
|
861
846
|
zombie_group = game_data.groups.zombie_group
|
|
862
847
|
all_sprites = game_data.groups.all_sprites
|
|
848
|
+
level_rect = game_data.layout.field_rect
|
|
863
849
|
spawn_pos = find_exterior_spawn_position(
|
|
864
|
-
|
|
865
|
-
|
|
850
|
+
level_rect.width,
|
|
851
|
+
level_rect.height,
|
|
866
852
|
hint_pos=(player.x, player.y),
|
|
867
853
|
)
|
|
868
854
|
new_zombie = _create_zombie(
|
|
@@ -879,7 +865,7 @@ def update_falling_zombies(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
879
865
|
state = game_data.state
|
|
880
866
|
if not state.falling_zombies:
|
|
881
867
|
return
|
|
882
|
-
now =
|
|
868
|
+
now = state.elapsed_play_ms
|
|
883
869
|
zombie_group = game_data.groups.zombie_group
|
|
884
870
|
all_sprites = game_data.groups.all_sprites
|
|
885
871
|
for fall in list(state.falling_zombies):
|