zombie-escape 1.14.4__py3-none-any.whl → 1.15.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zombie_escape/__about__.py +1 -1
- zombie_escape/config.py +1 -0
- zombie_escape/entities.py +126 -199
- zombie_escape/entities_constants.py +11 -1
- zombie_escape/export_images.py +4 -4
- zombie_escape/font_utils.py +47 -0
- zombie_escape/gameplay/__init__.py +2 -1
- zombie_escape/gameplay/constants.py +4 -0
- zombie_escape/gameplay/interactions.py +83 -16
- zombie_escape/gameplay/layout.py +9 -15
- zombie_escape/gameplay/movement.py +45 -29
- zombie_escape/gameplay/spawn.py +15 -29
- zombie_escape/gameplay/state.py +62 -7
- zombie_escape/gameplay/survivors.py +61 -10
- zombie_escape/gameplay/utils.py +33 -0
- zombie_escape/level_blueprints.py +35 -31
- zombie_escape/level_constants.py +2 -2
- zombie_escape/locales/ui.en.json +19 -8
- zombie_escape/locales/ui.ja.json +19 -8
- zombie_escape/localization.py +7 -1
- zombie_escape/models.py +21 -6
- zombie_escape/render/__init__.py +2 -2
- zombie_escape/render/core.py +113 -81
- zombie_escape/render/hud.py +112 -40
- zombie_escape/render/overview.py +93 -2
- zombie_escape/render/shadows.py +2 -2
- zombie_escape/render_constants.py +12 -0
- zombie_escape/screens/__init__.py +6 -189
- zombie_escape/screens/game_over.py +8 -21
- zombie_escape/screens/gameplay.py +71 -26
- zombie_escape/screens/settings.py +114 -43
- zombie_escape/screens/title.py +128 -47
- zombie_escape/stage_constants.py +37 -8
- zombie_escape/windowing.py +508 -0
- zombie_escape/world_grid.py +7 -5
- zombie_escape/zombie_escape.py +26 -13
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/METADATA +24 -24
- zombie_escape-1.15.2.dist-info/RECORD +54 -0
- zombie_escape-1.14.4.dist-info/RECORD +0 -53
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/WHEEL +0 -0
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/__about__.py
CHANGED
zombie_escape/config.py
CHANGED
|
@@ -11,6 +11,7 @@ APP_NAME = "ZombieEscape"
|
|
|
11
11
|
DEFAULT_CONFIG: dict[str, Any] = {
|
|
12
12
|
"language": "en",
|
|
13
13
|
"footprints": {"enabled": True},
|
|
14
|
+
"visual": {"shadows": {"enabled": True}},
|
|
14
15
|
"fast_zombies": {"enabled": False, "ratio": 0.1},
|
|
15
16
|
"car_hint": {"enabled": True, "delay_ms": 180_000},
|
|
16
17
|
"steel_beams": {"enabled": False, "chance": 0.05},
|
zombie_escape/entities.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
|
-
from typing import Callable, Iterable, Sequence, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, Iterable, Sequence, cast
|
|
7
7
|
|
|
8
8
|
try:
|
|
9
9
|
from typing import Self
|
|
@@ -13,10 +13,14 @@ except ImportError: # pragma: no cover - Python 3.10 fallback
|
|
|
13
13
|
import pygame
|
|
14
14
|
from pygame import rect
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .models import LevelLayout
|
|
18
|
+
|
|
16
19
|
from .entities_constants import (
|
|
17
20
|
BUDDY_FOLLOW_SPEED,
|
|
18
21
|
BUDDY_RADIUS,
|
|
19
22
|
BUDDY_WALL_DAMAGE,
|
|
23
|
+
BUDDY_WALL_DAMAGE_RANGE,
|
|
20
24
|
CAR_HEALTH,
|
|
21
25
|
CAR_HEIGHT,
|
|
22
26
|
CAR_SPEED,
|
|
@@ -50,9 +54,6 @@ from .entities_constants import (
|
|
|
50
54
|
ZOMBIE_SIGHT_RANGE,
|
|
51
55
|
ZOMBIE_SPEED,
|
|
52
56
|
ZOMBIE_TRACKER_FAR_SCENT_RADIUS,
|
|
53
|
-
ZOMBIE_TRACKER_CROWD_BAND_LENGTH,
|
|
54
|
-
ZOMBIE_TRACKER_CROWD_BAND_WIDTH,
|
|
55
|
-
ZOMBIE_TRACKER_CROWD_COUNT,
|
|
56
57
|
ZOMBIE_TRACKER_NEWER_FOOTPRINT_MS,
|
|
57
58
|
ZOMBIE_TRACKER_RELOCK_DELAY_MS,
|
|
58
59
|
ZOMBIE_TRACKER_SCAN_INTERVAL_MS,
|
|
@@ -97,7 +98,7 @@ from .render_assets import (
|
|
|
97
98
|
from .render_constants import ANGLE_BINS, ZOMBIE_NOSE_COLOR
|
|
98
99
|
from .rng import get_rng
|
|
99
100
|
from .screen_constants import SCREEN_HEIGHT, SCREEN_WIDTH
|
|
100
|
-
from .world_grid import WallIndex,
|
|
101
|
+
from .world_grid import WallIndex, apply_cell_edge_nudge, walls_for_radius
|
|
101
102
|
|
|
102
103
|
RNG = get_rng()
|
|
103
104
|
|
|
@@ -335,9 +336,7 @@ MovementStrategy = Callable[
|
|
|
335
336
|
Iterable["Zombie"],
|
|
336
337
|
list[Footprint],
|
|
337
338
|
int,
|
|
338
|
-
|
|
339
|
-
int,
|
|
340
|
-
set[tuple[int, int]] | None,
|
|
339
|
+
"LevelLayout",
|
|
341
340
|
],
|
|
342
341
|
tuple[float, float],
|
|
343
342
|
]
|
|
@@ -669,16 +668,15 @@ class Player(pygame.sprite.Sprite):
|
|
|
669
668
|
*,
|
|
670
669
|
wall_index: WallIndex | None = None,
|
|
671
670
|
cell_size: int | None = None,
|
|
672
|
-
|
|
673
|
-
level_height: int | None = None,
|
|
674
|
-
pitfall_cells: set[tuple[int, int]] | None = None,
|
|
675
|
-
walkable_cells: list[tuple[int, int]] | None = None,
|
|
671
|
+
layout: LevelLayout,
|
|
676
672
|
) -> None:
|
|
677
673
|
if self.in_car:
|
|
678
674
|
return
|
|
679
675
|
|
|
680
|
-
|
|
681
|
-
|
|
676
|
+
pitfall_cells = layout.pitfall_cells
|
|
677
|
+
walkable_cells = layout.walkable_cells
|
|
678
|
+
level_width = layout.field_rect.width
|
|
679
|
+
level_height = layout.field_rect.height
|
|
682
680
|
|
|
683
681
|
now = pygame.time.get_ticks()
|
|
684
682
|
if self.is_jumping:
|
|
@@ -882,18 +880,13 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
882
880
|
*,
|
|
883
881
|
wall_index: WallIndex | None = None,
|
|
884
882
|
cell_size: int | None = None,
|
|
885
|
-
|
|
886
|
-
pitfall_cells: set[tuple[int, int]] | None = None,
|
|
887
|
-
walkable_cells: list[tuple[int, int]] | None = None,
|
|
888
|
-
bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] | None = None,
|
|
889
|
-
grid_cols: int | None = None,
|
|
890
|
-
grid_rows: int | None = None,
|
|
891
|
-
level_width: int | None = None,
|
|
892
|
-
level_height: int | None = None,
|
|
883
|
+
layout: LevelLayout,
|
|
893
884
|
wall_target_cell: tuple[int, int] | None = None,
|
|
894
885
|
) -> None:
|
|
895
|
-
|
|
896
|
-
|
|
886
|
+
pitfall_cells = layout.pitfall_cells
|
|
887
|
+
walkable_cells = layout.walkable_cells
|
|
888
|
+
level_width = layout.field_rect.width
|
|
889
|
+
level_height = layout.field_rect.height
|
|
897
890
|
|
|
898
891
|
now = pygame.time.get_ticks()
|
|
899
892
|
if self.is_jumping:
|
|
@@ -928,17 +921,14 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
928
921
|
move_x = (dx / dist) * BUDDY_FOLLOW_SPEED
|
|
929
922
|
move_y = (dy / dist) * BUDDY_FOLLOW_SPEED
|
|
930
923
|
|
|
931
|
-
if cell_size is not None
|
|
932
|
-
move_x, move_y =
|
|
924
|
+
if cell_size is not None:
|
|
925
|
+
move_x, move_y = apply_cell_edge_nudge(
|
|
933
926
|
self.x,
|
|
934
927
|
self.y,
|
|
935
928
|
move_x,
|
|
936
929
|
move_y,
|
|
930
|
+
layout=layout,
|
|
937
931
|
cell_size=cell_size,
|
|
938
|
-
wall_cells=wall_cells,
|
|
939
|
-
bevel_corners=bevel_corners,
|
|
940
|
-
grid_cols=grid_cols,
|
|
941
|
-
grid_rows=grid_rows,
|
|
942
932
|
)
|
|
943
933
|
|
|
944
934
|
self._update_input_facing(move_x, move_y)
|
|
@@ -986,7 +976,12 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
986
976
|
if hit_wall or blocked_by_pitfall:
|
|
987
977
|
if hit_wall:
|
|
988
978
|
if hit_wall.alive():
|
|
989
|
-
|
|
979
|
+
dx_to_player = player_pos[0] - self.x
|
|
980
|
+
dy_to_player = player_pos[1] - self.y
|
|
981
|
+
if dx_to_player * dx_to_player + dy_to_player * dy_to_player <= (
|
|
982
|
+
BUDDY_WALL_DAMAGE_RANGE * BUDDY_WALL_DAMAGE_RANGE
|
|
983
|
+
):
|
|
984
|
+
hit_wall._take_damage(amount=max(1, BUDDY_WALL_DAMAGE))
|
|
990
985
|
if _is_inner_wall(hit_wall):
|
|
991
986
|
inner_wall_hit = True
|
|
992
987
|
self.x -= move_x
|
|
@@ -1017,7 +1012,12 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
1017
1012
|
if hit_wall or blocked_by_pitfall:
|
|
1018
1013
|
if hit_wall:
|
|
1019
1014
|
if hit_wall.alive():
|
|
1020
|
-
|
|
1015
|
+
dx_to_player = player_pos[0] - self.x
|
|
1016
|
+
dy_to_player = player_pos[1] - self.y
|
|
1017
|
+
if dx_to_player * dx_to_player + dy_to_player * dy_to_player <= (
|
|
1018
|
+
BUDDY_WALL_DAMAGE_RANGE * BUDDY_WALL_DAMAGE_RANGE
|
|
1019
|
+
):
|
|
1020
|
+
hit_wall._take_damage(amount=max(1, BUDDY_WALL_DAMAGE))
|
|
1021
1021
|
if _is_inner_wall(hit_wall):
|
|
1022
1022
|
inner_wall_hit = True
|
|
1023
1023
|
self.y -= move_y
|
|
@@ -1069,17 +1069,14 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
1069
1069
|
)
|
|
1070
1070
|
)
|
|
1071
1071
|
|
|
1072
|
-
if cell_size is not None
|
|
1073
|
-
move_x, move_y =
|
|
1072
|
+
if cell_size is not None:
|
|
1073
|
+
move_x, move_y = apply_cell_edge_nudge(
|
|
1074
1074
|
self.x,
|
|
1075
1075
|
self.y,
|
|
1076
1076
|
move_x,
|
|
1077
1077
|
move_y,
|
|
1078
|
+
layout=layout,
|
|
1078
1079
|
cell_size=cell_size,
|
|
1079
|
-
wall_cells=wall_cells,
|
|
1080
|
-
bevel_corners=bevel_corners,
|
|
1081
|
-
grid_cols=grid_cols,
|
|
1082
|
-
grid_rows=grid_rows,
|
|
1083
1080
|
)
|
|
1084
1081
|
|
|
1085
1082
|
if move_x:
|
|
@@ -1199,66 +1196,34 @@ def _zombie_tracker_movement(
|
|
|
1199
1196
|
nearby_zombies: Iterable[Zombie],
|
|
1200
1197
|
footprints: list[Footprint],
|
|
1201
1198
|
cell_size: int,
|
|
1202
|
-
|
|
1203
|
-
grid_rows: int,
|
|
1204
|
-
outer_wall_cells: set[tuple[int, int]] | None,
|
|
1205
|
-
pitfall_cells: set[tuple[int, int]] | None,
|
|
1199
|
+
layout: LevelLayout,
|
|
1206
1200
|
) -> tuple[float, float]:
|
|
1207
1201
|
is_in_sight = zombie._update_mode(player_center, ZOMBIE_TRACKER_SIGHT_RANGE)
|
|
1208
1202
|
if not is_in_sight:
|
|
1209
|
-
if
|
|
1203
|
+
if zombie.tracker_force_wander:
|
|
1210
1204
|
last_target_time = zombie.tracker_target_time
|
|
1211
1205
|
if last_target_time is None:
|
|
1212
1206
|
last_target_time = pygame.time.get_ticks()
|
|
1213
1207
|
zombie.tracker_relock_after_time = last_target_time + ZOMBIE_TRACKER_RELOCK_DELAY_MS
|
|
1214
1208
|
zombie.tracker_target_pos = None
|
|
1215
|
-
return
|
|
1209
|
+
return _zombie_wander_movement(
|
|
1216
1210
|
zombie,
|
|
1217
1211
|
walls,
|
|
1218
1212
|
cell_size=cell_size,
|
|
1219
|
-
|
|
1220
|
-
grid_rows=grid_rows,
|
|
1221
|
-
outer_wall_cells=outer_wall_cells,
|
|
1222
|
-
pitfall_cells=pitfall_cells,
|
|
1213
|
+
layout=layout,
|
|
1223
1214
|
)
|
|
1224
1215
|
_zombie_update_tracker_target(zombie, footprints, walls)
|
|
1225
1216
|
if zombie.tracker_target_pos is not None:
|
|
1226
1217
|
return _zombie_move_toward(zombie, zombie.tracker_target_pos)
|
|
1227
|
-
return
|
|
1218
|
+
return _zombie_wander_movement(
|
|
1228
1219
|
zombie,
|
|
1229
1220
|
walls,
|
|
1230
1221
|
cell_size=cell_size,
|
|
1231
|
-
|
|
1232
|
-
grid_rows=grid_rows,
|
|
1233
|
-
outer_wall_cells=outer_wall_cells,
|
|
1234
|
-
pitfall_cells=pitfall_cells,
|
|
1222
|
+
layout=layout,
|
|
1235
1223
|
)
|
|
1236
1224
|
return _zombie_move_toward(zombie, player_center)
|
|
1237
1225
|
|
|
1238
1226
|
|
|
1239
|
-
def _zombie_wander_movement(
|
|
1240
|
-
zombie: Zombie,
|
|
1241
|
-
_player_center: tuple[int, int],
|
|
1242
|
-
walls: list[Wall],
|
|
1243
|
-
_nearby_zombies: Iterable[Zombie],
|
|
1244
|
-
_footprints: list[Footprint],
|
|
1245
|
-
cell_size: int,
|
|
1246
|
-
grid_cols: int,
|
|
1247
|
-
grid_rows: int,
|
|
1248
|
-
outer_wall_cells: set[tuple[int, int]] | None,
|
|
1249
|
-
pitfall_cells: set[tuple[int, int]] | None,
|
|
1250
|
-
) -> tuple[float, float]:
|
|
1251
|
-
return _zombie_wander_move(
|
|
1252
|
-
zombie,
|
|
1253
|
-
walls,
|
|
1254
|
-
cell_size=cell_size,
|
|
1255
|
-
grid_cols=grid_cols,
|
|
1256
|
-
grid_rows=grid_rows,
|
|
1257
|
-
outer_wall_cells=outer_wall_cells,
|
|
1258
|
-
pitfall_cells=pitfall_cells,
|
|
1259
|
-
)
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
1227
|
def _zombie_wall_hug_has_wall(
|
|
1263
1228
|
zombie: Zombie,
|
|
1264
1229
|
walls: list[Wall],
|
|
@@ -1308,10 +1273,7 @@ def _zombie_wall_hug_movement(
|
|
|
1308
1273
|
_nearby_zombies: Iterable[Zombie],
|
|
1309
1274
|
_footprints: list[Footprint],
|
|
1310
1275
|
cell_size: int,
|
|
1311
|
-
|
|
1312
|
-
grid_rows: int,
|
|
1313
|
-
outer_wall_cells: set[tuple[int, int]] | None,
|
|
1314
|
-
pitfall_cells: set[tuple[int, int]] | None,
|
|
1276
|
+
layout: LevelLayout,
|
|
1315
1277
|
) -> tuple[float, float]:
|
|
1316
1278
|
is_in_sight = zombie._update_mode(player_center, ZOMBIE_TRACKER_SIGHT_RANGE)
|
|
1317
1279
|
if zombie.wall_hug_angle is None:
|
|
@@ -1342,14 +1304,11 @@ def _zombie_wall_hug_movement(
|
|
|
1342
1304
|
else:
|
|
1343
1305
|
if is_in_sight:
|
|
1344
1306
|
return _zombie_move_toward(zombie, player_center)
|
|
1345
|
-
return
|
|
1307
|
+
return _zombie_wander_movement(
|
|
1346
1308
|
zombie,
|
|
1347
1309
|
walls,
|
|
1348
1310
|
cell_size=cell_size,
|
|
1349
|
-
|
|
1350
|
-
grid_rows=grid_rows,
|
|
1351
|
-
outer_wall_cells=outer_wall_cells,
|
|
1352
|
-
pitfall_cells=pitfall_cells,
|
|
1311
|
+
layout=layout,
|
|
1353
1312
|
)
|
|
1354
1313
|
|
|
1355
1314
|
sensor_distance = ZOMBIE_WALL_HUG_SENSOR_DISTANCE + zombie.radius
|
|
@@ -1405,21 +1364,15 @@ def _zombie_normal_movement(
|
|
|
1405
1364
|
_nearby_zombies: Iterable[Zombie],
|
|
1406
1365
|
_footprints: list[Footprint],
|
|
1407
1366
|
cell_size: int,
|
|
1408
|
-
|
|
1409
|
-
grid_rows: int,
|
|
1410
|
-
outer_wall_cells: set[tuple[int, int]] | None,
|
|
1411
|
-
pitfall_cells: set[tuple[int, int]] | None,
|
|
1367
|
+
layout: LevelLayout,
|
|
1412
1368
|
) -> tuple[float, float]:
|
|
1413
1369
|
is_in_sight = zombie._update_mode(player_center, ZOMBIE_SIGHT_RANGE)
|
|
1414
1370
|
if not is_in_sight:
|
|
1415
|
-
return
|
|
1371
|
+
return _zombie_wander_movement(
|
|
1416
1372
|
zombie,
|
|
1417
1373
|
walls,
|
|
1418
1374
|
cell_size=cell_size,
|
|
1419
|
-
|
|
1420
|
-
grid_rows=grid_rows,
|
|
1421
|
-
outer_wall_cells=outer_wall_cells,
|
|
1422
|
-
pitfall_cells=pitfall_cells,
|
|
1375
|
+
layout=layout,
|
|
1423
1376
|
)
|
|
1424
1377
|
return _zombie_move_toward(zombie, player_center)
|
|
1425
1378
|
|
|
@@ -1429,6 +1382,7 @@ def _zombie_update_tracker_target(
|
|
|
1429
1382
|
footprints: list[Footprint],
|
|
1430
1383
|
walls: list[Wall],
|
|
1431
1384
|
) -> None:
|
|
1385
|
+
# footprints are ordered oldest -> newest by time.
|
|
1432
1386
|
now = pygame.time.get_ticks()
|
|
1433
1387
|
if now - zombie.tracker_last_scan_time < zombie.tracker_scan_interval_ms:
|
|
1434
1388
|
return
|
|
@@ -1436,43 +1390,42 @@ def _zombie_update_tracker_target(
|
|
|
1436
1390
|
if not footprints:
|
|
1437
1391
|
zombie.tracker_target_pos = None
|
|
1438
1392
|
return
|
|
1439
|
-
nearby: list[Footprint] = []
|
|
1440
1393
|
last_target_time = zombie.tracker_target_time
|
|
1441
1394
|
far_radius_sq = ZOMBIE_TRACKER_FAR_SCENT_RADIUS * ZOMBIE_TRACKER_FAR_SCENT_RADIUS
|
|
1442
|
-
latest_fp_time: int | None = None
|
|
1443
1395
|
relock_after = zombie.tracker_relock_after_time
|
|
1396
|
+
far_candidates: list[tuple[float, Footprint]] = []
|
|
1444
1397
|
for fp in footprints:
|
|
1445
1398
|
dx = fp.pos[0] - zombie.x
|
|
1446
1399
|
dy = fp.pos[1] - zombie.y
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1400
|
+
d2 = dx * dx + dy * dy
|
|
1401
|
+
if d2 <= far_radius_sq:
|
|
1402
|
+
far_candidates.append((d2, fp))
|
|
1403
|
+
if not far_candidates:
|
|
1404
|
+
return
|
|
1405
|
+
latest_fp_time = far_candidates[-1][1].time
|
|
1450
1406
|
use_far_scan = last_target_time is None or (
|
|
1451
1407
|
latest_fp_time is not None and latest_fp_time - last_target_time >= ZOMBIE_TRACKER_NEWER_FOOTPRINT_MS
|
|
1452
1408
|
)
|
|
1453
1409
|
scan_radius = ZOMBIE_TRACKER_FAR_SCENT_RADIUS if use_far_scan else ZOMBIE_TRACKER_SCENT_RADIUS
|
|
1454
1410
|
scent_radius_sq = scan_radius * scan_radius
|
|
1455
1411
|
min_target_dist_sq = (FOOTPRINT_STEP_DISTANCE * 0.5) ** 2
|
|
1456
|
-
|
|
1412
|
+
|
|
1413
|
+
newer: list[Footprint] = []
|
|
1414
|
+
for d2, fp in far_candidates:
|
|
1457
1415
|
pos = fp.pos
|
|
1458
1416
|
fp_time = fp.time
|
|
1459
1417
|
if relock_after is not None and fp_time < relock_after:
|
|
1460
1418
|
continue
|
|
1461
|
-
|
|
1462
|
-
dy = pos[1] - zombie.y
|
|
1463
|
-
if dx * dx + dy * dy <= min_target_dist_sq:
|
|
1419
|
+
if d2 <= min_target_dist_sq:
|
|
1464
1420
|
continue
|
|
1465
|
-
if
|
|
1466
|
-
|
|
1421
|
+
if d2 <= scent_radius_sq:
|
|
1422
|
+
if last_target_time is None or fp_time > last_target_time:
|
|
1423
|
+
newer.append(fp)
|
|
1467
1424
|
|
|
1468
|
-
if not
|
|
1425
|
+
if not newer:
|
|
1469
1426
|
return
|
|
1470
1427
|
|
|
1471
|
-
|
|
1472
|
-
if last_target_time is not None:
|
|
1473
|
-
newer = [fp for fp in nearby if fp.time > last_target_time]
|
|
1474
|
-
else:
|
|
1475
|
-
newer = nearby
|
|
1428
|
+
newer.sort(key=lambda fp: fp.time)
|
|
1476
1429
|
|
|
1477
1430
|
if use_far_scan or last_target_time is None:
|
|
1478
1431
|
candidates = list(reversed(newer))[:ZOMBIE_TRACKER_SCENT_TOP_K]
|
|
@@ -1504,11 +1457,7 @@ def _zombie_update_tracker_target(
|
|
|
1504
1457
|
if last_target_time is None:
|
|
1505
1458
|
return
|
|
1506
1459
|
|
|
1507
|
-
|
|
1508
|
-
if not candidates:
|
|
1509
|
-
return
|
|
1510
|
-
candidates.sort(key=lambda fp: fp.time)
|
|
1511
|
-
next_fp = candidates[0]
|
|
1460
|
+
next_fp = newer[0]
|
|
1512
1461
|
zombie.tracker_target_pos = next_fp.pos
|
|
1513
1462
|
zombie.tracker_target_time = next_fp.time
|
|
1514
1463
|
if relock_after is not None and next_fp.time >= relock_after:
|
|
@@ -1516,50 +1465,17 @@ def _zombie_update_tracker_target(
|
|
|
1516
1465
|
return
|
|
1517
1466
|
|
|
1518
1467
|
|
|
1519
|
-
def
|
|
1520
|
-
zombie: Zombie,
|
|
1521
|
-
nearby_zombies: Iterable[Zombie],
|
|
1522
|
-
) -> bool:
|
|
1523
|
-
dx = zombie.last_move_dx
|
|
1524
|
-
dy = zombie.last_move_dy
|
|
1525
|
-
if abs(dx) <= 0.001 and abs(dy) <= 0.001:
|
|
1526
|
-
return False
|
|
1527
|
-
angle = math.atan2(dy, dx)
|
|
1528
|
-
angle_step = math.pi / 4.0
|
|
1529
|
-
angle_bin = int(round(angle / angle_step)) % 8
|
|
1530
|
-
dir_x = math.cos(angle_bin * angle_step)
|
|
1531
|
-
dir_y = math.sin(angle_bin * angle_step)
|
|
1532
|
-
perp_x = -dir_y
|
|
1533
|
-
perp_y = dir_x
|
|
1534
|
-
half_width = ZOMBIE_TRACKER_CROWD_BAND_WIDTH * 0.5
|
|
1535
|
-
length = ZOMBIE_TRACKER_CROWD_BAND_LENGTH
|
|
1536
|
-
count = 0
|
|
1537
|
-
for other in nearby_zombies:
|
|
1538
|
-
if other is zombie or not other.alive() or not other.tracker:
|
|
1539
|
-
continue
|
|
1540
|
-
offset_x = other.x - zombie.x
|
|
1541
|
-
offset_y = other.y - zombie.y
|
|
1542
|
-
forward = offset_x * dir_x + offset_y * dir_y
|
|
1543
|
-
if forward <= 0 or forward > length:
|
|
1544
|
-
continue
|
|
1545
|
-
side = offset_x * perp_x + offset_y * perp_y
|
|
1546
|
-
if abs(side) <= half_width:
|
|
1547
|
-
count += 1
|
|
1548
|
-
if count >= ZOMBIE_TRACKER_CROWD_COUNT:
|
|
1549
|
-
return True
|
|
1550
|
-
return False
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
def _zombie_wander_move(
|
|
1468
|
+
def _zombie_wander_movement(
|
|
1554
1469
|
zombie: Zombie,
|
|
1555
1470
|
walls: list[Wall],
|
|
1556
1471
|
*,
|
|
1557
1472
|
cell_size: int,
|
|
1558
|
-
|
|
1559
|
-
grid_rows: int,
|
|
1560
|
-
outer_wall_cells: set[tuple[int, int]] | None,
|
|
1561
|
-
pitfall_cells: set[tuple[int, int]] | None,
|
|
1473
|
+
layout: LevelLayout,
|
|
1562
1474
|
) -> tuple[float, float]:
|
|
1475
|
+
grid_cols = layout.grid_cols
|
|
1476
|
+
grid_rows = layout.grid_rows
|
|
1477
|
+
outer_wall_cells = layout.outer_wall_cells
|
|
1478
|
+
pitfall_cells = layout.pitfall_cells
|
|
1563
1479
|
now = pygame.time.get_ticks()
|
|
1564
1480
|
changed_angle = False
|
|
1565
1481
|
if now - zombie.last_wander_change_time > zombie.wander_change_interval:
|
|
@@ -1700,12 +1616,12 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1700
1616
|
self.tracker_last_scan_time = 0
|
|
1701
1617
|
self.tracker_scan_interval_ms = ZOMBIE_TRACKER_SCAN_INTERVAL_MS
|
|
1702
1618
|
self.tracker_relock_after_time: int | None = None
|
|
1619
|
+
self.tracker_force_wander = False
|
|
1703
1620
|
self.wall_hug_side = RNG.choice([-1.0, 1.0]) if wall_hugging else 0.0
|
|
1704
1621
|
self.wall_hug_angle = RNG.uniform(0, math.tau) if wall_hugging else None
|
|
1705
1622
|
self.wall_hug_last_wall_time: int | None = None
|
|
1706
1623
|
self.wall_hug_last_side_has_wall = False
|
|
1707
1624
|
self.wall_hug_stuck_flag = False
|
|
1708
|
-
self.pos_history: list[tuple[float, float]] = []
|
|
1709
1625
|
self.wander_angle = RNG.uniform(0, math.tau)
|
|
1710
1626
|
self.wander_interval_ms = ZOMBIE_TRACKER_WANDER_INTERVAL_MS if tracker else ZOMBIE_WANDER_INTERVAL_MS
|
|
1711
1627
|
self.last_wander_change_time = pygame.time.get_ticks()
|
|
@@ -1721,6 +1637,13 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1721
1637
|
self.was_in_sight = is_in_sight
|
|
1722
1638
|
return is_in_sight
|
|
1723
1639
|
|
|
1640
|
+
def _apply_wall_contact_damage(self: Self, walls: list[Wall]) -> None:
|
|
1641
|
+
possible_walls = [w for w in walls if abs(w.rect.centerx - self.x) < 100 and abs(w.rect.centery - self.y) < 100]
|
|
1642
|
+
for wall in possible_walls:
|
|
1643
|
+
if _circle_wall_collision((self.x, self.y), self.radius, wall):
|
|
1644
|
+
if wall.alive():
|
|
1645
|
+
wall._take_damage(amount=ZOMBIE_WALL_DAMAGE)
|
|
1646
|
+
|
|
1724
1647
|
def _handle_wall_collision(self: Self, next_x: float, next_y: float, walls: list[Wall]) -> tuple[float, float]:
|
|
1725
1648
|
final_x, final_y = next_x, next_y
|
|
1726
1649
|
|
|
@@ -1753,6 +1676,7 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1753
1676
|
zombies: Iterable[Zombie],
|
|
1754
1677
|
) -> tuple[float, float]:
|
|
1755
1678
|
"""If another zombie is too close, steer directly away from the closest one."""
|
|
1679
|
+
orig_move_x, orig_move_y = move_x, move_y
|
|
1756
1680
|
next_x = self.x + move_x
|
|
1757
1681
|
next_y = self.y + move_y
|
|
1758
1682
|
|
|
@@ -1796,6 +1720,16 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1796
1720
|
|
|
1797
1721
|
move_x = (away_dx / away_dist) * self.speed
|
|
1798
1722
|
move_y = (away_dy / away_dist) * self.speed
|
|
1723
|
+
if self.wall_hugging:
|
|
1724
|
+
if orig_move_x or orig_move_y:
|
|
1725
|
+
orig_angle = math.atan2(orig_move_y, orig_move_x)
|
|
1726
|
+
new_angle = math.atan2(move_y, move_x)
|
|
1727
|
+
diff = (new_angle - orig_angle + math.pi) % math.tau - math.pi
|
|
1728
|
+
if abs(diff) > math.pi / 2.0:
|
|
1729
|
+
clamped = math.copysign(math.pi / 2.0, diff)
|
|
1730
|
+
new_angle = orig_angle + clamped
|
|
1731
|
+
move_x = math.cos(new_angle) * self.speed
|
|
1732
|
+
move_y = math.sin(new_angle) * self.speed
|
|
1799
1733
|
return move_x, move_y
|
|
1800
1734
|
|
|
1801
1735
|
def _apply_aging(self: Self) -> None:
|
|
@@ -1849,23 +1783,6 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1849
1783
|
color=ZOMBIE_NOSE_COLOR,
|
|
1850
1784
|
)
|
|
1851
1785
|
|
|
1852
|
-
def _update_stuck_state(self: Self) -> None:
|
|
1853
|
-
history = self.pos_history
|
|
1854
|
-
history.append((self.x, self.y))
|
|
1855
|
-
if len(history) > 20:
|
|
1856
|
-
history.pop(0)
|
|
1857
|
-
max_dist_sq = max((self.x - hx) ** 2 + (self.y - hy) ** 2 for hx, hy in history)
|
|
1858
|
-
self.wall_hug_stuck_flag = max_dist_sq < 25
|
|
1859
|
-
if not self.wall_hug_stuck_flag:
|
|
1860
|
-
return
|
|
1861
|
-
if self.wall_hugging:
|
|
1862
|
-
if self.wall_hug_angle is None:
|
|
1863
|
-
self.wall_hug_angle = self.wander_angle
|
|
1864
|
-
self.wall_hug_angle = (self.wall_hug_angle + math.pi) % math.tau
|
|
1865
|
-
self.wall_hug_side *= -1.0
|
|
1866
|
-
self.wall_hug_stuck_flag = False
|
|
1867
|
-
self.pos_history = []
|
|
1868
|
-
|
|
1869
1786
|
def _avoid_pitfalls(
|
|
1870
1787
|
self: Self,
|
|
1871
1788
|
pitfall_cells: set[tuple[int, int]],
|
|
@@ -1907,18 +1824,13 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1907
1824
|
footprints: list[Footprint] | None = None,
|
|
1908
1825
|
*,
|
|
1909
1826
|
cell_size: int,
|
|
1910
|
-
|
|
1911
|
-
grid_rows: int,
|
|
1912
|
-
level_width: int,
|
|
1913
|
-
level_height: int,
|
|
1914
|
-
outer_wall_cells: set[tuple[int, int]] | None = None,
|
|
1915
|
-
wall_cells: set[tuple[int, int]] | None = None,
|
|
1916
|
-
pitfall_cells: set[tuple[int, int]] | None = None,
|
|
1917
|
-
bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] | None = None,
|
|
1827
|
+
layout: LevelLayout,
|
|
1918
1828
|
) -> None:
|
|
1919
1829
|
if self.carbonized:
|
|
1920
1830
|
return
|
|
1921
|
-
|
|
1831
|
+
level_width = layout.field_rect.width
|
|
1832
|
+
level_height = layout.field_rect.height
|
|
1833
|
+
self._apply_wall_contact_damage(walls)
|
|
1922
1834
|
self._apply_aging()
|
|
1923
1835
|
dx_player = player_center[0] - self.x
|
|
1924
1836
|
dy_player = player_center[1] - self.y
|
|
@@ -1932,30 +1844,45 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
1932
1844
|
nearby_zombies,
|
|
1933
1845
|
footprints or [],
|
|
1934
1846
|
cell_size,
|
|
1935
|
-
|
|
1936
|
-
grid_rows,
|
|
1937
|
-
outer_wall_cells,
|
|
1938
|
-
pitfall_cells,
|
|
1847
|
+
layout,
|
|
1939
1848
|
)
|
|
1940
1849
|
if dist_to_player_sq <= avoid_radius_sq or self.wall_hugging:
|
|
1941
1850
|
move_x, move_y = self._avoid_other_zombies(move_x, move_y, nearby_zombies)
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
bevel_corners=bevel_corners,
|
|
1951
|
-
grid_cols=grid_cols,
|
|
1952
|
-
grid_rows=grid_rows,
|
|
1953
|
-
)
|
|
1851
|
+
move_x, move_y = apply_cell_edge_nudge(
|
|
1852
|
+
self.x,
|
|
1853
|
+
self.y,
|
|
1854
|
+
move_x,
|
|
1855
|
+
move_y,
|
|
1856
|
+
layout=layout,
|
|
1857
|
+
cell_size=cell_size,
|
|
1858
|
+
)
|
|
1954
1859
|
self._update_facing_from_movement(move_x, move_y)
|
|
1955
1860
|
self._apply_render_overlays()
|
|
1956
1861
|
self.last_move_dx = move_x
|
|
1957
1862
|
self.last_move_dy = move_y
|
|
1958
|
-
|
|
1863
|
+
possible_walls = [w for w in walls if abs(w.rect.centerx - self.x) < 100 and abs(w.rect.centery - self.y) < 100]
|
|
1864
|
+
final_x = self.x
|
|
1865
|
+
final_y = self.y
|
|
1866
|
+
if move_x:
|
|
1867
|
+
next_x = final_x + move_x
|
|
1868
|
+
for wall in possible_walls:
|
|
1869
|
+
if _circle_wall_collision((next_x, final_y), self.radius, wall):
|
|
1870
|
+
if wall.alive():
|
|
1871
|
+
wall._take_damage(amount=ZOMBIE_WALL_DAMAGE)
|
|
1872
|
+
if wall.alive():
|
|
1873
|
+
next_x = final_x
|
|
1874
|
+
break
|
|
1875
|
+
final_x = next_x
|
|
1876
|
+
if move_y:
|
|
1877
|
+
next_y = final_y + move_y
|
|
1878
|
+
for wall in possible_walls:
|
|
1879
|
+
if _circle_wall_collision((final_x, next_y), self.radius, wall):
|
|
1880
|
+
if wall.alive():
|
|
1881
|
+
wall._take_damage(amount=ZOMBIE_WALL_DAMAGE)
|
|
1882
|
+
if wall.alive():
|
|
1883
|
+
next_y = final_y
|
|
1884
|
+
break
|
|
1885
|
+
final_y = next_y
|
|
1959
1886
|
|
|
1960
1887
|
if not (0 <= final_x < level_width and 0 <= final_y < level_height):
|
|
1961
1888
|
self.kill()
|
|
@@ -11,10 +11,14 @@ PLAYER_SPEED = 1.4
|
|
|
11
11
|
FOV_RADIUS = 124 # approximate legacy FOV (80) * 1.55 cap
|
|
12
12
|
BUDDY_RADIUS = HUMANOID_RADIUS
|
|
13
13
|
BUDDY_FOLLOW_SPEED = PLAYER_SPEED * 0.7
|
|
14
|
+
BUDDY_FOLLOW_START_DISTANCE = 30
|
|
15
|
+
BUDDY_FOLLOW_STOP_DISTANCE = 100
|
|
16
|
+
BUDDY_WALL_DAMAGE_RANGE = 50
|
|
17
|
+
BUDDY_MERGE_DISTANCE = 40
|
|
14
18
|
HUMANOID_WALL_BUMP_FRAMES = 7
|
|
15
19
|
|
|
16
20
|
# --- Jump settings ---
|
|
17
|
-
PLAYER_JUMP_RANGE = 15 # px (enough to clear one 50px
|
|
21
|
+
PLAYER_JUMP_RANGE = 15 # px (enough to clear one 50px cell)
|
|
18
22
|
SURVIVOR_JUMP_RANGE = int(PLAYER_JUMP_RANGE * 0.7) # px
|
|
19
23
|
JUMP_DURATION_MS = 200
|
|
20
24
|
JUMP_SCALE_MAX = 0.15
|
|
@@ -54,6 +58,7 @@ ZOMBIE_TRACKER_WANDER_INTERVAL_MS = 2500
|
|
|
54
58
|
ZOMBIE_TRACKER_CROWD_BAND_WIDTH = 50
|
|
55
59
|
ZOMBIE_TRACKER_CROWD_BAND_LENGTH = 50
|
|
56
60
|
ZOMBIE_TRACKER_CROWD_COUNT = 5
|
|
61
|
+
ZOMBIE_TRACKER_GRID_CROWD_COUNT = 3
|
|
57
62
|
ZOMBIE_TRACKER_RELOCK_DELAY_MS = 3000
|
|
58
63
|
ZOMBIE_WALL_HUG_SENSOR_DISTANCE = 24
|
|
59
64
|
ZOMBIE_WALL_HUG_PROBE_ANGLE_DEG = 45
|
|
@@ -85,6 +90,10 @@ __all__ = [
|
|
|
85
90
|
"FOV_RADIUS",
|
|
86
91
|
"BUDDY_RADIUS",
|
|
87
92
|
"BUDDY_FOLLOW_SPEED",
|
|
93
|
+
"BUDDY_FOLLOW_START_DISTANCE",
|
|
94
|
+
"BUDDY_FOLLOW_STOP_DISTANCE",
|
|
95
|
+
"BUDDY_WALL_DAMAGE_RANGE",
|
|
96
|
+
"BUDDY_MERGE_DISTANCE",
|
|
88
97
|
"HUMANOID_WALL_BUMP_FRAMES",
|
|
89
98
|
"SURVIVOR_RADIUS",
|
|
90
99
|
"SURVIVOR_APPROACH_RADIUS",
|
|
@@ -113,6 +122,7 @@ __all__ = [
|
|
|
113
122
|
"ZOMBIE_TRACKER_CROWD_BAND_WIDTH",
|
|
114
123
|
"ZOMBIE_TRACKER_CROWD_BAND_LENGTH",
|
|
115
124
|
"ZOMBIE_TRACKER_CROWD_COUNT",
|
|
125
|
+
"ZOMBIE_TRACKER_GRID_CROWD_COUNT",
|
|
116
126
|
"ZOMBIE_TRACKER_RELOCK_DELAY_MS",
|
|
117
127
|
"ZOMBIE_WALL_HUG_SENSOR_DISTANCE",
|
|
118
128
|
"ZOMBIE_WALL_HUG_PROBE_ANGLE_DEG",
|
zombie_escape/export_images.py
CHANGED
|
@@ -22,7 +22,7 @@ from .entities_constants import (
|
|
|
22
22
|
SURVIVOR_RADIUS,
|
|
23
23
|
ZOMBIE_RADIUS,
|
|
24
24
|
)
|
|
25
|
-
from .level_constants import
|
|
25
|
+
from .level_constants import DEFAULT_CELL_SIZE
|
|
26
26
|
from .render_assets import (
|
|
27
27
|
build_car_directional_surfaces,
|
|
28
28
|
build_car_surface,
|
|
@@ -81,7 +81,7 @@ def _pick_directional_surface(surfaces: list[pygame.Surface], *, bin_index: int
|
|
|
81
81
|
return surfaces[bin_index % len(surfaces)]
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def
|
|
84
|
+
def _build_pitfall_cell(cell_size: int) -> pygame.Surface:
|
|
85
85
|
surface = pygame.Surface((cell_size, cell_size), pygame.SRCALPHA)
|
|
86
86
|
rect = surface.get_rect()
|
|
87
87
|
pygame.draw.rect(surface, PITFALL_ABYSS_COLOR, rect)
|
|
@@ -116,7 +116,7 @@ def _build_pitfall_tile(cell_size: int) -> pygame.Surface:
|
|
|
116
116
|
return surface
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
def export_images(output_dir: Path, *, cell_size: int =
|
|
119
|
+
def export_images(output_dir: Path, *, cell_size: int = DEFAULT_CELL_SIZE, output_scale: int = 4) -> list[Path]:
|
|
120
120
|
_ensure_pygame_ready()
|
|
121
121
|
|
|
122
122
|
saved: list[Path] = []
|
|
@@ -267,7 +267,7 @@ def export_images(output_dir: Path, *, cell_size: int = DEFAULT_TILE_SIZE, outpu
|
|
|
267
267
|
_save_surface(outer_wall, outer_wall_path, scale=output_scale)
|
|
268
268
|
saved.append(outer_wall_path)
|
|
269
269
|
|
|
270
|
-
pitfall =
|
|
270
|
+
pitfall = _build_pitfall_cell(cell_size)
|
|
271
271
|
pitfall_path = out / "pitfall.png"
|
|
272
272
|
_save_surface(pitfall, pitfall_path, scale=output_scale)
|
|
273
273
|
saved.append(pitfall_path)
|