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.
Files changed (41) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/colors.py +7 -21
  3. zombie_escape/entities.py +100 -191
  4. zombie_escape/export_images.py +39 -33
  5. zombie_escape/gameplay/ambient.py +2 -6
  6. zombie_escape/gameplay/footprints.py +8 -11
  7. zombie_escape/gameplay/interactions.py +17 -58
  8. zombie_escape/gameplay/layout.py +20 -46
  9. zombie_escape/gameplay/movement.py +7 -21
  10. zombie_escape/gameplay/spawn.py +12 -40
  11. zombie_escape/gameplay/state.py +1 -0
  12. zombie_escape/gameplay/survivors.py +5 -16
  13. zombie_escape/gameplay/utils.py +4 -13
  14. zombie_escape/input_utils.py +8 -31
  15. zombie_escape/level_blueprints.py +112 -69
  16. zombie_escape/level_constants.py +8 -0
  17. zombie_escape/locales/ui.en.json +12 -0
  18. zombie_escape/locales/ui.ja.json +12 -0
  19. zombie_escape/localization.py +3 -11
  20. zombie_escape/models.py +26 -9
  21. zombie_escape/render/__init__.py +30 -0
  22. zombie_escape/render/core.py +992 -0
  23. zombie_escape/render/hud.py +444 -0
  24. zombie_escape/render/overview.py +218 -0
  25. zombie_escape/render/shadows.py +343 -0
  26. zombie_escape/render_assets.py +11 -33
  27. zombie_escape/rng.py +4 -8
  28. zombie_escape/screens/__init__.py +14 -30
  29. zombie_escape/screens/game_over.py +43 -15
  30. zombie_escape/screens/gameplay.py +41 -104
  31. zombie_escape/screens/settings.py +19 -104
  32. zombie_escape/screens/title.py +36 -176
  33. zombie_escape/stage_constants.py +192 -67
  34. zombie_escape/zombie_escape.py +1 -1
  35. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/METADATA +100 -39
  36. zombie_escape-1.14.4.dist-info/RECORD +53 -0
  37. zombie_escape/render.py +0 -1746
  38. zombie_escape-1.13.1.dist-info/RECORD +0 -49
  39. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/WHEEL +0 -0
  40. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/entry_points.txt +0 -0
  41. {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 in (pygame.CONTROLLERBUTTONDOWN, pygame.JOYBUTTONDOWN):
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 LIGHT_GRAY, RED, WHITE, YELLOW
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
- draw(
128
- render_assets,
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
- pygame.key.get_mods() & pygame.KMOD_CTRL
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
- overlay = pygame.Surface((screen_width, screen_height), pygame.SRCALPHA)
345
- overlay.fill((0, 0, 0, 150))
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
- game_data.state.game_over or game_data.state.game_won
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
- min(dt, SURVIVAL_TIME_ACCEL_MAX_SUBSTEP) if accel_active else dt
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
- font_settings.resource, font_settings.scaled_size(12)
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 _wrap_text(window_hint, hint_font, hint_max_width):
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
- midtop=(screen_width // 2, screen_height - 32 - line_height)
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: