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
@@ -34,6 +34,9 @@ ZOMBIE_SEPARATION_DISTANCE = ZOMBIE_RADIUS * 2.2
34
34
  ZOMBIE_AGING_DURATION_FRAMES = FPS * 60 * 6 # ~6 minutes at target framerate
35
35
  ZOMBIE_AGING_MIN_SPEED_RATIO = 0.3
36
36
  ZOMBIE_TRACKER_SCENT_RADIUS = 70
37
+ ZOMBIE_TRACKER_SCAN_RADIUS_MULTIPLIER = 2
38
+ ZOMBIE_TRACKER_SCENT_TOP_K = 3
39
+ ZOMBIE_TRACKER_SCAN_INTERVAL_MS = int(1000 * 30 / FPS)
37
40
  ZOMBIE_TRACKER_WANDER_INTERVAL_MS = 2500
38
41
  ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE = 24
39
42
  ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG = 45
@@ -81,6 +84,9 @@ __all__ = [
81
84
  "ZOMBIE_AGING_DURATION_FRAMES",
82
85
  "ZOMBIE_AGING_MIN_SPEED_RATIO",
83
86
  "ZOMBIE_TRACKER_SCENT_RADIUS",
87
+ "ZOMBIE_TRACKER_SCAN_RADIUS_MULTIPLIER",
88
+ "ZOMBIE_TRACKER_SCENT_TOP_K",
89
+ "ZOMBIE_TRACKER_SCAN_INTERVAL_MS",
84
90
  "ZOMBIE_TRACKER_WANDER_INTERVAL_MS",
85
91
  "ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE",
86
92
  "ZOMBIE_WALL_FOLLOW_PROBE_ANGLE_DEG",
@@ -21,7 +21,7 @@ from .spawn import (
21
21
  spawn_waiting_car,
22
22
  spawn_weighted_zombie,
23
23
  )
24
- from .state import carbonize_outdoor_zombies, initialize_game_state, update_survival_timer
24
+ from .state import carbonize_outdoor_zombies, initialize_game_state, update_endurance_timer
25
25
  from .survivors import (
26
26
  add_survivor_message,
27
27
  apply_passenger_speed_penalty,
@@ -71,7 +71,7 @@ __all__ = [
71
71
  "initialize_game_state",
72
72
  "setup_player_and_cars",
73
73
  "spawn_initial_zombies",
74
- "update_survival_timer",
74
+ "update_endurance_timer",
75
75
  "carbonize_outdoor_zombies",
76
76
  "process_player_input",
77
77
  "update_entities",
@@ -8,11 +8,6 @@ from ..entities_constants import ZOMBIE_AGING_DURATION_FRAMES
8
8
  SURVIVOR_SPEED_PENALTY_PER_PASSENGER = 0.08
9
9
  SURVIVOR_OVERLOAD_DAMAGE_RATIO = 0.2
10
10
  SURVIVOR_MESSAGE_DURATION_MS = 2000
11
- SURVIVOR_CONVERSION_LINE_KEYS = [
12
- "stages.stage4.conversion_lines.line1",
13
- "stages.stage4.conversion_lines.line2",
14
- "stages.stage4.conversion_lines.line3",
15
- ]
16
11
 
17
12
  # --- Footprint settings (gameplay) ---
18
13
  FOOTPRINT_STEP_DISTANCE = 40
@@ -20,7 +15,7 @@ FOOTPRINT_MAX = 320
20
15
 
21
16
  # --- Zombie settings ---
22
17
  MAX_ZOMBIES = 400
23
- ZOMBIE_SPAWN_PLAYER_BUFFER = 140
18
+ ZOMBIE_SPAWN_PLAYER_BUFFER = 230
24
19
  ZOMBIE_TRACKER_AGING_DURATION_FRAMES = ZOMBIE_AGING_DURATION_FRAMES
25
20
 
26
21
  # --- Car and fuel settings ---
@@ -34,7 +29,6 @@ __all__ = [
34
29
  "SURVIVOR_SPEED_PENALTY_PER_PASSENGER",
35
30
  "SURVIVOR_OVERLOAD_DAMAGE_RATIO",
36
31
  "SURVIVOR_MESSAGE_DURATION_MS",
37
- "SURVIVOR_CONVERSION_LINE_KEYS",
38
32
  "FOOTPRINT_STEP_DISTANCE",
39
33
  "FOOTPRINT_MAX",
40
34
  "MAX_ZOMBIES",
@@ -5,7 +5,7 @@ from typing import Any
5
5
  import pygame
6
6
 
7
7
  from .constants import FOOTPRINT_MAX, FOOTPRINT_STEP_DISTANCE
8
- from ..models import GameData
8
+ from ..models import Footprint, GameData
9
9
 
10
10
 
11
11
  def get_shrunk_sprite(
@@ -51,7 +51,7 @@ def update_footprints(game_data: GameData, config: dict[str, Any]) -> None:
51
51
  dist_sq is not None
52
52
  and dist_sq >= FOOTPRINT_STEP_DISTANCE * FOOTPRINT_STEP_DISTANCE
53
53
  ):
54
- footprints.append({"pos": (player.x, player.y), "time": now})
54
+ footprints.append(Footprint(pos=(player.x, player.y), time=now))
55
55
  state.last_footprint_pos = (player.x, player.y)
56
56
 
57
57
  if len(footprints) > FOOTPRINT_MAX:
@@ -45,9 +45,7 @@ def _interaction_radius(width: float, height: float) -> float:
45
45
  RNG = get_rng()
46
46
 
47
47
 
48
- def check_interactions(
49
- game_data: GameData, config: dict[str, Any]
50
- ) -> pygame.sprite.Sprite | None:
48
+ def check_interactions(game_data: GameData, config: dict[str, Any]) -> None:
51
49
  """Check and handle interactions between entities."""
52
50
  player = game_data.player
53
51
  assert player is not None
@@ -193,7 +191,7 @@ def check_interactions(
193
191
  state.hint_target_type = None
194
192
  print("Player entered car!")
195
193
  else:
196
- if not stage.survival_stage:
194
+ if not stage.endurance_stage:
197
195
  now_ms = state.elapsed_play_ms
198
196
  state.fuel_message_until = now_ms + FUEL_HINT_DURATION_MS
199
197
  state.hint_target_type = "fuel"
@@ -221,7 +219,7 @@ def check_interactions(
221
219
  maintain_waiting_car_supply(game_data)
222
220
  print("Player claimed a waiting car!")
223
221
  else:
224
- if not stage.survival_stage:
222
+ if not stage.endurance_stage:
225
223
  now_ms = state.elapsed_play_ms
226
224
  state.fuel_message_until = now_ms + FUEL_HINT_DURATION_MS
227
225
  state.hint_target_type = "fuel"
@@ -321,7 +319,7 @@ def check_interactions(
321
319
 
322
320
  # Player escaping on foot after dawn (Stage 5)
323
321
  if (
324
- stage.survival_stage
322
+ stage.endurance_stage
325
323
  and state.dawn_ready
326
324
  and not player.in_car
327
325
  and outside_rects
@@ -344,11 +342,7 @@ def check_interactions(
344
342
  if stage.rescue_stage and state.survivors_onboard:
345
343
  state.survivors_rescued += state.survivors_onboard
346
344
  state.survivors_onboard = 0
347
- state.next_overload_check_ms = 0
348
345
  apply_passenger_speed_penalty(game_data)
349
346
  state.game_won = True
350
347
 
351
- # Return fog of view target
352
- if not state.game_over and not state.game_won:
353
- return car if player.in_car and car and car.alive() else player
354
348
  return None
@@ -23,6 +23,26 @@ def _rect_for_cell(x_idx: int, y_idx: int, cell_size: int) -> pygame.Rect:
23
23
  )
24
24
 
25
25
 
26
+ def _expand_zone_cells(
27
+ zones: list[tuple[int, int, int, int]],
28
+ *,
29
+ grid_cols: int,
30
+ grid_rows: int,
31
+ ) -> set[tuple[int, int]]:
32
+ cells: set[tuple[int, int]] = set()
33
+ for col, row, width, height in zones:
34
+ if width <= 0 or height <= 0:
35
+ continue
36
+ start_x = max(0, col)
37
+ start_y = max(0, row)
38
+ end_x = min(grid_cols, col + width)
39
+ end_y = min(grid_rows, row + height)
40
+ for y in range(start_y, end_y):
41
+ for x in range(start_x, end_x):
42
+ cells.add((x, y))
43
+ return cells
44
+
45
+
26
46
  def generate_level_from_blueprint(
27
47
  game_data: GameData, config: dict[str, Any]
28
48
  ) -> dict[str, list[pygame.Rect]]:
@@ -74,15 +94,20 @@ def generate_level_from_blueprint(
74
94
  player_cells: list[pygame.Rect] = []
75
95
  car_cells: list[pygame.Rect] = []
76
96
  zombie_cells: list[pygame.Rect] = []
97
+ bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] = {}
77
98
  palette = get_environment_palette(game_data.state.ambient_palette_key)
78
99
 
79
100
  def add_beam_to_groups(beam: SteelBeam) -> None:
80
- if getattr(beam, "_added_to_groups", False):
101
+ if beam._added_to_groups:
81
102
  return
82
103
  wall_group.add(beam)
83
104
  all_sprites.add(beam, layer=0)
84
105
  beam._added_to_groups = True
85
106
 
107
+ def remove_wall_cell(cell: tuple[int, int]) -> None:
108
+ wall_cells.discard(cell)
109
+ outer_wall_cells.discard(cell)
110
+
86
111
  for y, row in enumerate(blueprint):
87
112
  if len(row) != stage.grid_cols:
88
113
  raise ValueError(
@@ -97,6 +122,7 @@ def generate_level_from_blueprint(
97
122
  continue
98
123
  if ch == "B":
99
124
  draw_bottom_side = not _has_wall(x, y + 1)
125
+ wall_cell = (x, y)
100
126
  wall = Wall(
101
127
  cell_rect.x,
102
128
  cell_rect.y,
@@ -107,6 +133,7 @@ def generate_level_from_blueprint(
107
133
  palette_category="outer_wall",
108
134
  bevel_depth=0,
109
135
  draw_bottom_side=draw_bottom_side,
136
+ on_destroy=(lambda _w, cell=wall_cell: remove_wall_cell(cell)),
110
137
  )
111
138
  wall_group.add(wall)
112
139
  all_sprites.add(wall, layer=0)
@@ -139,6 +166,9 @@ def generate_level_from_blueprint(
139
166
  and not _has_wall(x - 1, y)
140
167
  and not _has_wall(x - 1, y + 1),
141
168
  )
169
+ if any(bevel_mask):
170
+ bevel_corners[(x, y)] = bevel_mask
171
+ wall_cell = (x, y)
142
172
  wall = Wall(
143
173
  cell_rect.x,
144
174
  cell_rect.y,
@@ -149,9 +179,11 @@ def generate_level_from_blueprint(
149
179
  palette_category="inner_wall",
150
180
  bevel_mask=bevel_mask,
151
181
  draw_bottom_side=draw_bottom_side,
152
- on_destroy=(lambda _w, b=beam: add_beam_to_groups(b))
153
- if beam
154
- else None,
182
+ on_destroy=(
183
+ (lambda _w, b=beam, cell=wall_cell: (remove_wall_cell(cell), add_beam_to_groups(b)))
184
+ if beam
185
+ else (lambda _w, cell=wall_cell: remove_wall_cell(cell))
186
+ ),
155
187
  )
156
188
  wall_group.add(wall)
157
189
  all_sprites.add(wall, layer=0)
@@ -181,6 +213,13 @@ def generate_level_from_blueprint(
181
213
  game_data.layout.outside_rects = outside_rects
182
214
  game_data.layout.walkable_cells = walkable_cells
183
215
  game_data.layout.outer_wall_cells = outer_wall_cells
216
+ game_data.layout.wall_cells = wall_cells
217
+ game_data.layout.fall_spawn_cells = _expand_zone_cells(
218
+ stage.fall_spawn_zones,
219
+ grid_cols=stage.grid_cols,
220
+ grid_rows=stage.grid_rows,
221
+ )
222
+ game_data.layout.bevel_corners = bevel_corners
184
223
 
185
224
  return {
186
225
  "player_cells": player_cells,
@@ -5,22 +5,31 @@ from typing import Any, Sequence
5
5
 
6
6
  import pygame
7
7
 
8
- from ..entities import Car, Player, Survivor, Wall, WallIndex, Zombie, walls_for_radius
8
+ from ..entities import (
9
+ Car,
10
+ Player,
11
+ Survivor,
12
+ Wall,
13
+ Zombie,
14
+ )
9
15
  from ..entities_constants import (
10
- CAR_SPEED,
11
16
  PLAYER_SPEED,
12
17
  ZOMBIE_SEPARATION_DISTANCE,
13
18
  ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE,
14
19
  )
15
20
  from ..models import GameData
21
+ from ..world_grid import WallIndex, apply_tile_edge_nudge, walls_for_radius
16
22
  from .constants import MAX_ZOMBIES
17
- from .spawn import spawn_weighted_zombie
23
+ from .spawn import spawn_weighted_zombie, update_falling_zombies
18
24
  from .survivors import update_survivors
19
25
  from .utils import rect_visible_on_screen
20
26
 
21
27
 
22
28
  def process_player_input(
23
- keys: Sequence[bool], player: Player, car: Car | None
29
+ keys: Sequence[bool],
30
+ player: Player,
31
+ car: Car | None,
32
+ pad_input: tuple[float, float] = (0.0, 0.0),
24
33
  ) -> tuple[float, float, float, float]:
25
34
  """Process keyboard input and return movement deltas."""
26
35
  dx_input, dy_input = 0, 0
@@ -32,11 +41,13 @@ def process_player_input(
32
41
  dx_input -= 1
33
42
  if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
34
43
  dx_input += 1
44
+ dx_input += pad_input[0]
45
+ dy_input += pad_input[1]
35
46
 
36
47
  player_dx, player_dy, car_dx, car_dy = 0, 0, 0, 0
37
48
 
38
49
  if player.in_car and car and car.alive():
39
- target_speed = getattr(car, "speed", CAR_SPEED)
50
+ target_speed = car.speed
40
51
  move_len = math.hypot(dx_input, dy_input)
41
52
  if move_len > 0:
42
53
  car_dx, car_dy = (
@@ -75,6 +86,8 @@ def update_entities(
75
86
  camera = game_data.camera
76
87
  stage = game_data.stage
77
88
  active_car = car if car and car.alive() else None
89
+ wall_cells = game_data.layout.wall_cells
90
+ bevel_corners = game_data.layout.bevel_corners
78
91
 
79
92
  all_walls = list(wall_group) if wall_index is None else None
80
93
 
@@ -92,14 +105,36 @@ def update_entities(
92
105
 
93
106
  # Update player/car movement
94
107
  if player.in_car and active_car:
108
+ car_dx, car_dy = apply_tile_edge_nudge(
109
+ active_car.x,
110
+ active_car.y,
111
+ car_dx,
112
+ car_dy,
113
+ cell_size=game_data.cell_size,
114
+ wall_cells=wall_cells,
115
+ bevel_corners=bevel_corners,
116
+ grid_cols=stage.grid_cols,
117
+ grid_rows=stage.grid_rows,
118
+ )
95
119
  car_walls = _walls_near((active_car.x, active_car.y), 150.0)
96
- active_car.move(car_dx, car_dy, car_walls)
120
+ active_car.move(car_dx, car_dy, car_walls, walls_nearby=wall_index is not None)
97
121
  player.rect.center = active_car.rect.center
98
122
  player.x, player.y = active_car.x, active_car.y
99
123
  elif not player.in_car:
100
124
  # Ensure player is in all_sprites if not in car
101
125
  if player not in all_sprites:
102
126
  all_sprites.add(player, layer=2)
127
+ player_dx, player_dy = apply_tile_edge_nudge(
128
+ player.x,
129
+ player.y,
130
+ player_dx,
131
+ player_dy,
132
+ cell_size=game_data.cell_size,
133
+ wall_cells=wall_cells,
134
+ bevel_corners=bevel_corners,
135
+ grid_cols=stage.grid_cols,
136
+ grid_rows=stage.grid_rows,
137
+ )
103
138
  player.move(
104
139
  player_dx,
105
140
  player_dy,
@@ -118,11 +153,12 @@ def update_entities(
118
153
  camera.update(target_for_camera)
119
154
 
120
155
  update_survivors(game_data, wall_index=wall_index)
156
+ update_falling_zombies(game_data, config)
121
157
 
122
158
  # Spawn new zombies if needed
123
159
  current_time = pygame.time.get_ticks()
124
160
  spawn_interval = max(1, stage.spawn_interval_ms)
125
- spawn_blocked = stage.survival_stage and game_data.state.dawn_ready
161
+ spawn_blocked = stage.endurance_stage and game_data.state.dawn_ready
126
162
  if (
127
163
  len(zombie_group) < MAX_ZOMBIES
128
164
  and not spawn_blocked
@@ -217,4 +253,6 @@ def update_entities(
217
253
  level_width=game_data.level_width,
218
254
  level_height=game_data.level_height,
219
255
  outer_wall_cells=game_data.layout.outer_wall_cells,
256
+ wall_cells=game_data.layout.wall_cells,
257
+ bevel_corners=game_data.layout.bevel_corners,
220
258
  )