zombie-escape 1.12.3__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/entities.py +554 -132
- zombie_escape/entities_constants.py +29 -12
- zombie_escape/export_images.py +296 -0
- zombie_escape/gameplay/__init__.py +2 -1
- zombie_escape/gameplay/constants.py +6 -0
- zombie_escape/gameplay/layout.py +85 -20
- zombie_escape/gameplay/movement.py +62 -4
- zombie_escape/gameplay/spawn.py +47 -40
- zombie_escape/gameplay/state.py +3 -0
- zombie_escape/gameplay/survivors.py +4 -1
- zombie_escape/level_blueprints.py +169 -15
- zombie_escape/locales/ui.en.json +12 -2
- zombie_escape/locales/ui.ja.json +12 -2
- zombie_escape/models.py +9 -3
- zombie_escape/render.py +98 -21
- zombie_escape/render_assets.py +94 -0
- zombie_escape/render_constants.py +16 -0
- zombie_escape/screens/game_over.py +11 -0
- zombie_escape/screens/gameplay.py +68 -14
- zombie_escape/screens/title.py +18 -7
- zombie_escape/stage_constants.py +46 -10
- zombie_escape/zombie_escape.py +23 -0
- {zombie_escape-1.12.3.dist-info → zombie_escape-1.13.1.dist-info}/METADATA +40 -15
- zombie_escape-1.13.1.dist-info/RECORD +49 -0
- zombie_escape-1.12.3.dist-info/RECORD +0 -47
- {zombie_escape-1.12.3.dist-info → zombie_escape-1.13.1.dist-info}/WHEEL +0 -0
- {zombie_escape-1.12.3.dist-info → zombie_escape-1.13.1.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.12.3.dist-info → zombie_escape-1.13.1.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/locales/ui.ja.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"settings": "設定",
|
|
17
17
|
"quit": "終了",
|
|
18
18
|
"locked_suffix": "[Locked]",
|
|
19
|
-
"window_hint": "[キーでウィンドウを小さく、]キーで大きく。\nF:
|
|
19
|
+
"window_hint": "[キーでウィンドウを小さく、]キーで大きく。\nF: フルスクリーン切替。",
|
|
20
20
|
"seed_label": "シード: %{value}",
|
|
21
21
|
"seed_hint": "数字キーで入力、BSでクリア",
|
|
22
22
|
"seed_empty": "(自動)",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"resources": "リソース"
|
|
26
26
|
},
|
|
27
27
|
"readme": "README を開く",
|
|
28
|
+
"readme_stage6": "ステージ6以降の説明を開く",
|
|
28
29
|
"hints": {
|
|
29
30
|
"navigate": "上下: 項目選択",
|
|
30
31
|
"page_switch": "左右: ステージ群切替",
|
|
@@ -33,7 +34,8 @@
|
|
|
33
34
|
"option_help": {
|
|
34
35
|
"settings": "設定画面を開いて言語や補助オプションを変更します。",
|
|
35
36
|
"quit": "ゲームを終了します。",
|
|
36
|
-
"readme": "GitHub の README ページをブラウザで開きます。"
|
|
37
|
+
"readme": "GitHub の README ページをブラウザで開きます。",
|
|
38
|
+
"readme_stage6": "ステージ6以降の説明ページをブラウザで開きます。"
|
|
37
39
|
}
|
|
38
40
|
},
|
|
39
41
|
"stages": {
|
|
@@ -127,6 +129,10 @@
|
|
|
127
129
|
"stage15": {
|
|
128
130
|
"name": "#15 分断ライン",
|
|
129
131
|
"description": "建物中央を分断する危険地帯。横断には注意。"
|
|
132
|
+
},
|
|
133
|
+
"stage16": {
|
|
134
|
+
"name": "#16 奈落",
|
|
135
|
+
"description": "崩落した床パネルを回り込んでゾンビを誘い込もう。"
|
|
130
136
|
}
|
|
131
137
|
},
|
|
132
138
|
"status": {
|
|
@@ -198,10 +204,14 @@
|
|
|
198
204
|
"game_over": {
|
|
199
205
|
"win": "脱出成功!",
|
|
200
206
|
"lose": "ゲームオーバー",
|
|
207
|
+
"fell_into_abyss": "奈落の底へ落ちてしまった...",
|
|
201
208
|
"prompt": "ESC/Space/Select/South: タイトルへ R: 再挑戦",
|
|
202
209
|
"scream": "ぎゃあーーー!!",
|
|
203
210
|
"survivors_summary": "救出人数: %{count}",
|
|
204
211
|
"endurance_duration": "逃げ延びた時間 %{time}"
|
|
212
|
+
},
|
|
213
|
+
"errors": {
|
|
214
|
+
"map_generation_failed": "マップの生成に失敗しました。シード値を変えるか、別のステージを選んでください。"
|
|
205
215
|
}
|
|
206
216
|
}
|
|
207
217
|
}
|
zombie_escape/models.py
CHANGED
|
@@ -31,13 +31,15 @@ class LevelLayout:
|
|
|
31
31
|
walkable_cells: list[tuple[int, int]]
|
|
32
32
|
outer_wall_cells: set[tuple[int, int]]
|
|
33
33
|
wall_cells: set[tuple[int, int]]
|
|
34
|
+
pitfall_cells: set[tuple[int, int]]
|
|
35
|
+
car_walkable_cells: set[tuple[int, int]]
|
|
34
36
|
fall_spawn_cells: set[tuple[int, int]]
|
|
35
37
|
bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]]
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
@dataclass
|
|
39
41
|
class FallingZombie:
|
|
40
|
-
"""Represents a zombie falling toward a target position."""
|
|
42
|
+
"""Represents a zombie falling toward a target position or into a pit."""
|
|
41
43
|
|
|
42
44
|
start_pos: tuple[int, int]
|
|
43
45
|
target_pos: tuple[int, int]
|
|
@@ -46,8 +48,9 @@ class FallingZombie:
|
|
|
46
48
|
fall_duration_ms: int
|
|
47
49
|
dust_duration_ms: int
|
|
48
50
|
tracker: bool
|
|
49
|
-
|
|
51
|
+
wall_hugging: bool
|
|
50
52
|
dust_started: bool = False
|
|
53
|
+
mode: str = "spawn" # "spawn" (falling in) or "pitfall" (falling out)
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
@dataclass
|
|
@@ -102,6 +105,7 @@ class ProgressState:
|
|
|
102
105
|
last_zombie_spawn_time: int
|
|
103
106
|
dawn_carbonized: bool
|
|
104
107
|
debug_mode: bool
|
|
108
|
+
show_fps: bool
|
|
105
109
|
falling_zombies: list[FallingZombie]
|
|
106
110
|
falling_spawn_carry: int
|
|
107
111
|
dust_rings: list[DustRing]
|
|
@@ -166,12 +170,14 @@ class Stage:
|
|
|
166
170
|
interior_fall_spawn_weight: float = 0.0
|
|
167
171
|
fall_spawn_zones: list[tuple[int, int, int, int]] = field(default_factory=list)
|
|
168
172
|
fall_spawn_floor_ratio: float = 0.0
|
|
173
|
+
pitfall_density: float = 0.0
|
|
169
174
|
zombie_tracker_ratio: float = 0.0
|
|
170
|
-
|
|
175
|
+
zombie_wall_hugging_ratio: float = 0.0
|
|
171
176
|
zombie_normal_ratio: float = 1.0
|
|
172
177
|
zombie_aging_duration_frames: int = ZOMBIE_AGING_DURATION_FRAMES
|
|
173
178
|
waiting_car_target_count: int = 1
|
|
174
179
|
wall_algorithm: str = "default"
|
|
180
|
+
wall_rubble_ratio: float = 0.0
|
|
175
181
|
|
|
176
182
|
@property
|
|
177
183
|
def name(self) -> str:
|
zombie_escape/render.py
CHANGED
|
@@ -34,6 +34,8 @@ from .entities_constants import (
|
|
|
34
34
|
FLASHLIGHT_WIDTH,
|
|
35
35
|
FUEL_CAN_HEIGHT,
|
|
36
36
|
FUEL_CAN_WIDTH,
|
|
37
|
+
INTERNAL_WALL_BEVEL_DEPTH,
|
|
38
|
+
JUMP_SHADOW_OFFSET,
|
|
37
39
|
SHOES_HEIGHT,
|
|
38
40
|
SHOES_WIDTH,
|
|
39
41
|
ZOMBIE_RADIUS,
|
|
@@ -63,6 +65,11 @@ from .render_constants import (
|
|
|
63
65
|
FALLING_ZOMBIE_COLOR,
|
|
64
66
|
FLASHLIGHT_FOG_SCALE_ONE,
|
|
65
67
|
FLASHLIGHT_FOG_SCALE_TWO,
|
|
68
|
+
PITFALL_ABYSS_COLOR,
|
|
69
|
+
PITFALL_EDGE_DEPTH_OFFSET,
|
|
70
|
+
PITFALL_EDGE_METAL_COLOR,
|
|
71
|
+
PITFALL_EDGE_STRIPE_COLOR,
|
|
72
|
+
PITFALL_EDGE_STRIPE_SPACING,
|
|
66
73
|
PLAYER_SHADOW_ALPHA_MULT,
|
|
67
74
|
PLAYER_SHADOW_RADIUS_MULT,
|
|
68
75
|
SHADOW_MIN_RATIO,
|
|
@@ -314,9 +321,7 @@ def draw_level_overview(
|
|
|
314
321
|
if flashlights:
|
|
315
322
|
for flashlight in flashlights:
|
|
316
323
|
if flashlight.alive():
|
|
317
|
-
pygame.draw.rect(
|
|
318
|
-
surface, YELLOW, flashlight.rect, border_radius=2
|
|
319
|
-
)
|
|
324
|
+
pygame.draw.rect(surface, YELLOW, flashlight.rect, border_radius=2)
|
|
320
325
|
pygame.draw.rect(
|
|
321
326
|
surface, BLACK, flashlight.rect, width=2, border_radius=2
|
|
322
327
|
)
|
|
@@ -587,6 +592,8 @@ def _draw_fall_whirlwind(
|
|
|
587
592
|
camera: Camera,
|
|
588
593
|
center: tuple[int, int],
|
|
589
594
|
progress: float,
|
|
595
|
+
*,
|
|
596
|
+
scale: float = 1.0,
|
|
590
597
|
) -> None:
|
|
591
598
|
base_alpha = FALLING_WHIRLWIND_COLOR[3]
|
|
592
599
|
alpha = int(max(0, min(255, base_alpha * (1.0 - progress))))
|
|
@@ -598,8 +605,9 @@ def _draw_fall_whirlwind(
|
|
|
598
605
|
FALLING_WHIRLWIND_COLOR[2],
|
|
599
606
|
alpha,
|
|
600
607
|
)
|
|
601
|
-
|
|
602
|
-
|
|
608
|
+
safe_scale = max(0.4, scale)
|
|
609
|
+
swirl_radius = max(2, int(ZOMBIE_RADIUS * 1.1 * safe_scale))
|
|
610
|
+
offset = max(1, int(ZOMBIE_RADIUS * 0.6 * safe_scale))
|
|
603
611
|
size = swirl_radius * 4
|
|
604
612
|
swirl = pygame.Surface((size, size), pygame.SRCALPHA)
|
|
605
613
|
cx = cy = size // 2
|
|
@@ -632,22 +640,42 @@ def _draw_falling_fx(
|
|
|
632
640
|
if now < fall_start:
|
|
633
641
|
if flashlight_count > 0 and pre_fx_ms > 0:
|
|
634
642
|
fx_progress = max(0.0, min(1.0, (now - fall.started_at_ms) / pre_fx_ms))
|
|
635
|
-
|
|
643
|
+
# Make the premonition grow with the impending drop scale.
|
|
644
|
+
pre_scale = 1.0 + (0.9 * fx_progress)
|
|
645
|
+
_draw_fall_whirlwind(
|
|
646
|
+
screen,
|
|
647
|
+
camera,
|
|
648
|
+
fall.start_pos,
|
|
649
|
+
fx_progress,
|
|
650
|
+
scale=pre_scale,
|
|
651
|
+
)
|
|
636
652
|
continue
|
|
637
653
|
if now >= impact_at:
|
|
638
654
|
continue
|
|
639
655
|
fall_progress = max(0.0, min(1.0, (now - fall_start) / fall_duration_ms))
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
656
|
+
|
|
657
|
+
if getattr(fall, "mode", "spawn") == "pitfall":
|
|
658
|
+
scale = 1.0 - fall_progress
|
|
659
|
+
scale = scale * scale
|
|
660
|
+
y_offset = 0.0
|
|
661
|
+
else:
|
|
662
|
+
eased = 1.0 - (1.0 - fall_progress) * (1.0 - fall_progress)
|
|
663
|
+
scale = 2.0 - (1.0 * eased)
|
|
664
|
+
# Add an extra vertical drop from above (1.5x wall depth)
|
|
665
|
+
y_offset = -INTERNAL_WALL_BEVEL_DEPTH * 1.5 * (1.0 - eased)
|
|
666
|
+
|
|
667
|
+
radius = ZOMBIE_RADIUS * scale
|
|
668
|
+
cx = fall.target_pos[0]
|
|
669
|
+
cy = fall.target_pos[1] + ZOMBIE_RADIUS - radius + y_offset
|
|
670
|
+
|
|
671
|
+
world_rect = pygame.Rect(0, 0, radius * 2, radius * 2)
|
|
672
|
+
world_rect.center = (int(cx), int(cy))
|
|
645
673
|
screen_rect = camera.apply_rect(world_rect)
|
|
646
674
|
pygame.draw.circle(
|
|
647
675
|
screen,
|
|
648
676
|
FALLING_ZOMBIE_COLOR,
|
|
649
677
|
screen_rect.center,
|
|
650
|
-
|
|
678
|
+
max(1, int(screen_rect.width / 2)),
|
|
651
679
|
)
|
|
652
680
|
|
|
653
681
|
for ring in list(dust_rings):
|
|
@@ -725,6 +753,7 @@ def _draw_status_bar(
|
|
|
725
753
|
debug_mode: bool = False,
|
|
726
754
|
zombie_group: sprite.Group | None = None,
|
|
727
755
|
falling_spawn_carry: int | None = None,
|
|
756
|
+
show_fps: bool = False,
|
|
728
757
|
fps: float | None = None,
|
|
729
758
|
) -> None:
|
|
730
759
|
"""Render a compact status bar with current config flags and stage info."""
|
|
@@ -766,14 +795,12 @@ def _draw_status_bar(
|
|
|
766
795
|
zombies = [z for z in zombie_group if z.alive()]
|
|
767
796
|
total = len(zombies)
|
|
768
797
|
tracker = sum(1 for z in zombies if z.tracker)
|
|
769
|
-
wall = sum(1 for z in zombies if z.
|
|
798
|
+
wall = sum(1 for z in zombies if z.wall_hugging)
|
|
770
799
|
normal = max(0, total - tracker - wall)
|
|
771
|
-
|
|
800
|
+
debug_counts = f"Z:{total} N:{normal} T:{tracker} W:{wall}"
|
|
772
801
|
if falling_spawn_carry is not None:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
parts.append(f"FPS:{fps:.1f}")
|
|
776
|
-
|
|
802
|
+
debug_counts = f"{debug_counts} C:{max(0, falling_spawn_carry)}"
|
|
803
|
+
parts.append(debug_counts)
|
|
777
804
|
status_text = " | ".join(parts)
|
|
778
805
|
color = LIGHT_GRAY
|
|
779
806
|
|
|
@@ -790,7 +817,7 @@ def _draw_status_bar(
|
|
|
790
817
|
right=bar_rect.right - 12, centery=bar_rect.centery
|
|
791
818
|
)
|
|
792
819
|
screen.blit(seed_surface, seed_rect)
|
|
793
|
-
if
|
|
820
|
+
if show_fps and fps is not None:
|
|
794
821
|
fps_text = f"FPS:{fps:.1f}"
|
|
795
822
|
fps_surface = font.render(fps_text, False, LIGHT_GRAY)
|
|
796
823
|
fps_rect = fps_surface.get_rect(
|
|
@@ -810,6 +837,7 @@ def _draw_play_area(
|
|
|
810
837
|
field_rect: pygame.Rect,
|
|
811
838
|
outside_cells: set[tuple[int, int]],
|
|
812
839
|
fall_spawn_cells: set[tuple[int, int]],
|
|
840
|
+
pitfall_cells: set[tuple[int, int]],
|
|
813
841
|
) -> tuple[int, int, int, int, set[tuple[int, int]]]:
|
|
814
842
|
xs, ys, xe, ye = (
|
|
815
843
|
field_rect.left,
|
|
@@ -865,6 +893,43 @@ def _draw_play_area(
|
|
|
865
893
|
if sr.colliderect(screen.get_rect()):
|
|
866
894
|
pygame.draw.rect(screen, palette.outside, sr)
|
|
867
895
|
continue
|
|
896
|
+
|
|
897
|
+
if (x, y) in pitfall_cells:
|
|
898
|
+
lx, ly = (
|
|
899
|
+
x * assets.internal_wall_grid_snap,
|
|
900
|
+
y * assets.internal_wall_grid_snap,
|
|
901
|
+
)
|
|
902
|
+
r = pygame.Rect(
|
|
903
|
+
lx,
|
|
904
|
+
ly,
|
|
905
|
+
assets.internal_wall_grid_snap,
|
|
906
|
+
assets.internal_wall_grid_snap,
|
|
907
|
+
)
|
|
908
|
+
sr = camera.apply_rect(r)
|
|
909
|
+
if not sr.colliderect(screen.get_rect()):
|
|
910
|
+
continue
|
|
911
|
+
pygame.draw.rect(screen, PITFALL_ABYSS_COLOR, sr)
|
|
912
|
+
|
|
913
|
+
if (x, y - 1) not in pitfall_cells:
|
|
914
|
+
edge_h = max(
|
|
915
|
+
1, INTERNAL_WALL_BEVEL_DEPTH - PITFALL_EDGE_DEPTH_OFFSET
|
|
916
|
+
)
|
|
917
|
+
pygame.draw.rect(
|
|
918
|
+
screen, PITFALL_EDGE_METAL_COLOR, (sr.x, sr.y, sr.w, edge_h)
|
|
919
|
+
)
|
|
920
|
+
for sx in range(
|
|
921
|
+
sr.x - edge_h, sr.right, PITFALL_EDGE_STRIPE_SPACING
|
|
922
|
+
):
|
|
923
|
+
pygame.draw.line(
|
|
924
|
+
screen,
|
|
925
|
+
PITFALL_EDGE_STRIPE_COLOR,
|
|
926
|
+
(max(sr.x, sx), sr.y),
|
|
927
|
+
(min(sr.right - 1, sx + edge_h), sr.y + edge_h - 1),
|
|
928
|
+
width=2,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
continue
|
|
932
|
+
|
|
868
933
|
use_secondary = ((x // 2) + (y // 2)) % 2 == 0
|
|
869
934
|
if (x, y) in fall_spawn_cells:
|
|
870
935
|
color = (
|
|
@@ -1021,8 +1086,13 @@ def _draw_entity_shadows(
|
|
|
1021
1086
|
else:
|
|
1022
1087
|
offset_x = 0.0
|
|
1023
1088
|
offset_y = 0.0
|
|
1089
|
+
|
|
1090
|
+
jump_dy = 0.0
|
|
1091
|
+
if getattr(entity, "is_jumping", False):
|
|
1092
|
+
jump_dy = JUMP_SHADOW_OFFSET
|
|
1093
|
+
|
|
1024
1094
|
shadow_rect = shadow_surface.get_rect(
|
|
1025
|
-
center=(int(cx + offset_x), int(cy + offset_y))
|
|
1095
|
+
center=(int(cx + offset_x), int(cy + offset_y + jump_dy))
|
|
1026
1096
|
)
|
|
1027
1097
|
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
1028
1098
|
if not shadow_screen_rect.colliderect(screen_rect):
|
|
@@ -1081,8 +1151,13 @@ def _draw_single_entity_shadow(
|
|
|
1081
1151
|
else:
|
|
1082
1152
|
offset_x = 0.0
|
|
1083
1153
|
offset_y = 0.0
|
|
1154
|
+
|
|
1155
|
+
jump_dy = 0.0
|
|
1156
|
+
if getattr(entity, "is_jumping", False):
|
|
1157
|
+
jump_dy = JUMP_SHADOW_OFFSET
|
|
1158
|
+
|
|
1084
1159
|
shadow_rect = shadow_surface.get_rect(
|
|
1085
|
-
center=(int(cx + offset_x), int(cy + offset_y))
|
|
1160
|
+
center=(int(cx + offset_x), int(cy + offset_y + jump_dy))
|
|
1086
1161
|
)
|
|
1087
1162
|
shadow_screen_rect = camera.apply_rect(shadow_rect)
|
|
1088
1163
|
if not shadow_screen_rect.colliderect(screen_rect):
|
|
@@ -1525,6 +1600,7 @@ def draw(
|
|
|
1525
1600
|
field_rect,
|
|
1526
1601
|
outside_cells,
|
|
1527
1602
|
game_data.layout.fall_spawn_cells,
|
|
1603
|
+
game_data.layout.pitfall_cells,
|
|
1528
1604
|
)
|
|
1529
1605
|
shadow_layer = _get_shadow_layer(screen.get_size())
|
|
1530
1606
|
shadow_layer.fill((0, 0, 0, 0))
|
|
@@ -1660,6 +1736,7 @@ def draw(
|
|
|
1660
1736
|
debug_mode=state.debug_mode,
|
|
1661
1737
|
zombie_group=zombie_group,
|
|
1662
1738
|
falling_spawn_carry=state.falling_spawn_carry,
|
|
1739
|
+
show_fps=state.show_fps,
|
|
1663
1740
|
fps=fps,
|
|
1664
1741
|
)
|
|
1665
1742
|
if do_flip:
|
zombie_escape/render_assets.py
CHANGED
|
@@ -18,6 +18,7 @@ from .colors import (
|
|
|
18
18
|
EnvironmentPalette,
|
|
19
19
|
get_environment_palette,
|
|
20
20
|
)
|
|
21
|
+
from .entities_constants import INTERNAL_WALL_BEVEL_DEPTH
|
|
21
22
|
from .render_constants import (
|
|
22
23
|
ANGLE_BINS,
|
|
23
24
|
BUDDY_COLOR,
|
|
@@ -61,6 +62,22 @@ _SURVIVOR_DIRECTIONAL_CACHE: dict[
|
|
|
61
62
|
tuple[int, bool, bool, int], list[pygame.Surface]
|
|
62
63
|
] = {}
|
|
63
64
|
_ZOMBIE_DIRECTIONAL_CACHE: dict[tuple[int, bool, int], list[pygame.Surface]] = {}
|
|
65
|
+
_RUBBLE_SURFACE_CACHE: dict[tuple, pygame.Surface] = {}
|
|
66
|
+
|
|
67
|
+
RUBBLE_ROTATION_DEG = 5.0
|
|
68
|
+
RUBBLE_OFFSET_RATIO = 0.06
|
|
69
|
+
RUBBLE_SCALE_RATIO = 0.9
|
|
70
|
+
RUBBLE_SHADOW_RATIO = 0.9
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _scale_color(
|
|
74
|
+
color: tuple[int, int, int], *, ratio: float
|
|
75
|
+
) -> tuple[int, int, int]:
|
|
76
|
+
return tuple(max(0, min(255, int(c * ratio + 0.5))) for c in color)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rubble_offset_for_size(size: int) -> int:
|
|
80
|
+
return max(1, int(round(size * RUBBLE_OFFSET_RATIO)))
|
|
64
81
|
|
|
65
82
|
|
|
66
83
|
def angle_bin_from_vector(
|
|
@@ -706,6 +723,80 @@ def paint_wall_surface(
|
|
|
706
723
|
_draw_face(surface)
|
|
707
724
|
|
|
708
725
|
|
|
726
|
+
def build_rubble_wall_surface(
|
|
727
|
+
size: int,
|
|
728
|
+
*,
|
|
729
|
+
fill_color: tuple[int, int, int],
|
|
730
|
+
border_color: tuple[int, int, int],
|
|
731
|
+
angle_deg: float,
|
|
732
|
+
offset_px: int | None = None,
|
|
733
|
+
scale_ratio: float = RUBBLE_SCALE_RATIO,
|
|
734
|
+
shadow_ratio: float = RUBBLE_SHADOW_RATIO,
|
|
735
|
+
bevel_depth: int = INTERNAL_WALL_BEVEL_DEPTH,
|
|
736
|
+
) -> pygame.Surface:
|
|
737
|
+
offset_px = offset_px if offset_px is not None else rubble_offset_for_size(size)
|
|
738
|
+
safe_size = max(1, size)
|
|
739
|
+
base_size = max(1, int(round(safe_size * scale_ratio)))
|
|
740
|
+
tuned_bevel = min(bevel_depth, max(1, base_size // 2))
|
|
741
|
+
cache_key = (
|
|
742
|
+
safe_size,
|
|
743
|
+
fill_color,
|
|
744
|
+
border_color,
|
|
745
|
+
angle_deg,
|
|
746
|
+
offset_px,
|
|
747
|
+
scale_ratio,
|
|
748
|
+
shadow_ratio,
|
|
749
|
+
tuned_bevel,
|
|
750
|
+
)
|
|
751
|
+
cached = _RUBBLE_SURFACE_CACHE.get(cache_key)
|
|
752
|
+
if cached is not None:
|
|
753
|
+
return cached
|
|
754
|
+
|
|
755
|
+
top_surface = pygame.Surface((base_size, base_size), pygame.SRCALPHA)
|
|
756
|
+
paint_wall_surface(
|
|
757
|
+
top_surface,
|
|
758
|
+
fill_color=fill_color,
|
|
759
|
+
border_color=border_color,
|
|
760
|
+
bevel_depth=tuned_bevel,
|
|
761
|
+
bevel_mask=(False, False, False, False),
|
|
762
|
+
draw_bottom_side=False,
|
|
763
|
+
bottom_side_ratio=0.1,
|
|
764
|
+
side_shade_ratio=0.9,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
shadow_fill = _scale_color(fill_color, ratio=shadow_ratio)
|
|
768
|
+
shadow_border = _scale_color(border_color, ratio=shadow_ratio)
|
|
769
|
+
shadow_surface = pygame.Surface((base_size, base_size), pygame.SRCALPHA)
|
|
770
|
+
paint_wall_surface(
|
|
771
|
+
shadow_surface,
|
|
772
|
+
fill_color=shadow_fill,
|
|
773
|
+
border_color=shadow_border,
|
|
774
|
+
bevel_depth=tuned_bevel,
|
|
775
|
+
bevel_mask=(False, False, False, False),
|
|
776
|
+
draw_bottom_side=False,
|
|
777
|
+
bottom_side_ratio=0.1,
|
|
778
|
+
side_shade_ratio=0.9,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
if angle_deg:
|
|
782
|
+
top_surface = pygame.transform.rotate(top_surface, angle_deg)
|
|
783
|
+
shadow_surface = pygame.transform.rotate(shadow_surface, angle_deg)
|
|
784
|
+
|
|
785
|
+
final_surface = pygame.Surface((safe_size, safe_size), pygame.SRCALPHA)
|
|
786
|
+
center = final_surface.get_rect().center
|
|
787
|
+
|
|
788
|
+
shadow_rect = shadow_surface.get_rect(
|
|
789
|
+
center=(center[0] + offset_px, center[1] + offset_px)
|
|
790
|
+
)
|
|
791
|
+
final_surface.blit(shadow_surface, shadow_rect.topleft)
|
|
792
|
+
|
|
793
|
+
top_rect = top_surface.get_rect(center=center)
|
|
794
|
+
final_surface.blit(top_surface, top_rect.topleft)
|
|
795
|
+
|
|
796
|
+
_RUBBLE_SURFACE_CACHE[cache_key] = final_surface
|
|
797
|
+
return final_surface
|
|
798
|
+
|
|
799
|
+
|
|
709
800
|
def paint_steel_beam_surface(
|
|
710
801
|
surface: pygame.Surface,
|
|
711
802
|
*,
|
|
@@ -788,6 +879,9 @@ __all__ = [
|
|
|
788
879
|
"build_car_directional_surfaces",
|
|
789
880
|
"paint_car_surface",
|
|
790
881
|
"paint_wall_surface",
|
|
882
|
+
"build_rubble_wall_surface",
|
|
883
|
+
"rubble_offset_for_size",
|
|
884
|
+
"RUBBLE_ROTATION_DEG",
|
|
791
885
|
"paint_steel_beam_surface",
|
|
792
886
|
"build_fuel_can_surface",
|
|
793
887
|
"build_flashlight_surface",
|
|
@@ -67,6 +67,15 @@ ENTITY_SHADOW_EDGE_SOFTNESS = 0.32
|
|
|
67
67
|
PLAYER_SHADOW_RADIUS_MULT = 1.6
|
|
68
68
|
PLAYER_SHADOW_ALPHA_MULT = 0.8
|
|
69
69
|
|
|
70
|
+
# --- Pitfall rendering ---
|
|
71
|
+
PITFALL_ABYSS_COLOR = (21, 20, 20)
|
|
72
|
+
PITFALL_SHADOW_RIM_COLOR = (38, 34, 34)
|
|
73
|
+
PITFALL_SHADOW_WIDTH = 6
|
|
74
|
+
PITFALL_EDGE_METAL_COLOR = (110, 110, 115)
|
|
75
|
+
PITFALL_EDGE_STRIPE_COLOR = (75, 75, 80)
|
|
76
|
+
PITFALL_EDGE_STRIPE_SPACING = 6
|
|
77
|
+
PITFALL_EDGE_DEPTH_OFFSET = 3
|
|
78
|
+
|
|
70
79
|
FOG_RINGS = [
|
|
71
80
|
FogRing(radius_factor=0.536, thickness=2),
|
|
72
81
|
FogRing(radius_factor=0.645, thickness=3),
|
|
@@ -121,6 +130,13 @@ __all__ = [
|
|
|
121
130
|
"ENTITY_SHADOW_EDGE_SOFTNESS",
|
|
122
131
|
"PLAYER_SHADOW_RADIUS_MULT",
|
|
123
132
|
"PLAYER_SHADOW_ALPHA_MULT",
|
|
133
|
+
"PITFALL_ABYSS_COLOR",
|
|
134
|
+
"PITFALL_SHADOW_RIM_COLOR",
|
|
135
|
+
"PITFALL_SHADOW_WIDTH",
|
|
136
|
+
"PITFALL_EDGE_METAL_COLOR",
|
|
137
|
+
"PITFALL_EDGE_STRIPE_COLOR",
|
|
138
|
+
"PITFALL_EDGE_STRIPE_SPACING",
|
|
139
|
+
"PITFALL_EDGE_DEPTH_OFFSET",
|
|
124
140
|
"FLASHLIGHT_HATCH_EXTRA_SCALE",
|
|
125
141
|
"build_render_assets",
|
|
126
142
|
]
|
|
@@ -18,8 +18,10 @@ from ..input_utils import is_confirm_event, is_select_event
|
|
|
18
18
|
from ..screens import (
|
|
19
19
|
ScreenID,
|
|
20
20
|
ScreenTransition,
|
|
21
|
+
nudge_window_scale,
|
|
21
22
|
present,
|
|
22
23
|
sync_window_size,
|
|
24
|
+
toggle_fullscreen,
|
|
23
25
|
)
|
|
24
26
|
from ..gameplay_constants import SURVIVAL_FAKE_CLOCK_RATIO
|
|
25
27
|
|
|
@@ -174,6 +176,15 @@ def game_over_screen(
|
|
|
174
176
|
sync_window_size(event, game_data=game_data)
|
|
175
177
|
continue
|
|
176
178
|
if event.type == pygame.KEYDOWN:
|
|
179
|
+
if event.key == pygame.K_LEFTBRACKET:
|
|
180
|
+
nudge_window_scale(0.5, game_data=game_data)
|
|
181
|
+
continue
|
|
182
|
+
if event.key == pygame.K_RIGHTBRACKET:
|
|
183
|
+
nudge_window_scale(2.0, game_data=game_data)
|
|
184
|
+
continue
|
|
185
|
+
if event.key == pygame.K_f:
|
|
186
|
+
toggle_fullscreen(game_data=game_data)
|
|
187
|
+
continue
|
|
177
188
|
if event.key in (pygame.K_ESCAPE, pygame.K_SPACE):
|
|
178
189
|
return ScreenTransition(ScreenID.TITLE)
|
|
179
190
|
if event.key == pygame.K_r and stage is not None:
|