zombie-escape 1.7.1__py3-none-any.whl → 1.10.0__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 +41 -8
- zombie_escape/entities.py +376 -306
- zombie_escape/entities_constants.py +6 -0
- zombie_escape/gameplay/__init__.py +2 -2
- zombie_escape/gameplay/constants.py +1 -7
- zombie_escape/gameplay/footprints.py +2 -2
- zombie_escape/gameplay/interactions.py +4 -10
- zombie_escape/gameplay/layout.py +43 -4
- zombie_escape/gameplay/movement.py +45 -7
- zombie_escape/gameplay/spawn.py +283 -43
- zombie_escape/gameplay/state.py +19 -16
- zombie_escape/gameplay/survivors.py +47 -15
- zombie_escape/gameplay/utils.py +19 -1
- zombie_escape/input_utils.py +167 -0
- zombie_escape/level_blueprints.py +28 -0
- zombie_escape/locales/ui.en.json +55 -11
- zombie_escape/locales/ui.ja.json +54 -10
- zombie_escape/localization.py +28 -0
- zombie_escape/models.py +54 -7
- zombie_escape/render.py +704 -267
- zombie_escape/render_constants.py +12 -0
- zombie_escape/screens/__init__.py +1 -0
- zombie_escape/screens/game_over.py +8 -4
- zombie_escape/screens/gameplay.py +88 -41
- zombie_escape/screens/settings.py +124 -13
- zombie_escape/screens/title.py +111 -0
- zombie_escape/stage_constants.py +116 -3
- zombie_escape/world_grid.py +134 -0
- zombie_escape/zombie_escape.py +68 -61
- {zombie_escape-1.7.1.dist-info → zombie_escape-1.10.0.dist-info}/METADATA +11 -3
- zombie_escape-1.10.0.dist-info/RECORD +47 -0
- zombie_escape-1.7.1.dist-info/RECORD +0 -45
- {zombie_escape-1.7.1.dist-info → zombie_escape-1.10.0.dist-info}/WHEEL +0 -0
- {zombie_escape-1.7.1.dist-info → zombie_escape-1.10.0.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.7.1.dist-info → zombie_escape-1.10.0.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/screens/title.py
CHANGED
|
@@ -14,6 +14,18 @@ from ..models import Stage
|
|
|
14
14
|
from ..progress import load_progress
|
|
15
15
|
from ..render import show_message
|
|
16
16
|
from ..rng import generate_seed
|
|
17
|
+
from ..input_utils import (
|
|
18
|
+
CONTROLLER_BUTTON_DOWN,
|
|
19
|
+
CONTROLLER_BUTTON_DPAD_DOWN,
|
|
20
|
+
CONTROLLER_BUTTON_DPAD_LEFT,
|
|
21
|
+
CONTROLLER_BUTTON_DPAD_RIGHT,
|
|
22
|
+
CONTROLLER_BUTTON_DPAD_UP,
|
|
23
|
+
CONTROLLER_DEVICE_ADDED,
|
|
24
|
+
CONTROLLER_DEVICE_REMOVED,
|
|
25
|
+
init_first_controller,
|
|
26
|
+
init_first_joystick,
|
|
27
|
+
is_confirm_event,
|
|
28
|
+
)
|
|
17
29
|
from ..screens import (
|
|
18
30
|
ScreenID,
|
|
19
31
|
ScreenTransition,
|
|
@@ -193,6 +205,8 @@ def title_screen(
|
|
|
193
205
|
0,
|
|
194
206
|
)
|
|
195
207
|
selected = min(selected_stage_index, len(options) - 1)
|
|
208
|
+
controller = init_first_controller()
|
|
209
|
+
joystick = init_first_joystick() if controller is None else None
|
|
196
210
|
|
|
197
211
|
while True:
|
|
198
212
|
for event in pygame.event.get():
|
|
@@ -205,6 +219,22 @@ def title_screen(
|
|
|
205
219
|
if event.type in (pygame.WINDOWSIZECHANGED, pygame.VIDEORESIZE):
|
|
206
220
|
sync_window_size(event)
|
|
207
221
|
continue
|
|
222
|
+
if event.type == pygame.JOYDEVICEADDED or (
|
|
223
|
+
CONTROLLER_DEVICE_ADDED is not None
|
|
224
|
+
and event.type == CONTROLLER_DEVICE_ADDED
|
|
225
|
+
):
|
|
226
|
+
if controller is None:
|
|
227
|
+
controller = init_first_controller()
|
|
228
|
+
if controller is None:
|
|
229
|
+
joystick = init_first_joystick()
|
|
230
|
+
if event.type == pygame.JOYDEVICEREMOVED or (
|
|
231
|
+
CONTROLLER_DEVICE_REMOVED is not None
|
|
232
|
+
and event.type == CONTROLLER_DEVICE_REMOVED
|
|
233
|
+
):
|
|
234
|
+
if controller and not controller.get_init():
|
|
235
|
+
controller = None
|
|
236
|
+
if joystick and not joystick.get_init():
|
|
237
|
+
joystick = None
|
|
208
238
|
if event.type == pygame.KEYDOWN:
|
|
209
239
|
if event.key == pygame.K_BACKSPACE:
|
|
210
240
|
current_seed_text = _generate_auto_seed_text()
|
|
@@ -273,6 +303,87 @@ def title_screen(
|
|
|
273
303
|
seed_text=current_seed_text,
|
|
274
304
|
seed_is_auto=current_seed_auto,
|
|
275
305
|
)
|
|
306
|
+
if event.type == pygame.JOYBUTTONDOWN or (
|
|
307
|
+
CONTROLLER_BUTTON_DOWN is not None
|
|
308
|
+
and event.type == CONTROLLER_BUTTON_DOWN
|
|
309
|
+
):
|
|
310
|
+
if is_confirm_event(event):
|
|
311
|
+
current = options[selected]
|
|
312
|
+
if current["type"] == "stage" and current.get("available"):
|
|
313
|
+
seed_value = (
|
|
314
|
+
int(current_seed_text) if current_seed_text else None
|
|
315
|
+
)
|
|
316
|
+
return ScreenTransition(
|
|
317
|
+
ScreenID.GAMEPLAY,
|
|
318
|
+
stage=current["stage"],
|
|
319
|
+
seed=seed_value,
|
|
320
|
+
seed_text=current_seed_text,
|
|
321
|
+
seed_is_auto=current_seed_auto,
|
|
322
|
+
)
|
|
323
|
+
if current["type"] == "settings":
|
|
324
|
+
return ScreenTransition(
|
|
325
|
+
ScreenID.SETTINGS,
|
|
326
|
+
seed_text=current_seed_text,
|
|
327
|
+
seed_is_auto=current_seed_auto,
|
|
328
|
+
)
|
|
329
|
+
if current["type"] == "readme":
|
|
330
|
+
_open_readme_link()
|
|
331
|
+
continue
|
|
332
|
+
if current["type"] == "quit":
|
|
333
|
+
return ScreenTransition(
|
|
334
|
+
ScreenID.EXIT,
|
|
335
|
+
seed_text=current_seed_text,
|
|
336
|
+
seed_is_auto=current_seed_auto,
|
|
337
|
+
)
|
|
338
|
+
if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
|
|
339
|
+
if (
|
|
340
|
+
CONTROLLER_BUTTON_DPAD_UP is not None
|
|
341
|
+
and event.button == CONTROLLER_BUTTON_DPAD_UP
|
|
342
|
+
):
|
|
343
|
+
selected = (selected - 1) % len(options)
|
|
344
|
+
if (
|
|
345
|
+
CONTROLLER_BUTTON_DPAD_DOWN is not None
|
|
346
|
+
and event.button == CONTROLLER_BUTTON_DPAD_DOWN
|
|
347
|
+
):
|
|
348
|
+
selected = (selected + 1) % len(options)
|
|
349
|
+
if (
|
|
350
|
+
CONTROLLER_BUTTON_DPAD_LEFT is not None
|
|
351
|
+
and event.button == CONTROLLER_BUTTON_DPAD_LEFT
|
|
352
|
+
):
|
|
353
|
+
if current_page > 0:
|
|
354
|
+
current_page -= 1
|
|
355
|
+
options, stage_options = _build_options(current_page)
|
|
356
|
+
selected = 0
|
|
357
|
+
if (
|
|
358
|
+
CONTROLLER_BUTTON_DPAD_RIGHT is not None
|
|
359
|
+
and event.button == CONTROLLER_BUTTON_DPAD_RIGHT
|
|
360
|
+
):
|
|
361
|
+
if (
|
|
362
|
+
current_page < len(stage_pages) - 1
|
|
363
|
+
and _page_available(current_page + 1)
|
|
364
|
+
):
|
|
365
|
+
current_page += 1
|
|
366
|
+
options, stage_options = _build_options(current_page)
|
|
367
|
+
selected = 0
|
|
368
|
+
if event.type == pygame.JOYHATMOTION:
|
|
369
|
+
hat_x, hat_y = event.value
|
|
370
|
+
if hat_y == 1:
|
|
371
|
+
selected = (selected - 1) % len(options)
|
|
372
|
+
elif hat_y == -1:
|
|
373
|
+
selected = (selected + 1) % len(options)
|
|
374
|
+
if hat_x == -1:
|
|
375
|
+
if current_page > 0:
|
|
376
|
+
current_page -= 1
|
|
377
|
+
options, stage_options = _build_options(current_page)
|
|
378
|
+
selected = 0
|
|
379
|
+
elif hat_x == 1:
|
|
380
|
+
if (
|
|
381
|
+
current_page < len(stage_pages) - 1
|
|
382
|
+
and _page_available(current_page + 1)
|
|
383
|
+
):
|
|
384
|
+
current_page += 1
|
|
385
|
+
options, stage_options = _build_options(current_page)
|
|
386
|
+
selected = 0
|
|
276
387
|
|
|
277
388
|
screen.fill(BLACK)
|
|
278
389
|
title_text = tr("game.title")
|
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,
|
|
@@ -67,6 +71,7 @@ STAGES: list[Stage] = [
|
|
|
67
71
|
zombie_normal_ratio=0.4,
|
|
68
72
|
zombie_tracker_ratio=0.6,
|
|
69
73
|
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
74
|
+
initial_interior_spawn_rate=0.01,
|
|
70
75
|
),
|
|
71
76
|
Stage(
|
|
72
77
|
id="stage7",
|
|
@@ -82,6 +87,7 @@ STAGES: list[Stage] = [
|
|
|
82
87
|
zombie_tracker_ratio=0.3,
|
|
83
88
|
zombie_wall_follower_ratio=0.3,
|
|
84
89
|
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
90
|
+
initial_interior_spawn_rate=0.01,
|
|
85
91
|
),
|
|
86
92
|
Stage(
|
|
87
93
|
id="stage8",
|
|
@@ -97,20 +103,127 @@ STAGES: list[Stage] = [
|
|
|
97
103
|
zombie_tracker_ratio=0.3,
|
|
98
104
|
zombie_wall_follower_ratio=0.7,
|
|
99
105
|
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
106
|
+
initial_interior_spawn_rate=0.01,
|
|
100
107
|
),
|
|
101
108
|
Stage(
|
|
102
109
|
id="stage9",
|
|
103
110
|
name_key="stages.stage9.name",
|
|
104
111
|
description_key="stages.stage9.description",
|
|
105
|
-
available=
|
|
112
|
+
available=True,
|
|
106
113
|
rescue_stage=True,
|
|
107
114
|
tile_size=35,
|
|
108
115
|
requires_fuel=True,
|
|
109
116
|
exterior_spawn_weight=0.4,
|
|
110
117
|
interior_spawn_weight=0.6,
|
|
111
118
|
waiting_car_target_count=1,
|
|
119
|
+
zombie_normal_ratio=0,
|
|
120
|
+
zombie_tracker_ratio=0.3,
|
|
121
|
+
zombie_wall_follower_ratio=0.7,
|
|
112
122
|
survivor_spawn_rate=SURVIVOR_SPAWN_RATE,
|
|
113
123
|
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
124
|
+
initial_interior_spawn_rate=0.01,
|
|
125
|
+
),
|
|
126
|
+
Stage(
|
|
127
|
+
id="stage10",
|
|
128
|
+
name_key="stages.stage10.name",
|
|
129
|
+
description_key="stages.stage10.description",
|
|
130
|
+
available=True,
|
|
131
|
+
rescue_stage=True,
|
|
132
|
+
tile_size=40,
|
|
133
|
+
wall_algorithm="sparse",
|
|
134
|
+
exterior_spawn_weight=0.7,
|
|
135
|
+
interior_spawn_weight=0.3,
|
|
136
|
+
zombie_normal_ratio=0.4,
|
|
137
|
+
zombie_tracker_ratio=0.4,
|
|
138
|
+
zombie_wall_follower_ratio=0.2,
|
|
139
|
+
zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
|
|
140
|
+
initial_interior_spawn_rate=0.02,
|
|
141
|
+
waiting_car_target_count=1,
|
|
142
|
+
survivor_spawn_rate=0.35,
|
|
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,
|
|
114
227
|
),
|
|
115
228
|
]
|
|
116
229
|
DEFAULT_STAGE_ID = "stage1"
|
|
@@ -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,
|
|
@@ -83,6 +83,9 @@ def main() -> None:
|
|
|
83
83
|
sys.argv = [sys.argv[0]] + remaining
|
|
84
84
|
|
|
85
85
|
pygame.init()
|
|
86
|
+
pygame.joystick.init()
|
|
87
|
+
if hasattr(pygame, "controller"):
|
|
88
|
+
pygame.controller.init()
|
|
86
89
|
try:
|
|
87
90
|
pygame.font.init()
|
|
88
91
|
except pygame.error as e:
|
|
@@ -92,6 +95,7 @@ def main() -> None:
|
|
|
92
95
|
from .screens.gameplay import gameplay_screen
|
|
93
96
|
|
|
94
97
|
apply_window_scale(DEFAULT_WINDOW_SCALE)
|
|
98
|
+
pygame.mouse.set_visible(True)
|
|
95
99
|
screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
|
|
96
100
|
clock = pygame.time.Clock()
|
|
97
101
|
|
|
@@ -106,15 +110,54 @@ def main() -> None:
|
|
|
106
110
|
save_config(config, config_path)
|
|
107
111
|
set_language(config.get("language"))
|
|
108
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
|
+
|
|
109
154
|
next_screen = ScreenID.TITLE
|
|
110
|
-
|
|
111
|
-
pending_game_data: GameData | None = None
|
|
112
|
-
pending_config: dict[str, Any] | None = None
|
|
113
|
-
pending_seed: int | None = None
|
|
155
|
+
transition: ScreenTransition | None = None
|
|
114
156
|
running = True
|
|
115
157
|
|
|
116
158
|
while running:
|
|
117
|
-
|
|
159
|
+
incoming = transition
|
|
160
|
+
transition = None
|
|
118
161
|
|
|
119
162
|
if next_screen == ScreenID.TITLE:
|
|
120
163
|
seed_input = None if title_seed_is_auto else title_seed_text
|
|
@@ -144,56 +187,26 @@ def main() -> None:
|
|
|
144
187
|
set_language(config.get("language"))
|
|
145
188
|
transition = ScreenTransition(ScreenID.TITLE)
|
|
146
189
|
elif next_screen == ScreenID.GAMEPLAY:
|
|
147
|
-
stage =
|
|
148
|
-
|
|
149
|
-
seed_value = pending_seed
|
|
150
|
-
pending_seed = None
|
|
190
|
+
stage = incoming.stage
|
|
191
|
+
seed_value = incoming.seed
|
|
151
192
|
if stage is None:
|
|
152
193
|
transition = ScreenTransition(ScreenID.TITLE)
|
|
153
194
|
else:
|
|
154
195
|
last_stage_id = stage.id
|
|
196
|
+
render_assets = build_render_assets(stage.tile_size)
|
|
155
197
|
try:
|
|
156
|
-
if args.profile
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
FPS,
|
|
169
|
-
stage,
|
|
170
|
-
show_pause_overlay=not debug_mode,
|
|
171
|
-
seed=seed_value,
|
|
172
|
-
render_assets=render_assets,
|
|
173
|
-
debug_mode=debug_mode,
|
|
174
|
-
)
|
|
175
|
-
finally:
|
|
176
|
-
output_path = Path(args.profile_output)
|
|
177
|
-
profiler.dump_stats(output_path)
|
|
178
|
-
summary_path = output_path.with_suffix(".txt")
|
|
179
|
-
stats = pstats.Stats(profiler).sort_stats("tottime")
|
|
180
|
-
with summary_path.open("w", encoding="utf-8") as handle:
|
|
181
|
-
stats.stream = handle
|
|
182
|
-
stats.print_stats(50)
|
|
183
|
-
print(f"Profile saved to {output_path} and {summary_path}")
|
|
184
|
-
else:
|
|
185
|
-
render_assets = build_render_assets(stage.tile_size)
|
|
186
|
-
transition = gameplay_screen(
|
|
187
|
-
screen,
|
|
188
|
-
clock,
|
|
189
|
-
config,
|
|
190
|
-
FPS,
|
|
191
|
-
stage,
|
|
192
|
-
show_pause_overlay=not debug_mode,
|
|
193
|
-
seed=seed_value,
|
|
194
|
-
render_assets=render_assets,
|
|
195
|
-
debug_mode=debug_mode,
|
|
196
|
-
)
|
|
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
|
+
)
|
|
197
210
|
except SystemExit:
|
|
198
211
|
running = False
|
|
199
212
|
break
|
|
@@ -203,12 +216,10 @@ def main() -> None:
|
|
|
203
216
|
running = False
|
|
204
217
|
break
|
|
205
218
|
elif next_screen == ScreenID.GAME_OVER:
|
|
206
|
-
game_data =
|
|
207
|
-
stage =
|
|
208
|
-
config_payload =
|
|
209
|
-
|
|
210
|
-
pending_stage = None
|
|
211
|
-
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
|
|
212
223
|
if game_data is not None:
|
|
213
224
|
render_assets = build_render_assets(game_data.cell_size)
|
|
214
225
|
elif stage is not None:
|
|
@@ -232,10 +243,6 @@ def main() -> None:
|
|
|
232
243
|
if not transition:
|
|
233
244
|
break
|
|
234
245
|
|
|
235
|
-
pending_stage = transition.stage
|
|
236
|
-
pending_game_data = transition.game_data
|
|
237
|
-
pending_config = transition.config
|
|
238
|
-
pending_seed = transition.seed
|
|
239
246
|
if transition.next_screen == ScreenID.GAMEPLAY:
|
|
240
247
|
title_seed_text = cli_seed_text
|
|
241
248
|
title_seed_is_auto = cli_seed_is_auto
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zombie-escape
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.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>
|
|
@@ -12,12 +12,19 @@ Classifier: Programming Language :: Python
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
17
|
Requires-Python: >=3.10
|
|
17
18
|
Requires-Dist: numpy
|
|
18
19
|
Requires-Dist: platformdirs
|
|
19
20
|
Requires-Dist: pygame
|
|
20
21
|
Requires-Dist: python-i18n
|
|
22
|
+
Requires-Dist: typing-extensions; python_version < '3.11'
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pydeps; extra == 'dev'
|
|
25
|
+
Requires-Dist: pyright; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
21
28
|
Description-Content-Type: text/markdown
|
|
22
29
|
|
|
23
30
|
# Zombie Escape
|
|
@@ -46,10 +53,11 @@ This game is a simple 2D top-down action game where the player aims to escape by
|
|
|
46
53
|
|
|
47
54
|
- **Player/Car Movement:** `W` / `↑` (Up), `A` / `←` (Left), `S` / `↓` (Down), `D` / `→` (Right)
|
|
48
55
|
- **Enter Car:** Overlap the player with the car.
|
|
49
|
-
- **
|
|
56
|
+
- **Pause:** `P`/Start or `ESC`/Select
|
|
57
|
+
- **Quit Game:** `ESC`/Select (from pause)
|
|
50
58
|
- **Restart:** `R` key (on Game Over/Clear screen)
|
|
51
59
|
- **Window/Fullscreen (title/settings only):** `[` to shrink, `]` to enlarge, `F` to toggle fullscreen
|
|
52
|
-
- **Time Acceleration:** Hold either `Shift` key to run the entire world 4x faster; release to return to normal speed.
|
|
60
|
+
- **Time Acceleration:** Hold either `Shift` key or `R1` to run the entire world 4x faster; release to return to normal speed.
|
|
53
61
|
|
|
54
62
|
## Title Screen
|
|
55
63
|
|