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
|
@@ -6,15 +6,19 @@ import pygame
|
|
|
6
6
|
from pygame import surface, time
|
|
7
7
|
|
|
8
8
|
from ..colors import BLACK, GREEN, LIGHT_GRAY, RED, WHITE
|
|
9
|
+
from ..gameplay_constants import SURVIVAL_FAKE_CLOCK_RATIO
|
|
10
|
+
from ..input_utils import (
|
|
11
|
+
CONTROLLER_BUTTON_DOWN,
|
|
12
|
+
CONTROLLER_DEVICE_ADDED,
|
|
13
|
+
CONTROLLER_DEVICE_REMOVED,
|
|
14
|
+
init_first_controller,
|
|
15
|
+
init_first_joystick,
|
|
16
|
+
is_confirm_event,
|
|
17
|
+
is_select_event,
|
|
18
|
+
)
|
|
9
19
|
from ..localization import translate as tr
|
|
10
20
|
from ..models import GameData, Stage
|
|
11
|
-
from ..render import
|
|
12
|
-
RenderAssets,
|
|
13
|
-
draw_level_overview,
|
|
14
|
-
_draw_status_bar,
|
|
15
|
-
show_message,
|
|
16
|
-
)
|
|
17
|
-
from ..input_utils import is_confirm_event, is_select_event
|
|
21
|
+
from ..render import RenderAssets, _draw_status_bar, compute_floor_cells, draw_level_overview, show_message
|
|
18
22
|
from ..screens import (
|
|
19
23
|
ScreenID,
|
|
20
24
|
ScreenTransition,
|
|
@@ -23,7 +27,6 @@ from ..screens import (
|
|
|
23
27
|
sync_window_size,
|
|
24
28
|
toggle_fullscreen,
|
|
25
29
|
)
|
|
26
|
-
from ..gameplay_constants import SURVIVAL_FAKE_CLOCK_RATIO
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
def game_over_screen(
|
|
@@ -46,6 +49,8 @@ def game_over_screen(
|
|
|
46
49
|
state = game_data.state
|
|
47
50
|
wall_group = game_data.groups.wall_group
|
|
48
51
|
footprints_enabled = config.get("footprints", {}).get("enabled", True)
|
|
52
|
+
controller = init_first_controller()
|
|
53
|
+
joystick = init_first_joystick() if controller is None else None
|
|
49
54
|
|
|
50
55
|
while True:
|
|
51
56
|
if not state.overview_created:
|
|
@@ -53,11 +58,22 @@ def game_over_screen(
|
|
|
53
58
|
level_width = level_rect.width
|
|
54
59
|
level_height = level_rect.height
|
|
55
60
|
overview_surface = pygame.Surface((level_width, level_height))
|
|
61
|
+
cell_size = render_assets.internal_wall_grid_snap
|
|
62
|
+
floor_cells: set[tuple[int, int]] = set()
|
|
63
|
+
if cell_size > 0:
|
|
64
|
+
floor_cells = compute_floor_cells(
|
|
65
|
+
cols=max(0, level_width // cell_size),
|
|
66
|
+
rows=max(0, level_height // cell_size),
|
|
67
|
+
wall_cells=game_data.layout.wall_cells,
|
|
68
|
+
outer_wall_cells=game_data.layout.outer_wall_cells,
|
|
69
|
+
pitfall_cells=game_data.layout.pitfall_cells,
|
|
70
|
+
)
|
|
56
71
|
footprints_to_draw = state.footprints if footprints_enabled else []
|
|
57
72
|
draw_level_overview(
|
|
58
73
|
render_assets,
|
|
59
74
|
overview_surface,
|
|
60
75
|
wall_group,
|
|
76
|
+
floor_cells,
|
|
61
77
|
game_data.player,
|
|
62
78
|
game_data.car,
|
|
63
79
|
game_data.waiting_cars,
|
|
@@ -84,18 +100,14 @@ def game_over_screen(
|
|
|
84
100
|
scaled_w = int(scaled_h * level_aspect)
|
|
85
101
|
scaled_w = max(1, scaled_w)
|
|
86
102
|
scaled_h = max(1, scaled_h)
|
|
87
|
-
state.scaled_overview = pygame.transform.smoothscale(
|
|
88
|
-
overview_surface, (scaled_w, scaled_h)
|
|
89
|
-
)
|
|
103
|
+
state.scaled_overview = pygame.transform.smoothscale(overview_surface, (scaled_w, scaled_h))
|
|
90
104
|
state.overview_created = True
|
|
91
105
|
|
|
92
106
|
screen.fill(BLACK)
|
|
93
107
|
if state.scaled_overview:
|
|
94
108
|
screen.blit(
|
|
95
109
|
state.scaled_overview,
|
|
96
|
-
state.scaled_overview.get_rect(
|
|
97
|
-
center=(screen_width // 2, screen_height // 2)
|
|
98
|
-
),
|
|
110
|
+
state.scaled_overview.get_rect(center=(screen_width // 2, screen_height // 2)),
|
|
99
111
|
)
|
|
100
112
|
if state.game_won:
|
|
101
113
|
show_message(
|
|
@@ -193,6 +205,22 @@ def game_over_screen(
|
|
|
193
205
|
stage=stage,
|
|
194
206
|
seed=state.seed,
|
|
195
207
|
)
|
|
196
|
-
if event.type
|
|
208
|
+
if event.type == pygame.JOYDEVICEADDED or (
|
|
209
|
+
CONTROLLER_DEVICE_ADDED is not None and event.type == CONTROLLER_DEVICE_ADDED
|
|
210
|
+
):
|
|
211
|
+
if controller is None:
|
|
212
|
+
controller = init_first_controller()
|
|
213
|
+
if controller is None:
|
|
214
|
+
joystick = init_first_joystick()
|
|
215
|
+
if event.type == pygame.JOYDEVICEREMOVED or (
|
|
216
|
+
CONTROLLER_DEVICE_REMOVED is not None and event.type == CONTROLLER_DEVICE_REMOVED
|
|
217
|
+
):
|
|
218
|
+
if controller and not controller.get_init():
|
|
219
|
+
controller = None
|
|
220
|
+
if joystick and not joystick.get_init():
|
|
221
|
+
joystick = None
|
|
222
|
+
if event.type == pygame.JOYBUTTONDOWN or (
|
|
223
|
+
CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN
|
|
224
|
+
):
|
|
197
225
|
if is_select_event(event) or is_confirm_event(event):
|
|
198
226
|
return ScreenTransition(ScreenID.TITLE)
|
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
5
5
|
import pygame
|
|
6
6
|
from pygame import surface, time
|
|
7
7
|
|
|
8
|
-
from ..colors import
|
|
8
|
+
from ..colors import RED, YELLOW
|
|
9
9
|
from ..gameplay_constants import (
|
|
10
10
|
CAR_HINT_DELAY_MS_DEFAULT,
|
|
11
11
|
SURVIVAL_TIME_ACCEL_SUBSTEPS,
|
|
@@ -47,7 +47,7 @@ from ..gameplay.spawn import _alive_waiting_cars
|
|
|
47
47
|
from ..world_grid import build_wall_index
|
|
48
48
|
from ..localization import translate as tr
|
|
49
49
|
from ..models import Stage
|
|
50
|
-
from ..render import draw, prewarm_fog_overlays, show_message
|
|
50
|
+
from ..render import draw, draw_debug_overview, draw_pause_overlay, prewarm_fog_overlays, show_message, show_message_wrapped
|
|
51
51
|
from ..rng import generate_seed, seed_rng
|
|
52
52
|
from ..progress import record_stage_clear
|
|
53
53
|
from ..screens import (
|
|
@@ -116,6 +116,7 @@ def gameplay_screen(
|
|
|
116
116
|
paused_manual = False
|
|
117
117
|
paused_focus = False
|
|
118
118
|
ignore_focus_loss_until = 0
|
|
119
|
+
debug_overview = False
|
|
119
120
|
controller = init_first_controller()
|
|
120
121
|
joystick = init_first_joystick() if controller is None else None
|
|
121
122
|
_set_mouse_hidden(pygame.mouse.get_focused())
|
|
@@ -124,21 +125,14 @@ def gameplay_screen(
|
|
|
124
125
|
layout_data = generate_level_from_blueprint(game_data, config)
|
|
125
126
|
except MapGenerationError:
|
|
126
127
|
# If generation fails after retries, show error and back to title
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
screen,
|
|
130
|
-
game_data,
|
|
131
|
-
config=config,
|
|
132
|
-
hint_color=None,
|
|
133
|
-
fps=fps,
|
|
134
|
-
present_fn=present,
|
|
135
|
-
)
|
|
136
|
-
show_message(
|
|
128
|
+
screen.fill((0, 0, 0))
|
|
129
|
+
show_message_wrapped(
|
|
137
130
|
screen,
|
|
138
131
|
tr("errors.map_generation_failed"),
|
|
139
132
|
16,
|
|
140
133
|
RED,
|
|
141
134
|
(screen_width // 2, screen_height // 2),
|
|
135
|
+
max_width=screen_width - 40,
|
|
142
136
|
)
|
|
143
137
|
present(screen)
|
|
144
138
|
pygame.time.delay(3000)
|
|
@@ -146,16 +140,12 @@ def gameplay_screen(
|
|
|
146
140
|
|
|
147
141
|
sync_ambient_palette_with_flashlights(game_data, force=True)
|
|
148
142
|
initial_waiting = max(0, stage.waiting_car_target_count)
|
|
149
|
-
player, waiting_cars = setup_player_and_cars(
|
|
150
|
-
game_data, layout_data, car_count=initial_waiting
|
|
151
|
-
)
|
|
143
|
+
player, waiting_cars = setup_player_and_cars(game_data, layout_data, car_count=initial_waiting)
|
|
152
144
|
game_data.player = player
|
|
153
145
|
game_data.waiting_cars = waiting_cars
|
|
154
146
|
game_data.car = None
|
|
155
147
|
# Only top up if initial placement spawned fewer than the intended baseline (shouldn't happen)
|
|
156
|
-
maintain_waiting_car_supply(
|
|
157
|
-
game_data, minimum=stage.waiting_car_target_count
|
|
158
|
-
)
|
|
148
|
+
maintain_waiting_car_supply(game_data, minimum=stage.waiting_car_target_count)
|
|
159
149
|
apply_passenger_speed_penalty(game_data)
|
|
160
150
|
|
|
161
151
|
spawn_survivors(game_data, layout_data)
|
|
@@ -204,6 +194,8 @@ def gameplay_screen(
|
|
|
204
194
|
|
|
205
195
|
spawn_initial_zombies(game_data, player, layout_data, config)
|
|
206
196
|
update_footprints(game_data, config)
|
|
197
|
+
level_rect = game_data.layout.field_rect
|
|
198
|
+
overview_surface = pygame.Surface((level_rect.width, level_rect.height))
|
|
207
199
|
while True:
|
|
208
200
|
dt = clock.tick(fps) / 1000.0
|
|
209
201
|
current_fps = clock.get_fps()
|
|
@@ -221,7 +213,6 @@ def gameplay_screen(
|
|
|
221
213
|
config=config,
|
|
222
214
|
hint_color=None,
|
|
223
215
|
fps=current_fps,
|
|
224
|
-
present_fn=present,
|
|
225
216
|
)
|
|
226
217
|
if game_data.state.game_over_message:
|
|
227
218
|
show_message(
|
|
@@ -257,16 +248,14 @@ def gameplay_screen(
|
|
|
257
248
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
258
249
|
paused_focus = False
|
|
259
250
|
if event.type == pygame.JOYDEVICEADDED or (
|
|
260
|
-
CONTROLLER_DEVICE_ADDED is not None
|
|
261
|
-
and event.type == CONTROLLER_DEVICE_ADDED
|
|
251
|
+
CONTROLLER_DEVICE_ADDED is not None and event.type == CONTROLLER_DEVICE_ADDED
|
|
262
252
|
):
|
|
263
253
|
if controller is None:
|
|
264
254
|
controller = init_first_controller()
|
|
265
255
|
if controller is None:
|
|
266
256
|
joystick = init_first_joystick()
|
|
267
257
|
if event.type == pygame.JOYDEVICEREMOVED or (
|
|
268
|
-
CONTROLLER_DEVICE_REMOVED is not None
|
|
269
|
-
and event.type == CONTROLLER_DEVICE_REMOVED
|
|
258
|
+
CONTROLLER_DEVICE_REMOVED is not None and event.type == CONTROLLER_DEVICE_REMOVED
|
|
270
259
|
):
|
|
271
260
|
if controller and not controller.get_init():
|
|
272
261
|
controller = None
|
|
@@ -282,14 +271,8 @@ def gameplay_screen(
|
|
|
282
271
|
if event.key == pygame.K_f:
|
|
283
272
|
toggle_fullscreen(game_data=game_data)
|
|
284
273
|
continue
|
|
285
|
-
if event.key == pygame.K_s and (
|
|
286
|
-
|
|
287
|
-
):
|
|
288
|
-
state_snapshot = {
|
|
289
|
-
k: v
|
|
290
|
-
for k, v in vars(game_data.state).items()
|
|
291
|
-
if k != "footprints"
|
|
292
|
-
}
|
|
274
|
+
if event.key == pygame.K_s and (pygame.key.get_mods() & pygame.KMOD_CTRL):
|
|
275
|
+
state_snapshot = {k: v for k, v in vars(game_data.state).items() if k != "footprints"}
|
|
293
276
|
print("STATE DEBUG:", state_snapshot)
|
|
294
277
|
continue
|
|
295
278
|
if debug_mode:
|
|
@@ -297,6 +280,8 @@ def gameplay_screen(
|
|
|
297
280
|
return ScreenTransition(ScreenID.TITLE)
|
|
298
281
|
if event.key == pygame.K_p:
|
|
299
282
|
paused_manual = not paused_manual
|
|
283
|
+
if event.key == pygame.K_o:
|
|
284
|
+
debug_overview = not debug_overview
|
|
300
285
|
continue
|
|
301
286
|
if paused_manual:
|
|
302
287
|
if event.key == pygame.K_ESCAPE:
|
|
@@ -308,8 +293,7 @@ def gameplay_screen(
|
|
|
308
293
|
paused_manual = True
|
|
309
294
|
continue
|
|
310
295
|
if event.type == pygame.JOYBUTTONDOWN or (
|
|
311
|
-
CONTROLLER_BUTTON_DOWN is not None
|
|
312
|
-
and event.type == CONTROLLER_BUTTON_DOWN
|
|
296
|
+
CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN
|
|
313
297
|
):
|
|
314
298
|
if debug_mode:
|
|
315
299
|
if is_select_event(event):
|
|
@@ -336,69 +320,20 @@ def gameplay_screen(
|
|
|
336
320
|
screen,
|
|
337
321
|
game_data,
|
|
338
322
|
config=config,
|
|
339
|
-
do_flip=not show_pause_overlay,
|
|
340
323
|
fps=current_fps,
|
|
341
|
-
present_fn=present,
|
|
342
324
|
)
|
|
343
325
|
if show_pause_overlay:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
pause_radius = 53
|
|
347
|
-
cx = screen_width // 2
|
|
348
|
-
cy = screen_height // 2 - 18
|
|
349
|
-
pygame.draw.circle(
|
|
350
|
-
overlay,
|
|
351
|
-
LIGHT_GRAY,
|
|
352
|
-
(cx, cy),
|
|
353
|
-
pause_radius,
|
|
354
|
-
width=3,
|
|
355
|
-
)
|
|
356
|
-
bar_width = 10
|
|
357
|
-
bar_height = 38
|
|
358
|
-
gap = 12
|
|
359
|
-
pygame.draw.rect(
|
|
360
|
-
overlay,
|
|
361
|
-
LIGHT_GRAY,
|
|
362
|
-
(cx - gap - bar_width, cy - bar_height // 2, bar_width, bar_height),
|
|
363
|
-
)
|
|
364
|
-
pygame.draw.rect(
|
|
365
|
-
overlay,
|
|
366
|
-
LIGHT_GRAY,
|
|
367
|
-
(cx + gap, cy - bar_height // 2, bar_width, bar_height),
|
|
368
|
-
)
|
|
369
|
-
screen.blit(overlay, (0, 0))
|
|
370
|
-
show_message(
|
|
371
|
-
screen,
|
|
372
|
-
tr("hud.paused"),
|
|
373
|
-
18,
|
|
374
|
-
WHITE,
|
|
375
|
-
(screen_width // 2, 28),
|
|
376
|
-
)
|
|
377
|
-
show_message(
|
|
378
|
-
screen,
|
|
379
|
-
tr("hud.pause_hint"),
|
|
380
|
-
16,
|
|
381
|
-
LIGHT_GRAY,
|
|
382
|
-
(screen_width // 2, screen_height // 2 + 70),
|
|
383
|
-
)
|
|
384
|
-
present(screen)
|
|
326
|
+
draw_pause_overlay(screen)
|
|
327
|
+
present(screen)
|
|
385
328
|
continue
|
|
386
329
|
|
|
387
330
|
keys = pygame.key.get_pressed()
|
|
388
|
-
accel_allowed = not (
|
|
389
|
-
|
|
390
|
-
)
|
|
391
|
-
accel_active = accel_allowed and is_accel_active(
|
|
392
|
-
keys, controller, joystick
|
|
393
|
-
)
|
|
331
|
+
accel_allowed = not (game_data.state.game_over or game_data.state.game_won)
|
|
332
|
+
accel_active = accel_allowed and is_accel_active(keys, controller, joystick)
|
|
394
333
|
game_data.state.time_accel_active = accel_active
|
|
395
334
|
substeps = SURVIVAL_TIME_ACCEL_SUBSTEPS if accel_active else 1
|
|
396
|
-
sub_dt = (
|
|
397
|
-
|
|
398
|
-
)
|
|
399
|
-
wall_index = build_wall_index(
|
|
400
|
-
game_data.groups.wall_group, cell_size=game_data.cell_size
|
|
401
|
-
)
|
|
335
|
+
sub_dt = min(dt, SURVIVAL_TIME_ACCEL_MAX_SUBSTEP) if accel_active else dt
|
|
336
|
+
wall_index = build_wall_index(game_data.groups.wall_group, cell_size=game_data.cell_size)
|
|
402
337
|
for _ in range(substeps):
|
|
403
338
|
player_ref = game_data.player
|
|
404
339
|
if player_ref is None:
|
|
@@ -436,6 +371,19 @@ def gameplay_screen(
|
|
|
436
371
|
if player is None:
|
|
437
372
|
raise ValueError("Player missing from game data")
|
|
438
373
|
|
|
374
|
+
if debug_overview:
|
|
375
|
+
draw_debug_overview(
|
|
376
|
+
render_assets,
|
|
377
|
+
screen,
|
|
378
|
+
overview_surface,
|
|
379
|
+
game_data,
|
|
380
|
+
config,
|
|
381
|
+
screen_width=screen_width,
|
|
382
|
+
screen_height=screen_height,
|
|
383
|
+
)
|
|
384
|
+
present(screen)
|
|
385
|
+
continue
|
|
386
|
+
|
|
439
387
|
car_hint_conf = config.get("car_hint", {})
|
|
440
388
|
hint_delay = car_hint_conf.get("delay_ms", CAR_HINT_DELAY_MS_DEFAULT)
|
|
441
389
|
elapsed_ms = game_data.state.elapsed_play_ms
|
|
@@ -450,36 +398,25 @@ def gameplay_screen(
|
|
|
450
398
|
if hint_enabled:
|
|
451
399
|
if not has_fuel and game_data.fuel and game_data.fuel.alive():
|
|
452
400
|
target_type = "fuel"
|
|
453
|
-
elif not player.in_car and (
|
|
454
|
-
active_car or _alive_waiting_cars(game_data)
|
|
455
|
-
):
|
|
401
|
+
elif not player.in_car and (active_car or _alive_waiting_cars(game_data)):
|
|
456
402
|
target_type = "car"
|
|
457
403
|
else:
|
|
458
404
|
target_type = None
|
|
459
405
|
|
|
460
406
|
if target_type != hint_target_type:
|
|
461
407
|
game_data.state.hint_target_type = target_type
|
|
462
|
-
game_data.state.hint_expires_at =
|
|
463
|
-
elapsed_ms + hint_delay if target_type else 0
|
|
464
|
-
)
|
|
408
|
+
game_data.state.hint_expires_at = elapsed_ms + hint_delay if target_type else 0
|
|
465
409
|
hint_expires_at = game_data.state.hint_expires_at
|
|
466
410
|
hint_target_type = target_type
|
|
467
411
|
|
|
468
|
-
if
|
|
469
|
-
target_type
|
|
470
|
-
and hint_expires_at
|
|
471
|
-
and elapsed_ms >= hint_expires_at
|
|
472
|
-
and not player.in_car
|
|
473
|
-
):
|
|
412
|
+
if target_type and hint_expires_at and elapsed_ms >= hint_expires_at and not player.in_car:
|
|
474
413
|
if target_type == "fuel" and game_data.fuel and game_data.fuel.alive():
|
|
475
414
|
hint_target = game_data.fuel.rect.center
|
|
476
415
|
elif target_type == "car":
|
|
477
416
|
if active_car:
|
|
478
417
|
hint_target = active_car.rect.center
|
|
479
418
|
else:
|
|
480
|
-
waiting_target = nearest_waiting_car(
|
|
481
|
-
game_data, (player.x, player.y)
|
|
482
|
-
)
|
|
419
|
+
waiting_target = nearest_waiting_car(game_data, (player.x, player.y))
|
|
483
420
|
if waiting_target:
|
|
484
421
|
hint_target = waiting_target.rect.center
|
|
485
422
|
|
|
@@ -491,8 +428,8 @@ def gameplay_screen(
|
|
|
491
428
|
hint_target=hint_target,
|
|
492
429
|
hint_color=hint_color,
|
|
493
430
|
fps=current_fps,
|
|
494
|
-
present_fn=present,
|
|
495
431
|
)
|
|
432
|
+
present(screen)
|
|
496
433
|
|
|
497
434
|
# Should not reach here, but return to title if it happens
|
|
498
435
|
return _finalize(ScreenTransition(ScreenID.TITLE))
|
|
@@ -33,7 +33,7 @@ from ..localization import (
|
|
|
33
33
|
from ..localization import (
|
|
34
34
|
translate as tr,
|
|
35
35
|
)
|
|
36
|
-
from ..render import show_message
|
|
36
|
+
from ..render import show_message, wrap_text
|
|
37
37
|
from ..progress import user_progress_path
|
|
38
38
|
from ..screens import (
|
|
39
39
|
nudge_window_scale,
|
|
@@ -43,54 +43,6 @@ from ..screens import (
|
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def _wrap_long_segment(
|
|
47
|
-
segment: str, font: pygame.font.Font, max_width: int
|
|
48
|
-
) -> list[str]:
|
|
49
|
-
lines: list[str] = []
|
|
50
|
-
current = ""
|
|
51
|
-
for char in segment:
|
|
52
|
-
candidate = current + char
|
|
53
|
-
if font.size(candidate)[0] <= max_width or not current:
|
|
54
|
-
current = candidate
|
|
55
|
-
else:
|
|
56
|
-
lines.append(current)
|
|
57
|
-
current = char
|
|
58
|
-
if current:
|
|
59
|
-
lines.append(current)
|
|
60
|
-
return lines
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _wrap_text(text: str, font: pygame.font.Font, max_width: int) -> list[str]:
|
|
64
|
-
if max_width <= 0:
|
|
65
|
-
return [text]
|
|
66
|
-
paragraphs = text.splitlines() or [text]
|
|
67
|
-
lines: list[str] = []
|
|
68
|
-
for paragraph in paragraphs:
|
|
69
|
-
if not paragraph:
|
|
70
|
-
lines.append("")
|
|
71
|
-
continue
|
|
72
|
-
words = paragraph.split(" ")
|
|
73
|
-
if len(words) == 1:
|
|
74
|
-
lines.extend(_wrap_long_segment(paragraph, font, max_width))
|
|
75
|
-
continue
|
|
76
|
-
current = ""
|
|
77
|
-
for word in words:
|
|
78
|
-
candidate = f"{current} {word}".strip() if current else word
|
|
79
|
-
if font.size(candidate)[0] <= max_width:
|
|
80
|
-
current = candidate
|
|
81
|
-
continue
|
|
82
|
-
if current:
|
|
83
|
-
lines.append(current)
|
|
84
|
-
if font.size(word)[0] <= max_width:
|
|
85
|
-
current = word
|
|
86
|
-
else:
|
|
87
|
-
lines.extend(_wrap_long_segment(word, font, max_width))
|
|
88
|
-
current = ""
|
|
89
|
-
if current:
|
|
90
|
-
lines.append(current)
|
|
91
|
-
return lines
|
|
92
|
-
|
|
93
|
-
|
|
94
46
|
def settings_screen(
|
|
95
47
|
screen: surface.Surface,
|
|
96
48
|
clock: time.Clock,
|
|
@@ -246,16 +198,14 @@ def settings_screen(
|
|
|
246
198
|
sync_window_size(event)
|
|
247
199
|
continue
|
|
248
200
|
if event.type == pygame.JOYDEVICEADDED or (
|
|
249
|
-
CONTROLLER_DEVICE_ADDED is not None
|
|
250
|
-
and event.type == CONTROLLER_DEVICE_ADDED
|
|
201
|
+
CONTROLLER_DEVICE_ADDED is not None and event.type == CONTROLLER_DEVICE_ADDED
|
|
251
202
|
):
|
|
252
203
|
if controller is None:
|
|
253
204
|
controller = init_first_controller()
|
|
254
205
|
if controller is None:
|
|
255
206
|
joystick = init_first_joystick()
|
|
256
207
|
if event.type == pygame.JOYDEVICEREMOVED or (
|
|
257
|
-
CONTROLLER_DEVICE_REMOVED is not None
|
|
258
|
-
and event.type == CONTROLLER_DEVICE_REMOVED
|
|
208
|
+
CONTROLLER_DEVICE_REMOVED is not None and event.type == CONTROLLER_DEVICE_REMOVED
|
|
259
209
|
):
|
|
260
210
|
if controller and not controller.get_init():
|
|
261
211
|
controller = None
|
|
@@ -302,8 +252,7 @@ def settings_screen(
|
|
|
302
252
|
working = copy.deepcopy(DEFAULT_CONFIG)
|
|
303
253
|
set_language(working.get("language"))
|
|
304
254
|
if event.type == pygame.JOYBUTTONDOWN or (
|
|
305
|
-
CONTROLLER_BUTTON_DOWN is not None
|
|
306
|
-
and event.type == CONTROLLER_BUTTON_DOWN
|
|
255
|
+
CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN
|
|
307
256
|
):
|
|
308
257
|
current_row = rows[selected]
|
|
309
258
|
row_type = current_row.get("type", "toggle")
|
|
@@ -315,15 +264,9 @@ def settings_screen(
|
|
|
315
264
|
elif row_type == "choice":
|
|
316
265
|
cycle_choice(current_row, 1)
|
|
317
266
|
if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
|
|
318
|
-
if
|
|
319
|
-
CONTROLLER_BUTTON_DPAD_UP is not None
|
|
320
|
-
and event.button == CONTROLLER_BUTTON_DPAD_UP
|
|
321
|
-
):
|
|
267
|
+
if CONTROLLER_BUTTON_DPAD_UP is not None and event.button == CONTROLLER_BUTTON_DPAD_UP:
|
|
322
268
|
selected = (selected - 1) % row_count
|
|
323
|
-
if
|
|
324
|
-
CONTROLLER_BUTTON_DPAD_DOWN is not None
|
|
325
|
-
and event.button == CONTROLLER_BUTTON_DPAD_DOWN
|
|
326
|
-
):
|
|
269
|
+
if CONTROLLER_BUTTON_DPAD_DOWN is not None and event.button == CONTROLLER_BUTTON_DPAD_DOWN:
|
|
327
270
|
selected = (selected + 1) % row_count
|
|
328
271
|
if (
|
|
329
272
|
CONTROLLER_BUTTON_DPAD_LEFT is not None
|
|
@@ -380,15 +323,9 @@ def settings_screen(
|
|
|
380
323
|
|
|
381
324
|
try:
|
|
382
325
|
font_settings = get_font_settings()
|
|
383
|
-
label_font = load_font(
|
|
384
|
-
|
|
385
|
-
)
|
|
386
|
-
value_font = load_font(
|
|
387
|
-
font_settings.resource, font_settings.scaled_size(12)
|
|
388
|
-
)
|
|
389
|
-
section_font = load_font(
|
|
390
|
-
font_settings.resource, font_settings.scaled_size(13)
|
|
391
|
-
)
|
|
326
|
+
label_font = load_font(font_settings.resource, font_settings.scaled_size(12))
|
|
327
|
+
value_font = load_font(font_settings.resource, font_settings.scaled_size(12))
|
|
328
|
+
section_font = load_font(font_settings.resource, font_settings.scaled_size(13))
|
|
392
329
|
highlight_color = (70, 70, 70)
|
|
393
330
|
|
|
394
331
|
row_height = 20
|
|
@@ -408,20 +345,14 @@ def settings_screen(
|
|
|
408
345
|
section_states: dict[str, dict] = {}
|
|
409
346
|
y_cursor = start_y
|
|
410
347
|
for section in sections:
|
|
411
|
-
header_surface = section_font.render(
|
|
412
|
-
section["label"], False, LIGHT_GRAY
|
|
413
|
-
)
|
|
348
|
+
header_surface = section_font.render(section["label"], False, LIGHT_GRAY)
|
|
414
349
|
section_states[section["label"]] = {
|
|
415
350
|
"next_y": y_cursor + header_surface.get_height() + 4,
|
|
416
351
|
"header_surface": header_surface,
|
|
417
352
|
"header_pos": (column_margin, y_cursor),
|
|
418
353
|
}
|
|
419
354
|
rows_in_section = len(section["rows"])
|
|
420
|
-
y_cursor =
|
|
421
|
-
section_states[section["label"]]["next_y"]
|
|
422
|
-
+ rows_in_section * row_height
|
|
423
|
-
+ section_spacing
|
|
424
|
-
)
|
|
355
|
+
y_cursor = section_states[section["label"]]["next_y"] + rows_in_section * row_height + section_spacing
|
|
425
356
|
|
|
426
357
|
for state in section_states.values():
|
|
427
358
|
screen.blit(state["header_surface"], state["header_pos"])
|
|
@@ -441,9 +372,7 @@ def settings_screen(
|
|
|
441
372
|
row_y_current = state["next_y"]
|
|
442
373
|
state["next_y"] += row_height
|
|
443
374
|
|
|
444
|
-
highlight_rect = pygame.Rect(
|
|
445
|
-
col_x, row_y_current - 2, row_width, row_height
|
|
446
|
-
)
|
|
375
|
+
highlight_rect = pygame.Rect(col_x, row_y_current - 2, row_width, row_height)
|
|
447
376
|
if idx == selected:
|
|
448
377
|
pygame.draw.rect(screen, highlight_color, highlight_rect)
|
|
449
378
|
|
|
@@ -457,11 +386,7 @@ def settings_screen(
|
|
|
457
386
|
screen.blit(label_surface, label_rect)
|
|
458
387
|
if row_type == "choice":
|
|
459
388
|
display_fn = row.get("get_display")
|
|
460
|
-
display_text = (
|
|
461
|
-
display_fn(value)
|
|
462
|
-
if display_fn and value is not None
|
|
463
|
-
else str(value)
|
|
464
|
-
)
|
|
389
|
+
display_text = display_fn(value) if display_fn and value is not None else str(value)
|
|
465
390
|
value_surface = value_font.render(display_text, False, WHITE)
|
|
466
391
|
value_rect = value_surface.get_rect(
|
|
467
392
|
midright=(
|
|
@@ -473,9 +398,7 @@ def settings_screen(
|
|
|
473
398
|
elif row_type == "toggle":
|
|
474
399
|
slider_y = row_y_current + (row_height - segment_height) // 2 - 2
|
|
475
400
|
slider_x = col_x + row_width - segment_total_width
|
|
476
|
-
left_rect = pygame.Rect(
|
|
477
|
-
slider_x, slider_y, segment_width, segment_height
|
|
478
|
-
)
|
|
401
|
+
left_rect = pygame.Rect(slider_x, slider_y, segment_width, segment_height)
|
|
479
402
|
right_rect = pygame.Rect(
|
|
480
403
|
left_rect.right + segment_gap,
|
|
481
404
|
slider_y,
|
|
@@ -486,9 +409,7 @@ def settings_screen(
|
|
|
486
409
|
left_active = value == row["easy_value"]
|
|
487
410
|
right_active = not left_active
|
|
488
411
|
|
|
489
|
-
def draw_segment(
|
|
490
|
-
rect: pygame.Rect, text: str, active: bool
|
|
491
|
-
) -> None:
|
|
412
|
+
def draw_segment(rect: pygame.Rect, text: str, active: bool) -> None:
|
|
492
413
|
base_color = (35, 35, 35)
|
|
493
414
|
active_color = (60, 90, 60) if active else base_color
|
|
494
415
|
outline_color = GREEN if active else LIGHT_GRAY
|
|
@@ -522,7 +443,7 @@ def settings_screen(
|
|
|
522
443
|
|
|
523
444
|
y_cursor += 26
|
|
524
445
|
window_hint = tr("menu.window_hint")
|
|
525
|
-
for line in
|
|
446
|
+
for line in wrap_text(window_hint, hint_font, hint_max_width):
|
|
526
447
|
hint_surface = hint_font.render(line, False, WHITE)
|
|
527
448
|
hint_rect = hint_surface.get_rect(topleft=(hint_start_x, y_cursor))
|
|
528
449
|
screen.blit(hint_surface, hint_rect)
|
|
@@ -530,18 +451,12 @@ def settings_screen(
|
|
|
530
451
|
|
|
531
452
|
path_font = load_font(font_settings.resource, font_settings.scaled_size(11))
|
|
532
453
|
config_text = tr("settings.config_path", path=str(config_path))
|
|
533
|
-
progress_text = tr(
|
|
534
|
-
"settings.progress_path", path=str(user_progress_path())
|
|
535
|
-
)
|
|
454
|
+
progress_text = tr("settings.progress_path", path=str(user_progress_path()))
|
|
536
455
|
line_height = path_font.get_linesize()
|
|
537
456
|
config_surface = path_font.render(config_text, False, LIGHT_GRAY)
|
|
538
457
|
progress_surface = path_font.render(progress_text, False, LIGHT_GRAY)
|
|
539
|
-
config_rect = config_surface.get_rect(
|
|
540
|
-
|
|
541
|
-
)
|
|
542
|
-
progress_rect = progress_surface.get_rect(
|
|
543
|
-
midtop=(screen_width // 2, screen_height - 32)
|
|
544
|
-
)
|
|
458
|
+
config_rect = config_surface.get_rect(midtop=(screen_width // 2, screen_height - 32 - line_height))
|
|
459
|
+
progress_rect = progress_surface.get_rect(midtop=(screen_width // 2, screen_height - 32))
|
|
545
460
|
screen.blit(config_surface, config_rect)
|
|
546
461
|
screen.blit(progress_surface, progress_rect)
|
|
547
462
|
except pygame.error as e:
|