zombie-escape 1.14.4__py3-none-any.whl → 1.15.2__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 (42) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/config.py +1 -0
  3. zombie_escape/entities.py +126 -199
  4. zombie_escape/entities_constants.py +11 -1
  5. zombie_escape/export_images.py +4 -4
  6. zombie_escape/font_utils.py +47 -0
  7. zombie_escape/gameplay/__init__.py +2 -1
  8. zombie_escape/gameplay/constants.py +4 -0
  9. zombie_escape/gameplay/interactions.py +83 -16
  10. zombie_escape/gameplay/layout.py +9 -15
  11. zombie_escape/gameplay/movement.py +45 -29
  12. zombie_escape/gameplay/spawn.py +15 -29
  13. zombie_escape/gameplay/state.py +62 -7
  14. zombie_escape/gameplay/survivors.py +61 -10
  15. zombie_escape/gameplay/utils.py +33 -0
  16. zombie_escape/level_blueprints.py +35 -31
  17. zombie_escape/level_constants.py +2 -2
  18. zombie_escape/locales/ui.en.json +19 -8
  19. zombie_escape/locales/ui.ja.json +19 -8
  20. zombie_escape/localization.py +7 -1
  21. zombie_escape/models.py +21 -6
  22. zombie_escape/render/__init__.py +2 -2
  23. zombie_escape/render/core.py +113 -81
  24. zombie_escape/render/hud.py +112 -40
  25. zombie_escape/render/overview.py +93 -2
  26. zombie_escape/render/shadows.py +2 -2
  27. zombie_escape/render_constants.py +12 -0
  28. zombie_escape/screens/__init__.py +6 -189
  29. zombie_escape/screens/game_over.py +8 -21
  30. zombie_escape/screens/gameplay.py +71 -26
  31. zombie_escape/screens/settings.py +114 -43
  32. zombie_escape/screens/title.py +128 -47
  33. zombie_escape/stage_constants.py +37 -8
  34. zombie_escape/windowing.py +508 -0
  35. zombie_escape/world_grid.py +7 -5
  36. zombie_escape/zombie_escape.py +26 -13
  37. {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/METADATA +24 -24
  38. zombie_escape-1.15.2.dist-info/RECORD +54 -0
  39. zombie_escape-1.14.4.dist-info/RECORD +0 -53
  40. {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/WHEEL +0 -0
  41. {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/entry_points.txt +0 -0
  42. {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/licenses/LICENSE.txt +0 -0
@@ -9,14 +9,15 @@
9
9
  "fonts": {
10
10
  "primary": {
11
11
  "resource": "assets/fonts/misaki_gothic.ttf",
12
- "scale": 0.7
12
+ "scale": 0.7,
13
+ "line_height_scale": 1.4
13
14
  }
14
15
  },
15
16
  "menu": {
16
17
  "settings": "設定",
17
18
  "quit": "終了",
18
19
  "locked_suffix": "[Locked]",
19
- "window_hint": "[キーでウィンドウを小さく、]キーで大きく。\nF: フルスクリーン切替。",
20
+ "window_hint": "[キーで1段階縮小(400x300)、]キーで1段階拡大。\nF: フルスクリーン切替。",
20
21
  "seed_label": "シード: %{value}",
21
22
  "seed_hint": "数字キーで入力、BSでクリア",
22
23
  "seed_empty": "(自動)",
@@ -24,7 +25,7 @@
24
25
  "stage_select": "ステージ",
25
26
  "resources": "リソース"
26
27
  },
27
- "readme": "README を開く",
28
+ "readme": "README/ライセンス",
28
29
  "readme_stage6": "ステージ6以降の説明を開く",
29
30
  "hints": {
30
31
  "navigate": "上下: 項目選択",
@@ -34,7 +35,7 @@
34
35
  "option_help": {
35
36
  "settings": "設定画面を開いて言語や補助オプションを変更します。",
36
37
  "quit": "ゲームを終了します。",
37
- "readme": "GitHub の README ページをブラウザで開きます。",
38
+ "readme": "GitHub の README ページをブラウザで開きます。ライセンス情報もここから確認できます",
38
39
  "readme_stage6": "ステージ6以降の説明ページをブラウザで開きます。"
39
40
  }
40
41
  },
@@ -67,7 +68,8 @@
67
68
  },
68
69
  "stage5": {
69
70
  "name": "#5 持久戦",
70
- "description": "燃料なし。20分耐え、夜明けになったら徒歩で脱出せよ。"
71
+ "description": "燃料なし。20分耐え、夜明けになったら徒歩で脱出せよ。",
72
+ "intro": "(屋内に燃料はなさそうだ...\n夜明けまで耐えるしかないか。)"
71
73
  },
72
74
  "stage6": {
73
75
  "name": "#6 追ってくる影",
@@ -94,6 +96,7 @@
94
96
  "stage10": {
95
97
  "name": "#10 感染爆発",
96
98
  "description": "建物に避難した生存者の中にゾンビが紛れ込んでいた。",
99
+ "intro": "(広いフロアに避難者が密集している\n...嫌な予感がする。)",
97
100
  "survivor_conversion_messages": [
98
101
  "痛いぃ!",
99
102
  "無理!無理!",
@@ -128,7 +131,8 @@
128
131
  },
129
132
  "stage15": {
130
133
  "name": "#15 分断ライン",
131
- "description": "建物中央を分断する危険地帯。横断には注意。"
134
+ "description": "建物中央を分断する危険地帯。横断には注意。",
135
+ "intro": "天井付近にメンテナンスハッチが\nぼんやりと見えている。"
132
136
  },
133
137
  "stage16": {
134
138
  "name": "#16 奈落",
@@ -145,6 +149,11 @@
145
149
  "stage19": {
146
150
  "name": "#19 回廊",
147
151
  "description": "細い通路が連なるフロア。"
152
+ },
153
+ "stage20": {
154
+ "name": "#20 救出要請",
155
+ "description": "合流して車で脱出せよ。",
156
+ "intro": "「... \nみんな中にいるはずだ。\n合流してくれ。」"
148
157
  }
149
158
  },
150
159
  "status": {
@@ -173,9 +182,9 @@
173
182
  "find_fuel": "燃料を探す",
174
183
  "find_car": "車を探す",
175
184
  "escape": "建物から脱出する",
185
+ "merge_buddy_single": "相棒と合流する",
186
+ "merge_buddy_multi": "同僚と合流する(合流済: %{count}/%{limit}人)",
176
187
  "pickup_buddy": "相棒を救出する",
177
- "find_buddy": "相棒を探す",
178
- "escape_with_buddy": "相棒を車に乗せて脱出する",
179
188
  "board_buddy": "相棒を車に乗せる",
180
189
  "buddy_onboard": "相棒乗車中: %{count}",
181
190
  "escape_with_survivors": "生存者を車に乗せて脱出する",
@@ -188,6 +197,7 @@
188
197
  "menu": "メニュー",
189
198
  "localization": "言語設定",
190
199
  "player_support": "サポート",
200
+ "visual": "視覚",
191
201
  "tougher_enemies": "強敵"
192
202
  },
193
203
  "rows": {
@@ -195,6 +205,7 @@
195
205
  "language": "テキスト言語",
196
206
  "footprints": "足跡",
197
207
  "car_hint": "車のヒント",
208
+ "shadows": "影の表示",
198
209
  "fast_zombies": "高速ゾンビ",
199
210
  "steel_beams": "鉄筋"
200
211
  },
@@ -27,6 +27,7 @@ class LanguageOption:
27
27
  class FontSettings:
28
28
  resource: str | None
29
29
  scale: float = 1.0
30
+ line_height_scale: float = 1.0
30
31
 
31
32
  def scaled_size(self, base_size: int) -> int:
32
33
  return max(1, round(base_size * self.scale))
@@ -122,11 +123,16 @@ def get_font_settings(*, name: str = "primary") -> FontSettings:
122
123
  data = fonts.get(name, {}) if isinstance(fonts, dict) else {}
123
124
  resource = data.get("resource") or DEFAULT_FONT_RESOURCE
124
125
  scale_raw = data.get("scale", DEFAULT_FONT_SCALE)
126
+ line_height_raw = data.get("line_height_scale", 1.0)
125
127
  try:
126
128
  scale = float(scale_raw)
127
129
  except (TypeError, ValueError):
128
130
  scale = DEFAULT_FONT_SCALE
129
- return FontSettings(resource=resource, scale=scale)
131
+ try:
132
+ line_height_scale = float(line_height_raw)
133
+ except (TypeError, ValueError):
134
+ line_height_scale = 1.0
135
+ return FontSettings(resource=resource, scale=scale, line_height_scale=line_height_scale)
130
136
 
131
137
 
132
138
  def _qualify_key(key: str) -> str:
zombie_escape/models.py CHANGED
@@ -20,6 +20,7 @@ from .localization import translate as tr
20
20
 
21
21
  if TYPE_CHECKING: # pragma: no cover - typing-only imports
22
22
  from .entities import Camera, Car, Flashlight, FuelCan, Player, Shoes
23
+ from .level_blueprints import Blueprint
23
24
 
24
25
 
25
26
  @dataclass
@@ -27,6 +28,8 @@ class LevelLayout:
27
28
  """Container for level layout rectangles and cell sets."""
28
29
 
29
30
  field_rect: pygame.Rect
31
+ grid_cols: int
32
+ grid_rows: int
30
33
  outside_cells: set[tuple[int, int]]
31
34
  walkable_cells: list[tuple[int, int]]
32
35
  outer_wall_cells: set[tuple[int, int]]
@@ -77,7 +80,8 @@ class ProgressState:
77
80
 
78
81
  game_over: bool
79
82
  game_won: bool
80
- game_over_message: str | None
83
+ timed_message: "TimedMessage | None"
84
+ fade_in_started_at_ms: int | None
81
85
  game_over_at: int | None
82
86
  scaled_overview: surface.Surface | None
83
87
  overview_created: bool
@@ -91,9 +95,9 @@ class ProgressState:
91
95
  ambient_palette_key: str
92
96
  hint_expires_at: int
93
97
  hint_target_type: str | None
94
- fuel_message_until: int
95
98
  buddy_rescued: int
96
99
  buddy_onboard: int
100
+ buddy_merged_count: int
97
101
  survivors_onboard: int
98
102
  survivors_rescued: int
99
103
  survivor_messages: list
@@ -115,6 +119,17 @@ class ProgressState:
115
119
  player_wall_target_ttl: int
116
120
 
117
121
 
122
+ @dataclass(frozen=True)
123
+ class TimedMessage:
124
+ """Timed HUD message with styling and behavior."""
125
+
126
+ text: str
127
+ expires_at_ms: int
128
+ clear_on_input: bool
129
+ color: tuple[int, int, int] | None
130
+ align: str
131
+
132
+
118
133
  @dataclass
119
134
  class Groups:
120
135
  """Sprite groups container."""
@@ -136,8 +151,7 @@ class GameData:
136
151
  fog: dict
137
152
  stage: Stage
138
153
  cell_size: int
139
- level_width: int
140
- level_height: int
154
+ blueprint: Blueprint | None = None
141
155
  fuel: FuelCan | None = None
142
156
  flashlights: list[Flashlight] | None = None
143
157
  shoes: list[Shoes] | None = None
@@ -154,9 +168,10 @@ class Stage:
154
168
  name_key: str
155
169
  description_key: str
156
170
  available: bool = True
171
+ intro_key: str | None = None
157
172
 
158
173
  # Map layout
159
- tile_size: int = 50
174
+ cell_size: int = 50
160
175
  grid_cols: int = DEFAULT_GRID_COLS
161
176
  grid_rows: int = DEFAULT_GRID_ROWS
162
177
  wall_algorithm: str = "default"
@@ -180,7 +195,7 @@ class Stage:
180
195
  waiting_car_target_count: int = 1
181
196
 
182
197
  # Zombie spawning/aging
183
- # - initial_interior_spawn_rate: fraction of interior floor tiles to seed.
198
+ # - initial_interior_spawn_rate: fraction of interior floor cells to seed.
184
199
  # - spawn weights: pick area by weight (normalized).
185
200
  # - zombie ratios: pick variant by weight (normalized).
186
201
  spawn_interval_ms: int = ZOMBIE_SPAWN_DELAY_MS
@@ -25,6 +25,6 @@ __all__ = [
25
25
  "show_message",
26
26
  "show_message_wrapped",
27
27
  "wrap_text",
28
- "_draw_status_bar",
29
- "_get_fog_scale",
28
+ "_draw_status_bar", # export for testing
29
+ "_get_fog_scale", # export for testing
30
30
  ]
@@ -11,7 +11,6 @@ from pygame import sprite, surface
11
11
  from ..colors import (
12
12
  FOOTPRINT_COLOR,
13
13
  LIGHT_GRAY,
14
- ORANGE,
15
14
  WHITE,
16
15
  YELLOW,
17
16
  get_environment_palette,
@@ -21,7 +20,7 @@ from ..entities import (
21
20
  Player,
22
21
  )
23
22
  from ..entities_constants import INTERNAL_WALL_BEVEL_DEPTH, ZOMBIE_RADIUS
24
- from ..font_utils import load_font
23
+ from ..font_utils import load_font, render_text_scaled
25
24
  from ..gameplay_constants import DEFAULT_FLASHLIGHT_SPAWN_COUNT
26
25
  from ..localization import get_font_settings
27
26
  from ..localization import translate as tr
@@ -32,6 +31,7 @@ from ..render_constants import (
32
31
  FALLING_DUST_COLOR,
33
32
  FALLING_WHIRLWIND_COLOR,
34
33
  FALLING_ZOMBIE_COLOR,
34
+ GAMEPLAY_FONT_SIZE,
35
35
  PITFALL_ABYSS_COLOR,
36
36
  PITFALL_EDGE_DEPTH_OFFSET,
37
37
  PITFALL_EDGE_METAL_COLOR,
@@ -39,11 +39,13 @@ from ..render_constants import (
39
39
  PITFALL_EDGE_STRIPE_SPACING,
40
40
  PLAYER_SHADOW_ALPHA_MULT,
41
41
  PLAYER_SHADOW_RADIUS_MULT,
42
+ FADE_IN_DURATION_MS,
42
43
  )
43
44
  from .hud import (
44
45
  _build_objective_lines,
45
46
  _draw_endurance_timer,
46
47
  _draw_hint_indicator,
48
+ _draw_timed_message,
47
49
  _draw_inventory_icons,
48
50
  _draw_objective,
49
51
  _draw_status_bar,
@@ -65,11 +67,19 @@ def show_message(
65
67
  size: int,
66
68
  color: tuple[int, int, int],
67
69
  position: tuple[int, int],
70
+ *,
71
+ scale_factor: int = 1,
68
72
  ) -> None:
69
73
  try:
70
74
  font_settings = get_font_settings()
71
- font = load_font(font_settings.resource, font_settings.scaled_size(size))
72
- text_surface = font.render(text, False, color)
75
+ scaled_size = font_settings.scaled_size(size)
76
+ text_surface = render_text_scaled(
77
+ font_settings.resource,
78
+ scaled_size,
79
+ text,
80
+ color,
81
+ scale_factor=scale_factor,
82
+ )
73
83
  text_rect = text_surface.get_rect(center=position)
74
84
 
75
85
  # Add a semi-transparent background rectangle for better visibility
@@ -84,6 +94,23 @@ def show_message(
84
94
  print(f"Error rendering font or surface: {e}")
85
95
 
86
96
 
97
+ def _draw_fade_in_overlay(screen: surface.Surface, state: GameData | Any) -> None:
98
+ started_at = getattr(state, "fade_in_started_at_ms", None)
99
+ if started_at is None:
100
+ return
101
+ elapsed = max(0, int(state.elapsed_play_ms) - int(started_at))
102
+ if elapsed <= 0:
103
+ alpha = 255
104
+ else:
105
+ alpha = int(255 * max(0.0, 1.0 - (elapsed / FADE_IN_DURATION_MS)))
106
+ if alpha <= 0:
107
+ return
108
+ overlay = pygame.Surface(screen.get_size())
109
+ overlay.fill((0, 0, 0))
110
+ overlay.set_alpha(alpha)
111
+ screen.blit(overlay, (0, 0))
112
+
113
+
87
114
  def wrap_long_segment(segment: str, font: pygame.font.Font, max_width: int) -> list[str]:
88
115
  lines: list[str] = []
89
116
  current = ""
@@ -137,16 +164,30 @@ def blit_wrapped_text(
137
164
  color: tuple[int, int, int],
138
165
  topleft: tuple[int, int],
139
166
  max_width: int,
167
+ *,
168
+ resource: str | None = None,
169
+ size: int | None = None,
170
+ scale_factor: int = 1,
171
+ line_height_scale: float = 1.0,
140
172
  ) -> None:
141
173
  """Render text with simple wrapping constrained to max_width."""
142
174
 
143
175
  x, y = topleft
144
- line_height = font.get_linesize()
176
+ line_height = int(round(font.get_linesize() * line_height_scale))
145
177
  for line in wrap_text(text, font, max_width):
146
178
  if not line:
147
179
  y += line_height
148
180
  continue
149
- rendered = font.render(line, False, color)
181
+ if resource is not None and size is not None:
182
+ rendered = render_text_scaled(
183
+ resource,
184
+ size,
185
+ line,
186
+ color,
187
+ scale_factor=scale_factor,
188
+ )
189
+ else:
190
+ rendered = font.render(line, False, color)
150
191
  target.blit(rendered, (x, y))
151
192
  y += line_height
152
193
 
@@ -224,14 +265,14 @@ def draw_pause_overlay(screen: pygame.Surface) -> None:
224
265
  show_message(
225
266
  screen,
226
267
  tr("hud.paused"),
227
- 18,
268
+ GAMEPLAY_FONT_SIZE,
228
269
  WHITE,
229
270
  (screen_width // 2, 28),
230
271
  )
231
272
  show_message(
232
273
  screen,
233
274
  tr("hud.pause_hint"),
234
- 16,
275
+ GAMEPLAY_FONT_SIZE,
235
276
  LIGHT_GRAY,
236
277
  (screen_width // 2, screen_height // 2 + 70),
237
278
  )
@@ -291,7 +332,7 @@ def _get_hatch_pattern(
291
332
  *,
292
333
  color: tuple[int, int, int, int] | None = None,
293
334
  ) -> surface.Surface:
294
- """Return cached dot hatch tile surface (Bayer-ordered, optionally chunky)."""
335
+ """Return cached dot hatch cell surface (Bayer-ordered, optionally chunky)."""
295
336
  cache = fog_data.setdefault("hatch_patterns", {})
296
337
  key = (thickness, color)
297
338
  if key in cache:
@@ -491,10 +532,11 @@ def _draw_falling_fx(
491
532
  falling_zombies: list[FallingZombie],
492
533
  flashlight_count: int,
493
534
  dust_rings: list[DustRing],
535
+ now_ms: int,
494
536
  ) -> None:
495
537
  if not falling_zombies and not dust_rings:
496
538
  return
497
- now = pygame.time.get_ticks()
539
+ now = now_ms
498
540
  for fall in falling_zombies:
499
541
  pre_fx_ms = max(0, fall.pre_fx_ms)
500
542
  fall_duration_ms = max(1, fall.fall_duration_ms)
@@ -719,6 +761,7 @@ def _draw_entities(
719
761
  player: Player,
720
762
  *,
721
763
  has_fuel: bool,
764
+ show_fuel_indicator: bool,
722
765
  ) -> pygame.Rect:
723
766
  screen_rect_inflated = screen.get_rect().inflate(100, 100)
724
767
  player_screen_rect: pygame.Rect | None = None
@@ -728,12 +771,13 @@ def _draw_entities(
728
771
  screen.blit(entity.image, sprite_screen_rect)
729
772
  if entity is player:
730
773
  player_screen_rect = sprite_screen_rect
731
- _draw_fuel_indicator(
732
- screen,
733
- player_screen_rect,
734
- has_fuel=has_fuel,
735
- in_car=player.in_car,
736
- )
774
+ if show_fuel_indicator:
775
+ _draw_fuel_indicator(
776
+ screen,
777
+ player_screen_rect,
778
+ has_fuel=has_fuel,
779
+ in_car=player.in_car,
780
+ )
737
781
  return player_screen_rect or camera.apply_rect(player.rect)
738
782
 
739
783
 
@@ -797,25 +841,6 @@ def _draw_fog_of_war(
797
841
  )
798
842
 
799
843
 
800
- def _draw_need_fuel_message(
801
- screen: surface.Surface,
802
- assets: RenderAssets,
803
- *,
804
- has_fuel: bool,
805
- fuel_message_until: int,
806
- elapsed_play_ms: int,
807
- ) -> None:
808
- if has_fuel or fuel_message_until <= elapsed_play_ms:
809
- return
810
- show_message(
811
- screen,
812
- tr("hud.need_fuel"),
813
- 18,
814
- ORANGE,
815
- (assets.screen_width // 2, assets.screen_height // 2),
816
- )
817
-
818
-
819
844
  def draw(
820
845
  assets: RenderAssets,
821
846
  screen: surface.Surface,
@@ -857,54 +882,58 @@ def draw(
857
882
  game_data.layout.fall_spawn_cells,
858
883
  game_data.layout.pitfall_cells,
859
884
  )
860
- shadow_layer = _get_shadow_layer(screen.get_size())
861
- shadow_layer.fill((0, 0, 0, 0))
862
- drew_shadow = _draw_wall_shadows(
863
- shadow_layer,
864
- camera,
865
- wall_cells=game_data.layout.wall_cells,
866
- wall_group=game_data.groups.wall_group,
867
- outer_wall_cells=game_data.layout.outer_wall_cells,
868
- cell_size=game_data.cell_size,
869
- light_source_pos=(None if (stage and stage.endurance_stage and state.dawn_ready) else fov_target.rect.center)
870
- if fov_target
871
- else None,
872
- )
873
- drew_shadow |= _draw_entity_shadows(
874
- shadow_layer,
875
- camera,
876
- all_sprites,
877
- light_source_pos=fov_target.rect.center if fov_target else None,
878
- exclude_car=active_car if player.in_car else None,
879
- outside_cells=outside_cells,
880
- cell_size=game_data.cell_size,
881
- )
882
- player_shadow_alpha = max(1, int(ENTITY_SHADOW_ALPHA * PLAYER_SHADOW_ALPHA_MULT))
883
- player_shadow_radius = int(ZOMBIE_RADIUS * PLAYER_SHADOW_RADIUS_MULT)
884
- if player.in_car:
885
- drew_shadow |= _draw_single_entity_shadow(
885
+ shadows_enabled = config.get("visual", {}).get("shadows", {}).get("enabled", True)
886
+ if shadows_enabled:
887
+ shadow_layer = _get_shadow_layer(screen.get_size())
888
+ shadow_layer.fill((0, 0, 0, 0))
889
+ drew_shadow = _draw_wall_shadows(
886
890
  shadow_layer,
887
891
  camera,
888
- entity=active_car,
889
- light_source_pos=fov_target.rect.center if fov_target else None,
890
- outside_cells=outside_cells,
892
+ wall_cells=game_data.layout.wall_cells,
893
+ wall_group=game_data.groups.wall_group,
894
+ outer_wall_cells=game_data.layout.outer_wall_cells,
891
895
  cell_size=game_data.cell_size,
892
- shadow_radius=player_shadow_radius,
893
- alpha=player_shadow_alpha,
896
+ light_source_pos=(
897
+ None if (stage and stage.endurance_stage and state.dawn_ready) else fov_target.rect.center
898
+ )
899
+ if fov_target
900
+ else None,
894
901
  )
895
- else:
896
- drew_shadow |= _draw_single_entity_shadow(
902
+ drew_shadow |= _draw_entity_shadows(
897
903
  shadow_layer,
898
904
  camera,
899
- entity=player,
905
+ all_sprites,
900
906
  light_source_pos=fov_target.rect.center if fov_target else None,
907
+ exclude_car=active_car if player.in_car else None,
901
908
  outside_cells=outside_cells,
902
909
  cell_size=game_data.cell_size,
903
- shadow_radius=player_shadow_radius,
904
- alpha=player_shadow_alpha,
905
910
  )
906
- if drew_shadow:
907
- screen.blit(shadow_layer, (0, 0))
911
+ player_shadow_alpha = max(1, int(ENTITY_SHADOW_ALPHA * PLAYER_SHADOW_ALPHA_MULT))
912
+ player_shadow_radius = int(ZOMBIE_RADIUS * PLAYER_SHADOW_RADIUS_MULT)
913
+ if player.in_car:
914
+ drew_shadow |= _draw_single_entity_shadow(
915
+ shadow_layer,
916
+ camera,
917
+ entity=active_car,
918
+ light_source_pos=fov_target.rect.center if fov_target else None,
919
+ outside_cells=outside_cells,
920
+ cell_size=game_data.cell_size,
921
+ shadow_radius=player_shadow_radius,
922
+ alpha=player_shadow_alpha,
923
+ )
924
+ else:
925
+ drew_shadow |= _draw_single_entity_shadow(
926
+ shadow_layer,
927
+ camera,
928
+ entity=player,
929
+ light_source_pos=fov_target.rect.center if fov_target else None,
930
+ outside_cells=outside_cells,
931
+ cell_size=game_data.cell_size,
932
+ shadow_radius=player_shadow_radius,
933
+ alpha=player_shadow_alpha,
934
+ )
935
+ if drew_shadow:
936
+ screen.blit(shadow_layer, (0, 0))
908
937
  _draw_footprints(
909
938
  screen,
910
939
  camera,
@@ -918,6 +947,7 @@ def draw(
918
947
  all_sprites,
919
948
  player,
920
949
  has_fuel=has_fuel,
950
+ show_fuel_indicator=not (stage and stage.endurance_stage),
921
951
  )
922
952
 
923
953
  _draw_falling_fx(
@@ -926,6 +956,7 @@ def draw(
926
956
  state.falling_zombies,
927
957
  state.flashlight_count,
928
958
  state.dust_rings,
959
+ state.elapsed_play_ms,
929
960
  )
930
961
 
931
962
  _draw_hint_indicator(
@@ -948,13 +979,6 @@ def draw(
948
979
  flashlight_count=flashlight_count,
949
980
  dawn_ready=state.dawn_ready,
950
981
  )
951
- _draw_need_fuel_message(
952
- screen,
953
- assets,
954
- has_fuel=has_fuel,
955
- fuel_message_until=state.fuel_message_until,
956
- elapsed_play_ms=state.elapsed_play_ms,
957
- )
958
982
 
959
983
  objective_lines = _build_objective_lines(
960
984
  stage=stage,
@@ -962,7 +986,7 @@ def draw(
962
986
  player=player,
963
987
  active_car=active_car,
964
988
  has_fuel=has_fuel,
965
- buddy_onboard=state.buddy_onboard,
989
+ buddy_merged_count=state.buddy_merged_count,
966
990
  buddy_required=stage.buddy_required_count if stage else 0,
967
991
  survivors_onboard=state.survivors_onboard,
968
992
  )
@@ -990,3 +1014,11 @@ def draw(
990
1014
  show_fps=state.show_fps,
991
1015
  fps=fps,
992
1016
  )
1017
+
1018
+ _draw_fade_in_overlay(screen, state)
1019
+ _draw_timed_message(
1020
+ screen,
1021
+ assets,
1022
+ message=state.timed_message,
1023
+ elapsed_play_ms=state.elapsed_play_ms,
1024
+ )