zombie-escape 1.12.0__py3-none-any.whl → 1.13.1__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 (34) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/__main__.py +7 -0
  3. zombie_escape/colors.py +22 -14
  4. zombie_escape/entities.py +756 -147
  5. zombie_escape/entities_constants.py +35 -14
  6. zombie_escape/export_images.py +296 -0
  7. zombie_escape/gameplay/__init__.py +2 -1
  8. zombie_escape/gameplay/constants.py +6 -0
  9. zombie_escape/gameplay/footprints.py +4 -0
  10. zombie_escape/gameplay/interactions.py +19 -7
  11. zombie_escape/gameplay/layout.py +103 -34
  12. zombie_escape/gameplay/movement.py +85 -5
  13. zombie_escape/gameplay/spawn.py +139 -90
  14. zombie_escape/gameplay/state.py +18 -9
  15. zombie_escape/gameplay/survivors.py +13 -2
  16. zombie_escape/gameplay/utils.py +40 -21
  17. zombie_escape/level_blueprints.py +256 -19
  18. zombie_escape/locales/ui.en.json +12 -2
  19. zombie_escape/locales/ui.ja.json +12 -2
  20. zombie_escape/models.py +14 -7
  21. zombie_escape/render.py +149 -37
  22. zombie_escape/render_assets.py +419 -124
  23. zombie_escape/render_constants.py +27 -0
  24. zombie_escape/screens/game_over.py +14 -3
  25. zombie_escape/screens/gameplay.py +72 -14
  26. zombie_escape/screens/title.py +18 -7
  27. zombie_escape/stage_constants.py +51 -15
  28. zombie_escape/zombie_escape.py +24 -1
  29. {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/METADATA +41 -15
  30. zombie_escape-1.13.1.dist-info/RECORD +49 -0
  31. zombie_escape-1.12.0.dist-info/RECORD +0 -47
  32. {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/WHEEL +0 -0
  33. {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/entry_points.txt +0 -0
  34. {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,22 +1,29 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import random
3
4
  from typing import Any
4
5
 
5
6
  import pygame
6
7
 
7
8
  from ..colors import get_environment_palette
8
- from ..entities import SteelBeam, Wall
9
- from ..entities_constants import INTERNAL_WALL_HEALTH, STEEL_BEAM_HEALTH
9
+ from ..entities import RubbleWall, SteelBeam, Wall
10
+ from ..entities_constants import (
11
+ INTERNAL_WALL_BEVEL_DEPTH,
12
+ INTERNAL_WALL_HEALTH,
13
+ STEEL_BEAM_HEALTH,
14
+ )
15
+ from ..render_assets import RUBBLE_ROTATION_DEG
10
16
  from .constants import OUTER_WALL_HEALTH
11
- from ..level_blueprints import choose_blueprint
17
+ from ..level_blueprints import MapGenerationError, choose_blueprint
12
18
  from ..models import GameData
13
19
  from ..rng import get_rng
14
20
 
15
- __all__ = ["generate_level_from_blueprint"]
21
+ __all__ = ["generate_level_from_blueprint", "MapGenerationError"]
16
22
 
17
23
  RNG = get_rng()
18
24
 
19
25
 
26
+
20
27
  def _rect_for_cell(x_idx: int, y_idx: int, cell_size: int) -> pygame.Rect:
21
28
  return pygame.Rect(
22
29
  x_idx * cell_size,
@@ -62,17 +69,22 @@ def generate_level_from_blueprint(
62
69
  cols=stage.grid_cols,
63
70
  rows=stage.grid_rows,
64
71
  wall_algo=stage.wall_algorithm,
72
+ pitfall_density=getattr(stage, "pitfall_density", 0.0),
73
+ base_seed=game_data.state.seed,
65
74
  )
66
75
  if isinstance(blueprint_data, dict):
67
76
  blueprint = blueprint_data.get("grid", [])
68
77
  steel_cells_raw = blueprint_data.get("steel_cells", set())
78
+ car_reachable_cells = blueprint_data.get("car_reachable_cells", set())
69
79
  else:
70
80
  blueprint = blueprint_data
71
81
  steel_cells_raw = set()
82
+ car_reachable_cells = set()
72
83
 
73
84
  steel_cells = (
74
85
  {(int(x), int(y)) for x, y in steel_cells_raw} if steel_enabled else set()
75
86
  )
87
+ game_data.layout.car_walkable_cells = car_reachable_cells
76
88
  cell_size = game_data.cell_size
77
89
  outer_wall_cells = {
78
90
  (x, y)
@@ -92,17 +104,22 @@ def generate_level_from_blueprint(
92
104
  return True
93
105
  return (nx, ny) in wall_cells
94
106
 
95
- outside_rects: list[pygame.Rect] = []
96
- walkable_cells: list[pygame.Rect] = []
97
- player_cells: list[pygame.Rect] = []
98
- car_cells: list[pygame.Rect] = []
99
- zombie_cells: list[pygame.Rect] = []
107
+ outside_cells: set[tuple[int, int]] = set()
108
+ walkable_cells: list[tuple[int, int]] = []
109
+ pitfall_cells: set[tuple[int, int]] = set()
110
+ player_cells: list[tuple[int, int]] = []
111
+ car_cells: list[tuple[int, int]] = []
112
+ zombie_cells: list[tuple[int, int]] = []
113
+ fuel_cells: list[tuple[int, int]] = []
114
+ flashlight_cells: list[tuple[int, int]] = []
115
+ shoes_cells: list[tuple[int, int]] = []
100
116
  interior_min_x = 2
101
117
  interior_max_x = stage.grid_cols - 3
102
118
  interior_min_y = 2
103
119
  interior_max_y = stage.grid_rows - 3
104
120
  bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] = {}
105
121
  palette = get_environment_palette(game_data.state.ambient_palette_key)
122
+ rubble_ratio = max(0.0, min(1.0, getattr(stage, "wall_rubble_ratio", 0.0)))
106
123
 
107
124
  def add_beam_to_groups(beam: SteelBeam) -> None:
108
125
  if beam._added_to_groups:
@@ -125,7 +142,7 @@ def generate_level_from_blueprint(
125
142
  cell_rect = _rect_for_cell(x, y, cell_size)
126
143
  cell_has_beam = steel_enabled and (x, y) in steel_cells
127
144
  if ch == "O":
128
- outside_rects.append(cell_rect)
145
+ outside_cells.add((x, y))
129
146
  continue
130
147
  if ch == "B":
131
148
  draw_bottom_side = not _has_wall(x, y + 1)
@@ -145,9 +162,12 @@ def generate_level_from_blueprint(
145
162
  wall_group.add(wall)
146
163
  all_sprites.add(wall, layer=0)
147
164
  continue
165
+ if ch == "x":
166
+ pitfall_cells.add((x, y))
167
+ continue
148
168
  if ch == "E":
149
169
  if not cell_has_beam:
150
- walkable_cells.append(cell_rect)
170
+ walkable_cells.append((x, y))
151
171
  elif ch == "1":
152
172
  beam = None
153
173
  if cell_has_beam:
@@ -176,34 +196,74 @@ def generate_level_from_blueprint(
176
196
  if any(bevel_mask):
177
197
  bevel_corners[(x, y)] = bevel_mask
178
198
  wall_cell = (x, y)
179
- wall = Wall(
180
- cell_rect.x,
181
- cell_rect.y,
182
- cell_rect.width,
183
- cell_rect.height,
184
- health=INTERNAL_WALL_HEALTH,
185
- palette=palette,
186
- palette_category="inner_wall",
187
- bevel_mask=bevel_mask,
188
- draw_bottom_side=draw_bottom_side,
189
- on_destroy=(
190
- (lambda _w, b=beam, cell=wall_cell: (remove_wall_cell(cell), add_beam_to_groups(b)))
191
- if beam
192
- else (lambda _w, cell=wall_cell: remove_wall_cell(cell))
193
- ),
194
- )
199
+ use_rubble = rubble_ratio > 0 and random.random() < rubble_ratio
200
+ if use_rubble:
201
+ rotation_deg = (
202
+ RUBBLE_ROTATION_DEG
203
+ if random.random() < 0.5
204
+ else -RUBBLE_ROTATION_DEG
205
+ )
206
+ wall = RubbleWall(
207
+ cell_rect.x,
208
+ cell_rect.y,
209
+ cell_rect.width,
210
+ cell_rect.height,
211
+ health=INTERNAL_WALL_HEALTH,
212
+ palette=palette,
213
+ palette_category="inner_wall",
214
+ bevel_depth=INTERNAL_WALL_BEVEL_DEPTH,
215
+ rubble_rotation_deg=rotation_deg,
216
+ on_destroy=(
217
+ (
218
+ lambda _w, b=beam, cell=wall_cell: (
219
+ remove_wall_cell(cell),
220
+ add_beam_to_groups(b),
221
+ )
222
+ )
223
+ if beam
224
+ else (lambda _w, cell=wall_cell: remove_wall_cell(cell))
225
+ ),
226
+ )
227
+ else:
228
+ wall = Wall(
229
+ cell_rect.x,
230
+ cell_rect.y,
231
+ cell_rect.width,
232
+ cell_rect.height,
233
+ health=INTERNAL_WALL_HEALTH,
234
+ palette=palette,
235
+ palette_category="inner_wall",
236
+ bevel_mask=bevel_mask,
237
+ draw_bottom_side=draw_bottom_side,
238
+ on_destroy=(
239
+ (
240
+ lambda _w, b=beam, cell=wall_cell: (
241
+ remove_wall_cell(cell),
242
+ add_beam_to_groups(b),
243
+ )
244
+ )
245
+ if beam
246
+ else (lambda _w, cell=wall_cell: remove_wall_cell(cell))
247
+ ),
248
+ )
195
249
  wall_group.add(wall)
196
250
  all_sprites.add(wall, layer=0)
197
251
  else:
198
252
  if not cell_has_beam:
199
- walkable_cells.append(cell_rect)
253
+ walkable_cells.append((x, y))
200
254
 
201
255
  if ch == "P":
202
- player_cells.append(cell_rect)
256
+ player_cells.append((x, y))
203
257
  if ch == "C":
204
- car_cells.append(cell_rect)
258
+ car_cells.append((x, y))
205
259
  if ch == "Z":
206
- zombie_cells.append(cell_rect)
260
+ zombie_cells.append((x, y))
261
+ if ch == "f":
262
+ fuel_cells.append((x, y))
263
+ if ch == "l":
264
+ flashlight_cells.append((x, y))
265
+ if ch == "s":
266
+ shoes_cells.append((x, y))
207
267
 
208
268
  if cell_has_beam and ch != "1":
209
269
  beam = SteelBeam(
@@ -215,12 +275,17 @@ def generate_level_from_blueprint(
215
275
  )
216
276
  add_beam_to_groups(beam)
217
277
 
218
- game_data.layout.outer_rect = (0, 0, game_data.level_width, game_data.level_height)
219
- game_data.layout.inner_rect = (0, 0, game_data.level_width, game_data.level_height)
220
- game_data.layout.outside_rects = outside_rects
278
+ game_data.layout.field_rect = pygame.Rect(
279
+ 0,
280
+ 0,
281
+ game_data.level_width,
282
+ game_data.level_height,
283
+ )
284
+ game_data.layout.outside_cells = outside_cells
221
285
  game_data.layout.walkable_cells = walkable_cells
222
286
  game_data.layout.outer_wall_cells = outer_wall_cells
223
287
  game_data.layout.wall_cells = wall_cells
288
+ game_data.layout.pitfall_cells = pitfall_cells
224
289
  fall_spawn_cells = _expand_zone_cells(
225
290
  stage.fall_spawn_zones,
226
291
  grid_cols=stage.grid_cols,
@@ -246,5 +311,9 @@ def generate_level_from_blueprint(
246
311
  "player_cells": player_cells,
247
312
  "car_cells": car_cells,
248
313
  "zombie_cells": zombie_cells,
314
+ "fuel_cells": fuel_cells,
315
+ "flashlight_cells": flashlight_cells,
316
+ "shoes_cells": shoes_cells,
249
317
  "walkable_cells": walkable_cells,
318
+ "car_walkable_cells": list(car_reachable_cells),
250
319
  }
@@ -13,15 +13,16 @@ from ..entities import (
13
13
  Zombie,
14
14
  )
15
15
  from ..entities_constants import (
16
+ HUMANOID_WALL_BUMP_FRAMES,
16
17
  PLAYER_SPEED,
17
18
  ZOMBIE_SEPARATION_DISTANCE,
18
- ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE,
19
+ ZOMBIE_WALL_HUG_SENSOR_DISTANCE,
19
20
  )
20
21
  from ..gameplay_constants import (
21
22
  SHOES_SPEED_MULTIPLIER_ONE,
22
23
  SHOES_SPEED_MULTIPLIER_TWO,
23
24
  )
24
- from ..models import GameData
25
+ from ..models import FallingZombie, GameData
25
26
  from ..world_grid import WallIndex, apply_tile_edge_nudge, walls_for_radius
26
27
  from .constants import MAX_ZOMBIES
27
28
  from .spawn import spawn_weighted_zombie, update_falling_zombies
@@ -49,9 +50,12 @@ def process_player_input(
49
50
  dx_input += pad_input[0]
50
51
  dy_input += pad_input[1]
51
52
 
53
+ player.update_facing_from_input(dx_input, dy_input)
54
+
52
55
  player_dx, player_dy, car_dx, car_dy = 0, 0, 0, 0
53
56
 
54
57
  if player.in_car and car and car.alive():
58
+ car.update_facing_from_input(dx_input, dy_input)
55
59
  target_speed = car.speed
56
60
  move_len = math.hypot(dx_input, dy_input)
57
61
  if move_len > 0:
@@ -80,6 +84,28 @@ def _shoes_speed_multiplier(shoes_count: int) -> float:
80
84
  return 1.0
81
85
 
82
86
 
87
+ def _handle_pitfall_detection(
88
+ x: float,
89
+ y: float,
90
+ cell_size: int,
91
+ pitfall_cells: set[tuple[int, int]],
92
+ pull_distance: float,
93
+ ) -> tuple[int, int] | None:
94
+ """Check if position is in pitfall and return pulled target coordinates if so."""
95
+ cx, cy = int(x // cell_size), int(y // cell_size)
96
+ if (cx, cy) not in pitfall_cells:
97
+ return None
98
+
99
+ cell_center_x = (cx * cell_size) + (cell_size // 2)
100
+ cell_center_y = (cy * cell_size) + (cell_size // 2)
101
+ dx, dy = cell_center_x - x, cell_center_y - y
102
+ dist = math.hypot(dx, dy)
103
+ if dist > 0:
104
+ move_factor = min(1.0, pull_distance / dist)
105
+ return int(x + dx * move_factor), int(y + dy * move_factor)
106
+ return int(x), int(y)
107
+
108
+
83
109
  def update_entities(
84
110
  game_data: GameData,
85
111
  player_dx: float,
@@ -101,6 +127,8 @@ def update_entities(
101
127
  stage = game_data.stage
102
128
  active_car = car if car and car.alive() else None
103
129
  wall_cells = game_data.layout.wall_cells
130
+ pitfall_cells = game_data.layout.pitfall_cells
131
+ walkable_cells = game_data.layout.walkable_cells
104
132
  bevel_corners = game_data.layout.bevel_corners
105
133
 
106
134
  all_walls = list(wall_group) if wall_index is None else None
@@ -131,7 +159,14 @@ def update_entities(
131
159
  grid_rows=stage.grid_rows,
132
160
  )
133
161
  car_walls = _walls_near((active_car.x, active_car.y), 150.0)
134
- active_car.move(car_dx, car_dy, car_walls, walls_nearby=wall_index is not None)
162
+ active_car.move(
163
+ car_dx,
164
+ car_dy,
165
+ car_walls,
166
+ walls_nearby=wall_index is not None,
167
+ cell_size=game_data.cell_size,
168
+ pitfall_cells=pitfall_cells,
169
+ )
135
170
  player.rect.center = active_car.rect.center
136
171
  player.x, player.y = active_car.x, active_car.y
137
172
  elif not player.in_car:
@@ -157,6 +192,8 @@ def update_entities(
157
192
  cell_size=game_data.cell_size,
158
193
  level_width=game_data.level_width,
159
194
  level_height=game_data.level_height,
195
+ pitfall_cells=pitfall_cells,
196
+ walkable_cells=walkable_cells,
160
197
  )
161
198
  else:
162
199
  # Player flagged as in-car but car is gone; drop them back to foot control
@@ -166,7 +203,25 @@ def update_entities(
166
203
  target_for_camera = active_car if player.in_car and active_car else player
167
204
  camera.update(target_for_camera)
168
205
 
169
- update_survivors(game_data, wall_index=wall_index)
206
+ if player.inner_wall_hit and player.inner_wall_cell is not None:
207
+ game_data.state.player_wall_target_cell = player.inner_wall_cell
208
+ game_data.state.player_wall_target_ttl = HUMANOID_WALL_BUMP_FRAMES
209
+ elif game_data.state.player_wall_target_ttl > 0:
210
+ game_data.state.player_wall_target_ttl -= 1
211
+ if game_data.state.player_wall_target_ttl <= 0:
212
+ game_data.state.player_wall_target_cell = None
213
+
214
+ wall_target_cell = (
215
+ game_data.state.player_wall_target_cell
216
+ if game_data.state.player_wall_target_ttl > 0
217
+ else None
218
+ )
219
+
220
+ update_survivors(
221
+ game_data,
222
+ wall_index=wall_index,
223
+ wall_target_cell=wall_target_cell,
224
+ )
170
225
  update_falling_zombies(game_data, config)
171
226
 
172
227
  # Spawn new zombies if needed
@@ -254,7 +309,7 @@ def update_entities(
254
309
  + (pos[1] - zombie.y) ** 2,
255
310
  )
256
311
  nearby_candidates = _nearby_zombies(idx)
257
- zombie_search_radius = ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE + zombie.radius + 120
312
+ zombie_search_radius = ZOMBIE_WALL_HUG_SENSOR_DISTANCE + zombie.radius + 120
258
313
  nearby_walls = _walls_near((zombie.x, zombie.y), zombie_search_radius)
259
314
  zombie.update(
260
315
  target,
@@ -268,5 +323,30 @@ def update_entities(
268
323
  level_height=game_data.level_height,
269
324
  outer_wall_cells=game_data.layout.outer_wall_cells,
270
325
  wall_cells=game_data.layout.wall_cells,
326
+ pitfall_cells=game_data.layout.pitfall_cells,
271
327
  bevel_corners=game_data.layout.bevel_corners,
272
328
  )
329
+
330
+ # Check zombie pitfall
331
+ pull_dist = zombie.radius * 0.5
332
+ pitfall_target = _handle_pitfall_detection(
333
+ zombie.x,
334
+ zombie.y,
335
+ game_data.cell_size,
336
+ pitfall_cells,
337
+ pull_dist,
338
+ )
339
+ if pitfall_target is not None:
340
+ zombie.kill()
341
+ fall = FallingZombie(
342
+ start_pos=(int(zombie.x), int(zombie.y)),
343
+ target_pos=pitfall_target,
344
+ started_at_ms=pygame.time.get_ticks(),
345
+ pre_fx_ms=0,
346
+ fall_duration_ms=500,
347
+ dust_duration_ms=0,
348
+ tracker=zombie.tracker,
349
+ wall_hugging=zombie.wall_hugging,
350
+ mode="pitfall",
351
+ )
352
+ game_data.state.falling_zombies.append(fall)