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.
- zombie_escape/__about__.py +1 -1
- zombie_escape/__main__.py +7 -0
- zombie_escape/colors.py +22 -14
- zombie_escape/entities.py +756 -147
- zombie_escape/entities_constants.py +35 -14
- zombie_escape/export_images.py +296 -0
- zombie_escape/gameplay/__init__.py +2 -1
- zombie_escape/gameplay/constants.py +6 -0
- zombie_escape/gameplay/footprints.py +4 -0
- zombie_escape/gameplay/interactions.py +19 -7
- zombie_escape/gameplay/layout.py +103 -34
- zombie_escape/gameplay/movement.py +85 -5
- zombie_escape/gameplay/spawn.py +139 -90
- zombie_escape/gameplay/state.py +18 -9
- zombie_escape/gameplay/survivors.py +13 -2
- zombie_escape/gameplay/utils.py +40 -21
- zombie_escape/level_blueprints.py +256 -19
- zombie_escape/locales/ui.en.json +12 -2
- zombie_escape/locales/ui.ja.json +12 -2
- zombie_escape/models.py +14 -7
- zombie_escape/render.py +149 -37
- zombie_escape/render_assets.py +419 -124
- zombie_escape/render_constants.py +27 -0
- zombie_escape/screens/game_over.py +14 -3
- zombie_escape/screens/gameplay.py +72 -14
- zombie_escape/screens/title.py +18 -7
- zombie_escape/stage_constants.py +51 -15
- zombie_escape/zombie_escape.py +24 -1
- {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/METADATA +41 -15
- zombie_escape-1.13.1.dist-info/RECORD +49 -0
- zombie_escape-1.12.0.dist-info/RECORD +0 -47
- {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/WHEEL +0 -0
- {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.12.0.dist-info → zombie_escape-1.13.1.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/gameplay/spawn.py
CHANGED
|
@@ -35,6 +35,9 @@ from ..render_constants import (
|
|
|
35
35
|
)
|
|
36
36
|
from ..rng import get_rng
|
|
37
37
|
from .constants import (
|
|
38
|
+
FALLING_ZOMBIE_DUST_DURATION_MS,
|
|
39
|
+
FALLING_ZOMBIE_DURATION_MS,
|
|
40
|
+
FALLING_ZOMBIE_PRE_FX_MS,
|
|
38
41
|
MAX_ZOMBIES,
|
|
39
42
|
ZOMBIE_SPAWN_PLAYER_BUFFER,
|
|
40
43
|
ZOMBIE_TRACKER_AGING_DURATION_FRAMES,
|
|
@@ -68,6 +71,13 @@ __all__ = [
|
|
|
68
71
|
]
|
|
69
72
|
|
|
70
73
|
|
|
74
|
+
def _cell_center(cell: tuple[int, int], cell_size: int) -> tuple[int, int]:
|
|
75
|
+
return (
|
|
76
|
+
int((cell[0] * cell_size) + (cell_size / 2)),
|
|
77
|
+
int((cell[1] * cell_size) + (cell_size / 2)),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
71
81
|
def _car_appearance_for_stage(stage: Stage | None) -> str:
|
|
72
82
|
return "disabled" if stage and stage.endurance_stage else "default"
|
|
73
83
|
|
|
@@ -75,22 +85,22 @@ def _car_appearance_for_stage(stage: Stage | None) -> str:
|
|
|
75
85
|
def _pick_zombie_variant(stage: Stage | None) -> tuple[bool, bool]:
|
|
76
86
|
normal_ratio = 1.0
|
|
77
87
|
tracker_ratio = 0.0
|
|
78
|
-
|
|
88
|
+
wall_hugging_ratio = 0.0
|
|
79
89
|
if stage is not None:
|
|
80
90
|
normal_ratio = max(0.0, min(1.0, stage.zombie_normal_ratio))
|
|
81
91
|
tracker_ratio = max(0.0, min(1.0, stage.zombie_tracker_ratio))
|
|
82
|
-
|
|
83
|
-
if normal_ratio + tracker_ratio +
|
|
92
|
+
wall_hugging_ratio = max(0.0, min(1.0, stage.zombie_wall_hugging_ratio))
|
|
93
|
+
if normal_ratio + tracker_ratio + wall_hugging_ratio <= 0:
|
|
84
94
|
normal_ratio = 1.0
|
|
85
95
|
tracker_ratio = 0.0
|
|
86
|
-
|
|
96
|
+
wall_hugging_ratio = 0.0
|
|
87
97
|
if (
|
|
88
98
|
normal_ratio == 1.0
|
|
89
|
-
and (tracker_ratio > 0.0 or
|
|
90
|
-
and tracker_ratio +
|
|
99
|
+
and (tracker_ratio > 0.0 or wall_hugging_ratio > 0.0)
|
|
100
|
+
and tracker_ratio + wall_hugging_ratio <= 1.0
|
|
91
101
|
):
|
|
92
|
-
normal_ratio = max(0.0, 1.0 - tracker_ratio -
|
|
93
|
-
total_ratio = normal_ratio + tracker_ratio +
|
|
102
|
+
normal_ratio = max(0.0, 1.0 - tracker_ratio - wall_hugging_ratio)
|
|
103
|
+
total_ratio = normal_ratio + tracker_ratio + wall_hugging_ratio
|
|
94
104
|
if total_ratio <= 0:
|
|
95
105
|
return False, False
|
|
96
106
|
pick = RNG.random() * total_ratio
|
|
@@ -203,7 +213,7 @@ def _schedule_falling_zombie(
|
|
|
203
213
|
if len(zombie_group) + len(state.falling_zombies) >= MAX_ZOMBIES:
|
|
204
214
|
return "blocked"
|
|
205
215
|
min_distance = game_data.stage.tile_size * 0.5
|
|
206
|
-
tracker,
|
|
216
|
+
tracker, wall_hugging = _pick_zombie_variant(game_data.stage)
|
|
207
217
|
|
|
208
218
|
def _candidate_clear(pos: tuple[int, int]) -> bool:
|
|
209
219
|
candidate = _create_zombie(
|
|
@@ -211,7 +221,7 @@ def _schedule_falling_zombie(
|
|
|
211
221
|
start_pos=pos,
|
|
212
222
|
stage=game_data.stage,
|
|
213
223
|
tracker=tracker,
|
|
214
|
-
|
|
224
|
+
wall_hugging=wall_hugging,
|
|
215
225
|
)
|
|
216
226
|
return _is_spawn_position_clear(game_data, candidate)
|
|
217
227
|
|
|
@@ -224,17 +234,17 @@ def _schedule_falling_zombie(
|
|
|
224
234
|
if allow_carry:
|
|
225
235
|
state.falling_spawn_carry += 1
|
|
226
236
|
return "no_position"
|
|
227
|
-
start_offset
|
|
228
|
-
start_pos = (int(spawn_pos[0]), int(spawn_pos[1]
|
|
237
|
+
# start_offset removed; animation handles "falling" via scaling now.
|
|
238
|
+
start_pos = (int(spawn_pos[0]), int(spawn_pos[1]))
|
|
229
239
|
fall = FallingZombie(
|
|
230
240
|
start_pos=start_pos,
|
|
231
241
|
target_pos=(int(spawn_pos[0]), int(spawn_pos[1])),
|
|
232
242
|
started_at_ms=pygame.time.get_ticks(),
|
|
233
|
-
pre_fx_ms=
|
|
234
|
-
fall_duration_ms=
|
|
235
|
-
dust_duration_ms=
|
|
243
|
+
pre_fx_ms=FALLING_ZOMBIE_PRE_FX_MS,
|
|
244
|
+
fall_duration_ms=FALLING_ZOMBIE_DURATION_MS,
|
|
245
|
+
dust_duration_ms=FALLING_ZOMBIE_DUST_DURATION_MS,
|
|
236
246
|
tracker=tracker,
|
|
237
|
-
|
|
247
|
+
wall_hugging=wall_hugging,
|
|
238
248
|
)
|
|
239
249
|
state.falling_zombies.append(fall)
|
|
240
250
|
return "scheduled"
|
|
@@ -247,7 +257,7 @@ def _create_zombie(
|
|
|
247
257
|
hint_pos: tuple[float, float] | None = None,
|
|
248
258
|
stage: Stage | None = None,
|
|
249
259
|
tracker: bool | None = None,
|
|
250
|
-
|
|
260
|
+
wall_hugging: bool | None = None,
|
|
251
261
|
) -> Zombie:
|
|
252
262
|
"""Factory to create zombies with optional fast variants."""
|
|
253
263
|
fast_conf = config.get("fast_zombies", {})
|
|
@@ -264,14 +274,14 @@ def _create_zombie(
|
|
|
264
274
|
)
|
|
265
275
|
else:
|
|
266
276
|
aging_duration_frames = ZOMBIE_AGING_DURATION_FRAMES
|
|
267
|
-
if tracker is None or
|
|
268
|
-
picked_tracker,
|
|
277
|
+
if tracker is None or wall_hugging is None:
|
|
278
|
+
picked_tracker, picked_wall_hugging = _pick_zombie_variant(stage)
|
|
269
279
|
if tracker is None:
|
|
270
280
|
tracker = picked_tracker
|
|
271
|
-
if
|
|
272
|
-
|
|
281
|
+
if wall_hugging is None:
|
|
282
|
+
wall_hugging = picked_wall_hugging
|
|
273
283
|
if tracker:
|
|
274
|
-
|
|
284
|
+
wall_hugging = False
|
|
275
285
|
if tracker:
|
|
276
286
|
ratio = (
|
|
277
287
|
ZOMBIE_TRACKER_AGING_DURATION_FRAMES / ZOMBIE_AGING_DURATION_FRAMES
|
|
@@ -305,13 +315,14 @@ def _create_zombie(
|
|
|
305
315
|
y=float(start_pos[1]),
|
|
306
316
|
speed=base_speed,
|
|
307
317
|
tracker=tracker,
|
|
308
|
-
|
|
318
|
+
wall_hugging=wall_hugging,
|
|
309
319
|
aging_duration_frames=aging_duration_frames,
|
|
310
320
|
)
|
|
311
321
|
|
|
312
322
|
|
|
313
323
|
def place_fuel_can(
|
|
314
|
-
walkable_cells: list[
|
|
324
|
+
walkable_cells: list[tuple[int, int]],
|
|
325
|
+
cell_size: int,
|
|
315
326
|
player: Player,
|
|
316
327
|
*,
|
|
317
328
|
cars: Sequence[Car] | None = None,
|
|
@@ -329,30 +340,33 @@ def place_fuel_can(
|
|
|
329
340
|
|
|
330
341
|
for _ in range(200):
|
|
331
342
|
cell = RNG.choice(walkable_cells)
|
|
332
|
-
|
|
343
|
+
center = _cell_center(cell, cell_size)
|
|
344
|
+
if reserved_centers and center in reserved_centers:
|
|
333
345
|
continue
|
|
334
|
-
dx =
|
|
335
|
-
dy =
|
|
346
|
+
dx = center[0] - player.x
|
|
347
|
+
dy = center[1] - player.y
|
|
336
348
|
if dx * dx + dy * dy < min_player_dist_sq:
|
|
337
349
|
continue
|
|
338
350
|
if cars:
|
|
339
351
|
too_close = False
|
|
340
352
|
for parked_car in cars:
|
|
341
|
-
dx =
|
|
342
|
-
dy =
|
|
353
|
+
dx = center[0] - parked_car.rect.centerx
|
|
354
|
+
dy = center[1] - parked_car.rect.centery
|
|
343
355
|
if dx * dx + dy * dy < min_car_dist_sq:
|
|
344
356
|
too_close = True
|
|
345
357
|
break
|
|
346
358
|
if too_close:
|
|
347
359
|
continue
|
|
348
|
-
return FuelCan(
|
|
360
|
+
return FuelCan(center[0], center[1])
|
|
349
361
|
|
|
350
362
|
cell = RNG.choice(walkable_cells)
|
|
351
|
-
|
|
363
|
+
center = _cell_center(cell, cell_size)
|
|
364
|
+
return FuelCan(center[0], center[1])
|
|
352
365
|
|
|
353
366
|
|
|
354
367
|
def _place_flashlight(
|
|
355
|
-
walkable_cells: list[
|
|
368
|
+
walkable_cells: list[tuple[int, int]],
|
|
369
|
+
cell_size: int,
|
|
356
370
|
player: Player,
|
|
357
371
|
*,
|
|
358
372
|
cars: Sequence[Car] | None = None,
|
|
@@ -369,28 +383,31 @@ def _place_flashlight(
|
|
|
369
383
|
|
|
370
384
|
for _ in range(200):
|
|
371
385
|
cell = RNG.choice(walkable_cells)
|
|
372
|
-
|
|
386
|
+
center = _cell_center(cell, cell_size)
|
|
387
|
+
if reserved_centers and center in reserved_centers:
|
|
373
388
|
continue
|
|
374
|
-
dx =
|
|
375
|
-
dy =
|
|
389
|
+
dx = center[0] - player.x
|
|
390
|
+
dy = center[1] - player.y
|
|
376
391
|
if dx * dx + dy * dy < min_player_dist_sq:
|
|
377
392
|
continue
|
|
378
393
|
if cars:
|
|
379
394
|
if any(
|
|
380
|
-
(
|
|
381
|
-
+ (
|
|
395
|
+
(center[0] - parked.rect.centerx) ** 2
|
|
396
|
+
+ (center[1] - parked.rect.centery) ** 2
|
|
382
397
|
< min_car_dist_sq
|
|
383
398
|
for parked in cars
|
|
384
399
|
):
|
|
385
400
|
continue
|
|
386
|
-
return Flashlight(
|
|
401
|
+
return Flashlight(center[0], center[1])
|
|
387
402
|
|
|
388
403
|
cell = RNG.choice(walkable_cells)
|
|
389
|
-
|
|
404
|
+
center = _cell_center(cell, cell_size)
|
|
405
|
+
return Flashlight(center[0], center[1])
|
|
390
406
|
|
|
391
407
|
|
|
392
408
|
def place_flashlights(
|
|
393
|
-
walkable_cells: list[
|
|
409
|
+
walkable_cells: list[tuple[int, int]],
|
|
410
|
+
cell_size: int,
|
|
394
411
|
player: Player,
|
|
395
412
|
*,
|
|
396
413
|
cars: Sequence[Car] | None = None,
|
|
@@ -404,7 +421,11 @@ def place_flashlights(
|
|
|
404
421
|
while len(placed) < count and attempts < max_attempts:
|
|
405
422
|
attempts += 1
|
|
406
423
|
fl = _place_flashlight(
|
|
407
|
-
walkable_cells,
|
|
424
|
+
walkable_cells,
|
|
425
|
+
cell_size,
|
|
426
|
+
player,
|
|
427
|
+
cars=cars,
|
|
428
|
+
reserved_centers=reserved_centers,
|
|
408
429
|
)
|
|
409
430
|
if not fl:
|
|
410
431
|
break
|
|
@@ -421,7 +442,8 @@ def place_flashlights(
|
|
|
421
442
|
|
|
422
443
|
|
|
423
444
|
def _place_shoes(
|
|
424
|
-
walkable_cells: list[
|
|
445
|
+
walkable_cells: list[tuple[int, int]],
|
|
446
|
+
cell_size: int,
|
|
425
447
|
player: Player,
|
|
426
448
|
*,
|
|
427
449
|
cars: Sequence[Car] | None = None,
|
|
@@ -438,28 +460,31 @@ def _place_shoes(
|
|
|
438
460
|
|
|
439
461
|
for _ in range(200):
|
|
440
462
|
cell = RNG.choice(walkable_cells)
|
|
441
|
-
|
|
463
|
+
center = _cell_center(cell, cell_size)
|
|
464
|
+
if reserved_centers and center in reserved_centers:
|
|
442
465
|
continue
|
|
443
|
-
dx =
|
|
444
|
-
dy =
|
|
466
|
+
dx = center[0] - player.x
|
|
467
|
+
dy = center[1] - player.y
|
|
445
468
|
if dx * dx + dy * dy < min_player_dist_sq:
|
|
446
469
|
continue
|
|
447
470
|
if cars:
|
|
448
471
|
if any(
|
|
449
|
-
(
|
|
450
|
-
+ (
|
|
472
|
+
(center[0] - parked.rect.centerx) ** 2
|
|
473
|
+
+ (center[1] - parked.rect.centery) ** 2
|
|
451
474
|
< min_car_dist_sq
|
|
452
475
|
for parked in cars
|
|
453
476
|
):
|
|
454
477
|
continue
|
|
455
|
-
return Shoes(
|
|
478
|
+
return Shoes(center[0], center[1])
|
|
456
479
|
|
|
457
480
|
cell = RNG.choice(walkable_cells)
|
|
458
|
-
|
|
481
|
+
center = _cell_center(cell, cell_size)
|
|
482
|
+
return Shoes(center[0], center[1])
|
|
459
483
|
|
|
460
484
|
|
|
461
485
|
def place_shoes(
|
|
462
|
-
walkable_cells: list[
|
|
486
|
+
walkable_cells: list[tuple[int, int]],
|
|
487
|
+
cell_size: int,
|
|
463
488
|
player: Player,
|
|
464
489
|
*,
|
|
465
490
|
cars: Sequence[Car] | None = None,
|
|
@@ -473,7 +498,11 @@ def place_shoes(
|
|
|
473
498
|
while len(placed) < count and attempts < max_attempts:
|
|
474
499
|
attempts += 1
|
|
475
500
|
shoes = _place_shoes(
|
|
476
|
-
walkable_cells,
|
|
501
|
+
walkable_cells,
|
|
502
|
+
cell_size,
|
|
503
|
+
player,
|
|
504
|
+
cars=cars,
|
|
505
|
+
reserved_centers=reserved_centers,
|
|
477
506
|
)
|
|
478
507
|
if not shoes:
|
|
479
508
|
break
|
|
@@ -489,7 +518,8 @@ def place_shoes(
|
|
|
489
518
|
|
|
490
519
|
|
|
491
520
|
def place_buddies(
|
|
492
|
-
walkable_cells: list[
|
|
521
|
+
walkable_cells: list[tuple[int, int]],
|
|
522
|
+
cell_size: int,
|
|
493
523
|
player: Player,
|
|
494
524
|
*,
|
|
495
525
|
cars: Sequence[Car] | None = None,
|
|
@@ -501,6 +531,7 @@ def place_buddies(
|
|
|
501
531
|
min_player_dist = 240
|
|
502
532
|
positions = find_interior_spawn_positions(
|
|
503
533
|
walkable_cells,
|
|
534
|
+
cell_size,
|
|
504
535
|
1.0,
|
|
505
536
|
player=player,
|
|
506
537
|
min_player_dist=min_player_dist,
|
|
@@ -510,7 +541,10 @@ def place_buddies(
|
|
|
510
541
|
placed.append(Survivor(pos[0], pos[1], is_buddy=True))
|
|
511
542
|
remaining = count - len(placed)
|
|
512
543
|
for _ in range(max(0, remaining)):
|
|
513
|
-
spawn_pos = find_nearby_offscreen_spawn_position(
|
|
544
|
+
spawn_pos = find_nearby_offscreen_spawn_position(
|
|
545
|
+
walkable_cells,
|
|
546
|
+
cell_size,
|
|
547
|
+
)
|
|
514
548
|
placed.append(Survivor(spawn_pos[0], spawn_pos[1], is_buddy=True))
|
|
515
549
|
return placed
|
|
516
550
|
|
|
@@ -518,7 +552,8 @@ def place_buddies(
|
|
|
518
552
|
def place_new_car(
|
|
519
553
|
wall_group: pygame.sprite.Group,
|
|
520
554
|
player: Player,
|
|
521
|
-
walkable_cells: list[
|
|
555
|
+
walkable_cells: list[tuple[int, int]],
|
|
556
|
+
cell_size: int,
|
|
522
557
|
*,
|
|
523
558
|
existing_cars: Sequence[Car] | None = None,
|
|
524
559
|
appearance: str = "default",
|
|
@@ -529,7 +564,7 @@ def place_new_car(
|
|
|
529
564
|
max_attempts = 150
|
|
530
565
|
for _ in range(max_attempts):
|
|
531
566
|
cell = RNG.choice(walkable_cells)
|
|
532
|
-
c_x, c_y = cell
|
|
567
|
+
c_x, c_y = _cell_center(cell, cell_size)
|
|
533
568
|
temp_car = Car(c_x, c_y, appearance=appearance)
|
|
534
569
|
temp_rect = temp_car.rect.inflate(30, 30)
|
|
535
570
|
nearby_walls = pygame.sprite.Group()
|
|
@@ -555,7 +590,7 @@ def place_new_car(
|
|
|
555
590
|
|
|
556
591
|
|
|
557
592
|
def spawn_survivors(
|
|
558
|
-
game_data: GameData, layout_data: Mapping[str, list[
|
|
593
|
+
game_data: GameData, layout_data: Mapping[str, list[tuple[int, int]]]
|
|
559
594
|
) -> list[Survivor]:
|
|
560
595
|
"""Populate rescue-stage survivors and buddy-stage buddies."""
|
|
561
596
|
survivors: list[Survivor] = []
|
|
@@ -566,10 +601,12 @@ def spawn_survivors(
|
|
|
566
601
|
wall_group = game_data.groups.wall_group
|
|
567
602
|
survivor_group = game_data.groups.survivor_group
|
|
568
603
|
all_sprites = game_data.groups.all_sprites
|
|
604
|
+
cell_size = game_data.cell_size
|
|
569
605
|
|
|
570
606
|
if game_data.stage.rescue_stage:
|
|
571
607
|
positions = find_interior_spawn_positions(
|
|
572
608
|
walkable,
|
|
609
|
+
cell_size,
|
|
573
610
|
game_data.stage.survivor_spawn_rate,
|
|
574
611
|
)
|
|
575
612
|
for pos in positions:
|
|
@@ -586,6 +623,7 @@ def spawn_survivors(
|
|
|
586
623
|
if game_data.player:
|
|
587
624
|
buddies = place_buddies(
|
|
588
625
|
walkable,
|
|
626
|
+
cell_size,
|
|
589
627
|
game_data.player,
|
|
590
628
|
cars=game_data.waiting_cars,
|
|
591
629
|
count=buddy_count,
|
|
@@ -602,17 +640,18 @@ def spawn_survivors(
|
|
|
602
640
|
|
|
603
641
|
def setup_player_and_cars(
|
|
604
642
|
game_data: GameData,
|
|
605
|
-
layout_data: Mapping[str, list[
|
|
643
|
+
layout_data: Mapping[str, list[tuple[int, int]]],
|
|
606
644
|
*,
|
|
607
645
|
car_count: int = 1,
|
|
608
646
|
) -> tuple[Player, list[Car]]:
|
|
609
647
|
"""Create the player plus one or more parked cars using blueprint candidates."""
|
|
610
648
|
all_sprites = game_data.groups.all_sprites
|
|
611
|
-
walkable_cells: list[
|
|
649
|
+
walkable_cells: list[tuple[int, int]] = layout_data["walkable_cells"]
|
|
650
|
+
cell_size = game_data.cell_size
|
|
612
651
|
|
|
613
|
-
def _pick_center(cells: list[
|
|
652
|
+
def _pick_center(cells: list[tuple[int, int]]) -> tuple[int, int]:
|
|
614
653
|
return (
|
|
615
|
-
RNG.choice(cells)
|
|
654
|
+
_cell_center(RNG.choice(cells), cell_size)
|
|
616
655
|
if cells
|
|
617
656
|
else (game_data.level_width // 2, game_data.level_height // 2)
|
|
618
657
|
)
|
|
@@ -620,7 +659,8 @@ def setup_player_and_cars(
|
|
|
620
659
|
player_pos = _pick_center(layout_data["player_cells"] or walkable_cells)
|
|
621
660
|
player = Player(*player_pos)
|
|
622
661
|
|
|
623
|
-
|
|
662
|
+
car_walkable = layout_data.get("car_walkable_cells") or walkable_cells
|
|
663
|
+
car_candidates = list(layout_data["car_cells"] or car_walkable)
|
|
624
664
|
waiting_cars: list[Car] = []
|
|
625
665
|
car_appearance = _car_appearance_for_stage(game_data.stage)
|
|
626
666
|
|
|
@@ -630,13 +670,14 @@ def setup_player_and_cars(
|
|
|
630
670
|
return (player_pos[0] + 200, player_pos[1])
|
|
631
671
|
RNG.shuffle(car_candidates)
|
|
632
672
|
for candidate in car_candidates:
|
|
633
|
-
|
|
634
|
-
|
|
673
|
+
center = _cell_center(candidate, cell_size)
|
|
674
|
+
if (center[0] - player_pos[0]) ** 2 + (
|
|
675
|
+
center[1] - player_pos[1]
|
|
635
676
|
) ** 2 >= 400 * 400:
|
|
636
677
|
car_candidates.remove(candidate)
|
|
637
|
-
return
|
|
678
|
+
return center
|
|
638
679
|
choice = car_candidates.pop()
|
|
639
|
-
return choice
|
|
680
|
+
return _cell_center(choice, cell_size)
|
|
640
681
|
|
|
641
682
|
for _ in range(max(1, car_count)):
|
|
642
683
|
car_pos = _pick_car_position()
|
|
@@ -653,7 +694,7 @@ def setup_player_and_cars(
|
|
|
653
694
|
def spawn_initial_zombies(
|
|
654
695
|
game_data: GameData,
|
|
655
696
|
player: Player,
|
|
656
|
-
layout_data: Mapping[str, list[
|
|
697
|
+
layout_data: Mapping[str, list[tuple[int, int]]],
|
|
657
698
|
config: dict[str, Any],
|
|
658
699
|
) -> None:
|
|
659
700
|
"""Spawn initial zombies using blueprint candidate cells."""
|
|
@@ -664,27 +705,28 @@ def spawn_initial_zombies(
|
|
|
664
705
|
spawn_cells = layout_data["walkable_cells"]
|
|
665
706
|
if not spawn_cells:
|
|
666
707
|
return
|
|
708
|
+
cell_size = game_data.cell_size
|
|
667
709
|
|
|
668
710
|
if game_data.stage.id == "debug_tracker":
|
|
669
711
|
player_pos = player.rect.center
|
|
670
712
|
min_dist_sq = 100 * 100
|
|
671
713
|
max_dist_sq = 240 * 240
|
|
672
|
-
candidates = [
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
]
|
|
714
|
+
candidates = []
|
|
715
|
+
for cell in spawn_cells:
|
|
716
|
+
center = _cell_center(cell, cell_size)
|
|
717
|
+
dist_sq = (center[0] - player_pos[0]) ** 2 + (center[1] - player_pos[1]) ** 2
|
|
718
|
+
if min_dist_sq <= dist_sq <= max_dist_sq:
|
|
719
|
+
candidates.append(cell)
|
|
679
720
|
if not candidates:
|
|
680
721
|
candidates = spawn_cells
|
|
681
722
|
candidate = RNG.choice(candidates)
|
|
723
|
+
candidate_center = _cell_center(candidate, cell_size)
|
|
682
724
|
tentative = _create_zombie(
|
|
683
725
|
config,
|
|
684
|
-
start_pos=
|
|
726
|
+
start_pos=candidate_center,
|
|
685
727
|
stage=game_data.stage,
|
|
686
728
|
tracker=True,
|
|
687
|
-
|
|
729
|
+
wall_hugging=False,
|
|
688
730
|
)
|
|
689
731
|
if not spritecollideany_walls(tentative, wall_group):
|
|
690
732
|
zombie_group.add(tentative)
|
|
@@ -696,19 +738,20 @@ def spawn_initial_zombies(
|
|
|
696
738
|
spawn_rate = max(0.0, game_data.stage.initial_interior_spawn_rate)
|
|
697
739
|
positions = find_interior_spawn_positions(
|
|
698
740
|
spawn_cells,
|
|
741
|
+
cell_size,
|
|
699
742
|
spawn_rate,
|
|
700
743
|
player=player,
|
|
701
744
|
min_player_dist=ZOMBIE_SPAWN_PLAYER_BUFFER,
|
|
702
745
|
)
|
|
703
746
|
|
|
704
747
|
for pos in positions:
|
|
705
|
-
tracker,
|
|
748
|
+
tracker, wall_hugging = _pick_zombie_variant(game_data.stage)
|
|
706
749
|
tentative = _create_zombie(
|
|
707
750
|
config,
|
|
708
751
|
start_pos=pos,
|
|
709
752
|
stage=game_data.stage,
|
|
710
753
|
tracker=tracker,
|
|
711
|
-
|
|
754
|
+
wall_hugging=wall_hugging,
|
|
712
755
|
)
|
|
713
756
|
if spritecollideany_walls(tentative, wall_group):
|
|
714
757
|
continue
|
|
@@ -724,11 +767,14 @@ def spawn_waiting_car(game_data: GameData) -> Car | None:
|
|
|
724
767
|
player = game_data.player
|
|
725
768
|
if not player:
|
|
726
769
|
return None
|
|
727
|
-
|
|
770
|
+
# Use cells that are 4-way reachable by car
|
|
771
|
+
car_walkable = list(game_data.layout.car_walkable_cells)
|
|
772
|
+
walkable_cells = car_walkable if car_walkable else game_data.layout.walkable_cells
|
|
728
773
|
if not walkable_cells:
|
|
729
774
|
return None
|
|
730
775
|
wall_group = game_data.groups.wall_group
|
|
731
776
|
all_sprites = game_data.groups.all_sprites
|
|
777
|
+
cell_size = game_data.cell_size
|
|
732
778
|
active_car = game_data.car if game_data.car and game_data.car.alive() else None
|
|
733
779
|
waiting = _alive_waiting_cars(game_data)
|
|
734
780
|
obstacles: list[Car] = list(waiting)
|
|
@@ -742,6 +788,7 @@ def spawn_waiting_car(game_data: GameData) -> Car | None:
|
|
|
742
788
|
wall_group,
|
|
743
789
|
player,
|
|
744
790
|
walkable_cells,
|
|
791
|
+
cell_size,
|
|
745
792
|
existing_cars=obstacles,
|
|
746
793
|
appearance=appearance,
|
|
747
794
|
)
|
|
@@ -813,6 +860,7 @@ def _spawn_nearby_zombie(
|
|
|
813
860
|
all_sprites = game_data.groups.all_sprites
|
|
814
861
|
spawn_pos = find_nearby_offscreen_spawn_position(
|
|
815
862
|
game_data.layout.walkable_cells,
|
|
863
|
+
game_data.cell_size,
|
|
816
864
|
player=player,
|
|
817
865
|
camera=camera,
|
|
818
866
|
min_player_dist=ZOMBIE_SPAWN_PLAYER_BUFFER,
|
|
@@ -877,18 +925,19 @@ def update_falling_zombies(game_data: GameData, config: dict[str, Any]) -> None:
|
|
|
877
925
|
fall.dust_started = True
|
|
878
926
|
if now < spawn_at:
|
|
879
927
|
continue
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
928
|
+
|
|
929
|
+
if getattr(fall, "mode", "spawn") == "spawn":
|
|
930
|
+
if len(zombie_group) < MAX_ZOMBIES:
|
|
931
|
+
candidate = _create_zombie(
|
|
932
|
+
config,
|
|
933
|
+
start_pos=fall.target_pos,
|
|
934
|
+
stage=game_data.stage,
|
|
935
|
+
tracker=fall.tracker,
|
|
936
|
+
wall_hugging=fall.wall_hugging,
|
|
937
|
+
)
|
|
938
|
+
zombie_group.add(candidate)
|
|
939
|
+
all_sprites.add(candidate, layer=1)
|
|
940
|
+
|
|
892
941
|
state.falling_zombies.remove(fall)
|
|
893
942
|
|
|
894
943
|
|
zombie_escape/gameplay/state.py
CHANGED
|
@@ -51,9 +51,12 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
51
51
|
last_zombie_spawn_time=0,
|
|
52
52
|
dawn_carbonized=False,
|
|
53
53
|
debug_mode=False,
|
|
54
|
+
show_fps=False,
|
|
54
55
|
falling_zombies=[],
|
|
55
56
|
falling_spawn_carry=0,
|
|
56
57
|
dust_rings=[],
|
|
58
|
+
player_wall_target_cell=None,
|
|
59
|
+
player_wall_target_ttl=0,
|
|
57
60
|
)
|
|
58
61
|
|
|
59
62
|
# Create sprite groups
|
|
@@ -69,8 +72,7 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
69
72
|
camera = Camera(level_width, level_height)
|
|
70
73
|
|
|
71
74
|
# Define level layout (will be filled by blueprint generation)
|
|
72
|
-
|
|
73
|
-
inner_rect = outer_rect
|
|
75
|
+
field_rect = pygame.Rect(0, 0, level_width, level_height)
|
|
74
76
|
|
|
75
77
|
return GameData(
|
|
76
78
|
state=game_state,
|
|
@@ -82,12 +84,13 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
82
84
|
),
|
|
83
85
|
camera=camera,
|
|
84
86
|
layout=LevelLayout(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
outside_rects=[],
|
|
87
|
+
field_rect=field_rect,
|
|
88
|
+
outside_cells=set(),
|
|
88
89
|
walkable_cells=[],
|
|
89
90
|
outer_wall_cells=set(),
|
|
90
91
|
wall_cells=set(),
|
|
92
|
+
pitfall_cells=set(),
|
|
93
|
+
car_walkable_cells=set(),
|
|
91
94
|
fall_spawn_cells=set(),
|
|
92
95
|
bevel_corners={},
|
|
93
96
|
),
|
|
@@ -107,8 +110,11 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
107
110
|
|
|
108
111
|
def carbonize_outdoor_zombies(game_data: GameData) -> None:
|
|
109
112
|
"""Petrify zombies that have already broken through to the exterior."""
|
|
110
|
-
|
|
111
|
-
if not
|
|
113
|
+
outside_cells = game_data.layout.outside_cells
|
|
114
|
+
if not outside_cells:
|
|
115
|
+
return
|
|
116
|
+
cell_size = game_data.cell_size
|
|
117
|
+
if cell_size <= 0:
|
|
112
118
|
return
|
|
113
119
|
group = game_data.groups.zombie_group
|
|
114
120
|
if not group:
|
|
@@ -116,8 +122,11 @@ def carbonize_outdoor_zombies(game_data: GameData) -> None:
|
|
|
116
122
|
for zombie in list(group):
|
|
117
123
|
if not zombie.alive():
|
|
118
124
|
continue
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
cell = (
|
|
126
|
+
int(zombie.rect.centerx // cell_size),
|
|
127
|
+
int(zombie.rect.centery // cell_size),
|
|
128
|
+
)
|
|
129
|
+
if cell in outside_cells:
|
|
121
130
|
zombie.carbonize()
|
|
122
131
|
|
|
123
132
|
|