zombie-escape 1.3.5__tar.gz → 1.3.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/PKG-INFO +1 -1
  2. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/__about__.py +1 -1
  3. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/entities.py +20 -5
  4. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/gameplay/logic.py +10 -2
  5. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/locales/ui.en.json +15 -1
  6. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/locales/ui.ja.json +16 -2
  7. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/screens/game_over.py +0 -1
  8. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/screens/gameplay.py +0 -1
  9. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/screens/settings.py +16 -13
  10. zombie_escape-1.3.6/src/zombie_escape/screens/title.py +363 -0
  11. zombie_escape-1.3.5/src/zombie_escape/screens/title.py +0 -186
  12. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/.gitignore +0 -0
  13. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/LICENSE.txt +0 -0
  14. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/README.md +0 -0
  15. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/pyproject.toml +0 -0
  16. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/__init__.py +0 -0
  17. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
  18. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
  19. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/colors.py +0 -0
  20. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/config.py +0 -0
  21. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/constants.py +0 -0
  22. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/font_utils.py +0 -0
  23. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/gameplay/__init__.py +0 -0
  24. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/level_blueprints.py +0 -0
  25. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/localization.py +0 -0
  26. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/models.py +0 -0
  27. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/render.py +0 -0
  28. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/render_assets.py +0 -0
  29. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/rng.py +0 -0
  30. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/screens/__init__.py +0 -0
  31. {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/zombie_escape.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zombie-escape
3
- Version: 1.3.5
3
+ Version: 1.3.6
4
4
  Summary: Top-down zombie survival game built with pygame.
5
5
  Project-URL: Homepage, https://github.com/tos-kamiya/zombie-escape
6
6
  Author-email: Toshihiro Kamiya <kamiya@mbj.nifty.com>
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Toshihiro Kamiya <kamiya@mbj.nifty.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "1.3.5"
4
+ __version__ = "1.3.6"
@@ -623,10 +623,24 @@ class Zombie(pygame.sprite.Sprite):
623
623
 
624
624
 
625
625
  class Car(pygame.sprite.Sprite):
626
- def __init__(self: Self, x: int, y: int) -> None:
626
+ COLOR_SCHEMES: dict[str, dict[str, tuple[int, int, int]]] = {
627
+ "default": {
628
+ "healthy": YELLOW,
629
+ "damaged": ORANGE,
630
+ "critical": DARK_RED,
631
+ },
632
+ "disabled": {
633
+ "healthy": (185, 185, 185),
634
+ "damaged": (150, 150, 150),
635
+ "critical": (110, 110, 110),
636
+ },
637
+ }
638
+
639
+ def __init__(self: Self, x: int, y: int, *, appearance: str = "default") -> None:
627
640
  super().__init__()
628
641
  self.original_image = pygame.Surface((CAR_WIDTH, CAR_HEIGHT), pygame.SRCALPHA)
629
- self.base_color = YELLOW
642
+ self.appearance = appearance if appearance in self.COLOR_SCHEMES else "default"
643
+ self.base_color = self.COLOR_SCHEMES[self.appearance]["healthy"]
630
644
  self.image = self.original_image.copy()
631
645
  self.rect = self.image.get_rect(center=(x, y))
632
646
  self.speed = CAR_SPEED
@@ -645,11 +659,12 @@ class Car(pygame.sprite.Sprite):
645
659
 
646
660
  def update_color(self: Self) -> None:
647
661
  health_ratio = max(0, self.health / self.max_health)
648
- color = YELLOW
662
+ palette = self.COLOR_SCHEMES.get(self.appearance, self.COLOR_SCHEMES["default"])
663
+ color = palette["healthy"]
649
664
  if health_ratio < 0.6:
650
- color = ORANGE
665
+ color = palette["damaged"]
651
666
  if health_ratio < 0.3:
652
- color = DARK_RED
667
+ color = palette["critical"]
653
668
  self.original_image.fill((0, 0, 0, 0))
654
669
 
655
670
  body_rect = pygame.Rect(1, 4, CAR_WIDTH - 2, CAR_HEIGHT - 8)
@@ -79,6 +79,10 @@ from ..entities import (
79
79
  LOGICAL_SCREEN_RECT = pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
80
80
  RNG = get_rng()
81
81
 
82
+
83
+ def car_appearance_for_stage(stage: Stage | None) -> str:
84
+ return "disabled" if stage and stage.survival_stage else "default"
85
+
82
86
  __all__ = [
83
87
  "create_zombie",
84
88
  "rect_for_cell",
@@ -276,6 +280,7 @@ def place_new_car(
276
280
  walkable_cells: list[pygame.Rect],
277
281
  *,
278
282
  existing_cars: Sequence[Car] | None = None,
283
+ appearance: str = "default",
279
284
  ) -> Car | None:
280
285
  if not walkable_cells:
281
286
  return None
@@ -284,7 +289,7 @@ def place_new_car(
284
289
  for attempt in range(max_attempts):
285
290
  cell = RNG.choice(walkable_cells)
286
291
  c_x, c_y = cell.center
287
- temp_car = Car(c_x, c_y)
292
+ temp_car = Car(c_x, c_y, appearance=appearance)
288
293
  temp_rect = temp_car.rect.inflate(30, 30)
289
294
  nearby_walls = pygame.sprite.Group()
290
295
  nearby_walls.add(
@@ -628,6 +633,7 @@ def spawn_waiting_car(game_data: GameData) -> Car | None:
628
633
  if active_car:
629
634
  obstacles.append(active_car)
630
635
  camera = game_data.camera
636
+ appearance = car_appearance_for_stage(game_data.stage)
631
637
  offscreen_attempts = 6
632
638
  while offscreen_attempts > 0:
633
639
  new_car = place_new_car(
@@ -635,6 +641,7 @@ def spawn_waiting_car(game_data: GameData) -> Car | None:
635
641
  player,
636
642
  walkable_cells,
637
643
  existing_cars=obstacles,
644
+ appearance=appearance,
638
645
  )
639
646
  if not new_car:
640
647
  return None
@@ -989,6 +996,7 @@ def setup_player_and_cars(
989
996
 
990
997
  car_candidates = list(layout_data["car_cells"] or walkable_cells)
991
998
  waiting_cars: list[Car] = []
999
+ car_appearance = car_appearance_for_stage(game_data.stage)
992
1000
 
993
1001
  def _pick_car_position() -> tuple[int, int]:
994
1002
  """Favor distant cells for the first car, otherwise fall back to random picks."""
@@ -1011,7 +1019,7 @@ def setup_player_and_cars(
1011
1019
 
1012
1020
  for idx in range(max(1, car_count)):
1013
1021
  car_pos = _pick_car_position()
1014
- car = Car(*car_pos)
1022
+ car = Car(*car_pos, appearance=car_appearance)
1015
1023
  waiting_cars.append(car)
1016
1024
  all_sprites.add(car, layer=1)
1017
1025
  if not car_candidates:
@@ -19,7 +19,21 @@
19
19
  "window_hint": "Resize window: [ to shrink, ] to enlarge (menu only)",
20
20
  "seed_label": "Seed: %{value}",
21
21
  "seed_hint": "Type 0-9 to set a custom seed, Backspace clears",
22
- "seed_empty": "(auto)"
22
+ "seed_empty": "(auto)",
23
+ "sections": {
24
+ "stage_select": "Stages",
25
+ "resources": "Resources"
26
+ },
27
+ "readme": "Open README",
28
+ "hints": {
29
+ "navigate": "Up/Down: choose an option",
30
+ "confirm": "Enter/Space: activate selection"
31
+ },
32
+ "option_help": {
33
+ "settings": "Open Settings to adjust assists and localization.",
34
+ "quit": "Close the game.",
35
+ "readme": "Open the GitHub README in your browser."
36
+ }
23
37
  },
24
38
  "stages": {
25
39
  "stage1": {
@@ -19,7 +19,21 @@
19
19
  "window_hint": "[キーでウィンドウを小さく、]キーで大きく(メニュー画面のみ)",
20
20
  "seed_label": "シード: %{value}",
21
21
  "seed_hint": "数字キーで入力、BSでクリア",
22
- "seed_empty": "(自動)"
22
+ "seed_empty": "(自動)",
23
+ "sections": {
24
+ "stage_select": "ステージ",
25
+ "resources": "リソース"
26
+ },
27
+ "readme": "README を開く",
28
+ "hints": {
29
+ "navigate": "↑↓: 項目選択",
30
+ "confirm": "Enter/Space: 決定または実行"
31
+ },
32
+ "option_help": {
33
+ "settings": "設定画面を開いて言語や補助オプションを変更します。",
34
+ "quit": "ゲームを終了します。",
35
+ "readme": "GitHub の README ページをブラウザで開きます。"
36
+ }
23
37
  },
24
38
  "stages": {
25
39
  "stage1": {
@@ -44,7 +58,7 @@
44
58
  }
45
59
  },
46
60
  "stage5": {
47
- "name": "#5 朝まで耐えろ",
61
+ "name": "#5 持久戦",
48
62
  "description": "燃料なし。20分耐え、夜明けになったら徒歩で脱出せよ。"
49
63
  }
50
64
  },
@@ -6,7 +6,6 @@ import pygame
6
6
  from pygame import surface, time
7
7
 
8
8
  from ..colors import BLACK, GREEN, LIGHT_GRAY, RED, WHITE
9
- from ..font_utils import load_font
10
9
  from ..localization import translate as tr
11
10
  from ..models import GameData, Stage
12
11
  from ..render import (
@@ -271,7 +271,6 @@ def gameplay_screen(
271
271
  player = game_data.player
272
272
  if player is None:
273
273
  raise ValueError("Player missing from game data")
274
- car = game_data.car
275
274
 
276
275
  car_hint_conf = config.get("car_hint", {})
277
276
  hint_delay = car_hint_conf.get("delay_ms", CAR_HINT_DELAY_MS_DEFAULT)
@@ -212,27 +212,29 @@ def settings_screen(
212
212
  try:
213
213
  font_settings = get_font_settings()
214
214
  label_font = load_font(
215
- font_settings.resource, font_settings.scaled_size(11)
215
+ font_settings.resource, font_settings.scaled_size(13)
216
216
  )
217
217
  value_font = load_font(
218
- font_settings.resource, font_settings.scaled_size(11)
218
+ font_settings.resource, font_settings.scaled_size(13)
219
219
  )
220
220
  section_font = load_font(
221
- font_settings.resource, font_settings.scaled_size(11)
221
+ font_settings.resource, font_settings.scaled_size(12)
222
222
  )
223
223
  highlight_color = (70, 70, 70)
224
224
 
225
- row_height = 18
226
- start_y = 44
225
+ row_height = 22
226
+ start_y = 52
227
227
 
228
- segment_width = 26
229
- segment_height = 16
230
- segment_gap = 8
228
+ segment_width = 30
229
+ segment_height = 18
230
+ segment_gap = 10
231
231
  segment_total_width = segment_width * 2 + segment_gap
232
232
 
233
- column_margin = 20
233
+ column_margin = 24
234
234
  column_width = screen_width // 2 - column_margin * 2
235
235
  section_spacing = 6
236
+ row_indent = 12
237
+ value_padding = 20
236
238
 
237
239
  section_states: dict[str, dict] = {}
238
240
  y_cursor = start_y
@@ -258,7 +260,8 @@ def settings_screen(
258
260
  for idx, row in enumerate(rows):
259
261
  section_label = row_sections[idx]
260
262
  state = section_states[section_label]
261
- col_x = column_margin
263
+ col_x = column_margin + row_indent
264
+ row_width = column_width - row_indent + value_padding
262
265
  value = _get_value(
263
266
  row["path"], row.get("easy_value", row.get("choices", [None])[0])
264
267
  )
@@ -266,7 +269,7 @@ def settings_screen(
266
269
  state["next_y"] += row_height
267
270
 
268
271
  highlight_rect = pygame.Rect(
269
- col_x, row_y_current - 2, column_width, row_height - 4
272
+ col_x, row_y_current - 2, row_width, row_height - 4
270
273
  )
271
274
  if idx == selected:
272
275
  pygame.draw.rect(screen, highlight_color, highlight_rect)
@@ -289,14 +292,14 @@ def settings_screen(
289
292
  value_surface = value_font.render(display_text, False, WHITE)
290
293
  value_rect = value_surface.get_rect(
291
294
  midright=(
292
- col_x + column_width,
295
+ col_x + row_width,
293
296
  row_y_current + row_height // 2,
294
297
  )
295
298
  )
296
299
  screen.blit(value_surface, value_rect)
297
300
  else:
298
301
  slider_y = row_y_current + (row_height - segment_height) // 2 - 2
299
- slider_x = col_x + column_width - segment_total_width
302
+ slider_x = col_x + row_width - segment_total_width
300
303
  left_rect = pygame.Rect(
301
304
  slider_x, slider_y, segment_width, segment_height
302
305
  )
@@ -0,0 +1,363 @@
1
+ from __future__ import annotations
2
+
3
+ import webbrowser
4
+ from typing import Any, Sequence
5
+
6
+ import pygame
7
+ from pygame import surface, time
8
+
9
+ from ..colors import BLACK, GRAY, LIGHT_GRAY, WHITE, YELLOW
10
+ from ..font_utils import load_font
11
+ from ..localization import get_font_settings, get_language, translate as tr
12
+ from ..models import Stage
13
+ from ..render import show_message
14
+ from ..screens import ScreenID, ScreenTransition, nudge_window_scale, present
15
+ from ..rng import generate_seed
16
+
17
+ MAX_SEED_DIGITS = 19
18
+ README_URLS: dict[str, str] = {
19
+ "en": "https://github.com/tos-kamiya/zombie-escape/blob/main/README.md",
20
+ "ja": "https://github.com/tos-kamiya/zombie-escape/blob/main/README-ja_JP.md",
21
+ }
22
+
23
+
24
+ def _open_readme_link() -> None:
25
+ """Open the GitHub README for the active UI language."""
26
+
27
+ language = get_language()
28
+ url = README_URLS.get(language, README_URLS["en"])
29
+ try:
30
+ webbrowser.open(url, new=0, autoraise=True)
31
+ except Exception as exc: # pragma: no cover - best effort only
32
+ print(f"Unable to open README URL {url}: {exc}")
33
+
34
+
35
+ def _wrap_long_segment(
36
+ segment: str, font: pygame.font.Font, max_width: int
37
+ ) -> list[str]:
38
+ lines: list[str] = []
39
+ current = ""
40
+ for char in segment:
41
+ candidate = current + char
42
+ if font.size(candidate)[0] <= max_width or not current:
43
+ current = candidate
44
+ else:
45
+ lines.append(current)
46
+ current = char
47
+ if current:
48
+ lines.append(current)
49
+ return lines
50
+
51
+
52
+ def _wrap_text(text: str, font: pygame.font.Font, max_width: int) -> list[str]:
53
+ """Break text into multiple lines within a max width (supports CJK text)."""
54
+
55
+ if max_width <= 0:
56
+ return [text]
57
+ paragraphs = text.splitlines() or [text]
58
+ lines: list[str] = []
59
+ for paragraph in paragraphs:
60
+ if not paragraph:
61
+ lines.append("")
62
+ continue
63
+ words = paragraph.split(" ")
64
+ if len(words) == 1:
65
+ lines.extend(_wrap_long_segment(paragraph, font, max_width))
66
+ continue
67
+ current = ""
68
+ for word in words:
69
+ candidate = f"{current} {word}".strip() if current else word
70
+ if font.size(candidate)[0] <= max_width:
71
+ current = candidate
72
+ continue
73
+ if current:
74
+ lines.append(current)
75
+ if font.size(word)[0] <= max_width:
76
+ current = word
77
+ else:
78
+ lines.extend(_wrap_long_segment(word, font, max_width))
79
+ current = ""
80
+ if current:
81
+ lines.append(current)
82
+ return lines
83
+
84
+
85
+ def _blit_wrapped_text(
86
+ target: surface.Surface,
87
+ text: str,
88
+ font: pygame.font.Font,
89
+ color: tuple[int, int, int],
90
+ topleft: tuple[int, int],
91
+ max_width: int,
92
+ ) -> None:
93
+ """Render text with simple wrapping constrained to max_width."""
94
+
95
+ x, y = topleft
96
+ line_height = font.get_linesize()
97
+ for line in _wrap_text(text, font, max_width):
98
+ if not line:
99
+ y += line_height
100
+ continue
101
+ rendered = font.render(line, False, color)
102
+ target.blit(rendered, (x, y))
103
+ y += line_height
104
+
105
+
106
+ def _generate_auto_seed_text() -> str:
107
+ raw = generate_seed()
108
+ trimmed = raw // 100 # drop lower 2 digits for stability
109
+ return str(trimmed % 100000).zfill(5)
110
+
111
+
112
+ def title_screen(
113
+ screen: surface.Surface,
114
+ clock: time.Clock,
115
+ config: dict[str, Any],
116
+ fps: int,
117
+ *,
118
+ stages: Sequence[Stage],
119
+ default_stage_id: str,
120
+ screen_size: tuple[int, int],
121
+ seed_text: str | None = None,
122
+ seed_is_auto: bool = False,
123
+ ) -> ScreenTransition:
124
+ """Display the title menu and return the selected transition."""
125
+
126
+ width, height = screen_size
127
+ options: list[dict] = [
128
+ {"type": "stage", "stage": stage, "available": stage.available}
129
+ for stage in stages
130
+ if stage.available
131
+ ]
132
+ action_options: list[dict[str, Any]] = [
133
+ {"type": "settings"},
134
+ {"type": "readme"},
135
+ {"type": "quit"},
136
+ ]
137
+ options += action_options
138
+
139
+ selected_stage_index = next(
140
+ (
141
+ i
142
+ for i, opt in enumerate(options)
143
+ if opt["type"] == "stage" and opt["stage"].id == default_stage_id
144
+ ),
145
+ 0,
146
+ )
147
+ selected = min(selected_stage_index, len(options) - 1)
148
+ generated = seed_text is None
149
+ current_seed_text = seed_text if seed_text is not None else _generate_auto_seed_text()
150
+ current_seed_auto = seed_is_auto or generated
151
+
152
+ while True:
153
+ for event in pygame.event.get():
154
+ if event.type == pygame.QUIT:
155
+ return ScreenTransition(
156
+ ScreenID.EXIT,
157
+ seed_text=current_seed_text,
158
+ seed_is_auto=current_seed_auto,
159
+ )
160
+ if event.type == pygame.KEYDOWN:
161
+ if event.key == pygame.K_BACKSPACE:
162
+ current_seed_text = _generate_auto_seed_text()
163
+ current_seed_auto = True
164
+ continue
165
+ if event.unicode and event.unicode.isdigit():
166
+ if current_seed_auto:
167
+ current_seed_text = ""
168
+ current_seed_auto = False
169
+ if len(current_seed_text) < MAX_SEED_DIGITS:
170
+ current_seed_text += event.unicode
171
+ continue
172
+ if event.key == pygame.K_LEFTBRACKET:
173
+ nudge_window_scale(0.5)
174
+ continue
175
+ if event.key == pygame.K_RIGHTBRACKET:
176
+ nudge_window_scale(2.0)
177
+ continue
178
+ if event.key in (pygame.K_UP, pygame.K_w):
179
+ selected = (selected - 1) % len(options)
180
+ elif event.key in (pygame.K_DOWN, pygame.K_s):
181
+ selected = (selected + 1) % len(options)
182
+ elif event.key in (pygame.K_RETURN, pygame.K_SPACE):
183
+ current = options[selected]
184
+ if current["type"] == "stage" and current.get("available"):
185
+ seed_value = int(current_seed_text) if current_seed_text else None
186
+ return ScreenTransition(
187
+ ScreenID.GAMEPLAY,
188
+ stage=current["stage"],
189
+ seed=seed_value,
190
+ seed_text=current_seed_text,
191
+ seed_is_auto=current_seed_auto,
192
+ )
193
+ if current["type"] == "settings":
194
+ return ScreenTransition(
195
+ ScreenID.SETTINGS,
196
+ seed_text=current_seed_text,
197
+ seed_is_auto=current_seed_auto,
198
+ )
199
+ if current["type"] == "readme":
200
+ _open_readme_link()
201
+ continue
202
+ if current["type"] == "quit":
203
+ return ScreenTransition(
204
+ ScreenID.EXIT,
205
+ seed_text=current_seed_text,
206
+ seed_is_auto=current_seed_auto,
207
+ )
208
+
209
+ screen.fill(BLACK)
210
+ show_message(screen, tr("game.title"), 32, LIGHT_GRAY, (width // 2, 40))
211
+
212
+ try:
213
+ font_settings = get_font_settings()
214
+ option_font = load_font(
215
+ font_settings.resource, font_settings.scaled_size(14)
216
+ )
217
+ desc_font = load_font(font_settings.resource, font_settings.scaled_size(11))
218
+ section_font = load_font(
219
+ font_settings.resource, font_settings.scaled_size(12)
220
+ )
221
+ hint_font = load_font(font_settings.resource, font_settings.scaled_size(11))
222
+
223
+ row_height = 22
224
+ stage_column_x = 24
225
+ stage_column_width = width // 2 - 48
226
+ resource_column_x = width // 2 + 12
227
+ resource_column_width = width - resource_column_x - 24
228
+ section_top = 70
229
+ highlight_color = (70, 70, 70)
230
+
231
+ stage_options = [opt for opt in options if opt["type"] == "stage"]
232
+ stage_count = len(stage_options)
233
+ resource_count = len(options) - stage_count
234
+
235
+ stage_header = section_font.render(
236
+ tr("menu.sections.stage_select"), False, LIGHT_GRAY
237
+ )
238
+ stage_header_pos = (stage_column_x, section_top)
239
+ screen.blit(stage_header, stage_header_pos)
240
+ stage_rows_start = stage_header_pos[1] + stage_header.get_height() + 6
241
+
242
+ resource_header = section_font.render(
243
+ tr("menu.sections.resources"), False, LIGHT_GRAY
244
+ )
245
+ resource_header_pos = (resource_column_x, section_top)
246
+ screen.blit(resource_header, resource_header_pos)
247
+ resource_rows_start = (
248
+ resource_header_pos[1] + resource_header.get_height() + 6
249
+ )
250
+
251
+ for idx, option in enumerate(stage_options):
252
+ row_top = stage_rows_start + idx * row_height
253
+ highlight_rect = pygame.Rect(
254
+ stage_column_x, row_top - 2, stage_column_width, row_height
255
+ )
256
+ color = YELLOW if idx == selected else WHITE
257
+ if idx == selected:
258
+ pygame.draw.rect(screen, highlight_color, highlight_rect)
259
+ label = option["stage"].name
260
+ if not option.get("available"):
261
+ locked_suffix = tr("menu.locked_suffix")
262
+ label = f"{label} {locked_suffix}"
263
+ color = GRAY
264
+ text_surface = option_font.render(label, False, color)
265
+ text_rect = text_surface.get_rect(
266
+ midleft=(
267
+ stage_column_x + 8,
268
+ row_top + row_height // 2,
269
+ )
270
+ )
271
+ screen.blit(text_surface, text_rect)
272
+
273
+ for idx, option in enumerate(action_options):
274
+ option_idx = stage_count + idx
275
+ row_top = resource_rows_start + idx * row_height
276
+ highlight_rect = pygame.Rect(
277
+ resource_column_x, row_top - 2, resource_column_width, row_height
278
+ )
279
+ is_selected = option_idx == selected
280
+ if is_selected:
281
+ pygame.draw.rect(screen, highlight_color, highlight_rect)
282
+ if option["type"] == "settings":
283
+ label = tr("menu.settings")
284
+ elif option["type"] == "readme":
285
+ label = f"> {tr('menu.readme')}"
286
+ else:
287
+ label = tr("menu.quit")
288
+ color = YELLOW if is_selected else WHITE
289
+ text_surface = option_font.render(label, False, color)
290
+ text_rect = text_surface.get_rect(
291
+ midleft=(
292
+ resource_column_x + 8,
293
+ row_top + row_height // 2,
294
+ )
295
+ )
296
+ screen.blit(text_surface, text_rect)
297
+
298
+ current = options[selected]
299
+ desc_area_top = stage_rows_start + stage_count * row_height + 12
300
+ if current["type"] == "stage":
301
+ desc_color = LIGHT_GRAY if current.get("available") else GRAY
302
+ _blit_wrapped_text(
303
+ screen,
304
+ current["stage"].description,
305
+ desc_font,
306
+ desc_color,
307
+ (stage_column_x, desc_area_top),
308
+ stage_column_width,
309
+ )
310
+
311
+ option_help_top = resource_rows_start + resource_count * row_height + 12
312
+ help_text = ""
313
+ if current["type"] == "settings":
314
+ help_text = tr("menu.option_help.settings")
315
+ elif current["type"] == "quit":
316
+ help_text = tr("menu.option_help.quit")
317
+ elif current["type"] == "readme":
318
+ help_text = tr("menu.option_help.readme")
319
+
320
+ if help_text:
321
+ _blit_wrapped_text(
322
+ screen,
323
+ help_text,
324
+ desc_font,
325
+ LIGHT_GRAY,
326
+ (resource_column_x, option_help_top),
327
+ resource_column_width,
328
+ )
329
+
330
+ hint_lines = [tr("menu.hints.navigate"), tr("menu.hints.confirm")]
331
+ hint_start_y = height - 96
332
+ for offset, line in enumerate(hint_lines):
333
+ hint_surface = hint_font.render(line, False, LIGHT_GRAY)
334
+ hint_rect = hint_surface.get_rect(
335
+ topleft=(resource_column_x, hint_start_y + offset * 18)
336
+ )
337
+ screen.blit(hint_surface, hint_rect)
338
+
339
+ window_hint = tr("menu.window_hint")
340
+ window_hint_surface = hint_font.render(window_hint, False, LIGHT_GRAY)
341
+ window_hint_rect = window_hint_surface.get_rect(
342
+ center=(width // 2, height - 46)
343
+ )
344
+ screen.blit(window_hint_surface, window_hint_rect)
345
+
346
+ seed_font = load_font(font_settings.resource, font_settings.scaled_size(12))
347
+ seed_value_display = (
348
+ current_seed_text if current_seed_text else tr("menu.seed_empty")
349
+ )
350
+ seed_label = tr("menu.seed_label", value=seed_value_display)
351
+ seed_surface = seed_font.render(seed_label, False, LIGHT_GRAY)
352
+ seed_rect = seed_surface.get_rect(right=width - 14, bottom=height - 12)
353
+ screen.blit(seed_surface, seed_rect)
354
+
355
+ seed_hint = tr("menu.seed_hint")
356
+ seed_hint_surface = hint_font.render(seed_hint, False, GRAY)
357
+ seed_hint_rect = seed_hint_surface.get_rect(left=14, bottom=height - 12)
358
+ screen.blit(seed_hint_surface, seed_hint_rect)
359
+ except pygame.error as e:
360
+ print(f"Error rendering title screen: {e}")
361
+
362
+ present(screen)
363
+ clock.tick(fps)
@@ -1,186 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any, Sequence
4
-
5
- import pygame
6
- from pygame import surface, time
7
-
8
- from ..colors import BLACK, GRAY, LIGHT_GRAY, WHITE, YELLOW
9
- from ..font_utils import load_font
10
- from ..localization import get_font_settings, translate as tr
11
- from ..models import Stage
12
- from ..render import show_message
13
- from ..screens import ScreenID, ScreenTransition, nudge_window_scale, present
14
- from ..rng import generate_seed
15
-
16
- MAX_SEED_DIGITS = 19
17
-
18
-
19
- def _generate_auto_seed_text() -> str:
20
- raw = generate_seed()
21
- trimmed = raw // 100 # drop lower 2 digits for stability
22
- return str(trimmed % 100000).zfill(5)
23
-
24
-
25
- def title_screen(
26
- screen: surface.Surface,
27
- clock: time.Clock,
28
- config: dict[str, Any],
29
- fps: int,
30
- *,
31
- stages: Sequence[Stage],
32
- default_stage_id: str,
33
- screen_size: tuple[int, int],
34
- seed_text: str | None = None,
35
- seed_is_auto: bool = False,
36
- ) -> ScreenTransition:
37
- """Display the title menu and return the selected transition."""
38
-
39
- width, height = screen_size
40
- options: list[dict] = [
41
- {"type": "stage", "stage": stage, "available": stage.available}
42
- for stage in stages
43
- if stage.available
44
- ]
45
- options += [{"type": "settings"}, {"type": "quit"}]
46
-
47
- selected = next(
48
- (
49
- i
50
- for i, opt in enumerate(options)
51
- if opt["type"] == "stage" and opt["stage"].id == default_stage_id
52
- ),
53
- 0,
54
- )
55
- generated = seed_text is None
56
- current_seed_text = seed_text if seed_text is not None else _generate_auto_seed_text()
57
- current_seed_auto = seed_is_auto or generated
58
-
59
- while True:
60
- for event in pygame.event.get():
61
- if event.type == pygame.QUIT:
62
- return ScreenTransition(
63
- ScreenID.EXIT,
64
- seed_text=current_seed_text,
65
- seed_is_auto=current_seed_auto,
66
- )
67
- if event.type == pygame.KEYDOWN:
68
- if event.key == pygame.K_BACKSPACE:
69
- current_seed_text = _generate_auto_seed_text()
70
- current_seed_auto = True
71
- continue
72
- if event.unicode and event.unicode.isdigit():
73
- if current_seed_auto:
74
- current_seed_text = ""
75
- current_seed_auto = False
76
- if len(current_seed_text) < MAX_SEED_DIGITS:
77
- current_seed_text += event.unicode
78
- continue
79
- if event.key == pygame.K_LEFTBRACKET:
80
- nudge_window_scale(0.5)
81
- continue
82
- if event.key == pygame.K_RIGHTBRACKET:
83
- nudge_window_scale(2.0)
84
- continue
85
- if event.key in (pygame.K_UP, pygame.K_w):
86
- selected = (selected - 1) % len(options)
87
- elif event.key in (pygame.K_DOWN, pygame.K_s):
88
- selected = (selected + 1) % len(options)
89
- elif event.key in (pygame.K_RETURN, pygame.K_SPACE):
90
- current = options[selected]
91
- if current["type"] == "stage" and current.get("available"):
92
- seed_value = int(current_seed_text) if current_seed_text else None
93
- return ScreenTransition(
94
- ScreenID.GAMEPLAY,
95
- stage=current["stage"],
96
- seed=seed_value,
97
- seed_text=current_seed_text,
98
- seed_is_auto=current_seed_auto,
99
- )
100
- if current["type"] == "settings":
101
- return ScreenTransition(
102
- ScreenID.SETTINGS,
103
- seed_text=current_seed_text,
104
- seed_is_auto=current_seed_auto,
105
- )
106
- if current["type"] == "quit":
107
- return ScreenTransition(
108
- ScreenID.EXIT,
109
- seed_text=current_seed_text,
110
- seed_is_auto=current_seed_auto,
111
- )
112
-
113
- screen.fill(BLACK)
114
- show_message(
115
- screen,
116
- tr("game.title"),
117
- 32,
118
- LIGHT_GRAY,
119
- (width // 2, 40),
120
- )
121
-
122
- try:
123
- font_settings = get_font_settings()
124
- font = load_font(font_settings.resource, font_settings.scaled_size(18))
125
- line_height = 18
126
- start_y = 84
127
- for idx, option in enumerate(options):
128
- if option["type"] == "stage":
129
- label = option["stage"].name
130
- if not option.get("available"):
131
- locked_suffix = tr("menu.locked_suffix")
132
- label += f" {locked_suffix}"
133
- color = (
134
- YELLOW
135
- if idx == selected
136
- else (WHITE if option.get("available") else GRAY)
137
- )
138
- elif option["type"] == "settings":
139
- label = tr("menu.settings")
140
- color = YELLOW if idx == selected else WHITE
141
- else:
142
- label = tr("menu.quit")
143
- color = YELLOW if idx == selected else WHITE
144
-
145
- text_surface = font.render(label, False, color)
146
- text_rect = text_surface.get_rect(
147
- center=(width // 2, start_y + idx * line_height)
148
- )
149
- screen.blit(text_surface, text_rect)
150
-
151
- current = options[selected]
152
- if current["type"] == "stage":
153
- desc_font = load_font(
154
- font_settings.resource, font_settings.scaled_size(11)
155
- )
156
- desc_color = LIGHT_GRAY if current.get("available") else GRAY
157
- desc_surface = desc_font.render(
158
- current["stage"].description, False, desc_color
159
- )
160
- desc_rect = desc_surface.get_rect(center=(width // 2, height // 2 + 74))
161
- screen.blit(desc_surface, desc_rect)
162
-
163
- seed_font = load_font(font_settings.resource, font_settings.scaled_size(12))
164
- seed_value_display = (
165
- current_seed_text if current_seed_text else tr("menu.seed_empty")
166
- )
167
- seed_label = tr("status.seed", value=seed_value_display)
168
- seed_surface = seed_font.render(seed_label, False, LIGHT_GRAY)
169
- seed_rect = seed_surface.get_rect(right=width - 14, bottom=height - 12)
170
- screen.blit(seed_surface, seed_rect)
171
-
172
- hint_font = load_font(font_settings.resource, font_settings.scaled_size(11))
173
- hint_text = tr("menu.window_hint")
174
- hint_surface = hint_font.render(hint_text, False, LIGHT_GRAY)
175
- hint_rect = hint_surface.get_rect(center=(width // 2, height - 60))
176
- screen.blit(hint_surface, hint_rect)
177
-
178
- seed_hint = tr("menu.seed_hint")
179
- seed_hint_surface = hint_font.render(seed_hint, False, GRAY)
180
- seed_hint_rect = seed_hint_surface.get_rect(left=14, bottom=height - 12)
181
- screen.blit(seed_hint_surface, seed_hint_rect)
182
- except pygame.error as e:
183
- print(f"Error rendering title screen: {e}")
184
-
185
- present(screen)
186
- clock.tick(fps)
File without changes
File without changes
File without changes