zombie-escape 1.12.0__py3-none-any.whl → 1.12.3__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.
@@ -12,20 +12,21 @@ from .colors import (
12
12
  BLUE,
13
13
  DARK_RED,
14
14
  ORANGE,
15
- RED,
16
15
  STEEL_BEAM_COLOR,
17
16
  STEEL_BEAM_LINE_COLOR,
18
- TRACKER_OUTLINE_COLOR,
19
- WALL_FOLLOWER_OUTLINE_COLOR,
20
17
  YELLOW,
21
18
  EnvironmentPalette,
22
19
  get_environment_palette,
23
20
  )
24
21
  from .render_constants import (
22
+ ANGLE_BINS,
25
23
  BUDDY_COLOR,
24
+ HAND_SPREAD_RAD,
26
25
  HUMANOID_OUTLINE_COLOR,
27
26
  HUMANOID_OUTLINE_WIDTH,
28
27
  SURVIVOR_COLOR,
28
+ ZOMBIE_BODY_COLOR,
29
+ ZOMBIE_OUTLINE_COLOR,
29
30
  FogRing,
30
31
  RenderAssets,
31
32
  )
@@ -44,6 +45,102 @@ def _draw_outlined_circle(
44
45
  pygame.draw.circle(surface, outline_color, center, radius, width=outline_width)
45
46
 
46
47
 
48
+ def _brighten_color(
49
+ color: tuple[int, int, int], *, factor: float = 1.25
50
+ ) -> tuple[int, int, int]:
51
+ return tuple(min(255, int(c * factor + 0.5)) for c in color)
52
+
53
+
54
+ ANGLE_STEP = math.tau / ANGLE_BINS
55
+
56
+ _PLAYER_UPSCALE_FACTOR = 4
57
+ _CAR_UPSCALE_FACTOR = 4
58
+
59
+ _PLAYER_DIRECTIONAL_CACHE: dict[tuple[int, int], list[pygame.Surface]] = {}
60
+ _SURVIVOR_DIRECTIONAL_CACHE: dict[
61
+ tuple[int, bool, bool, int], list[pygame.Surface]
62
+ ] = {}
63
+ _ZOMBIE_DIRECTIONAL_CACHE: dict[tuple[int, bool, int], list[pygame.Surface]] = {}
64
+
65
+
66
+ def angle_bin_from_vector(
67
+ dx: float, dy: float, *, bins: int = ANGLE_BINS
68
+ ) -> int | None:
69
+ if dx == 0 and dy == 0:
70
+ return None
71
+ angle = math.atan2(dy, dx)
72
+ if angle < 0:
73
+ angle += math.tau
74
+ step = math.tau / bins
75
+ return int(round(angle / step)) % bins
76
+
77
+
78
+ def _hand_defaults(radius: int) -> tuple[int, int]:
79
+ hand_radius = max(1, int(radius * 0.5))
80
+ hand_distance = max(hand_radius + 1, int(radius * 1.0))
81
+ return hand_radius, hand_distance
82
+
83
+
84
+ def _draw_capped_circle(
85
+ surface: pygame.Surface,
86
+ center: tuple[int, int],
87
+ radius: int,
88
+ base_color: tuple[int, int, int],
89
+ cap_color: tuple[int, int, int],
90
+ outline_color: tuple[int, int, int],
91
+ outline_width: int,
92
+ *,
93
+ angle_rad: float = 0.0,
94
+ hand_spread_rad: float = HAND_SPREAD_RAD,
95
+ hand_radius: int | None = None,
96
+ hand_distance: int | None = None,
97
+ draw_hands: bool = True,
98
+ ) -> None:
99
+ if hand_radius is None or hand_distance is None:
100
+ hand_radius, hand_distance = _hand_defaults(radius)
101
+ if draw_hands:
102
+ for direction in (-1, 1):
103
+ hand_angle = angle_rad + (hand_spread_rad * direction)
104
+ hand_x = int(round(center[0] + math.cos(hand_angle) * hand_distance))
105
+ hand_y = int(round(center[1] + math.sin(hand_angle) * hand_distance))
106
+ pygame.draw.circle(surface, base_color, (hand_x, hand_y), hand_radius)
107
+ pygame.draw.circle(surface, cap_color, center, radius)
108
+ if outline_width > 0:
109
+ pygame.draw.circle(surface, outline_color, center, radius, width=outline_width)
110
+
111
+
112
+ def _build_capped_surface(
113
+ radius: int,
114
+ base_color: tuple[int, int, int],
115
+ cap_color: tuple[int, int, int],
116
+ angle_bin: int,
117
+ *,
118
+ outline_scale: int = 1,
119
+ draw_hands: bool = True,
120
+ outline_color: tuple[int, int, int] = HUMANOID_OUTLINE_COLOR,
121
+ ) -> pygame.Surface:
122
+ hand_radius, hand_distance = _hand_defaults(radius)
123
+ max_extent = max(radius, hand_distance + hand_radius)
124
+ size = max_extent * 2 + 2
125
+ surface = pygame.Surface((size, size), pygame.SRCALPHA)
126
+ center = (max_extent + 1, max_extent + 1)
127
+ angle_rad = (angle_bin % ANGLE_BINS) * ANGLE_STEP
128
+ _draw_capped_circle(
129
+ surface,
130
+ center,
131
+ radius,
132
+ base_color,
133
+ cap_color,
134
+ outline_color,
135
+ HUMANOID_OUTLINE_WIDTH * outline_scale,
136
+ angle_rad=angle_rad,
137
+ hand_radius=hand_radius,
138
+ hand_distance=hand_distance,
139
+ draw_hands=draw_hands,
140
+ )
141
+ return surface
142
+
143
+
47
144
  @dataclass(frozen=True)
48
145
  class PolygonSpec:
49
146
  size: tuple[int, int]
@@ -93,7 +190,7 @@ SHOES_SPEC = PolygonSpec(
93
190
  [
94
191
  (1, 1),
95
192
  (7, 1),
96
- (7, 4),
193
+ (8, 4),
97
194
  (13, 6),
98
195
  (13, 9),
99
196
  (1, 9),
@@ -274,55 +371,145 @@ def resolve_steel_beam_colors(
274
371
  return STEEL_BEAM_COLOR, STEEL_BEAM_LINE_COLOR
275
372
 
276
373
 
277
- def build_player_surface(radius: int) -> pygame.Surface:
278
- surface = pygame.Surface((radius * 2 + 2, radius * 2 + 2), pygame.SRCALPHA)
279
- _draw_outlined_circle(
280
- surface,
281
- (radius + 1, radius + 1),
374
+ def build_player_directional_surfaces(
375
+ radius: int, *, bins: int = ANGLE_BINS
376
+ ) -> list[pygame.Surface]:
377
+ cache_key = (radius, bins)
378
+ if cache_key in _PLAYER_DIRECTIONAL_CACHE:
379
+ return _PLAYER_DIRECTIONAL_CACHE[cache_key]
380
+ surfaces = build_humanoid_directional_surfaces(
282
381
  radius,
283
- BLUE,
284
- HUMANOID_OUTLINE_COLOR,
285
- HUMANOID_OUTLINE_WIDTH,
382
+ base_color=BLUE,
383
+ cap_color=_brighten_color(BLUE),
384
+ bins=bins,
385
+ outline_color=HUMANOID_OUTLINE_COLOR,
286
386
  )
287
- return surface
387
+ _PLAYER_DIRECTIONAL_CACHE[cache_key] = surfaces
388
+ return surfaces
288
389
 
289
390
 
290
- def build_survivor_surface(radius: int, *, is_buddy: bool) -> pygame.Surface:
291
- surface = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
292
- fill_color = BUDDY_COLOR if is_buddy else SURVIVOR_COLOR
293
- _draw_outlined_circle(
391
+ def build_humanoid_directional_surfaces(
392
+ radius: int,
393
+ *,
394
+ base_color: tuple[int, int, int],
395
+ cap_color: tuple[int, int, int],
396
+ bins: int = ANGLE_BINS,
397
+ draw_hands: bool = True,
398
+ outline_color: tuple[int, int, int],
399
+ ) -> list[pygame.Surface]:
400
+ base_radius = radius * _PLAYER_UPSCALE_FACTOR
401
+ base_surface = _build_capped_surface(
402
+ base_radius,
403
+ base_color,
404
+ cap_color,
405
+ 0,
406
+ outline_scale=_PLAYER_UPSCALE_FACTOR,
407
+ draw_hands=draw_hands,
408
+ outline_color=outline_color,
409
+ )
410
+ target_surface = _build_capped_surface(
411
+ radius,
412
+ base_color,
413
+ cap_color,
414
+ 0,
415
+ draw_hands=draw_hands,
416
+ outline_color=outline_color,
417
+ )
418
+ target_size = target_surface.get_size()
419
+ scale = target_size[0] / base_surface.get_width()
420
+ half_step_deg = 360.0 / (bins * 5)
421
+ surfaces: list[pygame.Surface] = []
422
+ for idx in range(bins):
423
+ rotation_deg = -(idx * 360.0 / bins - half_step_deg)
424
+ rotated = pygame.transform.rotozoom(base_surface, rotation_deg, scale)
425
+ framed = pygame.Surface(target_size, pygame.SRCALPHA)
426
+ framed.blit(rotated, rotated.get_rect(center=framed.get_rect().center))
427
+ surfaces.append(framed)
428
+ return surfaces
429
+
430
+
431
+ def draw_humanoid_hand(
432
+ surface: pygame.Surface,
433
+ *,
434
+ radius: int,
435
+ angle_rad: float,
436
+ color: tuple[int, int, int],
437
+ hand_radius: int | None = None,
438
+ hand_distance: int | None = None,
439
+ ) -> None:
440
+ if hand_radius is None or hand_distance is None:
441
+ hand_radius, hand_distance = _hand_defaults(radius)
442
+ center_x, center_y = surface.get_rect().center
443
+ hand_x = int(round(center_x + math.cos(angle_rad) * hand_distance))
444
+ hand_y = int(round(center_y + math.sin(angle_rad) * hand_distance))
445
+ pygame.draw.circle(surface, color, (hand_x, hand_y), hand_radius)
446
+
447
+
448
+ def draw_humanoid_nose(
449
+ surface: pygame.Surface,
450
+ *,
451
+ radius: int,
452
+ angle_rad: float,
453
+ color: tuple[int, int, int],
454
+ ) -> None:
455
+ center_x, center_y = surface.get_rect().center
456
+ nose_length = max(2, int(radius * 0.45))
457
+ nose_offset = max(1, int(radius * 0.35))
458
+ start_x = center_x + math.cos(angle_rad) * nose_offset
459
+ start_y = center_y + math.sin(angle_rad) * nose_offset
460
+ end_x = center_x + math.cos(angle_rad) * (nose_offset + nose_length)
461
+ end_y = center_y + math.sin(angle_rad) * (nose_offset + nose_length)
462
+ pygame.draw.line(
294
463
  surface,
295
- (radius, radius),
464
+ color,
465
+ (int(start_x), int(start_y)),
466
+ (int(end_x), int(end_y)),
467
+ width=2,
468
+ )
469
+
470
+
471
+ def build_survivor_directional_surfaces(
472
+ radius: int,
473
+ *,
474
+ is_buddy: bool,
475
+ bins: int = ANGLE_BINS,
476
+ draw_hands: bool = True,
477
+ ) -> list[pygame.Surface]:
478
+ cache_key = (radius, is_buddy, draw_hands, bins)
479
+ if cache_key in _SURVIVOR_DIRECTIONAL_CACHE:
480
+ return _SURVIVOR_DIRECTIONAL_CACHE[cache_key]
481
+ fill_color = BUDDY_COLOR if is_buddy else SURVIVOR_COLOR
482
+ surfaces = build_humanoid_directional_surfaces(
296
483
  radius,
297
- fill_color,
298
- HUMANOID_OUTLINE_COLOR,
299
- HUMANOID_OUTLINE_WIDTH,
484
+ base_color=fill_color,
485
+ cap_color=_brighten_color(fill_color),
486
+ bins=bins,
487
+ draw_hands=draw_hands,
488
+ outline_color=HUMANOID_OUTLINE_COLOR,
300
489
  )
301
- return surface
490
+ _SURVIVOR_DIRECTIONAL_CACHE[cache_key] = surfaces
491
+ return surfaces
302
492
 
303
493
 
304
- def build_zombie_surface(
494
+ def build_zombie_directional_surfaces(
305
495
  radius: int,
306
496
  *,
307
- tracker: bool = False,
308
- wall_follower: bool = False,
309
- ) -> pygame.Surface:
310
- if tracker:
311
- outline_color = TRACKER_OUTLINE_COLOR
312
- elif wall_follower:
313
- outline_color = WALL_FOLLOWER_OUTLINE_COLOR
314
- else:
315
- outline_color = DARK_RED
316
- surface = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
317
- _draw_outlined_circle(
318
- surface,
319
- (radius, radius),
497
+ bins: int = ANGLE_BINS,
498
+ draw_hands: bool = True,
499
+ ) -> list[pygame.Surface]:
500
+ cache_key = (radius, draw_hands, bins)
501
+ if cache_key in _ZOMBIE_DIRECTIONAL_CACHE:
502
+ return _ZOMBIE_DIRECTIONAL_CACHE[cache_key]
503
+ surfaces = build_humanoid_directional_surfaces(
320
504
  radius,
321
- RED,
322
- outline_color,
323
- 1,
505
+ base_color=ZOMBIE_BODY_COLOR,
506
+ cap_color=_brighten_color(ZOMBIE_BODY_COLOR),
507
+ bins=bins,
508
+ draw_hands=draw_hands,
509
+ outline_color=ZOMBIE_OUTLINE_COLOR,
324
510
  )
325
- return surface
511
+ _ZOMBIE_DIRECTIONAL_CACHE[cache_key] = surfaces
512
+ return surfaces
326
513
 
327
514
 
328
515
  def build_car_surface(width: int, height: int) -> pygame.Surface:
@@ -336,54 +523,101 @@ def paint_car_surface(
336
523
  height: int,
337
524
  color: tuple[int, int, int],
338
525
  ) -> None:
339
- surface.fill((0, 0, 0, 0))
526
+ upscale = _CAR_UPSCALE_FACTOR
527
+ if upscale > 1:
528
+ up_width = width * upscale
529
+ up_height = height * upscale
530
+ up_surface = pygame.Surface((up_width, up_height), pygame.SRCALPHA)
531
+ _paint_car_surface_base(
532
+ up_surface, width=up_width, height=up_height, color=color
533
+ )
534
+ scaled = pygame.transform.smoothscale(up_surface, (width, height))
535
+ surface.fill((0, 0, 0, 0))
536
+ surface.blit(scaled, (0, 0))
537
+ return
538
+ _paint_car_surface_base(surface, width=width, height=height, color=color)
340
539
 
341
- body_rect = pygame.Rect(1, 4, width - 2, height - 8)
342
- front_cap_height = max(8, body_rect.height // 3)
343
- front_cap = pygame.Rect(
344
- body_rect.left, body_rect.top, body_rect.width, front_cap_height
345
- )
346
- windshield_rect = pygame.Rect(
347
- body_rect.left + 4,
348
- body_rect.top + 3,
349
- body_rect.width - 8,
350
- front_cap_height - 5,
351
- )
352
540
 
353
- trim_color = tuple(int(c * 0.55) for c in color)
354
- front_cap_color = tuple(min(255, int(c * 1.08)) for c in color)
355
- body_color = color
356
- window_color = (70, 110, 150)
357
- wheel_color = (35, 35, 35)
358
-
359
- wheel_width = width // 3
360
- wheel_height = 6
361
- for y in (body_rect.top + 4, body_rect.bottom - wheel_height - 4):
362
- left_wheel = pygame.Rect(2, y, wheel_width, wheel_height)
363
- right_wheel = pygame.Rect(width - wheel_width - 2, y, wheel_width, wheel_height)
364
- pygame.draw.rect(surface, wheel_color, left_wheel, border_radius=3)
365
- pygame.draw.rect(surface, wheel_color, right_wheel, border_radius=3)
366
-
367
- pygame.draw.rect(surface, body_color, body_rect, border_radius=4)
368
- pygame.draw.rect(surface, trim_color, body_rect, width=2, border_radius=4)
369
- pygame.draw.rect(surface, front_cap_color, front_cap, border_radius=10)
370
- pygame.draw.rect(surface, trim_color, front_cap, width=2, border_radius=10)
371
- pygame.draw.rect(surface, window_color, windshield_rect, border_radius=4)
372
-
373
- headlight_color = (245, 245, 200)
374
- for x in (front_cap.left + 5, front_cap.right - 5):
375
- pygame.draw.circle(surface, headlight_color, (x, body_rect.top + 5), 2)
376
- grille_rect = pygame.Rect(front_cap.centerx - 6, front_cap.top + 2, 12, 6)
377
- pygame.draw.rect(surface, trim_color, grille_rect, border_radius=2)
541
+ def _paint_car_surface_base(
542
+ surface: pygame.Surface,
543
+ *,
544
+ width: int,
545
+ height: int,
546
+ color: tuple[int, int, int],
547
+ ) -> None:
548
+ surface.fill((0, 0, 0, 0))
549
+
550
+ trim_color = tuple(int(c * 0.6) for c in color)
551
+ body_color = tuple(min(255, int(c * 1.15)) for c in color)
378
552
  tail_light_color = (255, 80, 50)
379
- for x in (body_rect.left + 5, body_rect.right - 5):
380
- pygame.draw.rect(
381
- surface,
382
- tail_light_color,
383
- (x - 2, body_rect.bottom - 5, 4, 3),
384
- border_radius=1,
553
+ headlight_color = (200, 200, 200)
554
+
555
+ base_width = 150.0
556
+ base_height = 210.0
557
+ scale_x = width / base_width
558
+ scale_y = height / base_height
559
+
560
+ def _rect(x: float, y: float, w: float, h: float) -> pygame.Rect:
561
+ return pygame.Rect(
562
+ int(round(x * scale_x)),
563
+ int(round(y * scale_y)),
564
+ max(1, int(round(w * scale_x))),
565
+ max(1, int(round(h * scale_y))),
385
566
  )
386
567
 
568
+ def _radius(value: float) -> int:
569
+ return max(1, int(round(value * min(scale_x, scale_y))))
570
+
571
+ body_top = _rect(0, 0, 150, 140)
572
+ body_bottom = _rect(0, 70, 150, 140)
573
+ rear_bed = _rect(16, 98, 118, 88)
574
+
575
+ pygame.draw.rect(surface, trim_color, body_top, border_radius=_radius(50))
576
+ pygame.draw.rect(surface, trim_color, body_bottom, border_radius=_radius(37))
577
+ pygame.draw.rect(surface, body_color, rear_bed)
578
+
579
+ tail_left = _rect(30, 190, 30, 20)
580
+ tail_right = _rect(90, 190, 30, 20)
581
+ pygame.draw.rect(surface, tail_light_color, tail_left)
582
+ pygame.draw.rect(surface, tail_light_color, tail_right)
583
+
584
+ headlight_left = _rect(15, 7, 40, 20)
585
+ headlight_right = _rect(95, 7, 40, 20)
586
+ pygame.draw.ellipse(surface, headlight_color, headlight_left)
587
+ pygame.draw.ellipse(surface, headlight_color, headlight_right)
588
+
589
+
590
+ def build_car_directional_surfaces(
591
+ base_surface: pygame.Surface, *, bins: int = ANGLE_BINS
592
+ ) -> list[pygame.Surface]:
593
+ """Return pre-rotated car surfaces matching angle_bin_from_vector bins."""
594
+ surfaces: list[pygame.Surface] = []
595
+ upscale = _CAR_UPSCALE_FACTOR
596
+ if upscale > 1:
597
+ src_size = base_surface.get_size()
598
+ upscale_surface = pygame.transform.scale(
599
+ base_surface,
600
+ (src_size[0] * upscale, src_size[1] * upscale),
601
+ )
602
+ else:
603
+ upscale_surface = base_surface
604
+ for idx in range(bins):
605
+ angle_rad = idx * ANGLE_STEP
606
+ rotation_deg = -math.degrees(angle_rad) - 90
607
+ rotated = pygame.transform.rotate(upscale_surface, rotation_deg)
608
+ if upscale > 1:
609
+ scaled = pygame.transform.smoothscale(
610
+ rotated,
611
+ (
612
+ max(1, rotated.get_width() // upscale),
613
+ max(1, rotated.get_height() // upscale),
614
+ ),
615
+ )
616
+ surfaces.append(scaled)
617
+ else:
618
+ surfaces.append(rotated)
619
+ return surfaces
620
+
387
621
 
388
622
  def paint_wall_surface(
389
623
  surface: pygame.Surface,
@@ -522,43 +756,6 @@ def paint_steel_beam_surface(
522
756
  surface.blit(top_surface, top_rect.topleft)
523
757
 
524
758
 
525
- def paint_zombie_surface(
526
- surface: pygame.Surface,
527
- *,
528
- radius: int,
529
- palm_angle: float | None = None,
530
- tracker: bool = False,
531
- wall_follower: bool = False,
532
- ) -> None:
533
- if tracker:
534
- outline_color = TRACKER_OUTLINE_COLOR
535
- elif wall_follower:
536
- outline_color = WALL_FOLLOWER_OUTLINE_COLOR
537
- else:
538
- outline_color = DARK_RED
539
- surface.fill((0, 0, 0, 0))
540
- _draw_outlined_circle(
541
- surface,
542
- (radius, radius),
543
- radius,
544
- RED,
545
- outline_color,
546
- 1,
547
- )
548
- if palm_angle is None:
549
- return
550
- palm_radius = max(1, radius // 3)
551
- palm_offset = radius - palm_radius * 0.3
552
- palm_x = radius + math.cos(palm_angle) * palm_offset
553
- palm_y = radius + math.sin(palm_angle) * palm_offset
554
- pygame.draw.circle(
555
- surface,
556
- outline_color,
557
- (int(palm_x), int(palm_y)),
558
- palm_radius,
559
- )
560
-
561
-
562
759
  def build_fuel_can_surface(width: int, height: int) -> pygame.Surface:
563
760
  return _draw_polygon_surface(width, height, FUEL_CAN_SPEC)
564
761
 
@@ -572,6 +769,7 @@ def build_shoes_surface(width: int, height: int) -> pygame.Surface:
572
769
 
573
770
 
574
771
  __all__ = [
772
+ "angle_bin_from_vector",
575
773
  "EnvironmentPalette",
576
774
  "FogRing",
577
775
  "RenderAssets",
@@ -580,14 +778,17 @@ __all__ = [
580
778
  "resolve_car_color",
581
779
  "resolve_steel_beam_colors",
582
780
  "CAR_COLOR_SCHEMES",
583
- "build_player_surface",
584
- "build_survivor_surface",
585
- "build_zombie_surface",
781
+ "build_player_directional_surfaces",
782
+ "build_humanoid_directional_surfaces",
783
+ "draw_humanoid_hand",
784
+ "draw_humanoid_nose",
785
+ "build_survivor_directional_surfaces",
786
+ "build_zombie_directional_surfaces",
586
787
  "build_car_surface",
788
+ "build_car_directional_surfaces",
587
789
  "paint_car_surface",
588
790
  "paint_wall_surface",
589
791
  "paint_steel_beam_surface",
590
- "paint_zombie_surface",
591
792
  "build_fuel_can_surface",
592
793
  "build_flashlight_surface",
593
794
  "build_shoes_surface",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import math
5
6
  from dataclasses import dataclass
6
7
 
7
8
  from .entities_constants import FOV_RADIUS, PLAYER_RADIUS
@@ -11,9 +12,14 @@ HUMANOID_OUTLINE_COLOR = (0, 80, 200)
11
12
  HUMANOID_OUTLINE_WIDTH = 1
12
13
  BUDDY_COLOR = (0, 180, 63)
13
14
  SURVIVOR_COLOR = (198, 198, 198)
15
+ ZOMBIE_BODY_COLOR = (180, 0, 0)
16
+ ZOMBIE_OUTLINE_COLOR = (255, 60, 60)
17
+ ZOMBIE_NOSE_COLOR = (255, 80, 80)
14
18
  FALLING_ZOMBIE_COLOR = (45, 45, 45)
15
19
  FALLING_WHIRLWIND_COLOR = (200, 200, 200, 120)
16
20
  FALLING_DUST_COLOR = (70, 70, 70, 130)
21
+ ANGLE_BINS = 16
22
+ HAND_SPREAD_RAD = math.radians(75)
17
23
 
18
24
 
19
25
  @dataclass(frozen=True)
@@ -93,6 +99,11 @@ __all__ = [
93
99
  "FALLING_ZOMBIE_COLOR",
94
100
  "FALLING_WHIRLWIND_COLOR",
95
101
  "FALLING_DUST_COLOR",
102
+ "ZOMBIE_BODY_COLOR",
103
+ "ZOMBIE_OUTLINE_COLOR",
104
+ "ZOMBIE_NOSE_COLOR",
105
+ "ANGLE_BINS",
106
+ "HAND_SPREAD_RAD",
96
107
  "HUMANOID_OUTLINE_COLOR",
97
108
  "HUMANOID_OUTLINE_WIDTH",
98
109
  "SURVIVOR_COLOR",
@@ -47,9 +47,9 @@ def game_over_screen(
47
47
 
48
48
  while True:
49
49
  if not state.overview_created:
50
- level_rect = game_data.layout.outer_rect
51
- level_width = level_rect[2]
52
- level_height = level_rect[3]
50
+ level_rect = game_data.layout.field_rect
51
+ level_width = level_rect.width
52
+ level_height = level_rect.height
53
53
  overview_surface = pygame.Surface((level_width, level_height))
54
54
  footprints_to_draw = state.footprints if footprints_enabled else []
55
55
  draw_level_overview(
@@ -120,10 +120,12 @@ def gameplay_screen(
120
120
  spawn_survivors(game_data, layout_data)
121
121
 
122
122
  occupied_centers: set[tuple[int, int]] = set()
123
+ cell_size = game_data.cell_size
123
124
  if stage.requires_fuel:
124
125
  fuel_spawn_count = stage.fuel_spawn_count
125
126
  fuel_can = place_fuel_can(
126
127
  layout_data["walkable_cells"],
128
+ cell_size,
127
129
  player,
128
130
  cars=game_data.waiting_cars,
129
131
  reserved_centers=occupied_centers,
@@ -136,6 +138,7 @@ def gameplay_screen(
136
138
  flashlight_count = stage.initial_flashlight_count
137
139
  flashlights = place_flashlights(
138
140
  layout_data["walkable_cells"],
141
+ cell_size,
139
142
  player,
140
143
  cars=game_data.waiting_cars,
141
144
  reserved_centers=occupied_centers,
@@ -149,6 +152,7 @@ def gameplay_screen(
149
152
  shoes_count = stage.initial_shoes_count
150
153
  shoes_list = place_shoes(
151
154
  layout_data["walkable_cells"],
155
+ cell_size,
152
156
  player,
153
157
  cars=game_data.waiting_cars,
154
158
  reserved_centers=occupied_centers,
@@ -130,7 +130,7 @@ STAGES: list[Stage] = [
130
130
  available=True,
131
131
  rescue_stage=True,
132
132
  tile_size=40,
133
- wall_algorithm="sparse",
133
+ wall_algorithm="sparse_moore.10%",
134
134
  exterior_spawn_weight=0.7,
135
135
  interior_spawn_weight=0.3,
136
136
  zombie_normal_ratio=0.4,
@@ -148,7 +148,7 @@ STAGES: list[Stage] = [
148
148
  grid_cols=120,
149
149
  grid_rows=7,
150
150
  available=True,
151
- wall_algorithm="sparse",
151
+ wall_algorithm="sparse_moore.10%",
152
152
  exterior_spawn_weight=0.3,
153
153
  interior_spawn_weight=0.7,
154
154
  zombie_normal_ratio=0.5,
@@ -224,8 +224,8 @@ STAGES: list[Stage] = [
224
224
  description_key="stages.stage15.description",
225
225
  available=True,
226
226
  buddy_required_count=1,
227
- grid_cols=70,
228
- grid_rows=20,
227
+ grid_cols=64,
228
+ grid_rows=24,
229
229
  tile_size=35,
230
230
  wall_algorithm="grid_wire",
231
231
  requires_fuel=True,
@@ -237,7 +237,7 @@ STAGES: list[Stage] = [
237
237
  zombie_normal_ratio=0.5,
238
238
  zombie_wall_follower_ratio=0.5,
239
239
  fall_spawn_zones=[
240
- (33, 2, 4, 16),
240
+ (33, 2, 4, 18),
241
241
  ],
242
242
  zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
243
243
  initial_shoes_count=1,
@@ -21,7 +21,7 @@ from .entities_constants import (
21
21
  from .gameplay import calculate_car_speed_for_passengers
22
22
  from .level_constants import DEFAULT_TILE_SIZE
23
23
  from .localization import set_language
24
- from .models import GameData, Stage
24
+ from .models import Stage
25
25
  from .render_constants import RenderAssets, build_render_assets
26
26
  from .screen_constants import (
27
27
  DEFAULT_WINDOW_SCALE,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zombie-escape
3
- Version: 1.12.0
3
+ Version: 1.12.3
4
4
  Summary: Top-down zombie survival game built with pygame.
5
5
  Project-URL: Homepage, https://github.com/tos-kamiya/zombie-escape
6
6
  Author-email: Toshihiro Kamiya <kamiya@mbj.nifty.com>
@@ -93,7 +93,7 @@ Open **Settings** from the title to toggle gameplay assists:
93
93
 
94
94
  ### Characters/Items
95
95
 
96
- - **Player:** A blue circle. Controlled with the WASD or arrow keys. When carrying fuel a tiny yellow square appears near the sprite so you can immediately see whether you're ready to drive.
96
+ - **Player:** A blue circle with small hands. Controlled with the WASD or arrow keys. When carrying fuel a tiny yellow square appears near the sprite so you can immediately see whether you're ready to drive.
97
97
  - **Zombie:** A red circle. Will chase the player (or car) once detected.
98
98
  - When out of sight, the zombie's movement mode will randomly switch every certain time (moving horizontally/vertically only, side-to-side movement, random movement, etc.).
99
99
  - Variants with different behavior have been observed.
@@ -108,10 +108,11 @@ Open **Settings** from the title to toggle gameplay assists:
108
108
  - **Flashlight:** Each pickup expands your visible radius by about 20% (grab two to reach the max boost).
109
109
  - **Steel Beam (optional):** A square post with crossed diagonals; same collision as inner walls but with triple durability. Spawns independently of inner walls (may overlap them). If an inner wall covers a beam, the beam appears once the wall is destroyed.
110
110
  - **Fuel Can (Stages 2 & 3):** A yellow jerrycan that only spawns on the fuel-run stages. Pick it up before driving the car; once collected the on-player indicator appears until you refuel the car.
111
- - **Buddy (Stage 3):** A green circle survivor with a blue outline who spawns somewhere in the building and waits.
111
+ - **Buddy (Stage 3):** A green circle survivor with small hands and a blue outline who spawns somewhere in the building and waits.
112
112
  - Zombies only choose to pursue the buddy if they are on-screen; otherwise they ignore them.
113
113
  - If a zombie tags the buddy off-screen, the buddy quietly respawns somewhere else instead of ending the run.
114
114
  - Touch the buddy on foot to make them follow you (at 70% of player speed). Touch them while driving to pick them up.
115
+ - If you bash an inner wall or steel beam, the buddy will drift toward that spot and help chip away at it.
115
116
  - **Survivors (Stage 4):** Pale gray civilians with a blue outline, scattered indoors.
116
117
  - They stand still until you get close, then shuffle toward you at about one-third of player speed.
117
118
  - Zombies can convert them if both are on-screen; the survivor shouts a line and turns instantly.