zombie-escape 1.10.1__tar.gz → 1.12.0__tar.gz
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-1.10.1 → zombie_escape-1.12.0}/PKG-INFO +4 -2
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/README.md +2 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/pyproject.toml +1 -1
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/__about__.py +1 -1
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/entities.py +13 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/entities_constants.py +9 -3
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/__init__.py +2 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/interactions.py +19 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/layout.py +22 -1
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/movement.py +15 -1
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/spawn.py +95 -4
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/state.py +2 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay_constants.py +8 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/level_blueprints.py +54 -22
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/locales/ui.en.json +9 -1
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/locales/ui.ja.json +8 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/models.py +6 -1
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/render.py +339 -27
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/render_assets.py +104 -52
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/render_constants.py +14 -4
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/screens/game_over.py +1 -1
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/screens/gameplay.py +27 -1
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/stage_constants.py +31 -14
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/.gitignore +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/LICENSE.txt +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/__init__.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/colors.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/config.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/font_utils.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/ambient.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/constants.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/footprints.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/survivors.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/gameplay/utils.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/input_utils.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/level_constants.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/localization.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/progress.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/rng.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/screen_constants.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/screens/__init__.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/screens/settings.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/screens/title.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/world_grid.py +0 -0
- {zombie_escape-1.10.1 → zombie_escape-1.12.0}/src/zombie_escape/zombie_escape.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zombie-escape
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.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>
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
17
17
|
Requires-Python: >=3.10
|
|
18
18
|
Requires-Dist: numpy
|
|
19
19
|
Requires-Dist: platformdirs
|
|
20
|
-
Requires-Dist: pygame
|
|
20
|
+
Requires-Dist: pygame-ce
|
|
21
21
|
Requires-Dist: python-i18n
|
|
22
22
|
Requires-Dist: typing-extensions; python_version < '3.11'
|
|
23
23
|
Provides-Extra: dev
|
|
@@ -155,6 +155,8 @@ zombie-escape
|
|
|
155
155
|
|
|
156
156
|
This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details.
|
|
157
157
|
|
|
158
|
+
This project depends on pygame-ce (repository: `https://github.com/pygame-community/pygame-ce`), which is licensed under GNU LGPL version 2.1.
|
|
159
|
+
|
|
158
160
|
The bundled Silkscreen-Regular.ttf font follows the license terms of its original distribution.
|
|
159
161
|
Please refer to the upstream website for details: https://fonts.google.com/specimen/Silkscreen
|
|
160
162
|
|
|
@@ -126,6 +126,8 @@ zombie-escape
|
|
|
126
126
|
|
|
127
127
|
This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details.
|
|
128
128
|
|
|
129
|
+
This project depends on pygame-ce (repository: `https://github.com/pygame-community/pygame-ce`), which is licensed under GNU LGPL version 2.1.
|
|
130
|
+
|
|
129
131
|
The bundled Silkscreen-Regular.ttf font follows the license terms of its original distribution.
|
|
130
132
|
Please refer to the upstream website for details: https://fonts.google.com/specimen/Silkscreen
|
|
131
133
|
|
|
@@ -26,6 +26,8 @@ from .entities_constants import (
|
|
|
26
26
|
FLASHLIGHT_WIDTH,
|
|
27
27
|
FUEL_CAN_HEIGHT,
|
|
28
28
|
FUEL_CAN_WIDTH,
|
|
29
|
+
SHOES_HEIGHT,
|
|
30
|
+
SHOES_WIDTH,
|
|
29
31
|
INTERNAL_WALL_BEVEL_DEPTH,
|
|
30
32
|
INTERNAL_WALL_HEALTH,
|
|
31
33
|
PLAYER_RADIUS,
|
|
@@ -63,6 +65,7 @@ from .render_assets import (
|
|
|
63
65
|
build_car_surface,
|
|
64
66
|
build_flashlight_surface,
|
|
65
67
|
build_fuel_can_surface,
|
|
68
|
+
build_shoes_surface,
|
|
66
69
|
build_player_surface,
|
|
67
70
|
build_survivor_surface,
|
|
68
71
|
build_zombie_surface,
|
|
@@ -1589,6 +1592,15 @@ class Flashlight(pygame.sprite.Sprite):
|
|
|
1589
1592
|
self.rect = self.image.get_rect(center=(x, y))
|
|
1590
1593
|
|
|
1591
1594
|
|
|
1595
|
+
class Shoes(pygame.sprite.Sprite):
|
|
1596
|
+
"""Shoes pickup that boosts the player's move speed when collected."""
|
|
1597
|
+
|
|
1598
|
+
def __init__(self: Self, x: int, y: int) -> None:
|
|
1599
|
+
super().__init__()
|
|
1600
|
+
self.image = build_shoes_surface(SHOES_WIDTH, SHOES_HEIGHT)
|
|
1601
|
+
self.rect = self.image.get_rect(center=(x, y))
|
|
1602
|
+
|
|
1603
|
+
|
|
1592
1604
|
def _car_body_radius(width: float, height: float) -> float:
|
|
1593
1605
|
"""Approximate car collision radius using only its own dimensions."""
|
|
1594
1606
|
return min(width, height) / 2
|
|
@@ -1604,5 +1616,6 @@ __all__ = [
|
|
|
1604
1616
|
"Car",
|
|
1605
1617
|
"FuelCan",
|
|
1606
1618
|
"Flashlight",
|
|
1619
|
+
"Shoes",
|
|
1607
1620
|
"random_position_outside_building",
|
|
1608
1621
|
]
|
|
@@ -20,8 +20,12 @@ SURVIVOR_MAX_SAFE_PASSENGERS = 5
|
|
|
20
20
|
SURVIVOR_MIN_SPEED_FACTOR = 0.35
|
|
21
21
|
|
|
22
22
|
# --- Flashlight settings ---
|
|
23
|
-
FLASHLIGHT_WIDTH =
|
|
24
|
-
FLASHLIGHT_HEIGHT =
|
|
23
|
+
FLASHLIGHT_WIDTH = 12
|
|
24
|
+
FLASHLIGHT_HEIGHT = 10
|
|
25
|
+
|
|
26
|
+
# --- Shoes settings ---
|
|
27
|
+
SHOES_WIDTH = 14
|
|
28
|
+
SHOES_HEIGHT = 12
|
|
25
29
|
|
|
26
30
|
# --- Zombie settings ---
|
|
27
31
|
ZOMBIE_RADIUS = HUMANOID_RADIUS
|
|
@@ -50,7 +54,7 @@ CAR_HEIGHT = 25
|
|
|
50
54
|
CAR_SPEED = 2
|
|
51
55
|
CAR_HEALTH = 20
|
|
52
56
|
CAR_WALL_DAMAGE = 1
|
|
53
|
-
FUEL_CAN_WIDTH =
|
|
57
|
+
FUEL_CAN_WIDTH = 12
|
|
54
58
|
FUEL_CAN_HEIGHT = 15
|
|
55
59
|
|
|
56
60
|
# --- Wall and beam settings ---
|
|
@@ -74,6 +78,8 @@ __all__ = [
|
|
|
74
78
|
"SURVIVOR_MIN_SPEED_FACTOR",
|
|
75
79
|
"FLASHLIGHT_WIDTH",
|
|
76
80
|
"FLASHLIGHT_HEIGHT",
|
|
81
|
+
"SHOES_WIDTH",
|
|
82
|
+
"SHOES_HEIGHT",
|
|
77
83
|
"ZOMBIE_RADIUS",
|
|
78
84
|
"ZOMBIE_SPEED",
|
|
79
85
|
"ZOMBIE_WANDER_INTERVAL_MS",
|
|
@@ -14,6 +14,7 @@ from .spawn import (
|
|
|
14
14
|
place_flashlights,
|
|
15
15
|
place_fuel_can,
|
|
16
16
|
place_new_car,
|
|
17
|
+
place_shoes,
|
|
17
18
|
setup_player_and_cars,
|
|
18
19
|
spawn_exterior_zombie,
|
|
19
20
|
spawn_initial_zombies,
|
|
@@ -46,6 +47,7 @@ __all__ = [
|
|
|
46
47
|
"place_new_car",
|
|
47
48
|
"place_fuel_can",
|
|
48
49
|
"place_flashlights",
|
|
50
|
+
"place_shoes",
|
|
49
51
|
"place_buddies",
|
|
50
52
|
"find_interior_spawn_positions",
|
|
51
53
|
"find_nearby_offscreen_spawn_position",
|
|
@@ -12,6 +12,8 @@ from ..entities_constants import (
|
|
|
12
12
|
FUEL_CAN_HEIGHT,
|
|
13
13
|
FUEL_CAN_WIDTH,
|
|
14
14
|
HUMANOID_RADIUS,
|
|
15
|
+
SHOES_HEIGHT,
|
|
16
|
+
SHOES_WIDTH,
|
|
15
17
|
SURVIVOR_APPROACH_RADIUS,
|
|
16
18
|
SURVIVOR_MAX_SAFE_PASSENGERS,
|
|
17
19
|
)
|
|
@@ -58,6 +60,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
58
60
|
outside_rects = game_data.layout.outside_rects
|
|
59
61
|
fuel = game_data.fuel
|
|
60
62
|
flashlights = game_data.flashlights or []
|
|
63
|
+
shoes_list = game_data.shoes or []
|
|
61
64
|
camera = game_data.camera
|
|
62
65
|
stage = game_data.stage
|
|
63
66
|
maintain_waiting_car_supply(game_data)
|
|
@@ -70,6 +73,7 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
70
73
|
flashlight_interaction_radius = _interaction_radius(
|
|
71
74
|
FLASHLIGHT_WIDTH, FLASHLIGHT_HEIGHT
|
|
72
75
|
)
|
|
76
|
+
shoes_interaction_radius = _interaction_radius(SHOES_WIDTH, SHOES_HEIGHT)
|
|
73
77
|
|
|
74
78
|
def _player_near_point(point: tuple[float, float], radius: float) -> bool:
|
|
75
79
|
dx = point[0] - player.x
|
|
@@ -118,6 +122,21 @@ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
118
122
|
print("Flashlight acquired!")
|
|
119
123
|
break
|
|
120
124
|
|
|
125
|
+
for shoes in list(shoes_list):
|
|
126
|
+
if not shoes.alive():
|
|
127
|
+
continue
|
|
128
|
+
if _player_near_point(shoes.rect.center, shoes_interaction_radius):
|
|
129
|
+
state.shoes_count += 1
|
|
130
|
+
state.hint_expires_at = 0
|
|
131
|
+
state.hint_target_type = None
|
|
132
|
+
shoes.kill()
|
|
133
|
+
try:
|
|
134
|
+
shoes_list.remove(shoes)
|
|
135
|
+
except ValueError:
|
|
136
|
+
pass
|
|
137
|
+
print("Shoes acquired!")
|
|
138
|
+
break
|
|
139
|
+
|
|
121
140
|
sync_ambient_palette_with_flashlights(game_data)
|
|
122
141
|
|
|
123
142
|
buddies = [
|
|
@@ -10,9 +10,12 @@ from ..entities_constants import INTERNAL_WALL_HEALTH, STEEL_BEAM_HEALTH
|
|
|
10
10
|
from .constants import OUTER_WALL_HEALTH
|
|
11
11
|
from ..level_blueprints import choose_blueprint
|
|
12
12
|
from ..models import GameData
|
|
13
|
+
from ..rng import get_rng
|
|
13
14
|
|
|
14
15
|
__all__ = ["generate_level_from_blueprint"]
|
|
15
16
|
|
|
17
|
+
RNG = get_rng()
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
def _rect_for_cell(x_idx: int, y_idx: int, cell_size: int) -> pygame.Rect:
|
|
18
21
|
return pygame.Rect(
|
|
@@ -94,6 +97,10 @@ def generate_level_from_blueprint(
|
|
|
94
97
|
player_cells: list[pygame.Rect] = []
|
|
95
98
|
car_cells: list[pygame.Rect] = []
|
|
96
99
|
zombie_cells: list[pygame.Rect] = []
|
|
100
|
+
interior_min_x = 2
|
|
101
|
+
interior_max_x = stage.grid_cols - 3
|
|
102
|
+
interior_min_y = 2
|
|
103
|
+
interior_max_y = stage.grid_rows - 3
|
|
97
104
|
bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] = {}
|
|
98
105
|
palette = get_environment_palette(game_data.state.ambient_palette_key)
|
|
99
106
|
|
|
@@ -214,11 +221,25 @@ def generate_level_from_blueprint(
|
|
|
214
221
|
game_data.layout.walkable_cells = walkable_cells
|
|
215
222
|
game_data.layout.outer_wall_cells = outer_wall_cells
|
|
216
223
|
game_data.layout.wall_cells = wall_cells
|
|
217
|
-
|
|
224
|
+
fall_spawn_cells = _expand_zone_cells(
|
|
218
225
|
stage.fall_spawn_zones,
|
|
219
226
|
grid_cols=stage.grid_cols,
|
|
220
227
|
grid_rows=stage.grid_rows,
|
|
221
228
|
)
|
|
229
|
+
floor_ratio = max(0.0, min(1.0, stage.fall_spawn_floor_ratio))
|
|
230
|
+
if floor_ratio > 0.0 and interior_min_x <= interior_max_x:
|
|
231
|
+
for y in range(interior_min_y, interior_max_y + 1):
|
|
232
|
+
for x in range(interior_min_x, interior_max_x + 1):
|
|
233
|
+
if RNG.random() < floor_ratio:
|
|
234
|
+
fall_spawn_cells.add((x, y))
|
|
235
|
+
if not fall_spawn_cells:
|
|
236
|
+
fall_spawn_cells.add(
|
|
237
|
+
(
|
|
238
|
+
RNG.randint(interior_min_x, interior_max_x),
|
|
239
|
+
RNG.randint(interior_min_y, interior_max_y),
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
game_data.layout.fall_spawn_cells = fall_spawn_cells
|
|
222
243
|
game_data.layout.bevel_corners = bevel_corners
|
|
223
244
|
|
|
224
245
|
return {
|
|
@@ -17,6 +17,10 @@ from ..entities_constants import (
|
|
|
17
17
|
ZOMBIE_SEPARATION_DISTANCE,
|
|
18
18
|
ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE,
|
|
19
19
|
)
|
|
20
|
+
from ..gameplay_constants import (
|
|
21
|
+
SHOES_SPEED_MULTIPLIER_ONE,
|
|
22
|
+
SHOES_SPEED_MULTIPLIER_TWO,
|
|
23
|
+
)
|
|
20
24
|
from ..models import GameData
|
|
21
25
|
from ..world_grid import WallIndex, apply_tile_edge_nudge, walls_for_radius
|
|
22
26
|
from .constants import MAX_ZOMBIES
|
|
@@ -29,6 +33,7 @@ def process_player_input(
|
|
|
29
33
|
keys: Sequence[bool],
|
|
30
34
|
player: Player,
|
|
31
35
|
car: Car | None,
|
|
36
|
+
shoes_count: int = 0,
|
|
32
37
|
pad_input: tuple[float, float] = (0.0, 0.0),
|
|
33
38
|
) -> tuple[float, float, float, float]:
|
|
34
39
|
"""Process keyboard input and return movement deltas."""
|
|
@@ -55,7 +60,7 @@ def process_player_input(
|
|
|
55
60
|
(dy_input / move_len) * target_speed,
|
|
56
61
|
)
|
|
57
62
|
elif not player.in_car:
|
|
58
|
-
target_speed = PLAYER_SPEED
|
|
63
|
+
target_speed = PLAYER_SPEED * _shoes_speed_multiplier(shoes_count)
|
|
59
64
|
move_len = math.hypot(dx_input, dy_input)
|
|
60
65
|
if move_len > 0:
|
|
61
66
|
player_dx, player_dy = (
|
|
@@ -66,6 +71,15 @@ def process_player_input(
|
|
|
66
71
|
return player_dx, player_dy, car_dx, car_dy
|
|
67
72
|
|
|
68
73
|
|
|
74
|
+
def _shoes_speed_multiplier(shoes_count: int) -> float:
|
|
75
|
+
count = max(0, int(shoes_count))
|
|
76
|
+
if count >= 2:
|
|
77
|
+
return SHOES_SPEED_MULTIPLIER_TWO
|
|
78
|
+
if count == 1:
|
|
79
|
+
return SHOES_SPEED_MULTIPLIER_ONE
|
|
80
|
+
return 1.0
|
|
81
|
+
|
|
82
|
+
|
|
69
83
|
def update_entities(
|
|
70
84
|
game_data: GameData,
|
|
71
85
|
player_dx: float,
|
|
@@ -9,6 +9,7 @@ from ..entities import (
|
|
|
9
9
|
Flashlight,
|
|
10
10
|
FuelCan,
|
|
11
11
|
Player,
|
|
12
|
+
Shoes,
|
|
12
13
|
Survivor,
|
|
13
14
|
Zombie,
|
|
14
15
|
random_position_outside_building,
|
|
@@ -21,10 +22,17 @@ from ..entities_constants import (
|
|
|
21
22
|
ZOMBIE_AGING_DURATION_FRAMES,
|
|
22
23
|
ZOMBIE_SPEED,
|
|
23
24
|
)
|
|
24
|
-
from ..gameplay_constants import
|
|
25
|
+
from ..gameplay_constants import (
|
|
26
|
+
DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
27
|
+
DEFAULT_SHOES_SPAWN_COUNT,
|
|
28
|
+
)
|
|
25
29
|
from ..level_constants import DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, DEFAULT_TILE_SIZE
|
|
26
30
|
from ..models import DustRing, FallingZombie, GameData, Stage
|
|
27
|
-
from ..render_constants import
|
|
31
|
+
from ..render_constants import (
|
|
32
|
+
FLASHLIGHT_FOG_SCALE_ONE,
|
|
33
|
+
FLASHLIGHT_FOG_SCALE_TWO,
|
|
34
|
+
FOG_RADIUS_SCALE,
|
|
35
|
+
)
|
|
28
36
|
from ..rng import get_rng
|
|
29
37
|
from .constants import (
|
|
30
38
|
MAX_ZOMBIES,
|
|
@@ -46,6 +54,7 @@ __all__ = [
|
|
|
46
54
|
"place_new_car",
|
|
47
55
|
"place_fuel_can",
|
|
48
56
|
"place_flashlights",
|
|
57
|
+
"place_shoes",
|
|
49
58
|
"place_buddies",
|
|
50
59
|
"spawn_survivors",
|
|
51
60
|
"setup_player_and_cars",
|
|
@@ -94,7 +103,12 @@ def _pick_zombie_variant(stage: Stage | None) -> tuple[bool, bool]:
|
|
|
94
103
|
|
|
95
104
|
def _fov_radius_for_flashlights(flashlight_count: int) -> float:
|
|
96
105
|
count = max(0, int(flashlight_count))
|
|
97
|
-
|
|
106
|
+
if count <= 0:
|
|
107
|
+
scale = FOG_RADIUS_SCALE
|
|
108
|
+
elif count == 1:
|
|
109
|
+
scale = FLASHLIGHT_FOG_SCALE_ONE
|
|
110
|
+
else:
|
|
111
|
+
scale = FLASHLIGHT_FOG_SCALE_TWO
|
|
98
112
|
return FOV_RADIUS * scale
|
|
99
113
|
|
|
100
114
|
|
|
@@ -301,6 +315,7 @@ def place_fuel_can(
|
|
|
301
315
|
player: Player,
|
|
302
316
|
*,
|
|
303
317
|
cars: Sequence[Car] | None = None,
|
|
318
|
+
reserved_centers: set[tuple[int, int]] | None = None,
|
|
304
319
|
count: int = 1,
|
|
305
320
|
) -> FuelCan | None:
|
|
306
321
|
"""Pick a spawn spot for the fuel can away from the player (and car if given)."""
|
|
@@ -314,6 +329,8 @@ def place_fuel_can(
|
|
|
314
329
|
|
|
315
330
|
for _ in range(200):
|
|
316
331
|
cell = RNG.choice(walkable_cells)
|
|
332
|
+
if reserved_centers and cell.center in reserved_centers:
|
|
333
|
+
continue
|
|
317
334
|
dx = cell.centerx - player.x
|
|
318
335
|
dy = cell.centery - player.y
|
|
319
336
|
if dx * dx + dy * dy < min_player_dist_sq:
|
|
@@ -339,6 +356,7 @@ def _place_flashlight(
|
|
|
339
356
|
player: Player,
|
|
340
357
|
*,
|
|
341
358
|
cars: Sequence[Car] | None = None,
|
|
359
|
+
reserved_centers: set[tuple[int, int]] | None = None,
|
|
342
360
|
) -> Flashlight | None:
|
|
343
361
|
"""Pick a spawn spot for the flashlight away from the player (and car if given)."""
|
|
344
362
|
if not walkable_cells:
|
|
@@ -351,6 +369,8 @@ def _place_flashlight(
|
|
|
351
369
|
|
|
352
370
|
for _ in range(200):
|
|
353
371
|
cell = RNG.choice(walkable_cells)
|
|
372
|
+
if reserved_centers and cell.center in reserved_centers:
|
|
373
|
+
continue
|
|
354
374
|
dx = cell.centerx - player.x
|
|
355
375
|
dy = cell.centery - player.y
|
|
356
376
|
if dx * dx + dy * dy < min_player_dist_sq:
|
|
@@ -374,6 +394,7 @@ def place_flashlights(
|
|
|
374
394
|
player: Player,
|
|
375
395
|
*,
|
|
376
396
|
cars: Sequence[Car] | None = None,
|
|
397
|
+
reserved_centers: set[tuple[int, int]] | None = None,
|
|
377
398
|
count: int = DEFAULT_FLASHLIGHT_SPAWN_COUNT,
|
|
378
399
|
) -> list[Flashlight]:
|
|
379
400
|
"""Spawn multiple flashlights using the single-place helper to spread them out."""
|
|
@@ -382,7 +403,9 @@ def place_flashlights(
|
|
|
382
403
|
max_attempts = max(200, count * 80)
|
|
383
404
|
while len(placed) < count and attempts < max_attempts:
|
|
384
405
|
attempts += 1
|
|
385
|
-
fl = _place_flashlight(
|
|
406
|
+
fl = _place_flashlight(
|
|
407
|
+
walkable_cells, player, cars=cars, reserved_centers=reserved_centers
|
|
408
|
+
)
|
|
386
409
|
if not fl:
|
|
387
410
|
break
|
|
388
411
|
# Avoid clustering too tightly
|
|
@@ -397,6 +420,74 @@ def place_flashlights(
|
|
|
397
420
|
return placed
|
|
398
421
|
|
|
399
422
|
|
|
423
|
+
def _place_shoes(
|
|
424
|
+
walkable_cells: list[pygame.Rect],
|
|
425
|
+
player: Player,
|
|
426
|
+
*,
|
|
427
|
+
cars: Sequence[Car] | None = None,
|
|
428
|
+
reserved_centers: set[tuple[int, int]] | None = None,
|
|
429
|
+
) -> Shoes | None:
|
|
430
|
+
"""Pick a spawn spot for the shoes away from the player (and car if given)."""
|
|
431
|
+
if not walkable_cells:
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
min_player_dist = 240
|
|
435
|
+
min_car_dist = 200
|
|
436
|
+
min_player_dist_sq = min_player_dist * min_player_dist
|
|
437
|
+
min_car_dist_sq = min_car_dist * min_car_dist
|
|
438
|
+
|
|
439
|
+
for _ in range(200):
|
|
440
|
+
cell = RNG.choice(walkable_cells)
|
|
441
|
+
if reserved_centers and cell.center in reserved_centers:
|
|
442
|
+
continue
|
|
443
|
+
dx = cell.centerx - player.x
|
|
444
|
+
dy = cell.centery - player.y
|
|
445
|
+
if dx * dx + dy * dy < min_player_dist_sq:
|
|
446
|
+
continue
|
|
447
|
+
if cars:
|
|
448
|
+
if any(
|
|
449
|
+
(cell.centerx - parked.rect.centerx) ** 2
|
|
450
|
+
+ (cell.centery - parked.rect.centery) ** 2
|
|
451
|
+
< min_car_dist_sq
|
|
452
|
+
for parked in cars
|
|
453
|
+
):
|
|
454
|
+
continue
|
|
455
|
+
return Shoes(cell.centerx, cell.centery)
|
|
456
|
+
|
|
457
|
+
cell = RNG.choice(walkable_cells)
|
|
458
|
+
return Shoes(cell.centerx, cell.centery)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def place_shoes(
|
|
462
|
+
walkable_cells: list[pygame.Rect],
|
|
463
|
+
player: Player,
|
|
464
|
+
*,
|
|
465
|
+
cars: Sequence[Car] | None = None,
|
|
466
|
+
reserved_centers: set[tuple[int, int]] | None = None,
|
|
467
|
+
count: int = DEFAULT_SHOES_SPAWN_COUNT,
|
|
468
|
+
) -> list[Shoes]:
|
|
469
|
+
"""Spawn multiple shoes using the single-place helper to spread them out."""
|
|
470
|
+
placed: list[Shoes] = []
|
|
471
|
+
attempts = 0
|
|
472
|
+
max_attempts = max(200, count * 80)
|
|
473
|
+
while len(placed) < count and attempts < max_attempts:
|
|
474
|
+
attempts += 1
|
|
475
|
+
shoes = _place_shoes(
|
|
476
|
+
walkable_cells, player, cars=cars, reserved_centers=reserved_centers
|
|
477
|
+
)
|
|
478
|
+
if not shoes:
|
|
479
|
+
break
|
|
480
|
+
if any(
|
|
481
|
+
(other.rect.centerx - shoes.rect.centerx) ** 2
|
|
482
|
+
+ (other.rect.centery - shoes.rect.centery) ** 2
|
|
483
|
+
< 120 * 120
|
|
484
|
+
for other in placed
|
|
485
|
+
):
|
|
486
|
+
continue
|
|
487
|
+
placed.append(shoes)
|
|
488
|
+
return placed
|
|
489
|
+
|
|
490
|
+
|
|
400
491
|
def place_buddies(
|
|
401
492
|
walkable_cells: list[pygame.Rect],
|
|
402
493
|
player: Player,
|
|
@@ -31,6 +31,7 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
31
31
|
elapsed_play_ms=0,
|
|
32
32
|
has_fuel=starts_with_fuel,
|
|
33
33
|
flashlight_count=initial_flashlights,
|
|
34
|
+
shoes_count=0,
|
|
34
35
|
ambient_palette_key=initial_palette_key,
|
|
35
36
|
hint_expires_at=0,
|
|
36
37
|
hint_target_type=None,
|
|
@@ -100,6 +101,7 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
100
101
|
level_height=level_height,
|
|
101
102
|
fuel=None,
|
|
102
103
|
flashlights=[],
|
|
104
|
+
shoes=[],
|
|
103
105
|
)
|
|
104
106
|
|
|
105
107
|
|
|
@@ -13,6 +13,11 @@ SURVIVOR_SPAWN_RATE = 0.07
|
|
|
13
13
|
# --- Flashlight settings ---
|
|
14
14
|
DEFAULT_FLASHLIGHT_SPAWN_COUNT = 2
|
|
15
15
|
|
|
16
|
+
# --- Shoes settings ---
|
|
17
|
+
DEFAULT_SHOES_SPAWN_COUNT = 0
|
|
18
|
+
SHOES_SPEED_MULTIPLIER_ONE = 1.176
|
|
19
|
+
SHOES_SPEED_MULTIPLIER_TWO = 1.25
|
|
20
|
+
|
|
16
21
|
# --- Zombie settings ---
|
|
17
22
|
ZOMBIE_SPAWN_DELAY_MS = 4000
|
|
18
23
|
|
|
@@ -25,6 +30,9 @@ __all__ = [
|
|
|
25
30
|
"SURVIVAL_FAKE_CLOCK_RATIO",
|
|
26
31
|
"SURVIVOR_SPAWN_RATE",
|
|
27
32
|
"DEFAULT_FLASHLIGHT_SPAWN_COUNT",
|
|
33
|
+
"DEFAULT_SHOES_SPAWN_COUNT",
|
|
34
|
+
"SHOES_SPEED_MULTIPLIER_ONE",
|
|
35
|
+
"SHOES_SPEED_MULTIPLIER_TWO",
|
|
28
36
|
"ZOMBIE_SPAWN_DELAY_MS",
|
|
29
37
|
"CAR_HINT_DELAY_MS_DEFAULT",
|
|
30
38
|
]
|
|
@@ -84,11 +84,17 @@ def _place_exits(grid: list[list[str]], exits_per_side: int) -> None:
|
|
|
84
84
|
grid[y][x] = "E"
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
def _place_walls_default(
|
|
87
|
+
def _place_walls_default(
|
|
88
|
+
grid: list[list[str]],
|
|
89
|
+
*,
|
|
90
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
91
|
+
) -> None:
|
|
88
92
|
cols, rows = len(grid[0]), len(grid)
|
|
89
93
|
rng = RNG.randint
|
|
90
|
-
# Avoid placing walls adjacent to exits
|
|
94
|
+
# Avoid placing walls adjacent to exits and on reserved cells.
|
|
91
95
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
96
|
+
if forbidden_cells:
|
|
97
|
+
forbidden |= forbidden_cells
|
|
92
98
|
|
|
93
99
|
for _ in range(NUM_WALL_LINES):
|
|
94
100
|
length = rng(WALL_MIN_LEN, WALL_MAX_LEN)
|
|
@@ -111,12 +117,20 @@ def _place_walls_default(grid: list[list[str]]) -> None:
|
|
|
111
117
|
grid[y + i][x] = "1"
|
|
112
118
|
|
|
113
119
|
|
|
114
|
-
def _place_walls_empty(
|
|
120
|
+
def _place_walls_empty(
|
|
121
|
+
grid: list[list[str]],
|
|
122
|
+
*,
|
|
123
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
124
|
+
) -> None:
|
|
115
125
|
"""Place no internal walls (open floor plan)."""
|
|
116
|
-
|
|
126
|
+
_ = (grid, forbidden_cells)
|
|
117
127
|
|
|
118
128
|
|
|
119
|
-
def _place_walls_grid_wire(
|
|
129
|
+
def _place_walls_grid_wire(
|
|
130
|
+
grid: list[list[str]],
|
|
131
|
+
*,
|
|
132
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
133
|
+
) -> None:
|
|
120
134
|
"""
|
|
121
135
|
Place walls using a 2-pass approach with independent layers.
|
|
122
136
|
Vertical and horizontal walls are generated on separate grids to ensure
|
|
@@ -127,6 +141,8 @@ def _place_walls_grid_wire(grid: list[list[str]]) -> None:
|
|
|
127
141
|
cols, rows = len(grid[0]), len(grid)
|
|
128
142
|
rng = RNG.randint
|
|
129
143
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
144
|
+
if forbidden_cells:
|
|
145
|
+
forbidden |= forbidden_cells
|
|
130
146
|
|
|
131
147
|
# Temporary grids for independent generation
|
|
132
148
|
# They only track the internal walls ("1").
|
|
@@ -202,10 +218,16 @@ def _place_walls_grid_wire(grid: list[list[str]]) -> None:
|
|
|
202
218
|
grid[y][x] = "1"
|
|
203
219
|
|
|
204
220
|
|
|
205
|
-
def _place_walls_sparse(
|
|
221
|
+
def _place_walls_sparse(
|
|
222
|
+
grid: list[list[str]],
|
|
223
|
+
*,
|
|
224
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
225
|
+
) -> None:
|
|
206
226
|
"""Place isolated wall tiles at a low density, avoiding adjacency."""
|
|
207
227
|
cols, rows = len(grid[0]), len(grid)
|
|
208
228
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
229
|
+
if forbidden_cells:
|
|
230
|
+
forbidden |= forbidden_cells
|
|
209
231
|
for y in range(2, rows - 2):
|
|
210
232
|
for x in range(2, cols - 2):
|
|
211
233
|
if (x, y) in forbidden:
|
|
@@ -237,11 +259,16 @@ WALL_ALGORITHMS = {
|
|
|
237
259
|
|
|
238
260
|
|
|
239
261
|
def _place_steel_beams(
|
|
240
|
-
grid: list[list[str]],
|
|
262
|
+
grid: list[list[str]],
|
|
263
|
+
*,
|
|
264
|
+
chance: float = STEEL_BEAM_CHANCE,
|
|
265
|
+
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
241
266
|
) -> set[tuple[int, int]]:
|
|
242
267
|
"""Pick individual cells for steel beams, avoiding exits and their neighbors."""
|
|
243
268
|
cols, rows = len(grid[0]), len(grid)
|
|
244
269
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
270
|
+
if forbidden_cells:
|
|
271
|
+
forbidden |= forbidden_cells
|
|
245
272
|
beams: set[tuple[int, int]] = set()
|
|
246
273
|
for y in range(2, rows - 2):
|
|
247
274
|
for x in range(2, cols - 2):
|
|
@@ -257,7 +284,6 @@ def _place_steel_beams(
|
|
|
257
284
|
def _pick_empty_cell(
|
|
258
285
|
grid: list[list[str]],
|
|
259
286
|
margin: int,
|
|
260
|
-
forbidden: set[tuple[int, int]],
|
|
261
287
|
) -> tuple[int, int]:
|
|
262
288
|
cols, rows = len(grid[0]), len(grid)
|
|
263
289
|
attempts = 0
|
|
@@ -265,12 +291,12 @@ def _pick_empty_cell(
|
|
|
265
291
|
attempts += 1
|
|
266
292
|
x = RNG.randint(margin, cols - margin - 1)
|
|
267
293
|
y = RNG.randint(margin, rows - margin - 1)
|
|
268
|
-
if grid[y][x] == "."
|
|
294
|
+
if grid[y][x] == ".":
|
|
269
295
|
return x, y
|
|
270
296
|
# Fallback: scan for any acceptable cell
|
|
271
297
|
for y in range(margin, rows - margin):
|
|
272
298
|
for x in range(margin, cols - margin):
|
|
273
|
-
if grid[y][x] == "."
|
|
299
|
+
if grid[y][x] == ".":
|
|
274
300
|
return x, y
|
|
275
301
|
return cols // 2, rows // 2
|
|
276
302
|
|
|
@@ -281,7 +307,20 @@ def _generate_random_blueprint(
|
|
|
281
307
|
grid = _init_grid(cols, rows)
|
|
282
308
|
_place_exits(grid, EXITS_PER_SIDE)
|
|
283
309
|
|
|
284
|
-
#
|
|
310
|
+
# Spawns: player, car, zombies
|
|
311
|
+
reserved_cells: set[tuple[int, int]] = set()
|
|
312
|
+
px, py = _pick_empty_cell(grid, SPAWN_MARGIN)
|
|
313
|
+
grid[py][px] = "P"
|
|
314
|
+
reserved_cells.add((px, py))
|
|
315
|
+
cx, cy = _pick_empty_cell(grid, SPAWN_MARGIN)
|
|
316
|
+
grid[cy][cx] = "C"
|
|
317
|
+
reserved_cells.add((cx, cy))
|
|
318
|
+
for _ in range(SPAWN_ZOMBIES):
|
|
319
|
+
zx, zy = _pick_empty_cell(grid, SPAWN_MARGIN)
|
|
320
|
+
grid[zy][zx] = "Z"
|
|
321
|
+
reserved_cells.add((zx, zy))
|
|
322
|
+
|
|
323
|
+
# Select and run the wall placement algorithm (after reserving spawns)
|
|
285
324
|
if wall_algo not in WALL_ALGORITHMS:
|
|
286
325
|
print(
|
|
287
326
|
f"WARNING: Unknown wall algorithm '{wall_algo}'. Falling back to 'default'."
|
|
@@ -289,18 +328,11 @@ def _generate_random_blueprint(
|
|
|
289
328
|
wall_algo = "default"
|
|
290
329
|
|
|
291
330
|
algo_func = WALL_ALGORITHMS[wall_algo]
|
|
292
|
-
algo_func(grid)
|
|
293
|
-
|
|
294
|
-
steel_beams = _place_steel_beams(grid, chance=steel_chance)
|
|
331
|
+
algo_func(grid, forbidden_cells=reserved_cells)
|
|
295
332
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
cx, cy = _pick_empty_cell(grid, SPAWN_MARGIN, forbidden=steel_beams)
|
|
300
|
-
grid[cy][cx] = "C"
|
|
301
|
-
for _ in range(SPAWN_ZOMBIES):
|
|
302
|
-
zx, zy = _pick_empty_cell(grid, SPAWN_MARGIN, forbidden=steel_beams)
|
|
303
|
-
grid[zy][zx] = "Z"
|
|
333
|
+
steel_beams = _place_steel_beams(
|
|
334
|
+
grid, chance=steel_chance, forbidden_cells=reserved_cells
|
|
335
|
+
)
|
|
304
336
|
|
|
305
337
|
blueprint_rows = ["".join(row) for row in grid]
|
|
306
338
|
return {"grid": blueprint_rows, "steel_cells": steel_beams}
|
|
@@ -118,7 +118,15 @@
|
|
|
118
118
|
},
|
|
119
119
|
"stage13": {
|
|
120
120
|
"name": "#13 Rescue Buddy 3",
|
|
121
|
-
"description": "Rescue your buddy.
|
|
121
|
+
"description": "Rescue your buddy. Falling zombies."
|
|
122
|
+
},
|
|
123
|
+
"stage14": {
|
|
124
|
+
"name": "#14 Falling Factory",
|
|
125
|
+
"description": "A factory with collapsed upper floors. Break through obstacles."
|
|
126
|
+
},
|
|
127
|
+
"stage15": {
|
|
128
|
+
"name": "#15 The Divide",
|
|
129
|
+
"description": "A central hazard splits the building. Cross with care."
|
|
122
130
|
}
|
|
123
131
|
},
|
|
124
132
|
"status": {
|
|
@@ -119,6 +119,14 @@
|
|
|
119
119
|
"stage13": {
|
|
120
120
|
"name": "#13 相棒を救え 3",
|
|
121
121
|
"description": "はぐれた相棒を救え。ゾンビの落下あり。"
|
|
122
|
+
},
|
|
123
|
+
"stage14": {
|
|
124
|
+
"name": "#14 崩落工場",
|
|
125
|
+
"description": "上階の床が崩れた工場。破壊しながら進め。"
|
|
126
|
+
},
|
|
127
|
+
"stage15": {
|
|
128
|
+
"name": "#15 分断ライン",
|
|
129
|
+
"description": "建物中央を分断する危険地帯。横断には注意。"
|
|
122
130
|
}
|
|
123
131
|
},
|
|
124
132
|
"status": {
|