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
@@ -12,7 +12,7 @@ from ..localization import get_font_settings, get_language
12
12
  from ..localization import translate as tr
13
13
  from ..models import Stage
14
14
  from ..progress import load_progress
15
- from ..render import show_message
15
+ from ..render import blit_wrapped_text, show_message, wrap_text
16
16
  from ..rng import generate_seed
17
17
  from ..input_utils import (
18
18
  CONTROLLER_BUTTON_DOWN,
@@ -34,6 +34,7 @@ from ..screens import (
34
34
  sync_window_size,
35
35
  toggle_fullscreen,
36
36
  )
37
+
37
38
  try: # pragma: no cover - version fallback not critical for tests
38
39
  from ..__about__ import __version__
39
40
  except Exception: # pragma: no cover - fallback version
@@ -65,77 +66,6 @@ def _open_readme_link(*, use_stage6: bool = False) -> None:
65
66
  print(f"Unable to open README URL {url}: {exc}")
66
67
 
67
68
 
68
- def _wrap_long_segment(
69
- segment: str, font: pygame.font.Font, max_width: int
70
- ) -> list[str]:
71
- lines: list[str] = []
72
- current = ""
73
- for char in segment:
74
- candidate = current + char
75
- if font.size(candidate)[0] <= max_width or not current:
76
- current = candidate
77
- else:
78
- lines.append(current)
79
- current = char
80
- if current:
81
- lines.append(current)
82
- return lines
83
-
84
-
85
- def _wrap_text(text: str, font: pygame.font.Font, max_width: int) -> list[str]:
86
- """Break text into multiple lines within a max width (supports CJK text)."""
87
-
88
- if max_width <= 0:
89
- return [text]
90
- paragraphs = text.splitlines() or [text]
91
- lines: list[str] = []
92
- for paragraph in paragraphs:
93
- if not paragraph:
94
- lines.append("")
95
- continue
96
- words = paragraph.split(" ")
97
- if len(words) == 1:
98
- lines.extend(_wrap_long_segment(paragraph, font, max_width))
99
- continue
100
- current = ""
101
- for word in words:
102
- candidate = f"{current} {word}".strip() if current else word
103
- if font.size(candidate)[0] <= max_width:
104
- current = candidate
105
- continue
106
- if current:
107
- lines.append(current)
108
- if font.size(word)[0] <= max_width:
109
- current = word
110
- else:
111
- lines.extend(_wrap_long_segment(word, font, max_width))
112
- current = ""
113
- if current:
114
- lines.append(current)
115
- return lines
116
-
117
-
118
- def _blit_wrapped_text(
119
- target: surface.Surface,
120
- text: str,
121
- font: pygame.font.Font,
122
- color: tuple[int, int, int],
123
- topleft: tuple[int, int],
124
- max_width: int,
125
- ) -> None:
126
- """Render text with simple wrapping constrained to max_width."""
127
-
128
- x, y = topleft
129
- line_height = font.get_linesize()
130
- for line in _wrap_text(text, font, max_width):
131
- if not line:
132
- y += line_height
133
- continue
134
- rendered = font.render(line, False, color)
135
- target.blit(rendered, (x, y))
136
- y += line_height
137
-
138
-
139
69
  def _generate_auto_seed_text() -> str:
140
70
  raw = generate_seed()
141
71
  trimmed = raw // 100 # drop lower 2 digits for stability
@@ -158,24 +88,17 @@ def title_screen(
158
88
 
159
89
  width, height = screen_size
160
90
  stage_options_all: list[dict] = [
161
- {"type": "stage", "stage": stage, "available": stage.available}
162
- for stage in stages
163
- if stage.available
91
+ {"type": "stage", "stage": stage, "available": stage.available} for stage in stages if stage.available
164
92
  ]
165
93
  page_size = 5
166
- stage_pages = [
167
- stage_options_all[i : i + page_size]
168
- for i in range(0, len(stage_options_all), page_size)
169
- ]
94
+ stage_pages = [stage_options_all[i : i + page_size] for i in range(0, len(stage_options_all), page_size)]
170
95
  action_options: list[dict[str, Any]] = [
171
96
  {"type": "settings"},
172
97
  {"type": "readme"},
173
98
  {"type": "quit"},
174
99
  ]
175
100
  generated = seed_text is None
176
- current_seed_text = (
177
- seed_text if seed_text is not None else _generate_auto_seed_text()
178
- )
101
+ current_seed_text = seed_text if seed_text is not None else _generate_auto_seed_text()
179
102
  current_seed_auto = seed_is_auto or generated
180
103
  stage_progress, _ = load_progress()
181
104
 
@@ -183,9 +106,7 @@ def title_screen(
183
106
  if page_index <= 0:
184
107
  return True
185
108
  required = stage_options_all[:page_size]
186
- return all(
187
- stage_progress.get(option["stage"].id, 0) > 0 for option in required
188
- )
109
+ return all(stage_progress.get(option["stage"].id, 0) > 0 for option in required)
189
110
 
190
111
  current_page = 0
191
112
  if stage_options_all:
@@ -204,11 +125,7 @@ def title_screen(
204
125
 
205
126
  options, stage_options = _build_options(current_page)
206
127
  selected_stage_index = next(
207
- (
208
- i
209
- for i, opt in enumerate(options)
210
- if opt["type"] == "stage" and opt["stage"].id == default_stage_id
211
- ),
128
+ (i for i, opt in enumerate(options) if opt["type"] == "stage" and opt["stage"].id == default_stage_id),
212
129
  0,
213
130
  )
214
131
  selected = min(selected_stage_index, len(options) - 1)
@@ -227,16 +144,14 @@ def title_screen(
227
144
  sync_window_size(event)
228
145
  continue
229
146
  if event.type == pygame.JOYDEVICEADDED or (
230
- CONTROLLER_DEVICE_ADDED is not None
231
- and event.type == CONTROLLER_DEVICE_ADDED
147
+ CONTROLLER_DEVICE_ADDED is not None and event.type == CONTROLLER_DEVICE_ADDED
232
148
  ):
233
149
  if controller is None:
234
150
  controller = init_first_controller()
235
151
  if controller is None:
236
152
  joystick = init_first_joystick()
237
153
  if event.type == pygame.JOYDEVICEREMOVED or (
238
- CONTROLLER_DEVICE_REMOVED is not None
239
- and event.type == CONTROLLER_DEVICE_REMOVED
154
+ CONTROLLER_DEVICE_REMOVED is not None and event.type == CONTROLLER_DEVICE_REMOVED
240
155
  ):
241
156
  if controller and not controller.get_init():
242
157
  controller = None
@@ -270,10 +185,7 @@ def title_screen(
270
185
  selected = 0
271
186
  continue
272
187
  if event.key == pygame.K_RIGHT:
273
- if (
274
- current_page < len(stage_pages) - 1
275
- and _page_available(current_page + 1)
276
- ):
188
+ if current_page < len(stage_pages) - 1 and _page_available(current_page + 1):
277
189
  current_page += 1
278
190
  options, stage_options = _build_options(current_page)
279
191
  selected = 0
@@ -285,9 +197,7 @@ def title_screen(
285
197
  elif event.key in (pygame.K_RETURN, pygame.K_SPACE):
286
198
  current = options[selected]
287
199
  if current["type"] == "stage" and current.get("available"):
288
- seed_value = (
289
- int(current_seed_text) if current_seed_text else None
290
- )
200
+ seed_value = int(current_seed_text) if current_seed_text else None
291
201
  return ScreenTransition(
292
202
  ScreenID.GAMEPLAY,
293
203
  stage=current["stage"],
@@ -311,15 +221,12 @@ def title_screen(
311
221
  seed_is_auto=current_seed_auto,
312
222
  )
313
223
  if event.type == pygame.JOYBUTTONDOWN or (
314
- CONTROLLER_BUTTON_DOWN is not None
315
- and event.type == CONTROLLER_BUTTON_DOWN
224
+ CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN
316
225
  ):
317
226
  if is_confirm_event(event):
318
227
  current = options[selected]
319
228
  if current["type"] == "stage" and current.get("available"):
320
- seed_value = (
321
- int(current_seed_text) if current_seed_text else None
322
- )
229
+ seed_value = int(current_seed_text) if current_seed_text else None
323
230
  return ScreenTransition(
324
231
  ScreenID.GAMEPLAY,
325
232
  stage=current["stage"],
@@ -343,32 +250,17 @@ def title_screen(
343
250
  seed_is_auto=current_seed_auto,
344
251
  )
345
252
  if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
346
- if (
347
- CONTROLLER_BUTTON_DPAD_UP is not None
348
- and event.button == CONTROLLER_BUTTON_DPAD_UP
349
- ):
253
+ if CONTROLLER_BUTTON_DPAD_UP is not None and event.button == CONTROLLER_BUTTON_DPAD_UP:
350
254
  selected = (selected - 1) % len(options)
351
- if (
352
- CONTROLLER_BUTTON_DPAD_DOWN is not None
353
- and event.button == CONTROLLER_BUTTON_DPAD_DOWN
354
- ):
255
+ if CONTROLLER_BUTTON_DPAD_DOWN is not None and event.button == CONTROLLER_BUTTON_DPAD_DOWN:
355
256
  selected = (selected + 1) % len(options)
356
- if (
357
- CONTROLLER_BUTTON_DPAD_LEFT is not None
358
- and event.button == CONTROLLER_BUTTON_DPAD_LEFT
359
- ):
257
+ if CONTROLLER_BUTTON_DPAD_LEFT is not None and event.button == CONTROLLER_BUTTON_DPAD_LEFT:
360
258
  if current_page > 0:
361
259
  current_page -= 1
362
260
  options, stage_options = _build_options(current_page)
363
261
  selected = 0
364
- if (
365
- CONTROLLER_BUTTON_DPAD_RIGHT is not None
366
- and event.button == CONTROLLER_BUTTON_DPAD_RIGHT
367
- ):
368
- if (
369
- current_page < len(stage_pages) - 1
370
- and _page_available(current_page + 1)
371
- ):
262
+ if CONTROLLER_BUTTON_DPAD_RIGHT is not None and event.button == CONTROLLER_BUTTON_DPAD_RIGHT:
263
+ if current_page < len(stage_pages) - 1 and _page_available(current_page + 1):
372
264
  current_page += 1
373
265
  options, stage_options = _build_options(current_page)
374
266
  selected = 0
@@ -384,10 +276,7 @@ def title_screen(
384
276
  options, stage_options = _build_options(current_page)
385
277
  selected = 0
386
278
  elif hat_x == 1:
387
- if (
388
- current_page < len(stage_pages) - 1
389
- and _page_available(current_page + 1)
390
- ):
279
+ if current_page < len(stage_pages) - 1 and _page_available(current_page + 1):
391
280
  current_page += 1
392
281
  options, stage_options = _build_options(current_page)
393
282
  selected = 0
@@ -398,16 +287,10 @@ def title_screen(
398
287
 
399
288
  try:
400
289
  font_settings = get_font_settings()
401
- title_font = load_font(
402
- font_settings.resource, font_settings.scaled_size(32)
403
- )
404
- option_font = load_font(
405
- font_settings.resource, font_settings.scaled_size(14)
406
- )
290
+ title_font = load_font(font_settings.resource, font_settings.scaled_size(32))
291
+ option_font = load_font(font_settings.resource, font_settings.scaled_size(14))
407
292
  desc_font = load_font(font_settings.resource, font_settings.scaled_size(11))
408
- section_font = load_font(
409
- font_settings.resource, font_settings.scaled_size(13)
410
- )
293
+ section_font = load_font(font_settings.resource, font_settings.scaled_size(13))
411
294
  hint_font = load_font(font_settings.resource, font_settings.scaled_size(11))
412
295
 
413
296
  row_height = 20
@@ -425,20 +308,13 @@ def title_screen(
425
308
  show_page_arrows = len(stage_pages) > 1 and _page_available(1)
426
309
  if show_page_arrows:
427
310
  left_arrow = "<- " if current_page > 0 else ""
428
- right_arrow = (
429
- " ->"
430
- if current_page < len(stage_pages) - 1
431
- and _page_available(current_page + 1)
432
- else ""
433
- )
311
+ right_arrow = " ->" if current_page < len(stage_pages) - 1 and _page_available(current_page + 1) else ""
434
312
  stage_header_text = f"{left_arrow}{stage_header_text}{right_arrow}"
435
313
  stage_header = section_font.render(stage_header_text, False, LIGHT_GRAY)
436
314
  stage_header_pos = (list_column_x, section_top)
437
315
  screen.blit(stage_header, stage_header_pos)
438
316
  stage_rows_start = stage_header_pos[1] + stage_header.get_height() + 6
439
- action_header = section_font.render(
440
- tr("menu.sections.resources"), False, LIGHT_GRAY
441
- )
317
+ action_header = section_font.render(tr("menu.sections.resources"), False, LIGHT_GRAY)
442
318
  action_header_pos = (
443
319
  list_column_x,
444
320
  stage_rows_start + stage_count * row_height + 14,
@@ -448,9 +324,7 @@ def title_screen(
448
324
 
449
325
  for idx, option in enumerate(stage_options):
450
326
  row_top = stage_rows_start + idx * row_height
451
- highlight_rect = pygame.Rect(
452
- list_column_x, row_top - 2, list_column_width, row_height
453
- )
327
+ highlight_rect = pygame.Rect(list_column_x, row_top - 2, list_column_width, row_height)
454
328
  cleared = stage_progress.get(option["stage"].id, 0) > 0
455
329
  base_color = WHITE if cleared else UNCLEARED_STAGE_COLOR
456
330
  color = base_color
@@ -473,9 +347,7 @@ def title_screen(
473
347
  for idx, option in enumerate(action_options):
474
348
  option_idx = stage_count + idx
475
349
  row_top = action_rows_start + idx * row_height
476
- highlight_rect = pygame.Rect(
477
- list_column_x, row_top - 2, list_column_width, row_height
478
- )
350
+ highlight_rect = pygame.Rect(list_column_x, row_top - 2, list_column_width, row_height)
479
351
  is_selected = option_idx == selected
480
352
  if is_selected:
481
353
  pygame.draw.rect(screen, highlight_color, highlight_rect)
@@ -500,7 +372,7 @@ def title_screen(
500
372
  desc_area_top = section_top
501
373
  if current["type"] == "stage":
502
374
  desc_color = WHITE if current.get("available") else GRAY
503
- _blit_wrapped_text(
375
+ blit_wrapped_text(
504
376
  screen,
505
377
  current["stage"].description,
506
378
  desc_font,
@@ -516,13 +388,11 @@ def title_screen(
516
388
  elif current["type"] == "quit":
517
389
  help_text = tr("menu.option_help.quit")
518
390
  elif current["type"] == "readme":
519
- help_key = (
520
- "menu.option_help.readme_stage6" if current_page > 0 else "menu.option_help.readme"
521
- )
391
+ help_key = "menu.option_help.readme_stage6" if current_page > 0 else "menu.option_help.readme"
522
392
  help_text = tr(help_key)
523
393
 
524
394
  if help_text:
525
- _blit_wrapped_text(
395
+ blit_wrapped_text(
526
396
  screen,
527
397
  help_text,
528
398
  desc_font,
@@ -540,27 +410,21 @@ def title_screen(
540
410
  hint_start_y = action_header_pos[1]
541
411
  for offset, line in enumerate(hint_lines):
542
412
  hint_surface = hint_font.render(line, False, WHITE)
543
- hint_rect = hint_surface.get_rect(
544
- topleft=(info_column_x, hint_start_y + offset * hint_line_height)
545
- )
413
+ hint_rect = hint_surface.get_rect(topleft=(info_column_x, hint_start_y + offset * hint_line_height))
546
414
  screen.blit(hint_surface, hint_rect)
547
415
 
548
- seed_value_display = (
549
- current_seed_text if current_seed_text else tr("menu.seed_empty")
550
- )
416
+ seed_value_display = current_seed_text if current_seed_text else tr("menu.seed_empty")
551
417
  seed_label = tr("menu.seed_label", value=seed_value_display)
552
418
  seed_surface = hint_font.render(seed_label, False, LIGHT_GRAY)
553
419
  seed_offset_y = hint_line_height
554
- seed_rect = seed_surface.get_rect(
555
- bottomleft=(info_column_x, height - 30 + seed_offset_y)
556
- )
420
+ seed_rect = seed_surface.get_rect(bottomleft=(info_column_x, height - 30 + seed_offset_y))
557
421
  screen.blit(seed_surface, seed_rect)
558
422
 
559
423
  seed_hint = tr("menu.seed_hint")
560
- seed_hint_lines = _wrap_text(seed_hint, hint_font, info_column_width)
424
+ seed_hint_lines = wrap_text(seed_hint, hint_font, info_column_width)
561
425
  seed_hint_height = len(seed_hint_lines) * hint_line_height
562
426
  seed_hint_top = seed_rect.top - 4 - seed_hint_height
563
- _blit_wrapped_text(
427
+ blit_wrapped_text(
564
428
  screen,
565
429
  seed_hint,
566
430
  hint_font,
@@ -571,13 +435,9 @@ def title_screen(
571
435
 
572
436
  title_surface = title_font.render(title_text, False, LIGHT_GRAY)
573
437
  title_rect = title_surface.get_rect(center=(width // 2, 40))
574
- version_font = load_font(
575
- font_settings.resource, font_settings.scaled_size(15)
576
- )
438
+ version_font = load_font(font_settings.resource, font_settings.scaled_size(15))
577
439
  version_surface = version_font.render(f"v{__version__}", False, LIGHT_GRAY)
578
- version_rect = version_surface.get_rect(
579
- topleft=(title_rect.right + 4, title_rect.bottom - 4)
580
- )
440
+ version_rect = version_surface.get_rect(topleft=(title_rect.right + 4, title_rect.bottom - 4))
581
441
  screen.blit(version_surface, version_rect)
582
442
 
583
443
  except pygame.error as e: