zombie-escape 1.7.1__py3-none-any.whl → 1.10.0__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 (36) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/colors.py +41 -8
  3. zombie_escape/entities.py +376 -306
  4. zombie_escape/entities_constants.py +6 -0
  5. zombie_escape/gameplay/__init__.py +2 -2
  6. zombie_escape/gameplay/constants.py +1 -7
  7. zombie_escape/gameplay/footprints.py +2 -2
  8. zombie_escape/gameplay/interactions.py +4 -10
  9. zombie_escape/gameplay/layout.py +43 -4
  10. zombie_escape/gameplay/movement.py +45 -7
  11. zombie_escape/gameplay/spawn.py +283 -43
  12. zombie_escape/gameplay/state.py +19 -16
  13. zombie_escape/gameplay/survivors.py +47 -15
  14. zombie_escape/gameplay/utils.py +19 -1
  15. zombie_escape/input_utils.py +167 -0
  16. zombie_escape/level_blueprints.py +28 -0
  17. zombie_escape/locales/ui.en.json +55 -11
  18. zombie_escape/locales/ui.ja.json +54 -10
  19. zombie_escape/localization.py +28 -0
  20. zombie_escape/models.py +54 -7
  21. zombie_escape/render.py +704 -267
  22. zombie_escape/render_constants.py +12 -0
  23. zombie_escape/screens/__init__.py +1 -0
  24. zombie_escape/screens/game_over.py +8 -4
  25. zombie_escape/screens/gameplay.py +88 -41
  26. zombie_escape/screens/settings.py +124 -13
  27. zombie_escape/screens/title.py +111 -0
  28. zombie_escape/stage_constants.py +116 -3
  29. zombie_escape/world_grid.py +134 -0
  30. zombie_escape/zombie_escape.py +68 -61
  31. {zombie_escape-1.7.1.dist-info → zombie_escape-1.10.0.dist-info}/METADATA +11 -3
  32. zombie_escape-1.10.0.dist-info/RECORD +47 -0
  33. zombie_escape-1.7.1.dist-info/RECORD +0 -45
  34. {zombie_escape-1.7.1.dist-info → zombie_escape-1.10.0.dist-info}/WHEEL +0 -0
  35. {zombie_escape-1.7.1.dist-info → zombie_escape-1.10.0.dist-info}/entry_points.txt +0 -0
  36. {zombie_escape-1.7.1.dist-info → zombie_escape-1.10.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Tuple
4
+
5
+ import pygame
6
+
7
+ DEADZONE = 0.25
8
+ JOY_BUTTON_A = 0
9
+ JOY_BUTTON_BACK = 6
10
+ JOY_BUTTON_START = 7
11
+ JOY_BUTTON_RB = 5
12
+
13
+ CONTROLLER_AVAILABLE = hasattr(pygame, "controller")
14
+ CONTROLLER_DEVICE_ADDED = getattr(pygame, "CONTROLLERDEVICEADDED", None)
15
+ CONTROLLER_DEVICE_REMOVED = getattr(pygame, "CONTROLLERDEVICEREMOVED", None)
16
+ CONTROLLER_BUTTON_DOWN = getattr(pygame, "CONTROLLERBUTTONDOWN", None)
17
+ CONTROLLER_BUTTON_A = getattr(pygame, "CONTROLLER_BUTTON_A", None)
18
+ CONTROLLER_BUTTON_BACK = getattr(pygame, "CONTROLLER_BUTTON_BACK", None)
19
+ CONTROLLER_BUTTON_START = getattr(pygame, "CONTROLLER_BUTTON_START", None)
20
+ CONTROLLER_BUTTON_DPAD_UP = getattr(pygame, "CONTROLLER_BUTTON_DPAD_UP", None)
21
+ CONTROLLER_BUTTON_DPAD_DOWN = getattr(pygame, "CONTROLLER_BUTTON_DPAD_DOWN", None)
22
+ CONTROLLER_BUTTON_DPAD_LEFT = getattr(pygame, "CONTROLLER_BUTTON_DPAD_LEFT", None)
23
+ CONTROLLER_BUTTON_DPAD_RIGHT = getattr(pygame, "CONTROLLER_BUTTON_DPAD_RIGHT", None)
24
+ CONTROLLER_BUTTON_RB = getattr(pygame, "CONTROLLER_BUTTON_RIGHTSHOULDER", None)
25
+ CONTROLLER_AXIS_LEFTX = getattr(pygame, "CONTROLLER_AXIS_LEFTX", None)
26
+ CONTROLLER_AXIS_LEFTY = getattr(pygame, "CONTROLLER_AXIS_LEFTY", None)
27
+ CONTROLLER_AXIS_TRIGGERRIGHT = getattr(
28
+ pygame, "CONTROLLER_AXIS_TRIGGERRIGHT", None
29
+ )
30
+
31
+
32
+ def init_first_controller() -> pygame.controller.Controller | None:
33
+ if not CONTROLLER_AVAILABLE:
34
+ return None
35
+ try:
36
+ if pygame.controller.get_count() > 0:
37
+ controller = pygame.controller.Controller(0)
38
+ if not controller.get_init():
39
+ controller.init()
40
+ return controller
41
+ except pygame.error:
42
+ return None
43
+ return None
44
+
45
+
46
+ def init_first_joystick() -> pygame.joystick.Joystick | None:
47
+ try:
48
+ if pygame.joystick.get_count() > 0:
49
+ joystick = pygame.joystick.Joystick(0)
50
+ if not joystick.get_init():
51
+ joystick.init()
52
+ return joystick
53
+ except pygame.error:
54
+ return None
55
+ return None
56
+
57
+
58
+ def is_confirm_event(event: pygame.event.Event) -> bool:
59
+ if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
60
+ return CONTROLLER_BUTTON_A is not None and event.button == CONTROLLER_BUTTON_A
61
+ if event.type == pygame.JOYBUTTONDOWN:
62
+ return event.button == JOY_BUTTON_A
63
+ return False
64
+
65
+
66
+ def is_start_event(event: pygame.event.Event) -> bool:
67
+ if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
68
+ return (
69
+ CONTROLLER_BUTTON_START is not None
70
+ and event.button == CONTROLLER_BUTTON_START
71
+ )
72
+ if event.type == pygame.JOYBUTTONDOWN:
73
+ return event.button == JOY_BUTTON_START
74
+ return False
75
+
76
+
77
+ def is_select_event(event: pygame.event.Event) -> bool:
78
+ if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
79
+ return (
80
+ CONTROLLER_BUTTON_BACK is not None
81
+ and event.button == CONTROLLER_BUTTON_BACK
82
+ )
83
+ if event.type == pygame.JOYBUTTONDOWN:
84
+ return event.button == JOY_BUTTON_BACK
85
+ return False
86
+
87
+
88
+ def read_gamepad_move(
89
+ controller: pygame.controller.Controller | None,
90
+ joystick: pygame.joystick.Joystick | None,
91
+ *,
92
+ deadzone: float = DEADZONE,
93
+ ) -> Tuple[float, float]:
94
+ x = 0.0
95
+ y = 0.0
96
+ if controller and controller.get_init():
97
+ if CONTROLLER_AXIS_LEFTX is None or CONTROLLER_AXIS_LEFTY is None:
98
+ return 0.0, 0.0
99
+ x = float(controller.get_axis(CONTROLLER_AXIS_LEFTX))
100
+ y = float(controller.get_axis(CONTROLLER_AXIS_LEFTY))
101
+ if abs(x) < deadzone:
102
+ x = 0.0
103
+ if abs(y) < deadzone:
104
+ y = 0.0
105
+ if (
106
+ CONTROLLER_BUTTON_DPAD_LEFT is not None
107
+ and controller.get_button(CONTROLLER_BUTTON_DPAD_LEFT)
108
+ ):
109
+ x = -1.0
110
+ elif (
111
+ CONTROLLER_BUTTON_DPAD_RIGHT is not None
112
+ and controller.get_button(CONTROLLER_BUTTON_DPAD_RIGHT)
113
+ ):
114
+ x = 1.0
115
+ if (
116
+ CONTROLLER_BUTTON_DPAD_UP is not None
117
+ and controller.get_button(CONTROLLER_BUTTON_DPAD_UP)
118
+ ):
119
+ y = -1.0
120
+ elif (
121
+ CONTROLLER_BUTTON_DPAD_DOWN is not None
122
+ and controller.get_button(CONTROLLER_BUTTON_DPAD_DOWN)
123
+ ):
124
+ y = 1.0
125
+ return x, y
126
+
127
+ if joystick and joystick.get_init():
128
+ if joystick.get_numaxes() >= 2:
129
+ x = float(joystick.get_axis(0))
130
+ y = float(joystick.get_axis(1))
131
+ if abs(x) < deadzone:
132
+ x = 0.0
133
+ if abs(y) < deadzone:
134
+ y = 0.0
135
+ if joystick.get_numhats() > 0:
136
+ hat_x, hat_y = joystick.get_hat(0)
137
+ if hat_x:
138
+ x = float(hat_x)
139
+ if hat_y:
140
+ y = float(-hat_y)
141
+ return x, y
142
+
143
+
144
+ def is_accel_active(
145
+ keys: pygame.key.ScancodeWrapper,
146
+ controller: pygame.controller.Controller | None,
147
+ joystick: pygame.joystick.Joystick | None,
148
+ ) -> bool:
149
+ if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
150
+ return True
151
+ if controller and controller.get_init():
152
+ if (
153
+ CONTROLLER_BUTTON_RB is not None
154
+ and controller.get_button(CONTROLLER_BUTTON_RB)
155
+ ):
156
+ return True
157
+ if CONTROLLER_AXIS_TRIGGERRIGHT is not None:
158
+ if controller.get_axis(CONTROLLER_AXIS_TRIGGERRIGHT) > DEADZONE:
159
+ return True
160
+ if joystick and joystick.get_init():
161
+ if joystick.get_numbuttons() > JOY_BUTTON_RB:
162
+ if joystick.get_button(JOY_BUTTON_RB):
163
+ return True
164
+ if joystick.get_numaxes() > 5:
165
+ if joystick.get_axis(5) > DEADZONE:
166
+ return True
167
+ return False
@@ -6,6 +6,7 @@ EXITS_PER_SIDE = 1 # currently fixed to 1 per side (can be tuned)
6
6
  NUM_WALL_LINES = 80 # reduced density (roughly 1/5 of previous 450)
7
7
  WALL_MIN_LEN = 3
8
8
  WALL_MAX_LEN = 10
9
+ SPARSE_WALL_DENSITY = 0.10
9
10
  SPAWN_MARGIN = 3 # keep spawns away from walls/edges
10
11
  SPAWN_ZOMBIES = 3
11
12
 
@@ -201,10 +202,37 @@ def _place_walls_grid_wire(grid: list[list[str]]) -> None:
201
202
  grid[y][x] = "1"
202
203
 
203
204
 
205
+ def _place_walls_sparse(grid: list[list[str]]) -> None:
206
+ """Place isolated wall tiles at a low density, avoiding adjacency."""
207
+ cols, rows = len(grid[0]), len(grid)
208
+ forbidden = _collect_exit_adjacent_cells(grid)
209
+ for y in range(2, rows - 2):
210
+ for x in range(2, cols - 2):
211
+ if (x, y) in forbidden:
212
+ continue
213
+ if grid[y][x] != ".":
214
+ continue
215
+ if RNG.random() >= SPARSE_WALL_DENSITY:
216
+ continue
217
+ if (
218
+ grid[y - 1][x] == "1"
219
+ or grid[y + 1][x] == "1"
220
+ or grid[y][x - 1] == "1"
221
+ or grid[y][x + 1] == "1"
222
+ or grid[y - 1][x - 1] == "1"
223
+ or grid[y - 1][x + 1] == "1"
224
+ or grid[y + 1][x - 1] == "1"
225
+ or grid[y + 1][x + 1] == "1"
226
+ ):
227
+ continue
228
+ grid[y][x] = "1"
229
+
230
+
204
231
  WALL_ALGORITHMS = {
205
232
  "default": _place_walls_default,
206
233
  "empty": _place_walls_empty,
207
234
  "grid_wire": _place_walls_grid_wire,
235
+ "sparse": _place_walls_sparse,
208
236
  }
209
237
 
210
238
 
@@ -28,7 +28,7 @@
28
28
  "hints": {
29
29
  "navigate": "Up/Down: choose an option",
30
30
  "page_switch": "Left/Right: switch stage groups",
31
- "confirm": "Enter/Space: activate selection"
31
+ "confirm": "Enter/Space/South: activate selection"
32
32
  },
33
33
  "option_help": {
34
34
  "settings": "Open Settings to adjust assists and localization.",
@@ -56,7 +56,12 @@
56
56
  "line1": "It hurts!",
57
57
  "line2": "I can't hold on!",
58
58
  "line3": "Stay back!"
59
- }
59
+ },
60
+ "survivor_conversion_messages": [
61
+ "It hurts!",
62
+ "I can't hold on!",
63
+ "Stay back!"
64
+ ]
60
65
  },
61
66
  "stage5": {
62
67
  "name": "#5 Survive Until Dawn",
@@ -75,8 +80,45 @@
75
80
  "description": "Narrow corridors. Get surrounded, and there’s no escape."
76
81
  },
77
82
  "stage9": {
78
- "name": "# Evacuate Survivors 2",
79
- "description": "Evacuate like stage 4, but narrow corridors. Requires fuel."
83
+ "name": "#9 Evacuate Survivors 2",
84
+ "description": "Evacuate like stage 4, but narrow corridors. Requires fuel.",
85
+ "survivor_conversion_messages": [
86
+ "It hurts!",
87
+ "I can't hold on!",
88
+ "Stay back!",
89
+ "Stop!"
90
+ ]
91
+ },
92
+ "stage10": {
93
+ "name": "#10 Outbreak",
94
+ "description": "Zombies have infiltrated a building where survivors took shelter.",
95
+ "survivor_conversion_messages": [
96
+ "It hurts!",
97
+ "I can't hold on!",
98
+ "Stay back!",
99
+ "Stop!",
100
+ "Whoa!",
101
+ "Eeeeek!",
102
+ "Noooo!",
103
+ "Help me!",
104
+ "Get away!",
105
+ "Someone help!",
106
+ "Don't touch me!",
107
+ "No, no, no!",
108
+ "Please!"
109
+ ]
110
+ },
111
+ "stage11": {
112
+ "name": "#11 Don't Look Back!",
113
+ "description": "Left or right. Trust your instinct."
114
+ },
115
+ "stage12": {
116
+ "name": "#12 OVERHEAD HAZARD",
117
+ "description": "An open factory floor. Zombies may fall from above."
118
+ },
119
+ "stage13": {
120
+ "name": "#13 Rescue Buddy 3",
121
+ "description": "Rescue your buddy. Zombies may fall from above."
80
122
  }
81
123
  },
82
124
  "status": {
@@ -91,10 +133,10 @@
91
133
  "hud": {
92
134
  "need_fuel": "Need fuel to drive!",
93
135
  "paused": "PAUSED",
94
- "resume_hint": "Press P or click to resume",
95
- "survival_timer_label": "Until dawn %{time}",
136
+ "pause_hint": "P/Start: Resume · ESC/Select: Return to Title",
137
+ "endurance_timer_label": "Until dawn %{time}",
96
138
  "time_accel": ">> 4x",
97
- "time_accel_hint": "Hold Shift: 4x"
139
+ "time_accel_hint": "Hold Shift/R1: 4x"
98
140
  },
99
141
  "survivors": {
100
142
  "too_many_aboard": "Too many aboard!"
@@ -117,11 +159,13 @@
117
159
  "settings": {
118
160
  "title": "Settings",
119
161
  "sections": {
162
+ "menu": "Menu",
120
163
  "localization": "Localization",
121
164
  "player_support": "Player support",
122
165
  "tougher_enemies": "Tougher enemies"
123
166
  },
124
167
  "rows": {
168
+ "return_to_title": "Return to Title",
125
169
  "language": "Language",
126
170
  "footprints": "Footprints",
127
171
  "car_hint": "Car hint",
@@ -131,9 +175,9 @@
131
175
  "hints": {
132
176
  "navigate": "Up/Down: select a setting",
133
177
  "adjust": "Left/Right: set value",
134
- "toggle": "Space/Enter: toggle value",
178
+ "toggle": "Space/Enter/South: toggle value",
135
179
  "reset": "R: reset to defaults",
136
- "exit": "Esc/Backspace: save and return",
180
+ "exit": "Esc/BS/Select: save and return",
137
181
  "fullscreen": "F: toggle fullscreen"
138
182
  },
139
183
  "config_path": "Config: %{path}",
@@ -146,10 +190,10 @@
146
190
  "game_over": {
147
191
  "win": "YOU ESCAPED!",
148
192
  "lose": "GAME OVER",
149
- "prompt": "ESC/SPACE: Title · R: Retry",
193
+ "prompt": "ESC/SPACE/Select/South: Title · R: Retry",
150
194
  "scream": "AAAAHHH!!",
151
195
  "survivors_summary": "Evacuated: %{count}",
152
- "survival_duration": "Time survived %{time}"
196
+ "endurance_duration": "Time survived %{time}"
153
197
  }
154
198
  }
155
199
  }
@@ -28,7 +28,7 @@
28
28
  "hints": {
29
29
  "navigate": "上下: 項目選択",
30
30
  "page_switch": "左右: ステージ群切替",
31
- "confirm": "Enter/Space: 決定または実行"
31
+ "confirm": "Enter/Space/South: 決定または実行"
32
32
  },
33
33
  "option_help": {
34
34
  "settings": "設定画面を開いて言語や補助オプションを変更します。",
@@ -56,7 +56,12 @@
56
56
  "line1": "痛いぃ!",
57
57
  "line2": "無理!無理!",
58
58
  "line3": "来るなぁ!"
59
- }
59
+ },
60
+ "survivor_conversion_messages": [
61
+ "痛いぃ!",
62
+ "無理!無理!",
63
+ "来るなぁ!"
64
+ ]
60
65
  },
61
66
  "stage5": {
62
67
  "name": "#5 持久戦",
@@ -76,7 +81,44 @@
76
81
  },
77
82
  "stage9": {
78
83
  "name": "#9 生存者救出 2",
79
- "description": "ステージ4と同じだが、狭い通路。燃料も必要。"
84
+ "description": "ステージ4と同じだが、狭い通路。燃料も必要。",
85
+ "survivor_conversion_messages": [
86
+ "痛いぃ!",
87
+ "無理!無理!",
88
+ "来るなぁ!",
89
+ "やめてぇ!"
90
+ ]
91
+ },
92
+ "stage10": {
93
+ "name": "#10 感染爆発",
94
+ "description": "建物に避難した生存者の中にゾンビが紛れ込んでいた。",
95
+ "survivor_conversion_messages": [
96
+ "痛いぃ!",
97
+ "無理!無理!",
98
+ "来るなぁ!",
99
+ "やめてぇ!",
100
+ "うわっ!",
101
+ "ひいいぃ",
102
+ "いやああ",
103
+ "助けて!",
104
+ "近寄るな!",
105
+ "誰か!",
106
+ "触るな!",
107
+ "やだ、やだ!",
108
+ "誰かー!"
109
+ ]
110
+ },
111
+ "stage11": {
112
+ "name": "#11 振り返るな!",
113
+ "description": "右か左か。直感を信じて。"
114
+ },
115
+ "stage12": {
116
+ "name": "#12 頭上注意",
117
+ "description": "吹き抜けの工場。落ちてくるゾンビあり。"
118
+ },
119
+ "stage13": {
120
+ "name": "#13 相棒を救え 3",
121
+ "description": "はぐれた相棒を救え。ゾンビの落下あり。"
80
122
  }
