zombie-escape 1.13.1__py3-none-any.whl → 1.14.4__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 +7 -21
- zombie_escape/entities.py +100 -191
- zombie_escape/export_images.py +39 -33
- zombie_escape/gameplay/ambient.py +2 -6
- zombie_escape/gameplay/footprints.py +8 -11
- zombie_escape/gameplay/interactions.py +17 -58
- zombie_escape/gameplay/layout.py +20 -46
- zombie_escape/gameplay/movement.py +7 -21
- zombie_escape/gameplay/spawn.py +12 -40
- zombie_escape/gameplay/state.py +1 -0
- zombie_escape/gameplay/survivors.py +5 -16
- zombie_escape/gameplay/utils.py +4 -13
- zombie_escape/input_utils.py +8 -31
- zombie_escape/level_blueprints.py +112 -69
- zombie_escape/level_constants.py +8 -0
- zombie_escape/locales/ui.en.json +12 -0
- zombie_escape/locales/ui.ja.json +12 -0
- zombie_escape/localization.py +3 -11
- zombie_escape/models.py +26 -9
- zombie_escape/render/__init__.py +30 -0
- zombie_escape/render/core.py +992 -0
- zombie_escape/render/hud.py +444 -0
- zombie_escape/render/overview.py +218 -0
- zombie_escape/render/shadows.py +343 -0
- zombie_escape/render_assets.py +11 -33
- zombie_escape/rng.py +4 -8
- zombie_escape/screens/__init__.py +14 -30
- zombie_escape/screens/game_over.py +43 -15
- zombie_escape/screens/gameplay.py +41 -104
- zombie_escape/screens/settings.py +19 -104
- zombie_escape/screens/title.py +36 -176
- zombie_escape/stage_constants.py +192 -67
- zombie_escape/zombie_escape.py +1 -1
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/METADATA +100 -39
- zombie_escape-1.14.4.dist-info/RECORD +53 -0
- zombie_escape/render.py +0 -1746
- zombie_escape-1.13.1.dist-info/RECORD +0 -49
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/WHEEL +0 -0
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import pygame
|
|
6
|
+
from pygame import sprite, surface
|
|
7
|
+
|
|
8
|
+
from ..entities import Camera, Car, Player, SteelBeam, Survivor, Zombie
|
|
9
|
+
from ..entities_constants import JUMP_SHADOW_OFFSET, ZOMBIE_RADIUS
|
|
10
|
+
from ..render_constants import (
|
|
11
|
+
ENTITY_SHADOW_ALPHA,
|
|
12
|
+
ENTITY_SHADOW_EDGE_SOFTNESS,
|
|
13
|
+
ENTITY_SHADOW_RADIUS_MULT,
|
|
14
|
+
SHADOW_MIN_RATIO,
|
|
15
|
+
SHADOW_OVERSAMPLE,
|
|
16
|
+
SHADOW_RADIUS_RATIO,
|
|
17
|
+
SHADOW_STEPS,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_SHADOW_TILE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
|
|
21
|
+
_SHADOW_LAYER_CACHE: dict[tuple[int, int], surface.Surface] = {}
|
|
22
|
+
_SHADOW_CIRCLE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_shadow_tile_surface(
|
|
26
|
+
cell_size: int,
|
|
27
|
+
alpha: int,
|
|
28
|
+
*,
|
|
29
|
+
edge_softness: float = 0.35,
|
|
30
|
+
) -> surface.Surface:
|
|
31
|
+
key = (max(1, cell_size), max(0, min(255, alpha)), edge_softness)
|
|
32
|
+
if key in _SHADOW_TILE_CACHE:
|
|
33
|
+
return _SHADOW_TILE_CACHE[key]
|
|
34
|
+
size = key[0]
|
|
35
|
+
oversample = SHADOW_OVERSAMPLE
|
|
36
|
+
render_size = size * oversample
|
|
37
|
+
render_surf = pygame.Surface((render_size, render_size), pygame.SRCALPHA)
|
|
38
|
+
base_alpha = key[1]
|
|
39
|
+
if edge_softness <= 0:
|
|
40
|
+
render_surf.fill((0, 0, 0, base_alpha))
|
|
41
|
+
if oversample > 1:
|
|
42
|
+
surf = pygame.transform.smoothscale(render_surf, (size, size))
|
|
43
|
+
else:
|
|
44
|
+
surf = render_surf
|
|
45
|
+
_SHADOW_TILE_CACHE[key] = surf
|
|
46
|
+
return surf
|
|
47
|
+
|
|
48
|
+
softness = max(0.0, min(1.0, edge_softness))
|
|
49
|
+
fade_band = max(1, int(render_size * softness))
|
|
50
|
+
base_radius = max(1, int(render_size * SHADOW_RADIUS_RATIO))
|
|
51
|
+
|
|
52
|
+
render_surf.fill((0, 0, 0, 0))
|
|
53
|
+
steps = SHADOW_STEPS
|
|
54
|
+
min_ratio = SHADOW_MIN_RATIO
|
|
55
|
+
for idx in range(steps):
|
|
56
|
+
t = idx / (steps - 1) if steps > 1 else 1.0
|
|
57
|
+
inset = int(fade_band * t)
|
|
58
|
+
rect_size = render_size - inset * 2
|
|
59
|
+
if rect_size <= 0:
|
|
60
|
+
continue
|
|
61
|
+
radius = max(0, base_radius - inset)
|
|
62
|
+
layer_alpha = int(base_alpha * (min_ratio + (1.0 - min_ratio) * t))
|
|
63
|
+
pygame.draw.rect(
|
|
64
|
+
render_surf,
|
|
65
|
+
(0, 0, 0, layer_alpha),
|
|
66
|
+
pygame.Rect(inset, inset, rect_size, rect_size),
|
|
67
|
+
border_radius=radius,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if oversample > 1:
|
|
71
|
+
surf = pygame.transform.smoothscale(render_surf, (size, size))
|
|
72
|
+
else:
|
|
73
|
+
surf = render_surf
|
|
74
|
+
_SHADOW_TILE_CACHE[key] = surf
|
|
75
|
+
return surf
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_shadow_layer(size: tuple[int, int]) -> surface.Surface:
|
|
79
|
+
key = (max(1, size[0]), max(1, size[1]))
|
|
80
|
+
if key in _SHADOW_LAYER_CACHE:
|
|
81
|
+
return _SHADOW_LAYER_CACHE[key]
|
|
82
|
+
layer = pygame.Surface(key, pygame.SRCALPHA)
|
|
83
|
+
_SHADOW_LAYER_CACHE[key] = layer
|
|
84
|
+
return layer
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_shadow_circle_surface(
|
|
88
|
+
radius: int,
|
|
89
|
+
alpha: int,
|
|
90
|
+
*,
|
|
91
|
+
edge_softness: float = 0.12,
|
|
92
|
+
) -> surface.Surface:
|
|
93
|
+
key = (max(1, radius), max(0, min(255, alpha)), edge_softness)
|
|
94
|
+
if key in _SHADOW_CIRCLE_CACHE:
|
|
95
|
+
return _SHADOW_CIRCLE_CACHE[key]
|
|
96
|
+
radius = key[0]
|
|
97
|
+
oversample = SHADOW_OVERSAMPLE
|
|
98
|
+
render_radius = radius * oversample
|
|
99
|
+
render_size = render_radius * 2
|
|
100
|
+
render_surf = pygame.Surface((render_size, render_size), pygame.SRCALPHA)
|
|
101
|
+
base_alpha = key[1]
|
|
102
|
+
if edge_softness <= 0:
|
|
103
|
+
pygame.draw.circle(
|
|
104
|
+
render_surf,
|
|
105
|
+
(0, 0, 0, base_alpha),
|
|
106
|
+
(render_radius, render_radius),
|
|
107
|
+
render_radius,
|
|
108
|
+
)
|
|
109
|
+
if oversample > 1:
|
|
110
|
+
surf = pygame.transform.smoothscale(render_surf, (radius * 2, radius * 2))
|
|
111
|
+
else:
|
|
112
|
+
surf = render_surf
|
|
113
|
+
_SHADOW_CIRCLE_CACHE[key] = surf
|
|
114
|
+
return surf
|
|
115
|
+
|
|
116
|
+
softness = max(0.0, min(1.0, edge_softness))
|
|
117
|
+
fade_band = max(1, int(render_radius * softness))
|
|
118
|
+
steps = SHADOW_STEPS
|
|
119
|
+
min_ratio = SHADOW_MIN_RATIO
|
|
120
|
+
render_surf.fill((0, 0, 0, 0))
|
|
121
|
+
for idx in range(steps):
|
|
122
|
+
t = idx / (steps - 1) if steps > 1 else 1.0
|
|
123
|
+
inset = int(fade_band * t)
|
|
124
|
+
circle_radius = render_radius - inset
|
|
125
|
+
if circle_radius <= 0:
|
|
126
|
+
continue
|
|
127
|
+
layer_alpha = int(base_alpha * (min_ratio + (1.0 - min_ratio) * t))
|
|
128
|
+
pygame.draw.circle(
|
|
129
|
+
render_surf,
|
|
130
|
+
(0, 0, 0, layer_alpha),
|
|
131
|
+
(render_radius, render_radius),
|
|
132
|
+
circle_radius,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if oversample > 1:
|
|
136
|
+
surf = pygame.transform.smoothscale(render_surf, (radius * 2, radius * 2))
|
|
137
|
+
else:
|
|
138
|
+
surf = render_surf
|
|
139
|
+
_SHADOW_CIRCLE_CACHE[key] = surf
|
|
140
|
+
return surf
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _abs_clip(value: float, min_v: float, max_v: float) -> float:
|
|
144
|
+
value_sign = 1.0 if value >= 0.0 else -1.0
|
|
145
|
+
value = abs(value)
|
|
146
|
+
if value < min_v:
|
|
147
|
+
value = min_v
|
|
148
|
+
elif value > max_v:
|
|
149
|
+
value = max_v
|
|
150
|
+
return value_sign * value
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _draw_wall_shadows(
|
|
154
|
+
shadow_layer: surface.Surface,
|
|
155
|
+
camera: Camera,
|
|
156
|
+
*,
|
|
157
|
+
wall_cells: set[tuple[int, int]],
|
|
158
|
+
wall_group: sprite.Group | None,
|
|
159
|
+
outer_wall_cells: set[tuple[int, int]] | None,
|
|
160
|
+
cell_size: int,
|
|
161
|
+
light_source_pos: tuple[int, int] | None,
|
|
162
|
+
alpha: int = 68,
|
|
163
|
+
) -> bool:
|
|
164
|
+
if not wall_cells or cell_size <= 0 or light_source_pos is None:
|
|
165
|
+
return False
|
|
166
|
+
inner_wall_cells = set(wall_cells)
|
|
167
|
+
if outer_wall_cells:
|
|
168
|
+
inner_wall_cells.difference_update(outer_wall_cells)
|
|
169
|
+
if wall_group and cell_size > 0:
|
|
170
|
+
for wall in wall_group:
|
|
171
|
+
if isinstance(wall, SteelBeam):
|
|
172
|
+
cell_x = int(wall.rect.centerx // cell_size)
|
|
173
|
+
cell_y = int(wall.rect.centery // cell_size)
|
|
174
|
+
inner_wall_cells.add((cell_x, cell_y))
|
|
175
|
+
if not inner_wall_cells:
|
|
176
|
+
return False
|
|
177
|
+
base_shadow_size = max(cell_size + 2, int(cell_size * 1.35))
|
|
178
|
+
shadow_size = max(1, int(base_shadow_size * 1.5))
|
|
179
|
+
shadow_surface = _get_shadow_tile_surface(
|
|
180
|
+
shadow_size,
|
|
181
|
+
alpha,
|
|
182
|
+
edge_softness=0.12,
|
|
183
|
+
)
|
|
184
|
+
screen_rect = shadow_layer.get_rect()
|
|
185
|
+
px, py = light_source_pos
|
|
186
|
+
drew = False
|
|
187
|
+
clip_max = shadow_size * 0.25
|
|
188
|
+
for cell_x, cell_y in inner_wall_cells:
|
|
189
|
+
world_x = cell_x * cell_size
|
|
190
|
+
world_y = cell_y * cell_size
|
|
191
|
+
wall_rect = pygame.Rect(world_x, world_y, cell_size, cell_size)
|
|
192
|
+
wall_screen_rect = camera.apply_rect(wall_rect)
|
|
193
|
+
if not wall_screen_rect.colliderect(screen_rect):
|
|
194
|
+
continue
|
|
195
|
+
center_x = world_x + cell_size / 2
|
|
196
|
+
center_y = world_y + cell_size / 2
|
|
197
|
+
dx = (center_x - px) * 0.5
|
|
198
|
+
dy = (center_y - py) * 0.5
|
|
199
|
+
dx = int(_abs_clip(dx, 0, clip_max))
|
|
200
|
+
dy = int(_abs_clip(dy, 0, clip_max))
|
|
201
|
+
shadow_rect = pygame.Rect(0, 0, shadow_size, shadow_size)
|
|
202
|
+
shadow_rect.center = (
|
|
203
|
+
int(center_x + dx),
|
|
204
|
+
int(center_y + dy),
|
|
205
|
+
)
|
|
206
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
207
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
208
|
+
continue
|
|
209
|
+
shadow_layer.blit(
|
|
210
|
+
shadow_surface,
|
|
211
|
+
shadow_screen_rect.topleft,
|
|
212
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
213
|
+
)
|
|
214
|
+
drew = True
|
|
215
|
+
return drew
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _draw_entity_shadows(
|
|
219
|
+
shadow_layer: surface.Surface,
|
|
220
|
+
camera: Camera,
|
|
221
|
+
all_sprites: sprite.LayeredUpdates,
|
|
222
|
+
*,
|
|
223
|
+
light_source_pos: tuple[int, int] | None,
|
|
224
|
+
exclude_car: Car | None,
|
|
225
|
+
outside_cells: set[tuple[int, int]] | None,
|
|
226
|
+
cell_size: int,
|
|
227
|
+
shadow_radius: int = int(ZOMBIE_RADIUS * ENTITY_SHADOW_RADIUS_MULT),
|
|
228
|
+
alpha: int = ENTITY_SHADOW_ALPHA,
|
|
229
|
+
) -> bool:
|
|
230
|
+
if light_source_pos is None or shadow_radius <= 0:
|
|
231
|
+
return False
|
|
232
|
+
if cell_size <= 0:
|
|
233
|
+
outside_cells = None
|
|
234
|
+
shadow_surface = _get_shadow_circle_surface(
|
|
235
|
+
shadow_radius,
|
|
236
|
+
alpha,
|
|
237
|
+
edge_softness=ENTITY_SHADOW_EDGE_SOFTNESS,
|
|
238
|
+
)
|
|
239
|
+
screen_rect = shadow_layer.get_rect()
|
|
240
|
+
px, py = light_source_pos
|
|
241
|
+
offset_dist = max(1.0, shadow_radius * 0.6)
|
|
242
|
+
drew = False
|
|
243
|
+
for entity in all_sprites:
|
|
244
|
+
if not entity.alive():
|
|
245
|
+
continue
|
|
246
|
+
if isinstance(entity, Player):
|
|
247
|
+
continue
|
|
248
|
+
if isinstance(entity, Car):
|
|
249
|
+
if exclude_car is not None and entity is exclude_car:
|
|
250
|
+
continue
|
|
251
|
+
if not isinstance(entity, (Zombie, Survivor, Car)):
|
|
252
|
+
continue
|
|
253
|
+
if outside_cells:
|
|
254
|
+
cell = (
|
|
255
|
+
int(entity.rect.centerx // cell_size),
|
|
256
|
+
int(entity.rect.centery // cell_size),
|
|
257
|
+
)
|
|
258
|
+
if cell in outside_cells:
|
|
259
|
+
continue
|
|
260
|
+
cx, cy = entity.rect.center
|
|
261
|
+
dx = cx - px
|
|
262
|
+
dy = cy - py
|
|
263
|
+
dist = math.hypot(dx, dy)
|
|
264
|
+
if dist > 0.001:
|
|
265
|
+
scale = offset_dist / dist
|
|
266
|
+
offset_x = dx * scale
|
|
267
|
+
offset_y = dy * scale
|
|
268
|
+
else:
|
|
269
|
+
offset_x = 0.0
|
|
270
|
+
offset_y = 0.0
|
|
271
|
+
|
|
272
|
+
jump_dy = 0.0
|
|
273
|
+
if getattr(entity, "is_jumping", False):
|
|
274
|
+
jump_dy = JUMP_SHADOW_OFFSET
|
|
275
|
+
|
|
276
|
+
shadow_rect = shadow_surface.get_rect(center=(int(cx + offset_x), int(cy + offset_y + jump_dy)))
|
|
277
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
278
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
279
|
+
continue
|
|
280
|
+
shadow_layer.blit(
|
|
281
|
+
shadow_surface,
|
|
282
|
+
shadow_screen_rect.topleft,
|
|
283
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
284
|
+
)
|
|
285
|
+
drew = True
|
|
286
|
+
return drew
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _draw_single_entity_shadow(
|
|
290
|
+
shadow_layer: surface.Surface,
|
|
291
|
+
camera: Camera,
|
|
292
|
+
*,
|
|
293
|
+
entity: pygame.sprite.Sprite | None,
|
|
294
|
+
light_source_pos: tuple[int, int] | None,
|
|
295
|
+
outside_cells: set[tuple[int, int]] | None,
|
|
296
|
+
cell_size: int,
|
|
297
|
+
shadow_radius: int,
|
|
298
|
+
alpha: int,
|
|
299
|
+
edge_softness: float = ENTITY_SHADOW_EDGE_SOFTNESS,
|
|
300
|
+
) -> bool:
|
|
301
|
+
if entity is None or not entity.alive() or light_source_pos is None or shadow_radius <= 0:
|
|
302
|
+
return False
|
|
303
|
+
if outside_cells and cell_size > 0:
|
|
304
|
+
cell = (
|
|
305
|
+
int(entity.rect.centerx // cell_size),
|
|
306
|
+
int(entity.rect.centery // cell_size),
|
|
307
|
+
)
|
|
308
|
+
if cell in outside_cells:
|
|
309
|
+
return False
|
|
310
|
+
shadow_surface = _get_shadow_circle_surface(
|
|
311
|
+
shadow_radius,
|
|
312
|
+
alpha,
|
|
313
|
+
edge_softness=edge_softness,
|
|
314
|
+
)
|
|
315
|
+
screen_rect = shadow_layer.get_rect()
|
|
316
|
+
px, py = light_source_pos
|
|
317
|
+
cx, cy = entity.rect.center
|
|
318
|
+
dx = cx - px
|
|
319
|
+
dy = cy - py
|
|
320
|
+
dist = math.hypot(dx, dy)
|
|
321
|
+
offset_dist = max(1.0, shadow_radius * 0.6)
|
|
322
|
+
if dist > 0.001:
|
|
323
|
+
scale = offset_dist / dist
|
|
324
|
+
offset_x = dx * scale
|
|
325
|
+
offset_y = dy * scale
|
|
326
|
+
else:
|
|
327
|
+
offset_x = 0.0
|
|
328
|
+
offset_y = 0.0
|
|
329
|
+
|
|
330
|
+
jump_dy = 0.0
|
|
331
|
+
if getattr(entity, "is_jumping", False):
|
|
332
|
+
jump_dy = JUMP_SHADOW_OFFSET
|
|
333
|
+
|
|
334
|
+
shadow_rect = shadow_surface.get_rect(center=(int(cx + offset_x), int(cy + offset_y + jump_dy)))
|
|
335
|
+
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
336
|
+
if not shadow_screen_rect.colliderect(screen_rect):
|
|
337
|
+
return False
|
|
338
|
+
shadow_layer.blit(
|
|
339
|
+
shadow_surface,
|
|
340
|
+
shadow_screen_rect.topleft,
|
|
341
|
+
special_flags=pygame.BLEND_RGBA_MAX,
|
|
342
|
+
)
|
|
343
|
+
return True
|
zombie_escape/render_assets.py
CHANGED
|
@@ -46,9 +46,7 @@ def _draw_outlined_circle(
|
|
|
46
46
|
pygame.draw.circle(surface, outline_color, center, radius, width=outline_width)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def _brighten_color(
|
|
50
|
-
color: tuple[int, int, int], *, factor: float = 1.25
|
|
51
|
-
) -> tuple[int, int, int]:
|
|
49
|
+
def _brighten_color(color: tuple[int, int, int], *, factor: float = 1.25) -> tuple[int, int, int]:
|
|
52
50
|
return tuple(min(255, int(c * factor + 0.5)) for c in color)
|
|
53
51
|
|
|
54
52
|
|
|
@@ -58,9 +56,7 @@ _PLAYER_UPSCALE_FACTOR = 4
|
|
|
58
56
|
_CAR_UPSCALE_FACTOR = 4
|
|
59
57
|
|
|
60
58
|
_PLAYER_DIRECTIONAL_CACHE: dict[tuple[int, int], list[pygame.Surface]] = {}
|
|
61
|
-
_SURVIVOR_DIRECTIONAL_CACHE: dict[
|
|
62
|
-
tuple[int, bool, bool, int], list[pygame.Surface]
|
|
63
|
-
] = {}
|
|
59
|
+
_SURVIVOR_DIRECTIONAL_CACHE: dict[tuple[int, bool, bool, int], list[pygame.Surface]] = {}
|
|
64
60
|
_ZOMBIE_DIRECTIONAL_CACHE: dict[tuple[int, bool, int], list[pygame.Surface]] = {}
|
|
65
61
|
_RUBBLE_SURFACE_CACHE: dict[tuple, pygame.Surface] = {}
|
|
66
62
|
|
|
@@ -70,9 +66,7 @@ RUBBLE_SCALE_RATIO = 0.9
|
|
|
70
66
|
RUBBLE_SHADOW_RATIO = 0.9
|
|
71
67
|
|
|
72
68
|
|
|
73
|
-
def _scale_color(
|
|
74
|
-
color: tuple[int, int, int], *, ratio: float
|
|
75
|
-
) -> tuple[int, int, int]:
|
|
69
|
+
def _scale_color(color: tuple[int, int, int], *, ratio: float) -> tuple[int, int, int]:
|
|
76
70
|
return tuple(max(0, min(255, int(c * ratio + 0.5))) for c in color)
|
|
77
71
|
|
|
78
72
|
|
|
@@ -80,9 +74,7 @@ def rubble_offset_for_size(size: int) -> int:
|
|
|
80
74
|
return max(1, int(round(size * RUBBLE_OFFSET_RATIO)))
|
|
81
75
|
|
|
82
76
|
|
|
83
|
-
def angle_bin_from_vector(
|
|
84
|
-
dx: float, dy: float, *, bins: int = ANGLE_BINS
|
|
85
|
-
) -> int | None:
|
|
77
|
+
def angle_bin_from_vector(dx: float, dy: float, *, bins: int = ANGLE_BINS) -> int | None:
|
|
86
78
|
if dx == 0 and dy == 0:
|
|
87
79
|
return None
|
|
88
80
|
angle = math.atan2(dy, dx)
|
|
@@ -388,9 +380,7 @@ def resolve_steel_beam_colors(
|
|
|
388
380
|
return STEEL_BEAM_COLOR, STEEL_BEAM_LINE_COLOR
|
|
389
381
|
|
|
390
382
|
|
|
391
|
-
def build_player_directional_surfaces(
|
|
392
|
-
radius: int, *, bins: int = ANGLE_BINS
|
|
393
|
-
) -> list[pygame.Surface]:
|
|
383
|
+
def build_player_directional_surfaces(radius: int, *, bins: int = ANGLE_BINS) -> list[pygame.Surface]:
|
|
394
384
|
cache_key = (radius, bins)
|
|
395
385
|
if cache_key in _PLAYER_DIRECTIONAL_CACHE:
|
|
396
386
|
return _PLAYER_DIRECTIONAL_CACHE[cache_key]
|
|
@@ -545,9 +535,7 @@ def paint_car_surface(
|
|
|
545
535
|
up_width = width * upscale
|
|
546
536
|
up_height = height * upscale
|
|
547
537
|
up_surface = pygame.Surface((up_width, up_height), pygame.SRCALPHA)
|
|
548
|
-
_paint_car_surface_base(
|
|
549
|
-
up_surface, width=up_width, height=up_height, color=color
|
|
550
|
-
)
|
|
538
|
+
_paint_car_surface_base(up_surface, width=up_width, height=up_height, color=color)
|
|
551
539
|
scaled = pygame.transform.smoothscale(up_surface, (width, height))
|
|
552
540
|
surface.fill((0, 0, 0, 0))
|
|
553
541
|
surface.blit(scaled, (0, 0))
|
|
@@ -604,9 +592,7 @@ def _paint_car_surface_base(
|
|
|
604
592
|
pygame.draw.ellipse(surface, headlight_color, headlight_right)
|
|
605
593
|
|
|
606
594
|
|
|
607
|
-
def build_car_directional_surfaces(
|
|
608
|
-
base_surface: pygame.Surface, *, bins: int = ANGLE_BINS
|
|
609
|
-
) -> list[pygame.Surface]:
|
|
595
|
+
def build_car_directional_surfaces(base_surface: pygame.Surface, *, bins: int = ANGLE_BINS) -> list[pygame.Surface]:
|
|
610
596
|
"""Return pre-rotated car surfaces matching angle_bin_from_vector bins."""
|
|
611
597
|
surfaces: list[pygame.Surface] = []
|
|
612
598
|
upscale = _CAR_UPSCALE_FACTOR
|
|
@@ -660,9 +646,7 @@ def paint_wall_surface(
|
|
|
660
646
|
) -> None:
|
|
661
647
|
face_width, face_height = face_size or target.get_size()
|
|
662
648
|
if bevel_depth > 0 and any(bevel_mask):
|
|
663
|
-
face_polygon = build_beveled_polygon(
|
|
664
|
-
face_width, face_height, bevel_depth, bevel_mask
|
|
665
|
-
)
|
|
649
|
+
face_polygon = build_beveled_polygon(face_width, face_height, bevel_depth, bevel_mask)
|
|
666
650
|
pygame.draw.polygon(target, border_color, face_polygon)
|
|
667
651
|
else:
|
|
668
652
|
target.fill(border_color)
|
|
@@ -671,9 +655,7 @@ def paint_wall_surface(
|
|
|
671
655
|
if inner_rect.width > 0 and inner_rect.height > 0:
|
|
672
656
|
inner_depth = max(0, bevel_depth - border_width)
|
|
673
657
|
if inner_depth > 0 and any(bevel_mask):
|
|
674
|
-
inner_polygon = build_beveled_polygon(
|
|
675
|
-
inner_rect.width, inner_rect.height, inner_depth, bevel_mask
|
|
676
|
-
)
|
|
658
|
+
inner_polygon = build_beveled_polygon(inner_rect.width, inner_rect.height, inner_depth, bevel_mask)
|
|
677
659
|
inner_offset_polygon = [
|
|
678
660
|
(
|
|
679
661
|
int(point[0] + inner_rect.left),
|
|
@@ -703,9 +685,7 @@ def paint_wall_surface(
|
|
|
703
685
|
side_color = tuple(int(c * side_shade_ratio) for c in fill_color)
|
|
704
686
|
side_surface = pygame.Surface(rect_obj.size, pygame.SRCALPHA)
|
|
705
687
|
if bevel_depth > 0 and any(bevel_mask):
|
|
706
|
-
side_polygon = build_beveled_polygon(
|
|
707
|
-
rect_obj.width, rect_obj.height, bevel_depth, bevel_mask
|
|
708
|
-
)
|
|
688
|
+
side_polygon = build_beveled_polygon(rect_obj.width, rect_obj.height, bevel_depth, bevel_mask)
|
|
709
689
|
pygame.draw.polygon(side_surface, side_color, side_polygon)
|
|
710
690
|
else:
|
|
711
691
|
pygame.draw.rect(side_surface, side_color, rect_obj)
|
|
@@ -785,9 +765,7 @@ def build_rubble_wall_surface(
|
|
|
785
765
|
final_surface = pygame.Surface((safe_size, safe_size), pygame.SRCALPHA)
|
|
786
766
|
center = final_surface.get_rect().center
|
|
787
767
|
|
|
788
|
-
shadow_rect = shadow_surface.get_rect(
|
|
789
|
-
center=(center[0] + offset_px, center[1] + offset_px)
|
|
790
|
-
)
|
|
768
|
+
shadow_rect = shadow_surface.get_rect(center=(center[0] + offset_px, center[1] + offset_px))
|
|
791
769
|
final_surface.blit(shadow_surface, shadow_rect.topleft)
|
|
792
770
|
|
|
793
771
|
top_rect = top_surface.get_rect(center=center)
|
zombie_escape/rng.py
CHANGED
|
@@ -43,9 +43,7 @@ class DeterministicRNG:
|
|
|
43
43
|
self._state[0] = seed32
|
|
44
44
|
for i in range(1, self._N):
|
|
45
45
|
prev = self._state[i - 1]
|
|
46
|
-
self._state[i] = (
|
|
47
|
-
(1812433253 * (prev ^ (prev >> 30)) + i) & 0xFFFFFFFF
|
|
48
|
-
)
|
|
46
|
+
self._state[i] = (1812433253 * (prev ^ (prev >> 30)) + i) & 0xFFFFFFFF
|
|
49
47
|
self._index = self._N
|
|
50
48
|
|
|
51
49
|
@property
|
|
@@ -93,17 +91,15 @@ class DeterministicRNG:
|
|
|
93
91
|
self._twist()
|
|
94
92
|
y = self._state[self._index]
|
|
95
93
|
self._index += 1
|
|
96
|
-
y ^=
|
|
94
|
+
y ^= y >> 11
|
|
97
95
|
y ^= (y << 7) & 0x9D2C5680
|
|
98
96
|
y ^= (y << 15) & 0xEFC60000
|
|
99
|
-
y ^=
|
|
97
|
+
y ^= y >> 18
|
|
100
98
|
return y & 0xFFFFFFFF
|
|
101
99
|
|
|
102
100
|
def _twist(self) -> None:
|
|
103
101
|
for i in range(self._N):
|
|
104
|
-
x = (self._state[i] & self._UPPER_MASK) + (
|
|
105
|
-
self._state[(i + 1) % self._N] & self._LOWER_MASK
|
|
106
|
-
)
|
|
102
|
+
x = (self._state[i] & self._UPPER_MASK) + (self._state[(i + 1) % self._N] & self._LOWER_MASK)
|
|
107
103
|
xA = x >> 1
|
|
108
104
|
if x & 1:
|
|
109
105
|
xA ^= self._MATRIX_A
|
|
@@ -93,18 +93,14 @@ def present(logical_surface: surface.Surface) -> None:
|
|
|
93
93
|
if (scaled_width, scaled_height) == logical_size:
|
|
94
94
|
scaled_surface = logical_surface
|
|
95
95
|
else:
|
|
96
|
-
scaled_surface = pygame.transform.scale(
|
|
97
|
-
logical_surface, (scaled_width, scaled_height)
|
|
98
|
-
)
|
|
96
|
+
scaled_surface = pygame.transform.scale(logical_surface, (scaled_width, scaled_height))
|
|
99
97
|
offset_x = (window_size[0] - scaled_width) // 2
|
|
100
98
|
offset_y = (window_size[1] - scaled_height) // 2
|
|
101
99
|
window.blit(scaled_surface, (offset_x, offset_y))
|
|
102
100
|
pygame.display.flip()
|
|
103
101
|
|
|
104
102
|
|
|
105
|
-
def apply_window_scale(
|
|
106
|
-
scale: float, *, game_data: "GameData | None" = None
|
|
107
|
-
) -> surface.Surface:
|
|
103
|
+
def apply_window_scale(scale: float, *, game_data: "GameData | None" = None) -> surface.Surface:
|
|
108
104
|
"""Resize the OS window; logical render surface stays constant."""
|
|
109
105
|
global current_window_scale, current_maximized, last_window_scale
|
|
110
106
|
|
|
@@ -116,11 +112,9 @@ def apply_window_scale(
|
|
|
116
112
|
window_width = max(1, int(SCREEN_WIDTH * current_window_scale))
|
|
117
113
|
window_height = max(1, int(SCREEN_HEIGHT * current_window_scale))
|
|
118
114
|
|
|
119
|
-
new_window = pygame.display.set_mode(
|
|
120
|
-
(window_width, window_height), pygame.RESIZABLE
|
|
121
|
-
)
|
|
115
|
+
new_window = pygame.display.set_mode((window_width, window_height), pygame.RESIZABLE)
|
|
122
116
|
_update_window_size((window_width, window_height), source="apply_scale")
|
|
123
|
-
_update_window_caption(
|
|
117
|
+
_update_window_caption()
|
|
124
118
|
|
|
125
119
|
if game_data is not None:
|
|
126
120
|
game_data.state.overview_created = False
|
|
@@ -128,28 +122,22 @@ def apply_window_scale(
|
|
|
128
122
|
return new_window
|
|
129
123
|
|
|
130
124
|
|
|
131
|
-
def nudge_window_scale(
|
|
132
|
-
multiplier: float, *, game_data: "GameData | None" = None
|
|
133
|
-
) -> surface.Surface:
|
|
125
|
+
def nudge_window_scale(multiplier: float, *, game_data: "GameData | None" = None) -> surface.Surface:
|
|
134
126
|
"""Scale the window relative to the current zoom level."""
|
|
135
127
|
target_scale = current_window_scale * multiplier
|
|
136
128
|
return apply_window_scale(target_scale, game_data=game_data)
|
|
137
129
|
|
|
138
130
|
|
|
139
|
-
def toggle_fullscreen(
|
|
140
|
-
*, game_data: "GameData | None" = None
|
|
141
|
-
) -> surface.Surface | None:
|
|
131
|
+
def toggle_fullscreen(*, game_data: "GameData | None" = None) -> surface.Surface | None:
|
|
142
132
|
"""Toggle a maximized window without persisting the setting."""
|
|
143
133
|
global current_maximized, last_window_scale
|
|
144
134
|
if current_maximized:
|
|
145
135
|
current_maximized = False
|
|
146
136
|
window_width = max(1, int(SCREEN_WIDTH * last_window_scale))
|
|
147
137
|
window_height = max(1, int(SCREEN_HEIGHT * last_window_scale))
|
|
148
|
-
window = pygame.display.set_mode(
|
|
149
|
-
(window_width, window_height), pygame.RESIZABLE
|
|
150
|
-
)
|
|
138
|
+
window = pygame.display.set_mode((window_width, window_height), pygame.RESIZABLE)
|
|
151
139
|
_restore_window()
|
|
152
|
-
_update_window_caption(
|
|
140
|
+
_update_window_caption()
|
|
153
141
|
_update_window_size((window_width, window_height), source="toggle_windowed")
|
|
154
142
|
else:
|
|
155
143
|
last_window_scale = current_window_scale
|
|
@@ -157,7 +145,7 @@ def toggle_fullscreen(
|
|
|
157
145
|
window = pygame.display.set_mode(_fetch_window_size(None), pygame.RESIZABLE)
|
|
158
146
|
_maximize_window()
|
|
159
147
|
window_width, window_height = _fetch_window_size(window)
|
|
160
|
-
_update_window_caption(
|
|
148
|
+
_update_window_caption()
|
|
161
149
|
_update_window_size((window_width, window_height), source="toggle_fullscreen")
|
|
162
150
|
pygame.mouse.set_visible(not current_maximized)
|
|
163
151
|
if game_data is not None:
|
|
@@ -165,9 +153,7 @@ def toggle_fullscreen(
|
|
|
165
153
|
return window
|
|
166
154
|
|
|
167
155
|
|
|
168
|
-
def sync_window_size(
|
|
169
|
-
event: pygame.event.Event, *, game_data: "GameData | None" = None
|
|
170
|
-
) -> None:
|
|
156
|
+
def sync_window_size(event: pygame.event.Event, *, game_data: "GameData | None" = None) -> None:
|
|
171
157
|
"""Synchronize tracked window size with SDL window events."""
|
|
172
158
|
global current_window_scale, last_window_scale
|
|
173
159
|
size = getattr(event, "size", None)
|
|
@@ -179,16 +165,14 @@ def sync_window_size(
|
|
|
179
165
|
if not size:
|
|
180
166
|
return
|
|
181
167
|
window_width, window_height = _normalize_window_size(size)
|
|
182
|
-
_update_window_size(
|
|
183
|
-
(window_width, window_height), source="window_event"
|
|
184
|
-
)
|
|
168
|
+
_update_window_size((window_width, window_height), source="window_event")
|
|
185
169
|
if not current_maximized:
|
|
186
170
|
scale_x = window_width / max(1, SCREEN_WIDTH)
|
|
187
171
|
scale_y = window_height / max(1, SCREEN_HEIGHT)
|
|
188
172
|
scale = max(WINDOW_SCALE_MIN, min(WINDOW_SCALE_MAX, min(scale_x, scale_y)))
|
|
189
173
|
current_window_scale = scale
|
|
190
174
|
last_window_scale = scale
|
|
191
|
-
_update_window_caption(
|
|
175
|
+
_update_window_caption()
|
|
192
176
|
if game_data is not None:
|
|
193
177
|
game_data.state.overview_created = False
|
|
194
178
|
|
|
@@ -219,8 +203,8 @@ def _update_window_size(size: tuple[int, int], *, source: str) -> None:
|
|
|
219
203
|
last_logged_window_size = size
|
|
220
204
|
|
|
221
205
|
|
|
222
|
-
def _update_window_caption(
|
|
223
|
-
pygame.display.set_caption(
|
|
206
|
+
def _update_window_caption() -> None:
|
|
207
|
+
pygame.display.set_caption("Zombie Escape")
|
|
224
208
|
|
|
225
209
|
|
|
226
210
|
def _maximize_window() -> None:
|