zombie-escape 1.8.0__py3-none-any.whl → 1.10.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/colors.py +49 -14
- zombie_escape/entities.py +295 -332
- zombie_escape/entities_constants.py +6 -0
- zombie_escape/gameplay/__init__.py +2 -2
- zombie_escape/gameplay/constants.py +1 -1
- zombie_escape/gameplay/footprints.py +2 -2
- zombie_escape/gameplay/interactions.py +4 -10
- zombie_escape/gameplay/layout.py +38 -4
- zombie_escape/gameplay/movement.py +5 -7
- zombie_escape/gameplay/spawn.py +245 -12
- zombie_escape/gameplay/state.py +17 -16
- zombie_escape/gameplay/survivors.py +5 -4
- zombie_escape/gameplay/utils.py +19 -1
- zombie_escape/locales/ui.en.json +14 -2
- zombie_escape/locales/ui.ja.json +14 -2
- zombie_escape/models.py +52 -7
- zombie_escape/render.py +760 -284
- zombie_escape/render_constants.py +26 -8
- zombie_escape/screens/__init__.py +1 -0
- zombie_escape/screens/game_over.py +4 -4
- zombie_escape/screens/gameplay.py +10 -24
- zombie_escape/stage_constants.py +90 -2
- zombie_escape/world_grid.py +134 -0
- zombie_escape/zombie_escape.py +65 -61
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.1.dist-info}/METADATA +9 -1
- zombie_escape-1.10.1.dist-info/RECORD +47 -0
- zombie_escape-1.8.0.dist-info/RECORD +0 -46
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.1.dist-info}/WHEEL +0 -0
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.1.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.8.0.dist-info → zombie_escape-1.10.1.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -11,6 +11,9 @@ HUMANOID_OUTLINE_COLOR = (0, 80, 200)
|
|
|
11
11
|
HUMANOID_OUTLINE_WIDTH = 1
|
|
12
12
|
BUDDY_COLOR = (0, 180, 63)
|
|
13
13
|
SURVIVOR_COLOR = (198, 198, 198)
|
|
14
|
+
FALLING_ZOMBIE_COLOR = (45, 45, 45)
|
|
15
|
+
FALLING_WHIRLWIND_COLOR = (200, 200, 200, 120)
|
|
16
|
+
FALLING_DUST_COLOR = (70, 70, 70, 130)
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
@dataclass(frozen=True)
|
|
@@ -27,7 +30,6 @@ class RenderAssets:
|
|
|
27
30
|
player_radius: int
|
|
28
31
|
fov_radius: int
|
|
29
32
|
fog_radius_scale: float
|
|
30
|
-
fog_hatch_pixel_scale: int
|
|
31
33
|
fog_rings: list[FogRing]
|
|
32
34
|
footprint_radius: int
|
|
33
35
|
footprint_overview_radius: int
|
|
@@ -35,11 +37,13 @@ class RenderAssets:
|
|
|
35
37
|
footprint_min_fade: float
|
|
36
38
|
internal_wall_grid_snap: int
|
|
37
39
|
flashlight_bonus_step: float
|
|
40
|
+
flashlight_hatch_extra_scale: float
|
|
41
|
+
|
|
38
42
|
|
|
39
43
|
FOG_RADIUS_SCALE = 1.2
|
|
40
|
-
FOG_HATCH_PIXEL_SCALE = 2
|
|
41
44
|
|
|
42
45
|
FLASHLIGHT_FOG_SCALE_STEP = 0.3
|
|
46
|
+
FLASHLIGHT_HATCH_EXTRA_SCALE = 0.12
|
|
43
47
|
|
|
44
48
|
FOOTPRINT_RADIUS = 2
|
|
45
49
|
FOOTPRINT_OVERVIEW_RADIUS = 3
|
|
@@ -47,14 +51,20 @@ FOOTPRINT_COLOR = (110, 200, 255)
|
|
|
47
51
|
FOOTPRINT_LIFETIME_MS = 135000
|
|
48
52
|
FOOTPRINT_MIN_FADE = 0.3
|
|
49
53
|
|
|
54
|
+
SHADOW_OVERSAMPLE = 2
|
|
55
|
+
SHADOW_STEPS = 10
|
|
56
|
+
SHADOW_MIN_RATIO = 0.0
|
|
57
|
+
SHADOW_RADIUS_RATIO = 0.3
|
|
58
|
+
|
|
50
59
|
FOG_RINGS = [
|
|
51
|
-
FogRing(radius_factor=0.
|
|
52
|
-
FogRing(radius_factor=0.
|
|
53
|
-
FogRing(radius_factor=0.
|
|
54
|
-
FogRing(radius_factor=0.
|
|
55
|
-
FogRing(radius_factor=0.
|
|
60
|
+
FogRing(radius_factor=0.536, thickness=2),
|
|
61
|
+
FogRing(radius_factor=0.645, thickness=3),
|
|
62
|
+
FogRing(radius_factor=0.754, thickness=5),
|
|
63
|
+
FogRing(radius_factor=0.863, thickness=8),
|
|
64
|
+
FogRing(radius_factor=0.972, thickness=12),
|
|
56
65
|
]
|
|
57
66
|
|
|
67
|
+
|
|
58
68
|
def build_render_assets(cell_size: int) -> RenderAssets:
|
|
59
69
|
return RenderAssets(
|
|
60
70
|
screen_width=SCREEN_WIDTH,
|
|
@@ -63,7 +73,6 @@ def build_render_assets(cell_size: int) -> RenderAssets:
|
|
|
63
73
|
player_radius=PLAYER_RADIUS,
|
|
64
74
|
fov_radius=FOV_RADIUS,
|
|
65
75
|
fog_radius_scale=FOG_RADIUS_SCALE,
|
|
66
|
-
fog_hatch_pixel_scale=FOG_HATCH_PIXEL_SCALE,
|
|
67
76
|
fog_rings=FOG_RINGS,
|
|
68
77
|
footprint_radius=FOOTPRINT_RADIUS,
|
|
69
78
|
footprint_overview_radius=FOOTPRINT_OVERVIEW_RADIUS,
|
|
@@ -71,17 +80,26 @@ def build_render_assets(cell_size: int) -> RenderAssets:
|
|
|
71
80
|
footprint_min_fade=FOOTPRINT_MIN_FADE,
|
|
72
81
|
internal_wall_grid_snap=cell_size,
|
|
73
82
|
flashlight_bonus_step=FLASHLIGHT_FOG_SCALE_STEP,
|
|
83
|
+
flashlight_hatch_extra_scale=FLASHLIGHT_HATCH_EXTRA_SCALE,
|
|
74
84
|
)
|
|
75
85
|
|
|
76
86
|
|
|
77
87
|
__all__ = [
|
|
78
88
|
"BUDDY_COLOR",
|
|
89
|
+
"FALLING_ZOMBIE_COLOR",
|
|
90
|
+
"FALLING_WHIRLWIND_COLOR",
|
|
91
|
+
"FALLING_DUST_COLOR",
|
|
79
92
|
"HUMANOID_OUTLINE_COLOR",
|
|
80
93
|
"HUMANOID_OUTLINE_WIDTH",
|
|
81
94
|
"SURVIVOR_COLOR",
|
|
82
95
|
"FogRing",
|
|
83
96
|
"RenderAssets",
|
|
84
97
|
"FOG_RADIUS_SCALE",
|
|
98
|
+
"SHADOW_OVERSAMPLE",
|
|
99
|
+
"SHADOW_STEPS",
|
|
100
|
+
"SHADOW_MIN_RATIO",
|
|
101
|
+
"SHADOW_RADIUS_RATIO",
|
|
85
102
|
"FLASHLIGHT_FOG_SCALE_STEP",
|
|
103
|
+
"FLASHLIGHT_HATCH_EXTRA_SCALE",
|
|
86
104
|
"build_render_assets",
|
|
87
105
|
]
|
|
@@ -159,6 +159,7 @@ def toggle_fullscreen(
|
|
|
159
159
|
window_width, window_height = _fetch_window_size(window)
|
|
160
160
|
_update_window_caption(window_width, window_height)
|
|
161
161
|
_update_window_size((window_width, window_height), source="toggle_fullscreen")
|
|
162
|
+
pygame.mouse.set_visible(not current_maximized)
|
|
162
163
|
if game_data is not None:
|
|
163
164
|
game_data.state.overview_created = False
|
|
164
165
|
return window
|
|
@@ -130,16 +130,16 @@ def game_over_screen(
|
|
|
130
130
|
LIGHT_GRAY,
|
|
131
131
|
(screen_width // 2, summary_y),
|
|
132
132
|
)
|
|
133
|
-
elif stage and stage.
|
|
134
|
-
elapsed_ms = max(0, state.
|
|
135
|
-
goal_ms = max(0, state.
|
|
133
|
+
elif stage and stage.endurance_stage:
|
|
134
|
+
elapsed_ms = max(0, state.endurance_elapsed_ms)
|
|
135
|
+
goal_ms = max(0, state.endurance_goal_ms)
|
|
136
136
|
if goal_ms:
|
|
137
137
|
elapsed_ms = min(elapsed_ms, goal_ms)
|
|
138
138
|
display_ms = int(elapsed_ms * SURVIVAL_FAKE_CLOCK_RATIO)
|
|
139
139
|
hours = display_ms // 3_600_000
|
|
140
140
|
minutes = (display_ms % 3_600_000) // 60_000
|
|
141
141
|
time_label = f"{int(hours):02d}:{int(minutes):02d}"
|
|
142
|
-
msg = tr("game_over.
|
|
142
|
+
msg = tr("game_over.endurance_duration", time=time_label)
|
|
143
143
|
show_message(
|
|
144
144
|
screen,
|
|
145
145
|
msg,
|
|
@@ -8,7 +8,6 @@ from pygame import surface, time
|
|
|
8
8
|
from ..colors import LIGHT_GRAY, RED, WHITE, YELLOW
|
|
9
9
|
from ..gameplay_constants import (
|
|
10
10
|
CAR_HINT_DELAY_MS_DEFAULT,
|
|
11
|
-
DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
12
11
|
SURVIVAL_TIME_ACCEL_SUBSTEPS,
|
|
13
12
|
SURVIVAL_TIME_ACCEL_MAX_SUBSTEP,
|
|
14
13
|
)
|
|
@@ -29,7 +28,7 @@ from ..gameplay import (
|
|
|
29
28
|
sync_ambient_palette_with_flashlights,
|
|
30
29
|
update_entities,
|
|
31
30
|
update_footprints,
|
|
32
|
-
|
|
31
|
+
update_endurance_timer,
|
|
33
32
|
)
|
|
34
33
|
from ..input_utils import (
|
|
35
34
|
CONTROLLER_BUTTON_DOWN,
|
|
@@ -43,7 +42,7 @@ from ..input_utils import (
|
|
|
43
42
|
read_gamepad_move,
|
|
44
43
|
)
|
|
45
44
|
from ..gameplay.spawn import _alive_waiting_cars
|
|
46
|
-
from ..
|
|
45
|
+
from ..world_grid import build_wall_index
|
|
47
46
|
from ..localization import translate as tr
|
|
48
47
|
from ..models import Stage
|
|
49
48
|
from ..render import draw, prewarm_fog_overlays, show_message
|
|
@@ -83,11 +82,11 @@ def gameplay_screen(
|
|
|
83
82
|
game_data = initialize_game_state(config, stage)
|
|
84
83
|
game_data.state.seed = applied_seed
|
|
85
84
|
game_data.state.debug_mode = debug_mode
|
|
86
|
-
if debug_mode and stage.
|
|
87
|
-
goal_ms = max(0, stage.
|
|
85
|
+
if debug_mode and stage.endurance_stage:
|
|
86
|
+
goal_ms = max(0, stage.endurance_goal_ms)
|
|
88
87
|
if goal_ms > 0:
|
|
89
88
|
remaining = 3 * 60 * 1000 # 3 minutes in ms
|
|
90
|
-
game_data.state.
|
|
89
|
+
game_data.state.endurance_elapsed_ms = max(0, goal_ms - remaining)
|
|
91
90
|
game_data.state.dawn_ready = False
|
|
92
91
|
game_data.state.dawn_prompt_at = None
|
|
93
92
|
game_data.state.dawn_carbonized = False
|
|
@@ -99,7 +98,6 @@ def gameplay_screen(
|
|
|
99
98
|
paused_manual = False
|
|
100
99
|
paused_focus = False
|
|
101
100
|
ignore_focus_loss_until = 0
|
|
102
|
-
last_fov_target = None
|
|
103
101
|
controller = init_first_controller()
|
|
104
102
|
joystick = init_first_joystick() if controller is None else None
|
|
105
103
|
|
|
@@ -131,19 +129,18 @@ def gameplay_screen(
|
|
|
131
129
|
if fuel_can:
|
|
132
130
|
game_data.fuel = fuel_can
|
|
133
131
|
game_data.groups.all_sprites.add(fuel_can, layer=1)
|
|
132
|
+
flashlight_count = stage.initial_flashlight_count
|
|
134
133
|
flashlights = place_flashlights(
|
|
135
134
|
layout_data["walkable_cells"],
|
|
136
135
|
player,
|
|
137
136
|
cars=game_data.waiting_cars,
|
|
138
|
-
count=max(
|
|
137
|
+
count=max(0, flashlight_count),
|
|
139
138
|
)
|
|
140
139
|
game_data.flashlights = flashlights
|
|
141
140
|
game_data.groups.all_sprites.add(flashlights, layer=1)
|
|
142
141
|
|
|
143
142
|
spawn_initial_zombies(game_data, player, layout_data, config)
|
|
144
143
|
update_footprints(game_data, config)
|
|
145
|
-
last_fov_target = player
|
|
146
|
-
|
|
147
144
|
while True:
|
|
148
145
|
dt = clock.tick(fps) / 1000.0
|
|
149
146
|
if game_data.state.game_over or game_data.state.game_won:
|
|
@@ -157,7 +154,6 @@ def gameplay_screen(
|
|
|
157
154
|
render_assets,
|
|
158
155
|
screen,
|
|
159
156
|
game_data,
|
|
160
|
-
last_fov_target,
|
|
161
157
|
config=config,
|
|
162
158
|
hint_color=None,
|
|
163
159
|
present_fn=present,
|
|
@@ -261,7 +257,6 @@ def gameplay_screen(
|
|
|
261
257
|
render_assets,
|
|
262
258
|
screen,
|
|
263
259
|
game_data,
|
|
264
|
-
last_fov_target,
|
|
265
260
|
config=config,
|
|
266
261
|
do_flip=not show_pause_overlay,
|
|
267
262
|
present_fn=present,
|
|
@@ -325,7 +320,6 @@ def gameplay_screen(
|
|
|
325
320
|
wall_index = build_wall_index(
|
|
326
321
|
game_data.groups.wall_group, cell_size=game_data.cell_size
|
|
327
322
|
)
|
|
328
|
-
frame_fov_target = None
|
|
329
323
|
for _ in range(substeps):
|
|
330
324
|
player_ref = game_data.player
|
|
331
325
|
if player_ref is None:
|
|
@@ -349,19 +343,12 @@ def gameplay_screen(
|
|
|
349
343
|
if accel_active:
|
|
350
344
|
step_ms = max(1, step_ms)
|
|
351
345
|
game_data.state.elapsed_play_ms += step_ms
|
|
352
|
-
|
|
346
|
+
update_endurance_timer(game_data, step_ms)
|
|
353
347
|
cleanup_survivor_messages(game_data.state)
|
|
354
|
-
|
|
355
|
-
if sub_fov_target:
|
|
356
|
-
frame_fov_target = sub_fov_target
|
|
348
|
+
check_interactions(game_data, config)
|
|
357
349
|
if game_data.state.game_over or game_data.state.game_won:
|
|
358
350
|
break
|
|
359
351
|
|
|
360
|
-
if frame_fov_target:
|
|
361
|
-
last_fov_target = frame_fov_target
|
|
362
|
-
else:
|
|
363
|
-
frame_fov_target = last_fov_target
|
|
364
|
-
|
|
365
352
|
player = game_data.player
|
|
366
353
|
if player is None:
|
|
367
354
|
raise ValueError("Player missing from game data")
|
|
@@ -370,7 +357,7 @@ def gameplay_screen(
|
|
|
370
357
|
hint_delay = car_hint_conf.get("delay_ms", CAR_HINT_DELAY_MS_DEFAULT)
|
|
371
358
|
elapsed_ms = game_data.state.elapsed_play_ms
|
|
372
359
|
has_fuel = game_data.state.has_fuel
|
|
373
|
-
hint_enabled = car_hint_conf.get("enabled", True) and not stage.
|
|
360
|
+
hint_enabled = car_hint_conf.get("enabled", True) and not stage.endurance_stage
|
|
374
361
|
hint_target = None
|
|
375
362
|
hint_color = YELLOW
|
|
376
363
|
hint_expires_at = game_data.state.hint_expires_at
|
|
@@ -417,7 +404,6 @@ def gameplay_screen(
|
|
|
417
404
|
render_assets,
|
|
418
405
|
screen,
|
|
419
406
|
game_data,
|
|
420
|
-
frame_fov_target,
|
|
421
407
|
config=config,
|
|
422
408
|
hint_target=hint_target,
|
|
423
409
|
hint_color=hint_color,
|
zombie_escape/stage_constants.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from .entities_constants import ZOMBIE_AGING_DURATION_FRAMES
|
|
6
6
|
from .gameplay_constants import SURVIVOR_SPAWN_RATE
|
|
7
|
+
from .level_constants import DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS
|
|
7
8
|
from .models import Stage
|
|
8
9
|
|
|
9
10
|
STAGES: list[Stage] = [
|
|
@@ -23,6 +24,7 @@ STAGES: list[Stage] = [
|
|
|
23
24
|
requires_fuel=True,
|
|
24
25
|
exterior_spawn_weight=0.97,
|
|
25
26
|
interior_spawn_weight=0.03,
|
|
27
|
+
initial_interior_spawn_rate=0.007,
|
|
26
28
|
),
|
|
27
29
|
Stage(
|
|
28
30
|
id="stage3",
|
|
@@ -33,6 +35,7 @@ STAGES: list[Stage] = [
|
|
|
33
35
|
requires_fuel=True,
|
|
34
36
|
exterior_spawn_weight=0.97,
|
|
35
37
|
interior_spawn_weight=0.03,
|
|
38
|
+
initial_interior_spawn_rate=0.007,
|
|
36
39
|
),
|
|
37
40
|
Stage(
|
|
38
41
|
id="stage4",
|
|
@@ -42,6 +45,7 @@ STAGES: list[Stage] = [
|
|
|
42
45
|
rescue_stage=True,
|
|
43
46
|
waiting_car_target_count=2,
|
|
44
47
|
survivor_spawn_rate=SURVIVOR_SPAWN_RATE,
|
|
48
|
+
initial_interior_spawn_rate=0.007,
|
|
45
49
|
),
|
|
46
50
|
Stage(
|
|
47
51
|
id="stage5",
|
|
@@ -49,8 +53,8 @@ STAGES: list[Stage] = [
|
|
|
49
53
|
description_key="stages.stage5.description",
|
|
50
54
|
available=True,
|
|
51
55
|
requires_fuel=True,
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
endurance_stage=True,
|
|
57
|
+
endurance_goal_ms=1_200_000,
|
|
54
58
|
fuel_spawn_count=0,
|
|
55
59
|
exterior_spawn_weight=0.4,
|
|
56
60
|
interior_spawn_weight=0.6,
|
|
@@ -137,6 +141,90 @@ STAGES: list[Stage] = [
|
|
|
137
141
|
waiting_car_target_count=1,
|
|
138
142
|
survivor_spawn_rate=0.35,
|
|
139
143
|
),
|
|
144
|
+
Stage(
|
|
145
|
+
id="stage11",
|
|
146
|
+
name_key="stages.stage11.name",
|
|
147
|
+
description_key="stages.stage11.description",
|
|
148
|
+
grid_cols=120,
|
|
149
|
+
grid_rows=7,
|
|
150
|
+
available=True,
|
|
151
|
+
wall_algorithm="sparse",
|
|
152
|
+
exterior_spawn_weight=0.3,
|
|
153
|
+
interior_spawn_weight=0.7,
|
|
154
|
+
zombie_normal_ratio=0.5,
|
|
155
|
+
zombie_tracker_ratio=0.5,
|
|
156
|
+
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
157
|
+
initial_interior_spawn_rate=0.1,
|
|
158
|
+
waiting_car_target_count=1,
|
|
159
|
+
),
|
|
160
|
+
Stage(
|
|
161
|
+
id="stage12",
|
|
162
|
+
name_key="stages.stage12.name",
|
|
163
|
+
description_key="stages.stage12.description",
|
|
164
|
+
grid_cols=32,
|
|
165
|
+
grid_rows=32,
|
|
166
|
+
available=True,
|
|
167
|
+
requires_fuel=True,
|
|
168
|
+
exterior_spawn_weight=0.5,
|
|
169
|
+
interior_spawn_weight=0.2,
|
|
170
|
+
interior_fall_spawn_weight=0.3,
|
|
171
|
+
fall_spawn_zones=[
|
|
172
|
+
(3, 3, 12, 12),
|
|
173
|
+
(3, 17, 12, 12),
|
|
174
|
+
(17, 3, 12, 12),
|
|
175
|
+
(17, 17, 12, 12),
|
|
176
|
+
],
|
|
177
|
+
initial_flashlight_count=5,
|
|
178
|
+
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
179
|
+
),
|
|
180
|
+
Stage(
|
|
181
|
+
id="stage13",
|
|
182
|
+
name_key="stages.stage13.name",
|
|
183
|
+
description_key="stages.stage13.description",
|
|
184
|
+
available=True,
|
|
185
|
+
wall_algorithm="grid_wire",
|
|
186
|
+
buddy_required_count=1,
|
|
187
|
+
requires_fuel=True,
|
|
188
|
+
exterior_spawn_weight=0.6,
|
|
189
|
+
interior_spawn_weight=0.1,
|
|
190
|
+
interior_fall_spawn_weight=0.3,
|
|
191
|
+
zombie_normal_ratio=0.4,
|
|
192
|
+
zombie_tracker_ratio=0.3,
|
|
193
|
+
zombie_wall_follower_ratio=0.3,
|
|
194
|
+
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
195
|
+
initial_flashlight_count=3,
|
|
196
|
+
fall_spawn_zones=[
|
|
197
|
+
(x, y, 2, 2)
|
|
198
|
+
for y in range(2, DEFAULT_GRID_ROWS - 2, 4)
|
|
199
|
+
for x in range(2, DEFAULT_GRID_COLS - 2, 4)
|
|
200
|
+
],
|
|
201
|
+
),
|
|
202
|
+
Stage(
|
|
203
|
+
id="stage14",
|
|
204
|
+
name_key="stages.stage14.name",
|
|
205
|
+
description_key="stages.stage14.description",
|
|
206
|
+
grid_cols=42,
|
|
207
|
+
grid_rows=27,
|
|
208
|
+
available=False,
|
|
209
|
+
requires_fuel=True,
|
|
210
|
+
exterior_spawn_weight=0.2,
|
|
211
|
+
interior_spawn_weight=0.1,
|
|
212
|
+
interior_fall_spawn_weight=0.7,
|
|
213
|
+
fall_spawn_zones=[
|
|
214
|
+
(4, 10, 3, 3),
|
|
215
|
+
(5, 20, 3, 3),
|
|
216
|
+
(15, 17, 3, 3),
|
|
217
|
+
(22, 16, 3, 3),
|
|
218
|
+
(17, 20, 3, 3),
|
|
219
|
+
(26, 22, 3, 3),
|
|
220
|
+
(31, 17, 3, 3),
|
|
221
|
+
(33, 10, 3, 3),
|
|
222
|
+
(34, 7, 3, 3),
|
|
223
|
+
(35, 13, 3, 3),
|
|
224
|
+
],
|
|
225
|
+
initial_flashlight_count=5,
|
|
226
|
+
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
227
|
+
),
|
|
140
228
|
]
|
|
141
229
|
DEFAULT_STAGE_ID = "stage1"
|
|
142
230
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Iterable, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .entities import Wall
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
WallIndex = dict[tuple[int, int], list["Wall"]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_wall_index(walls: Iterable["Wall"], *, cell_size: int) -> WallIndex:
|
|
14
|
+
index: WallIndex = {}
|
|
15
|
+
if cell_size <= 0:
|
|
16
|
+
return index
|
|
17
|
+
for wall in walls:
|
|
18
|
+
cell_x = int(wall.rect.centerx // cell_size)
|
|
19
|
+
cell_y = int(wall.rect.centery // cell_size)
|
|
20
|
+
index.setdefault((cell_x, cell_y), []).append(wall)
|
|
21
|
+
return index
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _infer_grid_size_from_index(wall_index: WallIndex) -> tuple[int | None, int | None]:
|
|
25
|
+
if not wall_index:
|
|
26
|
+
return None, None
|
|
27
|
+
max_col = max(cell[0] for cell in wall_index)
|
|
28
|
+
max_row = max(cell[1] for cell in wall_index)
|
|
29
|
+
return max_col + 1, max_row + 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def walls_for_radius(
|
|
33
|
+
wall_index: WallIndex,
|
|
34
|
+
center: tuple[float, float],
|
|
35
|
+
radius: float,
|
|
36
|
+
*,
|
|
37
|
+
cell_size: int,
|
|
38
|
+
grid_cols: int | None = None,
|
|
39
|
+
grid_rows: int | None = None,
|
|
40
|
+
) -> list["Wall"]:
|
|
41
|
+
if grid_cols is None or grid_rows is None:
|
|
42
|
+
grid_cols, grid_rows = _infer_grid_size_from_index(wall_index)
|
|
43
|
+
if grid_cols is None or grid_rows is None:
|
|
44
|
+
return []
|
|
45
|
+
search_radius = radius + cell_size
|
|
46
|
+
min_x = max(0, int((center[0] - search_radius) // cell_size))
|
|
47
|
+
max_x = min(grid_cols - 1, int((center[0] + search_radius) // cell_size))
|
|
48
|
+
min_y = max(0, int((center[1] - search_radius) // cell_size))
|
|
49
|
+
max_y = min(grid_rows - 1, int((center[1] + search_radius) // cell_size))
|
|
50
|
+
candidates: list[Wall] = []
|
|
51
|
+
for cy in range(min_y, max_y + 1):
|
|
52
|
+
for cx in range(min_x, max_x + 1):
|
|
53
|
+
candidates.extend(wall_index.get((cx, cy), []))
|
|
54
|
+
return candidates
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def apply_tile_edge_nudge(
|
|
58
|
+
x: float,
|
|
59
|
+
y: float,
|
|
60
|
+
dx: float,
|
|
61
|
+
dy: float,
|
|
62
|
+
*,
|
|
63
|
+
cell_size: int,
|
|
64
|
+
wall_cells: set[tuple[int, int]] | None,
|
|
65
|
+
bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] | None = None,
|
|
66
|
+
grid_cols: int,
|
|
67
|
+
grid_rows: int,
|
|
68
|
+
strength: float = 0.03,
|
|
69
|
+
edge_margin_ratio: float = 0.15,
|
|
70
|
+
min_margin: float = 2.0,
|
|
71
|
+
) -> tuple[float, float]:
|
|
72
|
+
if dx == 0 and dy == 0:
|
|
73
|
+
return dx, dy
|
|
74
|
+
if cell_size <= 0 or not wall_cells:
|
|
75
|
+
return dx, dy
|
|
76
|
+
cell_x = int(x // cell_size)
|
|
77
|
+
cell_y = int(y // cell_size)
|
|
78
|
+
if cell_x < 0 or cell_y < 0 or cell_x >= grid_cols or cell_y >= grid_rows:
|
|
79
|
+
return dx, dy
|
|
80
|
+
speed = math.hypot(dx, dy)
|
|
81
|
+
if speed <= 0:
|
|
82
|
+
return dx, dy
|
|
83
|
+
|
|
84
|
+
edge_margin = max(min_margin, cell_size * edge_margin_ratio)
|
|
85
|
+
left_dist = x - (cell_x * cell_size)
|
|
86
|
+
right_dist = ((cell_x + 1) * cell_size) - x
|
|
87
|
+
top_dist = y - (cell_y * cell_size)
|
|
88
|
+
bottom_dist = ((cell_y + 1) * cell_size) - y
|
|
89
|
+
|
|
90
|
+
def apply_push(dist: float, direction: float) -> float:
|
|
91
|
+
if dist >= edge_margin:
|
|
92
|
+
return 0.0
|
|
93
|
+
ratio = (edge_margin - dist) / edge_margin
|
|
94
|
+
return ratio * speed * strength * direction
|
|
95
|
+
|
|
96
|
+
if (cell_x - 1, cell_y) in wall_cells:
|
|
97
|
+
dx += apply_push(left_dist, 1.0)
|
|
98
|
+
if (cell_x + 1, cell_y) in wall_cells:
|
|
99
|
+
dx += apply_push(right_dist, -1.0)
|
|
100
|
+
if (cell_x, cell_y - 1) in wall_cells:
|
|
101
|
+
dy += apply_push(top_dist, 1.0)
|
|
102
|
+
if (cell_x, cell_y + 1) in wall_cells:
|
|
103
|
+
dy += apply_push(bottom_dist, -1.0)
|
|
104
|
+
|
|
105
|
+
def apply_corner_push(dist_a: float, dist_b: float, boost: float = 1.0) -> float:
|
|
106
|
+
if dist_a >= edge_margin or dist_b >= edge_margin:
|
|
107
|
+
return 0.0
|
|
108
|
+
ratio = (edge_margin - min(dist_a, dist_b)) / edge_margin
|
|
109
|
+
return ratio * speed * strength * boost
|
|
110
|
+
|
|
111
|
+
if bevel_corners:
|
|
112
|
+
boosted = 1.25
|
|
113
|
+
corner_wall = bevel_corners.get((cell_x - 1, cell_y - 1))
|
|
114
|
+
if corner_wall and corner_wall[2]:
|
|
115
|
+
push = apply_corner_push(left_dist, top_dist, boosted)
|
|
116
|
+
dx += push
|
|
117
|
+
dy += push
|
|
118
|
+
corner_wall = bevel_corners.get((cell_x + 1, cell_y - 1))
|
|
119
|
+
if corner_wall and corner_wall[3]:
|
|
120
|
+
push = apply_corner_push(right_dist, top_dist, boosted)
|
|
121
|
+
dx -= push
|
|
122
|
+
dy += push
|
|
123
|
+
corner_wall = bevel_corners.get((cell_x + 1, cell_y + 1))
|
|
124
|
+
if corner_wall and corner_wall[0]:
|
|
125
|
+
push = apply_corner_push(right_dist, bottom_dist, boosted)
|
|
126
|
+
dx -= push
|
|
127
|
+
dy -= push
|
|
128
|
+
corner_wall = bevel_corners.get((cell_x - 1, cell_y + 1))
|
|
129
|
+
if corner_wall and corner_wall[1]:
|
|
130
|
+
push = apply_corner_push(left_dist, bottom_dist, boosted)
|
|
131
|
+
dx += push
|
|
132
|
+
dy -= push
|
|
133
|
+
|
|
134
|
+
return dx, dy
|
zombie_escape/zombie_escape.py
CHANGED
|
@@ -22,7 +22,7 @@ from .gameplay import calculate_car_speed_for_passengers
|
|
|
22
22
|
from .level_constants import DEFAULT_TILE_SIZE
|
|
23
23
|
from .localization import set_language
|
|
24
24
|
from .models import GameData, Stage
|
|
25
|
-
from .render_constants import build_render_assets
|
|
25
|
+
from .render_constants import RenderAssets, build_render_assets
|
|
26
26
|
from .screen_constants import (
|
|
27
27
|
DEFAULT_WINDOW_SCALE,
|
|
28
28
|
FPS,
|
|
@@ -95,6 +95,7 @@ def main() -> None:
|
|
|
95
95
|
from .screens.gameplay import gameplay_screen
|
|
96
96
|
|
|
97
97
|
apply_window_scale(DEFAULT_WINDOW_SCALE)
|
|
98
|
+
pygame.mouse.set_visible(True)
|
|
98
99
|
screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
|
|
99
100
|
clock = pygame.time.Clock()
|
|
100
101
|
|
|
@@ -109,15 +110,54 @@ def main() -> None:
|
|
|
109
110
|
save_config(config, config_path)
|
|
110
111
|
set_language(config.get("language"))
|
|
111
112
|
|
|
113
|
+
def _profiled_gameplay_screen(
|
|
114
|
+
screen: pygame.Surface,
|
|
115
|
+
clock: pygame.time.Clock,
|
|
116
|
+
config: dict[str, Any],
|
|
117
|
+
fps: int,
|
|
118
|
+
stage: Stage,
|
|
119
|
+
*,
|
|
120
|
+
show_pause_overlay: bool,
|
|
121
|
+
seed: int | None,
|
|
122
|
+
render_assets: RenderAssets,
|
|
123
|
+
debug_mode: bool,
|
|
124
|
+
) -> ScreenTransition:
|
|
125
|
+
import cProfile
|
|
126
|
+
import pstats
|
|
127
|
+
|
|
128
|
+
profiler = cProfile.Profile()
|
|
129
|
+
try:
|
|
130
|
+
return profiler.runcall(
|
|
131
|
+
gameplay_screen,
|
|
132
|
+
screen,
|
|
133
|
+
clock,
|
|
134
|
+
config,
|
|
135
|
+
fps,
|
|
136
|
+
stage,
|
|
137
|
+
show_pause_overlay=show_pause_overlay,
|
|
138
|
+
seed=seed,
|
|
139
|
+
render_assets=render_assets,
|
|
140
|
+
debug_mode=debug_mode,
|
|
141
|
+
)
|
|
142
|
+
finally:
|
|
143
|
+
output_path = Path(args.profile_output)
|
|
144
|
+
profiler.dump_stats(output_path)
|
|
145
|
+
summary_path = output_path.with_suffix(".txt")
|
|
146
|
+
with summary_path.open("w", encoding="utf-8") as handle:
|
|
147
|
+
stats = pstats.Stats(
|
|
148
|
+
profiler,
|
|
149
|
+
stream=handle,
|
|
150
|
+
).sort_stats("tottime")
|
|
151
|
+
stats.print_stats(50)
|
|
152
|
+
print(f"Profile saved to {output_path} and {summary_path}")
|
|
153
|
+
|
|
112
154
|
next_screen = ScreenID.TITLE
|
|
113
|
-
|
|
114
|
-
pending_game_data: GameData | None = None
|
|
115
|
-
pending_config: dict[str, Any] | None = None
|
|
116
|
-
pending_seed: int | None = None
|
|
155
|
+
transition: ScreenTransition | None = None
|
|
117
156
|
running = True
|
|
118
157
|
|
|
119
158
|
while running:
|
|
120
|
-
|
|
159
|
+
incoming = transition
|
|
160
|
+
transition = None
|
|
121
161
|
|
|
122
162
|
if next_screen == ScreenID.TITLE:
|
|
123
163
|
seed_input = None if title_seed_is_auto else title_seed_text
|
|
@@ -147,56 +187,26 @@ def main() -> None:
|
|
|
147
187
|
set_language(config.get("language"))
|
|
148
188
|
transition = ScreenTransition(ScreenID.TITLE)
|
|
149
189
|
elif next_screen == ScreenID.GAMEPLAY:
|
|
150
|
-
stage =
|
|
151
|
-
|
|
152
|
-
seed_value = pending_seed
|
|
153
|
-
pending_seed = None
|
|
190
|
+
stage = incoming.stage
|
|
191
|
+
seed_value = incoming.seed
|
|
154
192
|
if stage is None:
|
|
155
193
|
transition = ScreenTransition(ScreenID.TITLE)
|
|
156
194
|
else:
|
|
157
195
|
last_stage_id = stage.id
|
|
196
|
+
render_assets = build_render_assets(stage.tile_size)
|
|
158
197
|
try:
|
|
159
|
-
if args.profile
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
FPS,
|
|
172
|
-
stage,
|
|
173
|
-
show_pause_overlay=not debug_mode,
|
|
174
|
-
seed=seed_value,
|
|
175
|
-
render_assets=render_assets,
|
|
176
|
-
debug_mode=debug_mode,
|
|
177
|
-
)
|
|
178
|
-
finally:
|
|
179
|
-
output_path = Path(args.profile_output)
|
|
180
|
-
profiler.dump_stats(output_path)
|
|
181
|
-
summary_path = output_path.with_suffix(".txt")
|
|
182
|
-
stats = pstats.Stats(profiler).sort_stats("tottime")
|
|
183
|
-
with summary_path.open("w", encoding="utf-8") as handle:
|
|
184
|
-
stats.stream = handle
|
|
185
|
-
stats.print_stats(50)
|
|
186
|
-
print(f"Profile saved to {output_path} and {summary_path}")
|
|
187
|
-
else:
|
|
188
|
-
render_assets = build_render_assets(stage.tile_size)
|
|
189
|
-
transition = gameplay_screen(
|
|
190
|
-
screen,
|
|
191
|
-
clock,
|
|
192
|
-
config,
|
|
193
|
-
FPS,
|
|
194
|
-
stage,
|
|
195
|
-
show_pause_overlay=not debug_mode,
|
|
196
|
-
seed=seed_value,
|
|
197
|
-
render_assets=render_assets,
|
|
198
|
-
debug_mode=debug_mode,
|
|
199
|
-
)
|
|
198
|
+
gs = _profiled_gameplay_screen if args.profile else gameplay_screen
|
|
199
|
+
transition = gs(
|
|
200
|
+
screen,
|
|
201
|
+
clock,
|
|
202
|
+
config,
|
|
203
|
+
FPS,
|
|
204
|
+
stage,
|
|
205
|
+
show_pause_overlay=not debug_mode,
|
|
206
|
+
seed=seed_value,
|
|
207
|
+
render_assets=render_assets,
|
|
208
|
+
debug_mode=debug_mode,
|
|
209
|
+
)
|
|
200
210
|
except SystemExit:
|
|
201
211
|
running = False
|
|
202
212
|
break
|
|
@@ -206,12 +216,10 @@ def main() -> None:
|
|
|
206
216
|
running = False
|
|
207
217
|
break
|
|
208
218
|
elif next_screen == ScreenID.GAME_OVER:
|
|
209
|
-
game_data =
|
|
210
|
-
stage =
|
|
211
|
-
config_payload =
|
|
212
|
-
|
|
213
|
-
pending_stage = None
|
|
214
|
-
pending_config = None
|
|
219
|
+
game_data = incoming.game_data if incoming else None
|
|
220
|
+
stage = incoming.stage if incoming else None
|
|
221
|
+
config_payload = incoming.config if incoming else None
|
|
222
|
+
assert config_payload is not None
|
|
215
223
|
if game_data is not None:
|
|
216
224
|
render_assets = build_render_assets(game_data.cell_size)
|
|
217
225
|
elif stage is not None:
|
|
@@ -235,10 +243,6 @@ def main() -> None:
|
|
|
235
243
|
if not transition:
|
|
236
244
|
break
|
|
237
245
|
|
|
238
|
-
pending_stage = transition.stage
|
|
239
|
-
pending_game_data = transition.game_data
|
|
240
|
-
pending_config = transition.config
|
|
241
|
-
pending_seed = transition.seed
|
|
242
246
|
if transition.next_screen == ScreenID.GAMEPLAY:
|
|
243
247
|
title_seed_text = cli_seed_text
|
|
244
248
|
title_seed_is_auto = cli_seed_is_auto
|