zombie-escape 1.7.1__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zombie_escape/__about__.py +1 -1
- zombie_escape/entities.py +172 -65
- zombie_escape/gameplay/constants.py +0 -6
- zombie_escape/gameplay/layout.py +5 -0
- zombie_escape/gameplay/movement.py +43 -3
- zombie_escape/gameplay/spawn.py +38 -31
- zombie_escape/gameplay/state.py +2 -0
- zombie_escape/gameplay/survivors.py +46 -15
- zombie_escape/input_utils.py +167 -0
- zombie_escape/level_blueprints.py +28 -0
- zombie_escape/locales/ui.en.json +41 -9
- zombie_escape/locales/ui.ja.json +40 -8
- zombie_escape/localization.py +28 -0
- zombie_escape/models.py +2 -0
- zombie_escape/screens/game_over.py +4 -0
- zombie_escape/screens/gameplay.py +78 -17
- zombie_escape/screens/settings.py +124 -13
- zombie_escape/screens/title.py +111 -0
- zombie_escape/stage_constants.py +26 -1
- zombie_escape/zombie_escape.py +3 -0
- {zombie_escape-1.7.1.dist-info → zombie_escape-1.8.0.dist-info}/METADATA +4 -3
- zombie_escape-1.8.0.dist-info/RECORD +46 -0
- zombie_escape-1.7.1.dist-info/RECORD +0 -45
- {zombie_escape-1.7.1.dist-info → zombie_escape-1.8.0.dist-info}/WHEEL +0 -0
- {zombie_escape-1.7.1.dist-info → zombie_escape-1.8.0.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.7.1.dist-info → zombie_escape-1.8.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -16,15 +16,11 @@ from ..entities_constants import (
|
|
|
16
16
|
SURVIVOR_RADIUS,
|
|
17
17
|
ZOMBIE_RADIUS,
|
|
18
18
|
)
|
|
19
|
-
from .constants import
|
|
20
|
-
|
|
21
|
-
SURVIVOR_MESSAGE_DURATION_MS,
|
|
22
|
-
SURVIVOR_SPEED_PENALTY_PER_PASSENGER,
|
|
23
|
-
)
|
|
24
|
-
from ..localization import translate as tr
|
|
19
|
+
from .constants import SURVIVOR_MESSAGE_DURATION_MS, SURVIVOR_SPEED_PENALTY_PER_PASSENGER
|
|
20
|
+
from ..localization import translate as tr, translate_dict, translate_list
|
|
25
21
|
from ..models import GameData, ProgressState
|
|
26
22
|
from ..rng import get_rng
|
|
27
|
-
from ..entities import Survivor, spritecollideany_walls, WallIndex
|
|
23
|
+
from ..entities import Survivor, Zombie, spritecollideany_walls, WallIndex
|
|
28
24
|
from .spawn import _create_zombie
|
|
29
25
|
from .utils import find_nearby_offscreen_spawn_position, rect_visible_on_screen
|
|
30
26
|
|
|
@@ -51,6 +47,10 @@ def update_survivors(
|
|
|
51
47
|
wall_group,
|
|
52
48
|
wall_index=wall_index,
|
|
53
49
|
cell_size=game_data.cell_size,
|
|
50
|
+
wall_cells=game_data.layout.wall_cells,
|
|
51
|
+
bevel_corners=game_data.layout.bevel_corners,
|
|
52
|
+
grid_cols=game_data.stage.grid_cols,
|
|
53
|
+
grid_rows=game_data.stage.grid_rows,
|
|
54
54
|
level_width=game_data.level_width,
|
|
55
55
|
level_height=game_data.level_height,
|
|
56
56
|
)
|
|
@@ -150,11 +150,40 @@ def add_survivor_message(game_data: GameData, text: str) -> None:
|
|
|
150
150
|
game_data.state.survivor_messages.append({"text": text, "expires_at": expires})
|
|
151
151
|
|
|
152
152
|
|
|
153
|
-
def
|
|
154
|
-
|
|
153
|
+
def _normalize_legacy_conversion_lines(data: dict[str, Any]) -> list[str]:
|
|
154
|
+
numbered: list[tuple[int, str]] = []
|
|
155
|
+
others: list[tuple[str, str]] = []
|
|
156
|
+
for key, value in data.items():
|
|
157
|
+
if not value:
|
|
158
|
+
continue
|
|
159
|
+
text = str(value)
|
|
160
|
+
if isinstance(key, str) and key.startswith("line"):
|
|
161
|
+
suffix = key[4:]
|
|
162
|
+
if suffix.isdigit():
|
|
163
|
+
numbered.append((int(suffix), text))
|
|
164
|
+
continue
|
|
165
|
+
others.append((str(key), text))
|
|
166
|
+
numbered.sort(key=lambda item: item[0])
|
|
167
|
+
others.sort(key=lambda item: item[0])
|
|
168
|
+
return [text for _, text in numbered] + [text for _, text in others]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _get_survivor_conversion_messages(stage_id: str) -> list[str]:
|
|
172
|
+
key = f"stages.{stage_id}.survivor_conversion_messages"
|
|
173
|
+
raw = translate_list(key)
|
|
174
|
+
if raw:
|
|
175
|
+
return [str(item) for item in raw if item]
|
|
176
|
+
legacy = translate_dict(f"stages.{stage_id}.conversion_lines")
|
|
177
|
+
if legacy:
|
|
178
|
+
return _normalize_legacy_conversion_lines(legacy)
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def random_survivor_conversion_line(stage_id: str) -> str:
|
|
183
|
+
lines = _get_survivor_conversion_messages(stage_id)
|
|
184
|
+
if not lines:
|
|
155
185
|
return ""
|
|
156
|
-
|
|
157
|
-
return tr(key)
|
|
186
|
+
return RNG.choice(lines)
|
|
158
187
|
|
|
159
188
|
|
|
160
189
|
def cleanup_survivor_messages(state: ProgressState) -> None:
|
|
@@ -224,7 +253,7 @@ def handle_survivor_zombie_collisions(
|
|
|
224
253
|
min_x = survivor.rect.centerx - search_radius
|
|
225
254
|
max_x = survivor.rect.centerx + search_radius
|
|
226
255
|
start_idx = bisect_left(zombie_xs, min_x)
|
|
227
|
-
|
|
256
|
+
collided_zombie: Zombie | None = None
|
|
228
257
|
for idx in range(start_idx, len(zombies)):
|
|
229
258
|
zombie_x = zombie_xs[idx]
|
|
230
259
|
if zombie_x > max_x:
|
|
@@ -237,10 +266,10 @@ def handle_survivor_zombie_collisions(
|
|
|
237
266
|
continue
|
|
238
267
|
dx = zombie_x - survivor.rect.centerx
|
|
239
268
|
if dx * dx + dy * dy <= search_radius_sq:
|
|
240
|
-
|
|
269
|
+
collided_zombie = zombie
|
|
241
270
|
break
|
|
242
271
|
|
|
243
|
-
if
|
|
272
|
+
if collided_zombie is None:
|
|
244
273
|
continue
|
|
245
274
|
if not rect_visible_on_screen(camera, survivor.rect):
|
|
246
275
|
spawn_pos = find_nearby_offscreen_spawn_position(
|
|
@@ -250,13 +279,15 @@ def handle_survivor_zombie_collisions(
|
|
|
250
279
|
survivor.teleport(spawn_pos)
|
|
251
280
|
continue
|
|
252
281
|
survivor.kill()
|
|
253
|
-
line = random_survivor_conversion_line()
|
|
282
|
+
line = random_survivor_conversion_line(game_data.stage.id)
|
|
254
283
|
if line:
|
|
255
284
|
add_survivor_message(game_data, line)
|
|
256
285
|
new_zombie = _create_zombie(
|
|
257
286
|
config,
|
|
258
287
|
start_pos=survivor.rect.center,
|
|
259
288
|
stage=game_data.stage,
|
|
289
|
+
tracker=bool(getattr(collided_zombie, "tracker", False)),
|
|
290
|
+
wall_follower=bool(getattr(collided_zombie, "wall_follower", False)),
|
|
260
291
|
)
|
|
261
292
|
zombie_group.add(new_zombie)
|
|
262
293
|
game_data.groups.all_sprites.add(new_zombie, layer=1)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
import pygame
|
|
6
|
+
|
|
7
|
+
DEADZONE = 0.25
|
|
8
|
+
JOY_BUTTON_A = 0
|
|
9
|
+
JOY_BUTTON_BACK = 6
|
|
10
|
+
JOY_BUTTON_START = 7
|
|
11
|
+
JOY_BUTTON_RB = 5
|
|
12
|
+
|
|
13
|
+
CONTROLLER_AVAILABLE = hasattr(pygame, "controller")
|
|
14
|
+
CONTROLLER_DEVICE_ADDED = getattr(pygame, "CONTROLLERDEVICEADDED", None)
|
|
15
|
+
CONTROLLER_DEVICE_REMOVED = getattr(pygame, "CONTROLLERDEVICEREMOVED", None)
|
|
16
|
+
CONTROLLER_BUTTON_DOWN = getattr(pygame, "CONTROLLERBUTTONDOWN", None)
|
|
17
|
+
CONTROLLER_BUTTON_A = getattr(pygame, "CONTROLLER_BUTTON_A", None)
|
|
18
|
+
CONTROLLER_BUTTON_BACK = getattr(pygame, "CONTROLLER_BUTTON_BACK", None)
|
|
19
|
+
CONTROLLER_BUTTON_START = getattr(pygame, "CONTROLLER_BUTTON_START", None)
|
|
20
|
+
CONTROLLER_BUTTON_DPAD_UP = getattr(pygame, "CONTROLLER_BUTTON_DPAD_UP", None)
|
|
21
|
+
CONTROLLER_BUTTON_DPAD_DOWN = getattr(pygame, "CONTROLLER_BUTTON_DPAD_DOWN", None)
|
|
22
|
+
CONTROLLER_BUTTON_DPAD_LEFT = getattr(pygame, "CONTROLLER_BUTTON_DPAD_LEFT", None)
|
|
23
|
+
CONTROLLER_BUTTON_DPAD_RIGHT = getattr(pygame, "CONTROLLER_BUTTON_DPAD_RIGHT", None)
|
|
24
|
+
CONTROLLER_BUTTON_RB = getattr(pygame, "CONTROLLER_BUTTON_RIGHTSHOULDER", None)
|
|
25
|
+
CONTROLLER_AXIS_LEFTX = getattr(pygame, "CONTROLLER_AXIS_LEFTX", None)
|
|
26
|
+
CONTROLLER_AXIS_LEFTY = getattr(pygame, "CONTROLLER_AXIS_LEFTY", None)
|
|
27
|
+
CONTROLLER_AXIS_TRIGGERRIGHT = getattr(
|
|
28
|
+
pygame, "CONTROLLER_AXIS_TRIGGERRIGHT", None
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def init_first_controller() -> pygame.controller.Controller | None:
|
|
33
|
+
if not CONTROLLER_AVAILABLE:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
if pygame.controller.get_count() > 0:
|
|
37
|
+
controller = pygame.controller.Controller(0)
|
|
38
|
+
if not controller.get_init():
|
|
39
|
+
controller.init()
|
|
40
|
+
return controller
|
|
41
|
+
except pygame.error:
|
|
42
|
+
return None
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def init_first_joystick() -> pygame.joystick.Joystick | None:
|
|
47
|
+
try:
|
|
48
|
+
if pygame.joystick.get_count() > 0:
|
|
49
|
+
joystick = pygame.joystick.Joystick(0)
|
|
50
|
+
if not joystick.get_init():
|
|
51
|
+
joystick.init()
|
|
52
|
+
return joystick
|
|
53
|
+
except pygame.error:
|
|
54
|
+
return None
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_confirm_event(event: pygame.event.Event) -> bool:
|
|
59
|
+
if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
|
|
60
|
+
return CONTROLLER_BUTTON_A is not None and event.button == CONTROLLER_BUTTON_A
|
|
61
|
+
if event.type == pygame.JOYBUTTONDOWN:
|
|
62
|
+
return event.button == JOY_BUTTON_A
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_start_event(event: pygame.event.Event) -> bool:
|
|
67
|
+
if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
|
|
68
|
+
return (
|
|
69
|
+
CONTROLLER_BUTTON_START is not None
|
|
70
|
+
and event.button == CONTROLLER_BUTTON_START
|
|
71
|
+
)
|
|
72
|
+
if event.type == pygame.JOYBUTTONDOWN:
|
|
73
|
+
return event.button == JOY_BUTTON_START
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_select_event(event: pygame.event.Event) -> bool:
|
|
78
|
+
if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
|
|
79
|
+
return (
|
|
80
|
+
CONTROLLER_BUTTON_BACK is not None
|
|
81
|
+
and event.button == CONTROLLER_BUTTON_BACK
|
|
82
|
+
)
|
|
83
|
+
if event.type == pygame.JOYBUTTONDOWN:
|
|
84
|
+
return event.button == JOY_BUTTON_BACK
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_gamepad_move(
|
|
89
|
+
controller: pygame.controller.Controller | None,
|
|
90
|
+
joystick: pygame.joystick.Joystick | None,
|
|
91
|
+
*,
|
|
92
|
+
deadzone: float = DEADZONE,
|
|
93
|
+
) -> Tuple[float, float]:
|
|
94
|
+
x = 0.0
|
|
95
|
+
y = 0.0
|
|
96
|
+
if controller and controller.get_init():
|
|
97
|
+
if CONTROLLER_AXIS_LEFTX is None or CONTROLLER_AXIS_LEFTY is None:
|
|
98
|
+
return 0.0, 0.0
|
|
99
|
+
x = float(controller.get_axis(CONTROLLER_AXIS_LEFTX))
|
|
100
|
+
y = float(controller.get_axis(CONTROLLER_AXIS_LEFTY))
|
|
101
|
+
if abs(x) < deadzone:
|
|
102
|
+
x = 0.0
|
|
103
|
+
if abs(y) < deadzone:
|
|
104
|
+
y = 0.0
|
|
105
|
+
if (
|
|
106
|
+
CONTROLLER_BUTTON_DPAD_LEFT is not None
|
|
107
|
+
and controller.get_button(CONTROLLER_BUTTON_DPAD_LEFT)
|
|
108
|
+
):
|
|
109
|
+
x = -1.0
|
|
110
|
+
elif (
|
|
111
|
+
CONTROLLER_BUTTON_DPAD_RIGHT is not None
|
|
112
|
+
and controller.get_button(CONTROLLER_BUTTON_DPAD_RIGHT)
|
|
113
|
+
):
|
|
114
|
+
x = 1.0
|
|
115
|
+
if (
|
|
116
|
+
CONTROLLER_BUTTON_DPAD_UP is not None
|
|
117
|
+
and controller.get_button(CONTROLLER_BUTTON_DPAD_UP)
|
|
118
|
+
):
|
|
119
|
+
y = -1.0
|
|
120
|
+
elif (
|
|
121
|
+
CONTROLLER_BUTTON_DPAD_DOWN is not None
|
|
122
|
+
and controller.get_button(CONTROLLER_BUTTON_DPAD_DOWN)
|
|
123
|
+
):
|
|
124
|
+
y = 1.0
|
|
125
|
+
return x, y
|
|
126
|
+
|
|
127
|
+
if joystick and joystick.get_init():
|
|
128
|
+
if joystick.get_numaxes() >= 2:
|
|
129
|
+
x = float(joystick.get_axis(0))
|
|
130
|
+
y = float(joystick.get_axis(1))
|
|
131
|
+
if abs(x) < deadzone:
|
|
132
|
+
x = 0.0
|
|
133
|
+
if abs(y) < deadzone:
|
|
134
|
+
y = 0.0
|
|
135
|
+
if joystick.get_numhats() > 0:
|
|
136
|
+
hat_x, hat_y = joystick.get_hat(0)
|
|
137
|
+
if hat_x:
|
|
138
|
+
x = float(hat_x)
|
|
139
|
+
if hat_y:
|
|
140
|
+
y = float(-hat_y)
|
|
141
|
+
return x, y
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_accel_active(
|
|
145
|
+
keys: pygame.key.ScancodeWrapper,
|
|
146
|
+
controller: pygame.controller.Controller | None,
|
|
147
|
+
joystick: pygame.joystick.Joystick | None,
|
|
148
|
+
) -> bool:
|
|
149
|
+
if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
|
|
150
|
+
return True
|
|
151
|
+
if controller and controller.get_init():
|
|
152
|
+
if (
|
|
153
|
+
CONTROLLER_BUTTON_RB is not None
|
|
154
|
+
and controller.get_button(CONTROLLER_BUTTON_RB)
|
|
155
|
+
):
|
|
156
|
+
return True
|
|
157
|
+
if CONTROLLER_AXIS_TRIGGERRIGHT is not None:
|
|
158
|
+
if controller.get_axis(CONTROLLER_AXIS_TRIGGERRIGHT) > DEADZONE:
|
|
159
|
+
return True
|
|
160
|
+
if joystick and joystick.get_init():
|
|
161
|
+
if joystick.get_numbuttons() > JOY_BUTTON_RB:
|
|
162
|
+
if joystick.get_button(JOY_BUTTON_RB):
|
|
163
|
+
return True
|
|
164
|
+
if joystick.get_numaxes() > 5:
|
|
165
|
+
if joystick.get_axis(5) > DEADZONE:
|
|
166
|
+
return True
|
|
167
|
+
return False
|
|
@@ -6,6 +6,7 @@ EXITS_PER_SIDE = 1 # currently fixed to 1 per side (can be tuned)
|
|
|
6
6
|
NUM_WALL_LINES = 80 # reduced density (roughly 1/5 of previous 450)
|
|
7
7
|
WALL_MIN_LEN = 3
|
|
8
8
|
WALL_MAX_LEN = 10
|
|
9
|
+
SPARSE_WALL_DENSITY = 0.10
|
|
9
10
|
SPAWN_MARGIN = 3 # keep spawns away from walls/edges
|
|
10
11
|
SPAWN_ZOMBIES = 3
|
|
11
12
|
|
|
@@ -201,10 +202,37 @@ def _place_walls_grid_wire(grid: list[list[str]]) -> None:
|
|
|
201
202
|
grid[y][x] = "1"
|
|
202
203
|
|
|
203
204
|
|
|
205
|
+
def _place_walls_sparse(grid: list[list[str]]) -> None:
|
|
206
|
+
"""Place isolated wall tiles at a low density, avoiding adjacency."""
|
|
207
|
+
cols, rows = len(grid[0]), len(grid)
|
|
208
|
+
forbidden = _collect_exit_adjacent_cells(grid)
|
|
209
|
+
for y in range(2, rows - 2):
|
|
210
|
+
for x in range(2, cols - 2):
|
|
211
|
+
if (x, y) in forbidden:
|
|
212
|
+
continue
|
|
213
|
+
if grid[y][x] != ".":
|
|
214
|
+
continue
|
|
215
|
+
if RNG.random() >= SPARSE_WALL_DENSITY:
|
|
216
|
+
continue
|
|
217
|
+
if (
|
|
218
|
+
grid[y - 1][x] == "1"
|
|
219
|
+
or grid[y + 1][x] == "1"
|
|
220
|
+
or grid[y][x - 1] == "1"
|
|
221
|
+
or grid[y][x + 1] == "1"
|
|
222
|
+
or grid[y - 1][x - 1] == "1"
|
|
223
|
+
or grid[y - 1][x + 1] == "1"
|
|
224
|
+
or grid[y + 1][x - 1] == "1"
|
|
225
|
+
or grid[y + 1][x + 1] == "1"
|
|
226
|
+
):
|
|
227
|
+
continue
|
|
228
|
+
grid[y][x] = "1"
|
|
229
|
+
|
|
230
|
+
|
|
204
231
|
WALL_ALGORITHMS = {
|
|
205
232
|
"default": _place_walls_default,
|
|
206
233
|
"empty": _place_walls_empty,
|
|
207
234
|
"grid_wire": _place_walls_grid_wire,
|
|
235
|
+
"sparse": _place_walls_sparse,
|
|
208
236
|
}
|
|
209
237
|
|
|
210
238
|
|
zombie_escape/locales/ui.en.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"hints": {
|
|
29
29
|
"navigate": "Up/Down: choose an option",
|
|
30
30
|
"page_switch": "Left/Right: switch stage groups",
|
|
31
|
-
"confirm": "Enter/Space: activate selection"
|
|
31
|
+
"confirm": "Enter/Space/South: activate selection"
|
|
32
32
|
},
|
|
33
33
|
"option_help": {
|
|
34
34
|
"settings": "Open Settings to adjust assists and localization.",
|
|
@@ -56,7 +56,12 @@
|
|
|
56
56
|
"line1": "It hurts!",
|
|
57
57
|
"line2": "I can't hold on!",
|
|
58
58
|
"line3": "Stay back!"
|
|
59
|
-
}
|
|
59
|
+
},
|
|
60
|
+
"survivor_conversion_messages": [
|
|
61
|
+
"It hurts!",
|
|
62
|
+
"I can't hold on!",
|
|
63
|
+
"Stay back!"
|
|
64
|
+
]
|
|
60
65
|
},
|
|
61
66
|
"stage5": {
|
|
62
67
|
"name": "#5 Survive Until Dawn",
|
|
@@ -75,8 +80,33 @@
|
|
|
75
80
|
"description": "Narrow corridors. Get surrounded, and there’s no escape."
|
|
76
81
|
},
|
|
77
82
|
"stage9": {
|
|
78
|
-
"name": "# Evacuate Survivors 2",
|
|
79
|
-
"description": "Evacuate like stage 4, but narrow corridors. Requires fuel."
|
|
83
|
+
"name": "#9 Evacuate Survivors 2",
|
|
84
|
+
"description": "Evacuate like stage 4, but narrow corridors. Requires fuel.",
|
|
85
|
+
"survivor_conversion_messages": [
|
|
86
|
+
"It hurts!",
|
|
87
|
+
"I can't hold on!",
|
|
88
|
+
"Stay back!",
|
|
89
|
+
"Stop!"
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
"stage10": {
|
|
93
|
+
"name": "#10 Outbreak",
|
|
94
|
+
"description": "Zombies have infiltrated a building where survivors took shelter.",
|
|
95
|
+
"survivor_conversion_messages": [
|
|
96
|
+
"It hurts!",
|
|
97
|
+
"I can't hold on!",
|
|
98
|
+
"Stay back!",
|
|
99
|
+
"Stop!",
|
|
100
|
+
"Whoa!",
|
|
101
|
+
"Eeeeek!",
|
|
102
|
+
"Noooo!",
|
|
103
|
+
"Help me!",
|
|
104
|
+
"Get away!",
|
|
105
|
+
"Someone help!",
|
|
106
|
+
"Don't touch me!",
|
|
107
|
+
"No, no, no!",
|
|
108
|
+
"Please!"
|
|
109
|
+
]
|
|
80
110
|
}
|
|
81
111
|
},
|
|
82
112
|
"status": {
|
|
@@ -91,10 +121,10 @@
|
|
|
91
121
|
"hud": {
|
|
92
122
|
"need_fuel": "Need fuel to drive!",
|
|
93
123
|
"paused": "PAUSED",
|
|
94
|
-
"
|
|
124
|
+
"pause_hint": "P/Start: Resume · ESC/Select: Return to Title",
|
|
95
125
|
"survival_timer_label": "Until dawn %{time}",
|
|
96
126
|
"time_accel": ">> 4x",
|
|
97
|
-
"time_accel_hint": "Hold Shift: 4x"
|
|
127
|
+
"time_accel_hint": "Hold Shift/R1: 4x"
|
|
98
128
|
},
|
|
99
129
|
"survivors": {
|
|
100
130
|
"too_many_aboard": "Too many aboard!"
|
|
@@ -117,11 +147,13 @@
|
|
|
117
147
|
"settings": {
|
|
118
148
|
"title": "Settings",
|
|
119
149
|
"sections": {
|
|
150
|
+
"menu": "Menu",
|
|
120
151
|
"localization": "Localization",
|
|
121
152
|
"player_support": "Player support",
|
|
122
153
|
"tougher_enemies": "Tougher enemies"
|
|
123
154
|
},
|
|
124
155
|
"rows": {
|
|
156
|
+
"return_to_title": "Return to Title",
|
|
125
157
|
"language": "Language",
|
|
126
158
|
"footprints": "Footprints",
|
|
127
159
|
"car_hint": "Car hint",
|
|
@@ -131,9 +163,9 @@
|
|
|
131
163
|
"hints": {
|
|
132
164
|
"navigate": "Up/Down: select a setting",
|
|
133
165
|
"adjust": "Left/Right: set value",
|
|
134
|
-
"toggle": "Space/Enter: toggle value",
|
|
166
|
+
"toggle": "Space/Enter/South: toggle value",
|
|
135
167
|
"reset": "R: reset to defaults",
|
|
136
|
-
"exit": "Esc/
|
|
168
|
+
"exit": "Esc/BS/Select: save and return",
|
|
137
169
|
"fullscreen": "F: toggle fullscreen"
|
|
138
170
|
},
|
|
139
171
|
"config_path": "Config: %{path}",
|
|
@@ -146,7 +178,7 @@
|
|
|
146
178
|
"game_over": {
|
|
147
179
|
"win": "YOU ESCAPED!",
|
|
148
180
|
"lose": "GAME OVER",
|
|
149
|
-
"prompt": "ESC/SPACE: Title · R: Retry",
|
|
181
|
+
"prompt": "ESC/SPACE/Select/South: Title · R: Retry",
|
|
150
182
|
"scream": "AAAAHHH!!",
|
|
151
183
|
"survivors_summary": "Evacuated: %{count}",
|
|
152
184
|
"survival_duration": "Time survived %{time}"
|
zombie_escape/locales/ui.ja.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"hints": {
|
|
29
29
|
"navigate": "上下: 項目選択",
|
|
30
30
|
"page_switch": "左右: ステージ群切替",
|
|
31
|
-
"confirm": "Enter/Space: 決定または実行"
|
|
31
|
+
"confirm": "Enter/Space/South: 決定または実行"
|
|
32
32
|
},
|
|
33
33
|
"option_help": {
|
|
34
34
|
"settings": "設定画面を開いて言語や補助オプションを変更します。",
|
|
@@ -56,7 +56,12 @@
|
|
|
56
56
|
"line1": "痛いぃ!",
|
|
57
57
|
"line2": "無理!無理!",
|
|
58
58
|
"line3": "来るなぁ!"
|
|
59
|
-
}
|
|
59
|
+
},
|
|
60
|
+
"survivor_conversion_messages": [
|
|
61
|
+
"痛いぃ!",
|
|
62
|
+
"無理!無理!",
|
|
63
|
+
"来るなぁ!"
|
|
64
|
+
]
|
|
60
65
|
},
|
|
61
66
|
"stage5": {
|
|
62
67
|
"name": "#5 持久戦",
|
|
@@ -76,7 +81,32 @@
|
|
|
76
81
|
},
|
|
77
82
|
"stage9": {
|
|
78
83
|
"name": "#9 生存者救出 2",
|
|
79
|
-
"description": "ステージ4と同じだが、狭い通路。燃料も必要。"
|
|
84
|
+
"description": "ステージ4と同じだが、狭い通路。燃料も必要。",
|
|
85
|
+
"survivor_conversion_messages": [
|
|
86
|
+
"痛いぃ!",
|
|
87
|
+
"無理!無理!",
|
|
88
|
+
"来るなぁ!",
|
|
89
|
+
"やめてぇ!"
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
"stage10": {
|
|
93
|
+
"name": "#10 感染爆発",
|
|
94
|
+
"description": "建物に避難した生存者の中にゾンビが紛れ込んでいた。",
|
|
95
|
+
"survivor_conversion_messages": [
|
|
96
|
+
"痛いぃ!",
|
|
97
|
+
"無理!無理!",
|
|
98
|
+
"来るなぁ!",
|
|
99
|
+
"やめてぇ!",
|
|
100
|
+
"うわっ!",
|
|
101
|
+
"ひいいぃ",
|
|
102
|
+
"いやああ",
|
|
103
|
+
"助けて!",
|
|
104
|
+
"近寄るな!",
|
|
105
|
+
"誰か!",
|
|
106
|
+
"触るな!",
|
|
107
|
+
"やだ、やだ!",
|
|
108
|
+
"誰かー!"
|
|
109
|
+
]
|
|
80
110
|
}
|
|
81
111
|
},
|
|
82
112
|
"status": {
|
|
@@ -91,10 +121,10 @@
|
|
|
91
121
|
"hud": {
|
|
92
122
|
"need_fuel": "燃料が必要です!",
|
|
93
123
|
"paused": "一時停止",
|
|
94
|
-
"
|
|
124
|
+
"pause_hint": "P/Start: ゲーム再開 ESC/Select: タイトルに戻る",
|
|
95
125
|
"survival_timer_label": "夜明けまであと %{time}",
|
|
96
126
|
"time_accel": ">> 4x",
|
|
97
|
-
"time_accel_hint": "Shift
|
|
127
|
+
"time_accel_hint": "Shift/R1押下: 4x"
|
|
98
128
|
},
|
|
99
129
|
"survivors": {
|
|
100
130
|
"too_many_aboard": "定員オーバー!"
|
|
@@ -117,11 +147,13 @@
|
|
|
117
147
|
"settings": {
|
|
118
148
|
"title": "設定",
|
|
119
149
|
"sections": {
|
|
150
|
+
"menu": "メニュー",
|
|
120
151
|
"localization": "言語設定",
|
|
121
152
|
"player_support": "サポート",
|
|
122
153
|
"tougher_enemies": "強敵"
|
|
123
154
|
},
|
|
124
155
|
"rows": {
|
|
156
|
+
"return_to_title": "タイトルに戻る",
|
|
125
157
|
"language": "テキスト言語",
|
|
126
158
|
"footprints": "足跡",
|
|
127
159
|
"car_hint": "車のヒント",
|
|
@@ -131,9 +163,9 @@
|
|
|
131
163
|
"hints": {
|
|
132
164
|
"navigate": "上下: 項目選択",
|
|
133
165
|
"adjust": "左右: 値の設定",
|
|
134
|
-
"toggle": "
|
|
166
|
+
"toggle": "Space/Enter/South: 値のトグル",
|
|
135
167
|
"reset": "R: デフォルト設定に戻す",
|
|
136
|
-
"exit": "Esc/BS: セーブして戻る",
|
|
168
|
+
"exit": "Esc/BS/Select: セーブして戻る",
|
|
137
169
|
"fullscreen": "F: フルスクリーン切替"
|
|
138
170
|
},
|
|
139
171
|
"config_path": "設定ファイル: %{path}",
|
|
@@ -146,7 +178,7 @@
|
|
|
146
178
|
"game_over": {
|
|
147
179
|
"win": "脱出成功!",
|
|
148
180
|
"lose": "ゲームオーバー",
|
|
149
|
-
"prompt": "ESC/Space: タイトルへ R: 再挑戦",
|
|
181
|
+
"prompt": "ESC/Space/Select/South: タイトルへ R: 再挑戦",
|
|
150
182
|
"scream": "ぎゃあーーー!!",
|
|
151
183
|
"survivors_summary": "救出人数: %{count}",
|
|
152
184
|
"survival_duration": "逃げ延びた時間 %{time}"
|
zombie_escape/localization.py
CHANGED
|
@@ -108,6 +108,13 @@ def translate_dict(key: str) -> dict[str, Any]:
|
|
|
108
108
|
return result if isinstance(result, dict) else {}
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def translate_list(key: str) -> list[Any]:
|
|
112
|
+
if not _CONFIGURED:
|
|
113
|
+
set_language(_CURRENT_LANGUAGE)
|
|
114
|
+
result = _lookup_locale_value(key)
|
|
115
|
+
return result if isinstance(result, list) else []
|
|
116
|
+
|
|
117
|
+
|
|
111
118
|
def get_font_settings(*, name: str = "primary") -> FontSettings:
|
|
112
119
|
_get_language_options() # ensure locale data is loaded
|
|
113
120
|
locale_data = _LOCALE_DATA.get(_CURRENT_LANGUAGE) or _LOCALE_DATA.get(
|
|
@@ -128,6 +135,26 @@ def _qualify_key(key: str) -> str:
|
|
|
128
135
|
return key if key.startswith("ui.") else f"ui.{key}"
|
|
129
136
|
|
|
130
137
|
|
|
138
|
+
def _lookup_locale_value(key: str) -> Any:
|
|
139
|
+
locale_data = _LOCALE_DATA.get(_CURRENT_LANGUAGE) or _LOCALE_DATA.get(
|
|
140
|
+
DEFAULT_LANGUAGE, {}
|
|
141
|
+
)
|
|
142
|
+
if not isinstance(locale_data, dict):
|
|
143
|
+
return None
|
|
144
|
+
qualified = _qualify_key(key)
|
|
145
|
+
path = qualified.split(".")
|
|
146
|
+
if path and path[0] == "ui":
|
|
147
|
+
path = path[1:]
|
|
148
|
+
current: Any = locale_data
|
|
149
|
+
for segment in path:
|
|
150
|
+
if not isinstance(current, dict):
|
|
151
|
+
return None
|
|
152
|
+
current = current.get(segment)
|
|
153
|
+
if current is None:
|
|
154
|
+
return None
|
|
155
|
+
return current
|
|
156
|
+
|
|
157
|
+
|
|
131
158
|
def _get_language_options() -> tuple[LanguageOption, ...]:
|
|
132
159
|
global _LANGUAGE_OPTIONS
|
|
133
160
|
if _LANGUAGE_OPTIONS is not None:
|
|
@@ -182,5 +209,6 @@ __all__ = [
|
|
|
182
209
|
"language_options",
|
|
183
210
|
"set_language",
|
|
184
211
|
"translate",
|
|
212
|
+
"translate_list",
|
|
185
213
|
"translate_dict",
|
|
186
214
|
]
|
zombie_escape/models.py
CHANGED
|
@@ -26,6 +26,8 @@ class LevelLayout:
|
|
|
26
26
|
outside_rects: list[pygame.Rect]
|
|
27
27
|
walkable_cells: list[pygame.Rect]
|
|
28
28
|
outer_wall_cells: set[tuple[int, int]]
|
|
29
|
+
wall_cells: set[tuple[int, int]]
|
|
30
|
+
bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]]
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
@dataclass
|
|
@@ -14,6 +14,7 @@ from ..render import (
|
|
|
14
14
|
_draw_status_bar,
|
|
15
15
|
show_message,
|
|
16
16
|
)
|
|
17
|
+
from ..input_utils import is_confirm_event, is_select_event
|
|
17
18
|
from ..screens import (
|
|
18
19
|
ScreenID,
|
|
19
20
|
ScreenTransition,
|
|
@@ -181,3 +182,6 @@ def game_over_screen(
|
|
|
181
182
|
stage=stage,
|
|
182
183
|
seed=state.seed,
|
|
183
184
|
)
|
|
185
|
+
if event.type in (pygame.CONTROLLERBUTTONDOWN, pygame.JOYBUTTONDOWN):
|
|
186
|
+
if is_select_event(event) or is_confirm_event(event):
|
|
187
|
+
return ScreenTransition(ScreenID.TITLE)
|