81
123
  },
82
124
  "status": {
@@ -91,10 +133,10 @@
91
133
  "hud": {
92
134
  "need_fuel": "燃料が必要です!",
93
135
  "paused": "一時停止",
94
- "resume_hint": "Pキーかクリックで再開",
95
- "survival_timer_label": "夜明けまであと %{time}",
136
+ "pause_hint": "P/Start: ゲーム再開 ESC/Select: タイトルに戻る",
137
+ "endurance_timer_label": "夜明けまであと %{time}",
96
138
  "time_accel": ">> 4x",
97
- "time_accel_hint": "Shiftキー押下: 4x"
139
+ "time_accel_hint": "Shift/R1押下: 4x"
98
140
  },
99
141
  "survivors": {
100
142
  "too_many_aboard": "定員オーバー!"
@@ -117,11 +159,13 @@
117
159
  "settings": {
118
160
  "title": "設定",
119
161
  "sections": {
162
+ "menu": "メニュー",
120
163
  "localization": "言語設定",
121
164
  "player_support": "サポート",
122
165
  "tougher_enemies": "強敵"
123
166
  },
124
167
  "rows": {
168
+ "return_to_title": "タイトルに戻る",
125
169
  "language": "テキスト言語",
126
170
  "footprints": "足跡",
127
171
  "car_hint": "車のヒント",
@@ -131,9 +175,9 @@
131
175
  "hints": {
132
176
  "navigate": "上下: 項目選択",
133
177
  "adjust": "左右: 値の設定",
134
- "toggle": "スペース/エンター: 値のトグル",
178
+ "toggle": "Space/Enter/South: 値のトグル",
135
179
  "reset": "R: デフォルト設定に戻す",
136
- "exit": "Esc/BS: セーブして戻る",
180
+ "exit": "Esc/BS/Select: セーブして戻る",
137
181
  "fullscreen": "F: フルスクリーン切替"
138
182
  },
139
183
  "config_path": "設定ファイル: %{path}",
@@ -146,10 +190,10 @@
146
190
  "game_over": {
147
191
  "win": "脱出成功!",
148
192
  "lose": "ゲームオーバー",
149
- "prompt": "ESC/Space: タイトルへ R: 再挑戦",
193
+ "prompt": "ESC/Space/Select/South: タイトルへ R: 再挑戦",
150
194
  "scream": "ぎゃあーーー!!",
151
195
  "survivors_summary": "救出人数: %{count}",
152
- "survival_duration": "逃げ延びた時間 %{time}"
196
+ "endurance_duration": "逃げ延びた時間 %{time}"
153
197
  }
154
198
  }
155
199
  }
@@ -108,6 +108,13 @@ def translate_dict(key: str) -> dict[str, Any]:
108
108
  return result if isinstance(result, dict) else {}
109
109
 
110
110
 
111
+ def translate_list(key: str) -> list[Any]:
112
+ if not _CONFIGURED:
113
+ set_language(_CURRENT_LANGUAGE)
114
+ result = _lookup_locale_value(key)
115
+ return result if isinstance(result, list) else []
116
+
117
+
111
118
  def get_font_settings(*, name: str = "primary") -> FontSettings:
112
119
  _get_language_options() # ensure locale data is loaded
113
120
  locale_data = _LOCALE_DATA.get(_CURRENT_LANGUAGE) or _LOCALE_DATA.get(
@@ -128,6 +135,26 @@ def _qualify_key(key: str) -> str:
128
135
  return key if key.startswith("ui.") else f"ui.{key}"
129
136
 
130
137
 
138
+ def _lookup_locale_value(key: str) -> Any:
139
+ locale_data = _LOCALE_DATA.get(_CURRENT_LANGUAGE) or _LOCALE_DATA.get(
140
+ DEFAULT_LANGUAGE, {}
141
+ )
142
+ if not isinstance(locale_data, dict):
143
+ return None
144
+ qualified = _qualify_key(key)
145
+ path = qualified.split(".")
146
+ if path and path[0] == "ui":
147
+ path = path[1:]
148
+ current: Any = locale_data
149
+ for segment in path:
150
+ if not isinstance(current, dict):
151
+ return None
152
+ current = current.get(segment)
153
+ if current is None:
154
+ return None
155
+ return current
156
+
157
+
131
158
  def _get_language_options() -> tuple[LanguageOption, ...]:
132
159
  global _LANGUAGE_OPTIONS
133
160
  if _LANGUAGE_OPTIONS is not None:
@@ -182,5 +209,6 @@ __all__ = [
182
209
  "language_options",
183
210
  "set_language",
184
211
  "translate",
212
+ "translate_list",
185
213
  "translate_dict",
186
214
  ]
zombie_escape/models.py CHANGED
@@ -9,7 +9,11 @@ import pygame
9
9
  from pygame import sprite, surface
10
10
 
11
11
  from .entities_constants import ZOMBIE_AGING_DURATION_FRAMES
12
- from .gameplay_constants import SURVIVOR_SPAWN_RATE, ZOMBIE_SPAWN_DELAY_MS
12
+ from .gameplay_constants import (
13
+ DEFAULT_FLASHLIGHT_SPAWN_COUNT,
14
+ SURVIVOR_SPAWN_RATE,
15
+ ZOMBIE_SPAWN_DELAY_MS,
16
+ )
13
17
  from .level_constants import DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS
14
18
  from .localization import translate as tr
15
19
 
@@ -26,6 +30,41 @@ class LevelLayout:
26
30
  outside_rects: list[pygame.Rect]
27
31
  walkable_cells: list[pygame.Rect]
28
32
  outer_wall_cells: set[tuple[int, int]]
33
+ wall_cells: set[tuple[int, int]]
34
+ fall_spawn_cells: set[tuple[int, int]]
35
+ bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]]
36
+
37
+
38
+ @dataclass
39
+ class FallingZombie:
40
+ """Represents a zombie falling toward a target position."""
41
+
42
+ start_pos: tuple[int, int]
43
+ target_pos: tuple[int, int]
44
+ started_at_ms: int
45
+ pre_fx_ms: int
46
+ fall_duration_ms: int
47
+ dust_duration_ms: int
48
+ tracker: bool
49
+ wall_follower: bool
50
+ dust_started: bool = False
51
+
52
+
53
+ @dataclass
54
+ class DustRing:
55
+ """Short-lived dust ring spawned on impact."""
56
+
57
+ pos: tuple[int, int]
58
+ started_at_ms: int
59
+ duration_ms: int
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Footprint:
64
+ """Tracked player footprint."""
65
+
66
+ pos: tuple[float, float]
67
+ time: int
29
68
 
30
69
 
31
70
  @dataclass
@@ -38,8 +77,8 @@ class ProgressState:
38
77
  game_over_at: int | None
39
78
  scaled_overview: surface.Surface | None
40
79
  overview_created: bool
41
- footprints: list
42
- last_footprint_pos: tuple | None
80
+ footprints: list[Footprint]
81
+ last_footprint_pos: tuple[float, float] | None
43
82
  elapsed_play_ms: int
44
83
  has_fuel: bool
45
84
  flashlight_count: int
@@ -54,14 +93,17 @@ class ProgressState:
54
93
  survivor_messages: list
55
94
  survivor_capacity: int
56
95
  seed: int | None
57
- survival_elapsed_ms: int
58
- survival_goal_ms: int
96
+ endurance_elapsed_ms: int
97
+ endurance_goal_ms: int
59
98
  dawn_ready: bool
60
99
  dawn_prompt_at: int | None
61
100
  time_accel_active: bool
62
101
  last_zombie_spawn_time: int
63
102
  dawn_carbonized: bool
64
103
  debug_mode: bool
104
+ falling_zombies: list[FallingZombie]
105
+ falling_spawn_carry: int
106
+ dust_rings: list[DustRing]
65
107
 
66
108
 
67
109
  @dataclass
@@ -107,14 +149,17 @@ class Stage:
107
149
  requires_fuel: bool = False
108
150
  buddy_required_count: int = 0
109
151
  rescue_stage: bool = False
110
- survival_stage: bool = False
111
- survival_goal_ms: int = 0
152
+ endurance_stage: bool = False
153
+ endurance_goal_ms: int = 0
112
154
  fuel_spawn_count: int = 1
155
+ initial_flashlight_count: int = DEFAULT_FLASHLIGHT_SPAWN_COUNT
113
156
  survivor_spawn_rate: float = SURVIVOR_SPAWN_RATE
114
157
  spawn_interval_ms: int = ZOMBIE_SPAWN_DELAY_MS
115
158
  initial_interior_spawn_rate: float = 0.015
116
159
  exterior_spawn_weight: float = 1.0
117
160
  interior_spawn_weight: float = 0.0
161
+ interior_fall_spawn_weight: float = 0.0
162
+ fall_spawn_zones: list[tuple[int, int, int, int]] = field(default_factory=list)
118
163
  zombie_tracker_ratio: float = 0.0
119
164
  zombie_wall_follower_ratio: float = 0.0
120
165
  zombie_normal_ratio: float = 1.0
@@ -133,6 +178,8 @@ class Stage:
133
178
 
134
179
  __all__ = [
135
180
  "LevelLayout",
181
+ "FallingZombie",
182
+ "DustRing",
136
183
  "ProgressState",
137
184
  "Groups",
138
185
  "GameData",