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
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""Window and presentation helpers for zombie_escape."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import pygame
|
|
8
|
+
from pygame import surface
|
|
9
|
+
|
|
10
|
+
from .screen_constants import (
|
|
11
|
+
DEFAULT_WINDOW_SCALE,
|
|
12
|
+
SCREEN_HEIGHT,
|
|
13
|
+
SCREEN_WIDTH,
|
|
14
|
+
WINDOW_SCALE_MAX,
|
|
15
|
+
WINDOW_SCALE_MIN,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
19
|
+
from .models import GameData
|
|
20
|
+
|
|
21
|
+
current_window_scale = DEFAULT_WINDOW_SCALE # Applied to the OS window only
|
|
22
|
+
current_maximized = False
|
|
23
|
+
last_window_scale = DEFAULT_WINDOW_SCALE
|
|
24
|
+
last_window_position: tuple[int, int] | None = None
|
|
25
|
+
current_window_size = (
|
|
26
|
+
int(SCREEN_WIDTH * DEFAULT_WINDOW_SCALE),
|
|
27
|
+
int(SCREEN_HEIGHT * DEFAULT_WINDOW_SCALE),
|
|
28
|
+
)
|
|
29
|
+
last_logged_window_size = current_window_size
|
|
30
|
+
_scaled_logical_size = (SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"present",
|
|
34
|
+
"present_direct",
|
|
35
|
+
"apply_window_scale",
|
|
36
|
+
"prime_scaled_logical_size",
|
|
37
|
+
"nudge_window_scale",
|
|
38
|
+
"nudge_menu_window_scale",
|
|
39
|
+
"toggle_fullscreen",
|
|
40
|
+
"sync_window_size",
|
|
41
|
+
"adjust_menu_logical_size",
|
|
42
|
+
"set_scaled_logical_size",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def present(logical_surface: surface.Surface) -> None:
|
|
47
|
+
"""Scale the logical surface directly to the window and flip buffers."""
|
|
48
|
+
window = pygame.display.get_surface()
|
|
49
|
+
if window is None:
|
|
50
|
+
return
|
|
51
|
+
window_size = _fetch_window_size(window)
|
|
52
|
+
_update_window_size(window_size, source="frame")
|
|
53
|
+
logical_size = logical_surface.get_size()
|
|
54
|
+
if _use_scaled_display():
|
|
55
|
+
target_size = window.get_size()
|
|
56
|
+
if logical_size == target_size:
|
|
57
|
+
scaled_surface = logical_surface
|
|
58
|
+
elif logical_size[0] * 2 == target_size[0] and logical_size[1] * 2 == target_size[1]:
|
|
59
|
+
scaled_surface = pygame.transform.scale2x(logical_surface)
|
|
60
|
+
else:
|
|
61
|
+
scaled_surface = pygame.transform.scale(logical_surface, target_size)
|
|
62
|
+
window.blit(scaled_surface, (0, 0))
|
|
63
|
+
elif window_size == logical_size:
|
|
64
|
+
window.blit(logical_surface, (0, 0))
|
|
65
|
+
else:
|
|
66
|
+
# Preserve aspect ratio with letterboxing.
|
|
67
|
+
scale_x = window_size[0] / max(1, logical_size[0])
|
|
68
|
+
scale_y = window_size[1] / max(1, logical_size[1])
|
|
69
|
+
scale = min(scale_x, scale_y)
|
|
70
|
+
scaled_width = max(1, int(logical_size[0] * scale))
|
|
71
|
+
scaled_height = max(1, int(logical_size[1] * scale))
|
|
72
|
+
window.fill((0, 0, 0))
|
|
73
|
+
if (scaled_width, scaled_height) == logical_size:
|
|
74
|
+
scaled_surface = logical_surface
|
|
75
|
+
else:
|
|
76
|
+
scaled_surface = pygame.transform.scale(logical_surface, (scaled_width, scaled_height))
|
|
77
|
+
offset_x = (window_size[0] - scaled_width) // 2
|
|
78
|
+
offset_y = (window_size[1] - scaled_height) // 2
|
|
79
|
+
window.blit(scaled_surface, (offset_x, offset_y))
|
|
80
|
+
pygame.display.flip()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def present_direct(screen: surface.Surface) -> None:
|
|
84
|
+
"""Flip the display without scaling; intended for direct window rendering."""
|
|
85
|
+
window = pygame.display.get_surface()
|
|
86
|
+
if window is None:
|
|
87
|
+
return
|
|
88
|
+
if window is not screen:
|
|
89
|
+
window.blit(screen, (0, 0))
|
|
90
|
+
pygame.display.flip()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def apply_window_scale(scale: float, *, game_data: "GameData | None" = None) -> surface.Surface:
|
|
94
|
+
"""Resize the OS window; logical render surface stays constant."""
|
|
95
|
+
global current_window_scale, current_maximized, last_window_scale
|
|
96
|
+
|
|
97
|
+
clamped_scale = max(WINDOW_SCALE_MIN, min(WINDOW_SCALE_MAX, scale))
|
|
98
|
+
current_window_scale = clamped_scale
|
|
99
|
+
last_window_scale = clamped_scale
|
|
100
|
+
current_maximized = False
|
|
101
|
+
|
|
102
|
+
window_width = max(1, int(SCREEN_WIDTH * current_window_scale))
|
|
103
|
+
window_height = max(1, int(SCREEN_HEIGHT * current_window_scale))
|
|
104
|
+
|
|
105
|
+
flags = pygame.RESIZABLE
|
|
106
|
+
if _use_scaled_display():
|
|
107
|
+
flags |= pygame.SCALED
|
|
108
|
+
new_window = pygame.display.set_mode(_scaled_logical_size, flags)
|
|
109
|
+
_set_window_size((window_width, window_height))
|
|
110
|
+
else:
|
|
111
|
+
new_window = pygame.display.set_mode((window_width, window_height), flags)
|
|
112
|
+
_update_window_size((window_width, window_height), source="apply_scale")
|
|
113
|
+
_update_window_caption()
|
|
114
|
+
|
|
115
|
+
if game_data is not None:
|
|
116
|
+
game_data.state.overview_created = False
|
|
117
|
+
|
|
118
|
+
return new_window
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def prime_scaled_logical_size(size: tuple[int, int]) -> None:
|
|
122
|
+
"""Set initial logical render size before the first window is created."""
|
|
123
|
+
global _scaled_logical_size
|
|
124
|
+
target = _normalize_window_size(size)
|
|
125
|
+
_scaled_logical_size = target
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def nudge_window_scale(multiplier: float, *, game_data: "GameData | None" = None) -> surface.Surface:
|
|
129
|
+
"""Scale the window relative to the current zoom level."""
|
|
130
|
+
delta = 1.0 if multiplier >= 1.0 else -1.0
|
|
131
|
+
target_scale = current_window_scale + delta
|
|
132
|
+
return apply_window_scale(target_scale, game_data=game_data)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def nudge_menu_window_scale(multiplier: float, *, game_data: "GameData | None" = None) -> surface.Surface:
|
|
136
|
+
"""Scale the window and update menu logical size consistently."""
|
|
137
|
+
delta = 1.0 if multiplier >= 1.0 else -1.0
|
|
138
|
+
target_scale = current_window_scale + delta
|
|
139
|
+
set_scaled_logical_size((SCREEN_WIDTH, SCREEN_HEIGHT), preserve_window_size=False, game_data=game_data)
|
|
140
|
+
return apply_window_scale(target_scale, game_data=game_data)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def toggle_fullscreen(*, game_data: "GameData | None" = None) -> surface.Surface | None:
|
|
144
|
+
"""Toggle fullscreen without persisting the setting."""
|
|
145
|
+
global current_maximized, last_window_scale, last_window_position
|
|
146
|
+
if current_maximized:
|
|
147
|
+
current_maximized = False
|
|
148
|
+
window_width = max(1, int(SCREEN_WIDTH * last_window_scale))
|
|
149
|
+
window_height = max(1, int(SCREEN_HEIGHT * last_window_scale))
|
|
150
|
+
_set_sdl2_fullscreen(False, None)
|
|
151
|
+
flags = pygame.RESIZABLE
|
|
152
|
+
if _use_scaled_display():
|
|
153
|
+
flags |= pygame.SCALED
|
|
154
|
+
window = pygame.display.set_mode(_scaled_logical_size, flags)
|
|
155
|
+
_set_window_size((window_width, window_height))
|
|
156
|
+
else:
|
|
157
|
+
window = pygame.display.set_mode((window_width, window_height), flags)
|
|
158
|
+
if last_window_position is not None:
|
|
159
|
+
_restore_window_position(last_window_position)
|
|
160
|
+
_restore_window()
|
|
161
|
+
_update_window_caption()
|
|
162
|
+
_update_window_size((window_width, window_height), source="toggle_windowed")
|
|
163
|
+
else:
|
|
164
|
+
last_window_scale = current_window_scale
|
|
165
|
+
last_window_position = _fetch_window_position()
|
|
166
|
+
current_maximized = True
|
|
167
|
+
display_index = _fetch_display_index()
|
|
168
|
+
window = None
|
|
169
|
+
if _set_sdl2_fullscreen(True, display_index):
|
|
170
|
+
window = pygame.display.get_surface()
|
|
171
|
+
if window is None:
|
|
172
|
+
flags = pygame.FULLSCREEN
|
|
173
|
+
render_size = (0, 0)
|
|
174
|
+
if _use_scaled_display():
|
|
175
|
+
flags |= pygame.SCALED
|
|
176
|
+
render_size = _scaled_logical_size
|
|
177
|
+
if display_index is None:
|
|
178
|
+
window = pygame.display.set_mode(render_size, flags)
|
|
179
|
+
else:
|
|
180
|
+
window = pygame.display.set_mode(render_size, flags, display=display_index)
|
|
181
|
+
window_width, window_height = _fetch_window_size(window)
|
|
182
|
+
_update_window_caption()
|
|
183
|
+
_update_window_size((window_width, window_height), source="toggle_fullscreen")
|
|
184
|
+
pygame.mouse.set_visible(not current_maximized)
|
|
185
|
+
if game_data is not None:
|
|
186
|
+
game_data.state.overview_created = False
|
|
187
|
+
return window
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def sync_window_size(event: pygame.event.Event, *, game_data: "GameData | None" = None) -> None:
|
|
191
|
+
"""Synchronize tracked window size with SDL window events."""
|
|
192
|
+
global current_window_scale, last_window_scale
|
|
193
|
+
size = getattr(event, "size", None)
|
|
194
|
+
if not size:
|
|
195
|
+
width = getattr(event, "x", None)
|
|
196
|
+
height = getattr(event, "y", None)
|
|
197
|
+
if width is not None and height is not None:
|
|
198
|
+
size = (width, height)
|
|
199
|
+
if not size:
|
|
200
|
+
return
|
|
201
|
+
window_width, window_height = _normalize_window_size(size)
|
|
202
|
+
_update_window_size((window_width, window_height), source="window_event")
|
|
203
|
+
if not current_maximized:
|
|
204
|
+
scale_x = window_width / max(1, SCREEN_WIDTH)
|
|
205
|
+
scale_y = window_height / max(1, SCREEN_HEIGHT)
|
|
206
|
+
scale = max(WINDOW_SCALE_MIN, min(WINDOW_SCALE_MAX, min(scale_x, scale_y)))
|
|
207
|
+
current_window_scale = scale
|
|
208
|
+
last_window_scale = scale
|
|
209
|
+
_update_window_caption()
|
|
210
|
+
if game_data is not None:
|
|
211
|
+
game_data.state.overview_created = False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def set_scaled_logical_size(
|
|
215
|
+
size: tuple[int, int],
|
|
216
|
+
*,
|
|
217
|
+
preserve_window_size: bool = True,
|
|
218
|
+
game_data: "GameData | None" = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Update the logical render size when using pygame.SCALED."""
|
|
221
|
+
global _scaled_logical_size
|
|
222
|
+
if not _use_scaled_display():
|
|
223
|
+
return
|
|
224
|
+
target = _normalize_window_size(size)
|
|
225
|
+
if target == _scaled_logical_size:
|
|
226
|
+
return
|
|
227
|
+
previous_window_size = _fetch_window_size(pygame.display.get_surface())
|
|
228
|
+
_scaled_logical_size = target
|
|
229
|
+
flags = pygame.SCALED
|
|
230
|
+
if current_maximized:
|
|
231
|
+
flags |= pygame.FULLSCREEN
|
|
232
|
+
pygame.display.set_mode(_scaled_logical_size, flags)
|
|
233
|
+
else:
|
|
234
|
+
flags |= pygame.RESIZABLE
|
|
235
|
+
pygame.display.set_mode(_scaled_logical_size, flags)
|
|
236
|
+
if preserve_window_size:
|
|
237
|
+
_set_window_size(previous_window_size)
|
|
238
|
+
_update_window_caption()
|
|
239
|
+
if game_data is not None:
|
|
240
|
+
game_data.state.overview_created = False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def adjust_menu_logical_size(*, game_data: "GameData | None" = None) -> None:
|
|
244
|
+
"""Match menu render size to the current window scale."""
|
|
245
|
+
set_scaled_logical_size((SCREEN_WIDTH, SCREEN_HEIGHT), game_data=game_data)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _fetch_window_size(window: surface.Surface | None) -> tuple[int, int]:
|
|
249
|
+
if hasattr(pygame.display, "get_window_size"):
|
|
250
|
+
size = pygame.display.get_window_size()
|
|
251
|
+
if size != (0, 0):
|
|
252
|
+
return _normalize_window_size(size)
|
|
253
|
+
if window is not None:
|
|
254
|
+
return _normalize_window_size(window.get_size())
|
|
255
|
+
window_width = max(1, int(SCREEN_WIDTH * last_window_scale))
|
|
256
|
+
window_height = max(1, int(SCREEN_HEIGHT * last_window_scale))
|
|
257
|
+
return window_width, window_height
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _normalize_window_size(size: tuple[int, int]) -> tuple[int, int]:
|
|
261
|
+
width = max(1, int(size[0]))
|
|
262
|
+
height = max(1, int(size[1]))
|
|
263
|
+
return width, height
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _set_window_size(size: tuple[int, int]) -> None:
|
|
267
|
+
setter = getattr(pygame.display, "set_window_size", None)
|
|
268
|
+
if callable(setter):
|
|
269
|
+
try:
|
|
270
|
+
setter(size)
|
|
271
|
+
return
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
try:
|
|
275
|
+
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
276
|
+
except Exception:
|
|
277
|
+
return
|
|
278
|
+
try:
|
|
279
|
+
window = sdl2.Window.from_display_module()
|
|
280
|
+
except Exception:
|
|
281
|
+
return
|
|
282
|
+
setter = getattr(window, "set_size", None)
|
|
283
|
+
if callable(setter):
|
|
284
|
+
try:
|
|
285
|
+
setter(size)
|
|
286
|
+
return
|
|
287
|
+
except Exception:
|
|
288
|
+
return
|
|
289
|
+
try:
|
|
290
|
+
window.size = size
|
|
291
|
+
except Exception:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _use_scaled_display() -> bool:
|
|
296
|
+
return hasattr(pygame, "SCALED")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _fetch_window_position() -> tuple[int, int] | None:
|
|
300
|
+
try:
|
|
301
|
+
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
302
|
+
except Exception:
|
|
303
|
+
return None
|
|
304
|
+
try:
|
|
305
|
+
window = sdl2.Window.from_display_module()
|
|
306
|
+
except Exception:
|
|
307
|
+
return None
|
|
308
|
+
try:
|
|
309
|
+
position = window.position
|
|
310
|
+
except Exception:
|
|
311
|
+
return None
|
|
312
|
+
if not position:
|
|
313
|
+
return None
|
|
314
|
+
return (int(position[0]), int(position[1]))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _restore_window_position(position: tuple[int, int]) -> None:
|
|
318
|
+
try:
|
|
319
|
+
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
320
|
+
except Exception:
|
|
321
|
+
return
|
|
322
|
+
try:
|
|
323
|
+
window = sdl2.Window.from_display_module()
|
|
324
|
+
except Exception:
|
|
325
|
+
return
|
|
326
|
+
setter = getattr(window, "set_position", None)
|
|
327
|
+
if setter is not None:
|
|
328
|
+
try:
|
|
329
|
+
setter(position)
|
|
330
|
+
return
|
|
331
|
+
except Exception:
|
|
332
|
+
return
|
|
333
|
+
try:
|
|
334
|
+
window.position = position
|
|
335
|
+
except Exception:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _fetch_display_index() -> int | None:
|
|
340
|
+
try:
|
|
341
|
+
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
342
|
+
except Exception:
|
|
343
|
+
return None
|
|
344
|
+
try:
|
|
345
|
+
window = sdl2.Window.from_display_module()
|
|
346
|
+
except Exception:
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
display_index = _infer_display_index_from_position(window, sdl2)
|
|
350
|
+
if display_index is not None:
|
|
351
|
+
return display_index
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
return window.get_display_index()
|
|
355
|
+
except Exception:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _infer_display_index_from_position(window: object, sdl2: object) -> int | None:
|
|
360
|
+
try:
|
|
361
|
+
position = window.position # type: ignore[attr-defined]
|
|
362
|
+
except Exception:
|
|
363
|
+
return None
|
|
364
|
+
if not position:
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
center_x, center_y = _window_center_from_position(window, position)
|
|
368
|
+
display_count = _get_display_count(sdl2)
|
|
369
|
+
if display_count is None:
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
for display_index in range(display_count):
|
|
373
|
+
bounds = _get_display_bounds(sdl2, display_index)
|
|
374
|
+
if bounds is None:
|
|
375
|
+
continue
|
|
376
|
+
x, y, width, height = bounds
|
|
377
|
+
if x <= center_x < x + width and y <= center_y < y + height:
|
|
378
|
+
return display_index
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _window_center_from_position(window: object, position: tuple[int, int]) -> tuple[int, int]:
|
|
383
|
+
x, y = position
|
|
384
|
+
try:
|
|
385
|
+
width, height = window.size # type: ignore[attr-defined]
|
|
386
|
+
except Exception:
|
|
387
|
+
width, height = _fetch_window_size(None)
|
|
388
|
+
return x + width // 2, y + height // 2
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _get_display_count(sdl2: object) -> int | None:
|
|
392
|
+
candidate = getattr(sdl2, "get_num_video_displays", None)
|
|
393
|
+
if candidate is None:
|
|
394
|
+
candidate = getattr(getattr(sdl2, "video", None), "get_num_video_displays", None)
|
|
395
|
+
if candidate is None:
|
|
396
|
+
return None
|
|
397
|
+
try:
|
|
398
|
+
return int(candidate())
|
|
399
|
+
except Exception:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _get_display_bounds(sdl2: object, display_index: int) -> tuple[int, int, int, int] | None:
|
|
404
|
+
candidate = getattr(sdl2, "get_display_bounds", None)
|
|
405
|
+
if candidate is None:
|
|
406
|
+
candidate = getattr(getattr(sdl2, "video", None), "get_display_bounds", None)
|
|
407
|
+
if candidate is None:
|
|
408
|
+
return None
|
|
409
|
+
try:
|
|
410
|
+
bounds = candidate(display_index)
|
|
411
|
+
except Exception:
|
|
412
|
+
return None
|
|
413
|
+
if bounds is None:
|
|
414
|
+
return None
|
|
415
|
+
if hasattr(bounds, "x"):
|
|
416
|
+
return (int(bounds.x), int(bounds.y), int(bounds.w), int(bounds.h))
|
|
417
|
+
if isinstance(bounds, (tuple, list)) and len(bounds) >= 4:
|
|
418
|
+
return (int(bounds[0]), int(bounds[1]), int(bounds[2]), int(bounds[3]))
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _set_sdl2_fullscreen(enable: bool, display_index: int | None) -> bool:
|
|
423
|
+
try:
|
|
424
|
+
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
425
|
+
except Exception:
|
|
426
|
+
return False
|
|
427
|
+
try:
|
|
428
|
+
window = sdl2.Window.from_display_module()
|
|
429
|
+
except Exception:
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
if enable and display_index is not None:
|
|
433
|
+
setter = getattr(window, "set_display_index", None)
|
|
434
|
+
if setter is not None:
|
|
435
|
+
try:
|
|
436
|
+
setter(display_index)
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
if hasattr(window, "fullscreen"):
|
|
441
|
+
try:
|
|
442
|
+
window.fullscreen = enable
|
|
443
|
+
return True
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
setter = getattr(window, "set_fullscreen", None)
|
|
448
|
+
if setter is None:
|
|
449
|
+
return False
|
|
450
|
+
try:
|
|
451
|
+
setter(enable)
|
|
452
|
+
return True
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
if enable:
|
|
457
|
+
for attr_name in ("WINDOW_FULLSCREEN_DESKTOP", "FULLSCREEN_DESKTOP", "WINDOW_FULLSCREEN"):
|
|
458
|
+
mode = getattr(sdl2, attr_name, None)
|
|
459
|
+
if mode is None:
|
|
460
|
+
continue
|
|
461
|
+
try:
|
|
462
|
+
setter(mode)
|
|
463
|
+
return True
|
|
464
|
+
except Exception:
|
|
465
|
+
continue
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _update_window_size(size: tuple[int, int], *, source: str) -> None:
|
|
470
|
+
global current_window_size, last_logged_window_size
|
|
471
|
+
current_window_size = size
|
|
472
|
+
if size != last_logged_window_size:
|
|
473
|
+
print(f"WINDOW_SIZE {size[0]}x{size[1]}")
|
|
474
|
+
last_logged_window_size = size
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _update_window_caption() -> None:
|
|
478
|
+
pygame.display.set_caption("Zombie Escape")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _maximize_window() -> None:
|
|
482
|
+
try:
|
|
483
|
+
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
484
|
+
except Exception:
|
|
485
|
+
return
|
|
486
|
+
try:
|
|
487
|
+
window = sdl2.Window.from_display_module()
|
|
488
|
+
except Exception:
|
|
489
|
+
return
|
|
490
|
+
try:
|
|
491
|
+
window.maximize()
|
|
492
|
+
except Exception:
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _restore_window() -> None:
|
|
497
|
+
try:
|
|
498
|
+
from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
|
|
499
|
+
except Exception:
|
|
500
|
+
return
|
|
501
|
+
try:
|
|
502
|
+
window = sdl2.Window.from_display_module()
|
|
503
|
+
except Exception:
|
|
504
|
+
return
|
|
505
|
+
try:
|
|
506
|
+
window.restore()
|
|
507
|
+
except Exception:
|
|
508
|
+
return
|
zombie_escape/world_grid.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Iterable, TYPE_CHECKING
|
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
7
|
from .entities import Wall
|
|
8
|
+
from .models import LevelLayout
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
WallIndex = dict[tuple[int, int], list["Wall"]]
|
|
@@ -54,23 +55,24 @@ def walls_for_radius(
|
|
|
54
55
|
return candidates
|
|
55
56
|
|
|
56
57
|
|
|
57
|
-
def
|
|
58
|
+
def apply_cell_edge_nudge(
|
|
58
59
|
x: float,
|
|
59
60
|
y: float,
|
|
60
61
|
dx: float,
|
|
61
62
|
dy: float,
|
|
62
63
|
*,
|
|
64
|
+
layout: LevelLayout,
|
|
63
65
|
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
66
|
strength: float = 0.03,
|
|
69
67
|
edge_margin_ratio: float = 0.15,
|
|
70
68
|
min_margin: float = 2.0,
|
|
71
69
|
) -> tuple[float, float]:
|
|
72
70
|
if dx == 0 and dy == 0:
|
|
73
71
|
return dx, dy
|
|
72
|
+
wall_cells = layout.wall_cells
|
|
73
|
+
bevel_corners = layout.bevel_corners
|
|
74
|
+
grid_cols = layout.grid_cols
|
|
75
|
+
grid_rows = layout.grid_rows
|
|
74
76
|
if cell_size <= 0 or not wall_cells:
|
|
75
77
|
return dx, dy
|
|
76
78
|
cell_x = int(x // cell_size)
|
zombie_escape/zombie_escape.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
import traceback # For error reporting
|
|
6
7
|
from pathlib import Path
|
|
@@ -19,7 +20,7 @@ from .entities_constants import (
|
|
|
19
20
|
SURVIVOR_MIN_SPEED_FACTOR,
|
|
20
21
|
)
|
|
21
22
|
from .gameplay import calculate_car_speed_for_passengers
|
|
22
|
-
from .level_constants import
|
|
23
|
+
from .level_constants import DEFAULT_CELL_SIZE
|
|
23
24
|
from .localization import set_language
|
|
24
25
|
from .models import Stage
|
|
25
26
|
from .render_constants import RenderAssets, build_render_assets
|
|
@@ -29,7 +30,13 @@ from .screen_constants import (
|
|
|
29
30
|
SCREEN_HEIGHT,
|
|
30
31
|
SCREEN_WIDTH,
|
|
31
32
|
)
|
|
32
|
-
from .screens import ScreenID, ScreenTransition
|
|
33
|
+
from .screens import ScreenID, ScreenTransition
|
|
34
|
+
from .windowing import (
|
|
35
|
+
adjust_menu_logical_size,
|
|
36
|
+
apply_window_scale,
|
|
37
|
+
prime_scaled_logical_size,
|
|
38
|
+
set_scaled_logical_size,
|
|
39
|
+
)
|
|
33
40
|
from .screens.game_over import game_over_screen
|
|
34
41
|
from .screens.settings import settings_screen
|
|
35
42
|
from .screens.title import MAX_SEED_DIGITS, title_screen
|
|
@@ -92,6 +99,7 @@ def main() -> None:
|
|
|
92
99
|
args, remaining = _parse_cli_args(sys.argv[1:])
|
|
93
100
|
sys.argv = [sys.argv[0]] + remaining
|
|
94
101
|
|
|
102
|
+
os.environ.setdefault("SDL_RENDER_SCALE_QUALITY", "0")
|
|
95
103
|
pygame.init()
|
|
96
104
|
pygame.joystick.init()
|
|
97
105
|
if hasattr(pygame, "controller"):
|
|
@@ -104,9 +112,11 @@ def main() -> None:
|
|
|
104
112
|
|
|
105
113
|
from .screens.gameplay import gameplay_screen
|
|
106
114
|
|
|
115
|
+
prime_scaled_logical_size((SCREEN_WIDTH, SCREEN_HEIGHT))
|
|
107
116
|
apply_window_scale(DEFAULT_WINDOW_SCALE)
|
|
108
117
|
pygame.mouse.set_visible(True)
|
|
109
|
-
|
|
118
|
+
logical_screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
|
|
119
|
+
menu_screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
|
|
110
120
|
clock = pygame.time.Clock()
|
|
111
121
|
|
|
112
122
|
debug_mode = bool(args.debug)
|
|
@@ -119,7 +129,7 @@ def main() -> None:
|
|
|
119
129
|
from .export_images import export_images
|
|
120
130
|
|
|
121
131
|
output_dir = Path.cwd() / "imgs" / "exports"
|
|
122
|
-
saved = export_images(output_dir, cell_size=
|
|
132
|
+
saved = export_images(output_dir, cell_size=DEFAULT_CELL_SIZE)
|
|
123
133
|
print(f"Exported {len(saved)} images to {output_dir}")
|
|
124
134
|
pygame.quit()
|
|
125
135
|
return
|
|
@@ -182,15 +192,16 @@ def main() -> None:
|
|
|
182
192
|
transition = None
|
|
183
193
|
|
|
184
194
|
if next_screen == ScreenID.TITLE:
|
|
195
|
+
adjust_menu_logical_size()
|
|
185
196
|
seed_input = None if title_seed_is_auto else title_seed_text
|
|
186
197
|
transition = title_screen(
|
|
187
|
-
|
|
198
|
+
menu_screen,
|
|
188
199
|
clock,
|
|
189
200
|
config,
|
|
190
201
|
FPS,
|
|
191
202
|
stages=STAGES,
|
|
192
203
|
default_stage_id=last_stage_id or DEFAULT_STAGE_ID,
|
|
193
|
-
screen_size=(
|
|
204
|
+
screen_size=menu_screen.get_size(),
|
|
194
205
|
seed_text=seed_input,
|
|
195
206
|
seed_is_auto=title_seed_is_auto,
|
|
196
207
|
)
|
|
@@ -198,28 +209,30 @@ def main() -> None:
|
|
|
198
209
|
title_seed_text = transition.seed_text
|
|
199
210
|
title_seed_is_auto = transition.seed_is_auto
|
|
200
211
|
elif next_screen == ScreenID.SETTINGS:
|
|
212
|
+
adjust_menu_logical_size()
|
|
201
213
|
config = settings_screen(
|
|
202
|
-
|
|
214
|
+
menu_screen,
|
|
203
215
|
clock,
|
|
204
216
|
config,
|
|
205
217
|
FPS,
|
|
206
218
|
config_path=config_path,
|
|
207
|
-
screen_size=(
|
|
219
|
+
screen_size=menu_screen.get_size(),
|
|
208
220
|
)
|
|
209
221
|
set_language(config.get("language"))
|
|
210
222
|
transition = ScreenTransition(ScreenID.TITLE)
|
|
211
223
|
elif next_screen == ScreenID.GAMEPLAY:
|
|
224
|
+
set_scaled_logical_size((SCREEN_WIDTH, SCREEN_HEIGHT))
|
|
212
225
|
stage = incoming.stage
|
|
213
226
|
seed_value = incoming.seed
|
|
214
227
|
if stage is None:
|
|
215
228
|
transition = ScreenTransition(ScreenID.TITLE)
|
|
216
229
|
else:
|
|
217
230
|
last_stage_id = stage.id
|
|
218
|
-
render_assets = build_render_assets(stage.
|
|
231
|
+
render_assets = build_render_assets(stage.cell_size)
|
|
219
232
|
try:
|
|
220
233
|
gs = _profiled_gameplay_screen if args.profile else gameplay_screen
|
|
221
234
|
transition = gs(
|
|
222
|
-
|
|
235
|
+
logical_screen,
|
|
223
236
|
clock,
|
|
224
237
|
config,
|
|
225
238
|
FPS,
|
|
@@ -246,11 +259,11 @@ def main() -> None:
|
|
|
246
259
|
if game_data is not None:
|
|
247
260
|
render_assets = build_render_assets(game_data.cell_size)
|
|
248
261
|
elif stage is not None:
|
|
249
|
-
render_assets = build_render_assets(stage.
|
|
262
|
+
render_assets = build_render_assets(stage.cell_size)
|
|
250
263
|
else:
|
|
251
|
-
render_assets = build_render_assets(
|
|
264
|
+
render_assets = build_render_assets(DEFAULT_CELL_SIZE)
|
|
252
265
|
transition = game_over_screen(
|
|
253
|
-
|
|
266
|
+
logical_screen,
|
|
254
267
|
clock,
|
|
255
268
|
config_payload,
|
|
256
269
|
FPS,
|