zombie-escape 1.10.1__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.
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import math
6
+ from dataclasses import dataclass
6
7
 
7
8
  import pygame
8
9
 
@@ -10,21 +11,22 @@ from .colors import (
10
11
  BLACK,
11
12
  BLUE,
12
13
  DARK_RED,
13
- RED,
14
- TRACKER_OUTLINE_COLOR,
15
- WALL_FOLLOWER_OUTLINE_COLOR,
16
- YELLOW,
17
- EnvironmentPalette,
18
14
  ORANGE,
19
15
  STEEL_BEAM_COLOR,
20
16
  STEEL_BEAM_LINE_COLOR,
17
+ YELLOW,
18
+ EnvironmentPalette,
21
19
  get_environment_palette,
22
20
  )
23
21
  from .render_constants import (
22
+ ANGLE_BINS,
24
23
  BUDDY_COLOR,
24
+ HAND_SPREAD_RAD,
25
25
  HUMANOID_OUTLINE_COLOR,
26
26
  HUMANOID_OUTLINE_WIDTH,
27
27
  SURVIVOR_COLOR,
28
+ ZOMBIE_BODY_COLOR,
29
+ ZOMBIE_OUTLINE_COLOR,
28
30
  FogRing,
29
31
  RenderAssets,
30
32
  )
@@ -43,6 +45,197 @@ def _draw_outlined_circle(
43
45
  pygame.draw.circle(surface, outline_color, center, radius, width=outline_width)
44
46
 
45
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
+
144
+ @dataclass(frozen=True)
145
+ class PolygonSpec:
146
+ size: tuple[int, int]
147
+ polygons: list[list[tuple[int, int]]]
148
+
149
+
150
+ FUEL_CAN_SPEC = PolygonSpec(
151
+ size=(13, 17),
152
+ polygons=[
153
+ [
154
+ (1, 1),
155
+ (7, 1),
156
+ (12, 6),
157
+ (12, 16),
158
+ (1, 16),
159
+ ],
160
+ [
161
+ (10, 1),
162
+ (12, 3),
163
+ (9, 4),
164
+ (7, 3),
165
+ ],
166
+ ],
167
+ )
168
+
169
+ FLASHLIGHT_SPEC = PolygonSpec(
170
+ size=(12, 10),
171
+ polygons=[
172
+ [
173
+ (1, 2),
174
+ (8, 2),
175
+ (8, 7),
176
+ (1, 7),
177
+ ],
178
+ [
179
+ (8, 1),
180
+ (11, 1),
181
+ (11, 8),
182
+ (8, 8),
183
+ ],
184
+ ],
185
+ )
186
+
187
+ SHOES_SPEC = PolygonSpec(
188
+ size=(14, 10),
189
+ polygons=[
190
+ [
191
+ (1, 1),
192
+ (7, 1),
193
+ (8, 4),
194
+ (13, 6),
195
+ (13, 9),
196
+ (1, 9),
197
+ ],
198
+ ],
199
+ )
200
+
201
+
202
+ def _scale_polygons(
203
+ spec: PolygonSpec,
204
+ dst_size: tuple[int, int],
205
+ ) -> list[list[tuple[int, int]]]:
206
+ src_w, src_h = spec.size
207
+ dst_w, dst_h = dst_size
208
+ scale_x = dst_w / max(1, src_w)
209
+ scale_y = dst_h / max(1, src_h)
210
+ scaled = []
211
+ for poly in spec.polygons:
212
+ scaled.append(
213
+ [
214
+ (
215
+ int(round(x * scale_x)),
216
+ int(round(y * scale_y)),
217
+ )
218
+ for x, y in poly
219
+ ]
220
+ )
221
+ return scaled
222
+
223
+
224
+ def _draw_polygon_surface(
225
+ width: int,
226
+ height: int,
227
+ spec: PolygonSpec,
228
+ ) -> pygame.Surface:
229
+ surface = pygame.Surface((width, height), pygame.SRCALPHA)
230
+ draw_polygons = spec.polygons
231
+ if (width, height) != spec.size:
232
+ draw_polygons = _scale_polygons(spec, (width, height))
233
+ for poly in draw_polygons:
234
+ pygame.draw.polygon(surface, YELLOW, poly)
235
+ pygame.draw.polygon(surface, BLACK, poly, width=1)
236
+ return surface
237
+
238
+
46
239
  def build_beveled_polygon(
47
240
  width: int,
48
241
  height: int,
@@ -178,55 +371,145 @@ def resolve_steel_beam_colors(
178
371
  return STEEL_BEAM_COLOR, STEEL_BEAM_LINE_COLOR
179
372
 
180
373
 
181
- def build_player_surface(radius: int) -> pygame.Surface:
182
- surface = pygame.Surface((radius * 2 + 2, radius * 2 + 2), pygame.SRCALPHA)
183
- _draw_outlined_circle(
184
- surface,
185
- (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(
186
381
  radius,
187
- BLUE,
188
- HUMANOID_OUTLINE_COLOR,
189
- HUMANOID_OUTLINE_WIDTH,
382
+ base_color=BLUE,
383
+ cap_color=_brighten_color(BLUE),
384
+ bins=bins,
385
+ outline_color=HUMANOID_OUTLINE_COLOR,
190
386
  )
191
- return surface
387
+ _PLAYER_DIRECTIONAL_CACHE[cache_key] = surfaces
388
+ return surfaces
192
389
 
193
390
 
194
- def build_survivor_surface(radius: int, *, is_buddy: bool) -> pygame.Surface:
195
- surface = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
196
- fill_color = BUDDY_COLOR if is_buddy else SURVIVOR_COLOR
197
- _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(
198
463
  surface,
199
- (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(
200
483
  radius,
201
- fill_color,
202
- HUMANOID_OUTLINE_COLOR,
203
- 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,
204
489
  )
205
- return surface
490
+ _SURVIVOR_DIRECTIONAL_CACHE[cache_key] = surfaces
491
+ return surfaces
206
492
 
207
493
 
208
- def build_zombie_surface(
494
+ def build_zombie_directional_surfaces(
209
495
  radius: int,
210
496
  *,
211
- tracker: bool = False,
212
- wall_follower: bool = False,
213
- ) -> pygame.Surface:
214
- if tracker:
215
- outline_color = TRACKER_OUTLINE_COLOR
216
- elif wall_follower:
217
- outline_color = WALL_FOLLOWER_OUTLINE_COLOR
218
- else:
219
- outline_color = DARK_RED
220
- surface = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
221
- _draw_outlined_circle(
222
- surface,
223
- (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(
224
504
  radius,
225
- RED,
226
- outline_color,
227
- 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,
228
510
  )
229
- return surface
511
+ _ZOMBIE_DIRECTIONAL_CACHE[cache_key] = surfaces
512
+ return surfaces
230
513
 
231
514
 
232
515
  def build_car_surface(width: int, height: int) -> pygame.Surface:
@@ -240,54 +523,101 @@ def paint_car_surface(
240
523
  height: int,
241
524
  color: tuple[int, int, int],
242
525
  ) -> None:
243
- 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)
244
539
 
245
- body_rect = pygame.Rect(1, 4, width - 2, height - 8)
246
- front_cap_height = max(8, body_rect.height // 3)
247
- front_cap = pygame.Rect(
248
- body_rect.left, body_rect.top, body_rect.width, front_cap_height
249
- )
250
- windshield_rect = pygame.Rect(
251
- body_rect.left + 4,
252
- body_rect.top + 3,
253
- body_rect.width - 8,
254
- front_cap_height - 5,
255
- )
256
540
 
257
- trim_color = tuple(int(c * 0.55) for c in color)
258
- front_cap_color = tuple(min(255, int(c * 1.08)) for c in color)
259
- body_color = color
260
- window_color = (70, 110, 150)
261
- wheel_color = (35, 35, 35)
262
-
263
- wheel_width = width // 3
264
- wheel_height = 6
265
- for y in (body_rect.top + 4, body_rect.bottom - wheel_height - 4):
266
- left_wheel = pygame.Rect(2, y, wheel_width, wheel_height)
267
- right_wheel = pygame.Rect(width - wheel_width - 2, y, wheel_width, wheel_height)
268
- pygame.draw.rect(surface, wheel_color, left_wheel, border_radius=3)
269
- pygame.draw.rect(surface, wheel_color, right_wheel, border_radius=3)
270
-
271
- pygame.draw.rect(surface, body_color, body_rect, border_radius=4)
272
- pygame.draw.rect(surface, trim_color, body_rect, width=2, border_radius=4)
273
- pygame.draw.rect(surface, front_cap_color, front_cap, border_radius=10)
274
- pygame.draw.rect(surface, trim_color, front_cap, width=2, border_radius=10)
275
- pygame.draw.rect(surface, window_color, windshield_rect, border_radius=4)
276
-
277
- headlight_color = (245, 245, 200)
278
- for x in (front_cap.left + 5, front_cap.right - 5):
279
- pygame.draw.circle(surface, headlight_color, (x, body_rect.top + 5), 2)
280
- grille_rect = pygame.Rect(front_cap.centerx - 6, front_cap.top + 2, 12, 6)
281
- 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)
282
552
  tail_light_color = (255, 80, 50)
283
- for x in (body_rect.left + 5, body_rect.right - 5):
284
- pygame.draw.rect(
285
- surface,
286
- tail_light_color,
287
- (x - 2, body_rect.bottom - 5, 4, 3),
288
- 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))),
289
566
  )
290
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
+
291
621
 
292
622
  def paint_wall_surface(
293
623
  surface: pygame.Surface,
@@ -426,101 +756,20 @@ def paint_steel_beam_surface(
426
756
  surface.blit(top_surface, top_rect.topleft)
427
757
 
428
758
 
429
- def paint_zombie_surface(
430
- surface: pygame.Surface,
431
- *,
432
- radius: int,
433
- palm_angle: float | None = None,
434
- tracker: bool = False,
435
- wall_follower: bool = False,
436
- ) -> None:
437
- if tracker:
438
- outline_color = TRACKER_OUTLINE_COLOR
439
- elif wall_follower:
440
- outline_color = WALL_FOLLOWER_OUTLINE_COLOR
441
- else:
442
- outline_color = DARK_RED
443
- surface.fill((0, 0, 0, 0))
444
- _draw_outlined_circle(
445
- surface,
446
- (radius, radius),
447
- radius,
448
- RED,
449
- outline_color,
450
- 1,
451
- )
452
- if palm_angle is None:
453
- return
454
- palm_radius = max(1, radius // 3)
455
- palm_offset = radius - palm_radius * 0.3
456
- palm_x = radius + math.cos(palm_angle) * palm_offset
457
- palm_y = radius + math.sin(palm_angle) * palm_offset
458
- pygame.draw.circle(
459
- surface,
460
- outline_color,
461
- (int(palm_x), int(palm_y)),
462
- palm_radius,
463
- )
464
-
465
-
466
759
  def build_fuel_can_surface(width: int, height: int) -> pygame.Surface:
467
- surface = pygame.Surface((width, height), pygame.SRCALPHA)
468
-
469
- # Jerrycan silhouette with cut corner
470
- body_pts = [
471
- (1, 4),
472
- (width - 2, 4),
473
- (width - 2, height - 2),
474
- (1, height - 2),
475
- (1, 8),
476
- (4, 4),
477
- ]
478
- pygame.draw.polygon(surface, YELLOW, body_pts)
479
- pygame.draw.polygon(surface, BLACK, body_pts, width=2)
480
-
481
- cap_size = max(2, width // 4)
482
- cap_rect = pygame.Rect(width - cap_size - 2, 1, cap_size, 3)
483
- pygame.draw.rect(surface, YELLOW, cap_rect, border_radius=1)
484
- pygame.draw.rect(surface, BLACK, cap_rect, width=1, border_radius=1)
485
-
486
- # Cross brace accent
487
- brace_color = (240, 200, 40)
488
- pygame.draw.line(
489
- surface, brace_color, (3, height // 2), (width - 4, height // 2), width=2
490
- )
491
- pygame.draw.line(
492
- surface, BLACK, (3, height // 2), (width - 4, height // 2), width=1
493
- )
494
- return surface
760
+ return _draw_polygon_surface(width, height, FUEL_CAN_SPEC)
495
761
 
496
762
 
497
763
  def build_flashlight_surface(width: int, height: int) -> pygame.Surface:
498
- surface = pygame.Surface((width, height), pygame.SRCALPHA)
764
+ return _draw_polygon_surface(width, height, FLASHLIGHT_SPEC)
499
765
 
500
- body_color = (230, 200, 70)
501
- trim_color = (80, 70, 40)
502
- head_color = (200, 180, 90)
503
- beam_color = (255, 240, 180, 150)
504
766
 
505
- body_rect = pygame.Rect(1, 2, width - 4, height - 4)
506
- head_rect = pygame.Rect(
507
- body_rect.right - 3, body_rect.top - 1, 4, body_rect.height + 2
508
- )
509
- beam_points = [
510
- (head_rect.right + 4, head_rect.centery),
511
- (head_rect.right + 2, head_rect.top),
512
- (head_rect.right + 2, head_rect.bottom),
513
- ]
514
-
515
- pygame.draw.rect(surface, body_color, body_rect, border_radius=2)
516
- pygame.draw.rect(surface, trim_color, body_rect, width=1, border_radius=2)
517
- pygame.draw.rect(surface, head_color, head_rect, border_radius=2)
518
- pygame.draw.rect(surface, trim_color, head_rect, width=1, border_radius=2)
519
- pygame.draw.polygon(surface, beam_color, beam_points)
520
- return surface
767
+ def build_shoes_surface(width: int, height: int) -> pygame.Surface:
768
+ return _draw_polygon_surface(width, height, SHOES_SPEC)
521
769
 
522
770
 
523
771
  __all__ = [
772
+ "angle_bin_from_vector",
524
773
  "EnvironmentPalette",
525
774
  "FogRing",
526
775
  "RenderAssets",
@@ -529,14 +778,18 @@ __all__ = [
529
778
  "resolve_car_color",
530
779
  "resolve_steel_beam_colors",
531
780
  "CAR_COLOR_SCHEMES",
532
- "build_player_surface",
533
- "build_survivor_surface",
534
- "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",
535
787
  "build_car_surface",
788
+ "build_car_directional_surfaces",
536
789
  "paint_car_surface",
537
790
  "paint_wall_surface",
538
791
  "paint_steel_beam_surface",
539
- "paint_zombie_surface",
540
792
  "build_fuel_can_surface",
541
793
  "build_flashlight_surface",
794
+ "build_shoes_surface",
542
795
  ]