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.
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/PKG-INFO +1 -1
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/__about__.py +1 -1
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/entities.py +20 -5
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/gameplay/logic.py +10 -2
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/locales/ui.en.json +15 -1
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/locales/ui.ja.json +16 -2
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/screens/game_over.py +0 -1
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/screens/gameplay.py +0 -1
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/screens/settings.py +16 -13
- zombie_escape-1.3.6/src/zombie_escape/screens/title.py +363 -0
- zombie_escape-1.3.5/src/zombie_escape/screens/title.py +0 -186
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/.gitignore +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/LICENSE.txt +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/README.md +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/pyproject.toml +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/__init__.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/colors.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/config.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/constants.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/font_utils.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/gameplay/__init__.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/level_blueprints.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/localization.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/models.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/render.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/render_assets.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/rng.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/screens/__init__.py +0 -0
- {zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/zombie_escape.py +0 -0
|
@@ -623,10 +623,24 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
623
623
|
|
|
624
624
|
|
|
625
625
|
class Car(pygame.sprite.Sprite):
|
|
626
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
665
|
+
color = palette["damaged"]
|
|
651
666
|
if health_ratio < 0.3:
|
|
652
|
-
color =
|
|
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
|
},
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
221
|
+
font_settings.resource, font_settings.scaled_size(12)
|
|
222
222
|
)
|
|
223
223
|
highlight_color = (70, 70, 70)
|
|
224
224
|
|
|
225
|
-
row_height =
|
|
226
|
-
start_y =
|
|
225
|
+
row_height = 22
|
|
226
|
+
start_y = 52
|
|
227
227
|
|
|
228
|
-
segment_width =
|
|
229
|
-
segment_height =
|
|
230
|
-
segment_gap =
|
|
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 =
|
|
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,
|
|
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 +
|
|
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 +
|
|
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
|
|
File without changes
|
|
File without changes
|
{zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf
RENAMED
|
File without changes
|
{zombie_escape-1.3.5 → zombie_escape-1.3.6}/src/zombie_escape/assets/fonts/misaki_gothic.ttf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|