crimsonland 0.1.0.dev5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. crimson/__init__.py +24 -0
  2. crimson/assets_fetch.py +60 -0
  3. crimson/atlas.py +92 -0
  4. crimson/audio_router.py +155 -0
  5. crimson/bonuses.py +167 -0
  6. crimson/camera.py +75 -0
  7. crimson/cli.py +380 -0
  8. crimson/creatures/__init__.py +8 -0
  9. crimson/creatures/ai.py +186 -0
  10. crimson/creatures/anim.py +173 -0
  11. crimson/creatures/damage.py +103 -0
  12. crimson/creatures/runtime.py +1019 -0
  13. crimson/creatures/spawn.py +2871 -0
  14. crimson/debug.py +7 -0
  15. crimson/demo.py +1360 -0
  16. crimson/demo_trial.py +140 -0
  17. crimson/effects.py +1086 -0
  18. crimson/effects_atlas.py +73 -0
  19. crimson/frontend/__init__.py +1 -0
  20. crimson/frontend/assets.py +43 -0
  21. crimson/frontend/boot.py +424 -0
  22. crimson/frontend/menu.py +700 -0
  23. crimson/frontend/panels/__init__.py +1 -0
  24. crimson/frontend/panels/base.py +410 -0
  25. crimson/frontend/panels/controls.py +132 -0
  26. crimson/frontend/panels/mods.py +128 -0
  27. crimson/frontend/panels/options.py +409 -0
  28. crimson/frontend/panels/play_game.py +627 -0
  29. crimson/frontend/panels/stats.py +351 -0
  30. crimson/frontend/transitions.py +31 -0
  31. crimson/game.py +2533 -0
  32. crimson/game_modes.py +15 -0
  33. crimson/game_world.py +652 -0
  34. crimson/gameplay.py +2467 -0
  35. crimson/input_codes.py +176 -0
  36. crimson/modes/__init__.py +1 -0
  37. crimson/modes/base_gameplay_mode.py +219 -0
  38. crimson/modes/quest_mode.py +502 -0
  39. crimson/modes/rush_mode.py +300 -0
  40. crimson/modes/survival_mode.py +792 -0
  41. crimson/modes/tutorial_mode.py +648 -0
  42. crimson/modes/typo_mode.py +472 -0
  43. crimson/paths.py +23 -0
  44. crimson/perks.py +828 -0
  45. crimson/persistence/__init__.py +1 -0
  46. crimson/persistence/highscores.py +385 -0
  47. crimson/persistence/save_status.py +245 -0
  48. crimson/player_damage.py +77 -0
  49. crimson/projectiles.py +1133 -0
  50. crimson/quests/__init__.py +18 -0
  51. crimson/quests/helpers.py +147 -0
  52. crimson/quests/registry.py +49 -0
  53. crimson/quests/results.py +164 -0
  54. crimson/quests/runtime.py +91 -0
  55. crimson/quests/tier1.py +620 -0
  56. crimson/quests/tier2.py +652 -0
  57. crimson/quests/tier3.py +579 -0
  58. crimson/quests/tier4.py +721 -0
  59. crimson/quests/tier5.py +886 -0
  60. crimson/quests/timeline.py +115 -0
  61. crimson/quests/types.py +70 -0
  62. crimson/render/__init__.py +1 -0
  63. crimson/render/terrain_fx.py +88 -0
  64. crimson/render/world_renderer.py +1941 -0
  65. crimson/sim/__init__.py +1 -0
  66. crimson/sim/world_defs.py +67 -0
  67. crimson/sim/world_state.py +422 -0
  68. crimson/terrain_assets.py +19 -0
  69. crimson/tutorial/__init__.py +12 -0
  70. crimson/tutorial/timeline.py +291 -0
  71. crimson/typo/__init__.py +2 -0
  72. crimson/typo/names.py +233 -0
  73. crimson/typo/player.py +43 -0
  74. crimson/typo/spawns.py +73 -0
  75. crimson/typo/typing.py +52 -0
  76. crimson/ui/__init__.py +3 -0
  77. crimson/ui/cursor.py +95 -0
  78. crimson/ui/demo_trial_overlay.py +235 -0
  79. crimson/ui/game_over.py +660 -0
  80. crimson/ui/hud.py +601 -0
  81. crimson/ui/perk_menu.py +388 -0
  82. crimson/views/__init__.py +40 -0
  83. crimson/views/aim_debug.py +276 -0
  84. crimson/views/animations.py +274 -0
  85. crimson/views/arsenal_debug.py +404 -0
  86. crimson/views/audio_bootstrap.py +47 -0
  87. crimson/views/bonuses.py +201 -0
  88. crimson/views/camera_debug.py +359 -0
  89. crimson/views/camera_shake.py +229 -0
  90. crimson/views/corpse_stamp_debug.py +324 -0
  91. crimson/views/decals_debug.py +739 -0
  92. crimson/views/empty.py +19 -0
  93. crimson/views/fonts.py +114 -0
  94. crimson/views/game_over.py +117 -0
  95. crimson/views/ground.py +259 -0
  96. crimson/views/lighting_debug.py +1166 -0
  97. crimson/views/particles.py +293 -0
  98. crimson/views/perk_menu_debug.py +430 -0
  99. crimson/views/perks.py +398 -0
  100. crimson/views/player.py +434 -0
  101. crimson/views/player_sprite_debug.py +314 -0
  102. crimson/views/projectile_fx.py +609 -0
  103. crimson/views/projectile_render_debug.py +393 -0
  104. crimson/views/projectiles.py +221 -0
  105. crimson/views/quest_title_overlay.py +108 -0
  106. crimson/views/registry.py +34 -0
  107. crimson/views/rush.py +16 -0
  108. crimson/views/small_font_debug.py +204 -0
  109. crimson/views/spawn_plan.py +363 -0
  110. crimson/views/sprites.py +214 -0
  111. crimson/views/survival.py +15 -0
  112. crimson/views/terrain.py +132 -0
  113. crimson/views/ui.py +123 -0
  114. crimson/views/wicons.py +166 -0
  115. crimson/weapon_sfx.py +63 -0
  116. crimson/weapons.py +860 -0
  117. crimsonland-0.1.0.dev5.dist-info/METADATA +9 -0
  118. crimsonland-0.1.0.dev5.dist-info/RECORD +139 -0
  119. crimsonland-0.1.0.dev5.dist-info/WHEEL +4 -0
  120. crimsonland-0.1.0.dev5.dist-info/entry_points.txt +4 -0
  121. grim/__init__.py +20 -0
  122. grim/app.py +92 -0
  123. grim/assets.py +231 -0
  124. grim/audio.py +106 -0
  125. grim/config.py +294 -0
  126. grim/console.py +737 -0
  127. grim/fonts/__init__.py +7 -0
  128. grim/fonts/grim_mono.py +111 -0
  129. grim/fonts/small.py +120 -0
  130. grim/input.py +44 -0
  131. grim/jaz.py +103 -0
  132. grim/math.py +17 -0
  133. grim/music.py +403 -0
  134. grim/paq.py +76 -0
  135. grim/rand.py +37 -0
  136. grim/sfx.py +276 -0
  137. grim/sfx_map.py +103 -0
  138. grim/terrain_render.py +840 -0
  139. grim/view.py +16 -0
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+
6
+ import pyray as rl
7
+
8
+ from grim.config import ensure_crimson_cfg
9
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
10
+ from grim.view import View, ViewContext
11
+
12
+ from ..bonuses import BonusId
13
+ from ..creatures.spawn import CreatureInit, CreatureTypeId
14
+ from ..game_world import GameWorld
15
+ from ..gameplay import PlayerInput, bonus_apply
16
+ from ..paths import default_runtime_dir
17
+ from .registry import register_view
18
+
19
+
20
+ WORLD_SIZE = 1024.0
21
+
22
+ UI_TEXT_SCALE = 1.0
23
+ UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
24
+ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
25
+ UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
26
+
27
+
28
+ def _clamp(value: float, lo: float, hi: float) -> float:
29
+ if value < lo:
30
+ return lo
31
+ if value > hi:
32
+ return hi
33
+ return value
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class _SpawnSpec:
38
+ r: float
39
+ angle_rad: float
40
+ type_id: CreatureTypeId
41
+ hp: float
42
+
43
+
44
+ class CameraShakeView:
45
+ def __init__(self, ctx: ViewContext) -> None:
46
+ self._assets_root = ctx.assets_dir
47
+ self._missing_assets: list[str] = []
48
+ self._small: SmallFontData | None = None
49
+
50
+ runtime_dir = default_runtime_dir()
51
+ config = None
52
+ if runtime_dir.is_dir():
53
+ try:
54
+ config = ensure_crimson_cfg(runtime_dir)
55
+ except Exception:
56
+ config = None
57
+
58
+ self.close_requested = False
59
+ self._world = GameWorld(
60
+ assets_dir=self._assets_root,
61
+ world_size=WORLD_SIZE,
62
+ demo_mode_active=False,
63
+ difficulty_level=0,
64
+ hardcore=False,
65
+ texture_cache=None,
66
+ config=config,
67
+ audio=None,
68
+ audio_rng=None,
69
+ )
70
+ self._reflex_boost_locked = False
71
+ self._reset_scene()
72
+
73
+ def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
74
+ if self._small is not None:
75
+ return int(self._small.cell_size * scale)
76
+ return int(20 * scale)
77
+
78
+ def _draw_ui_text(
79
+ self,
80
+ text: str,
81
+ x: float,
82
+ y: float,
83
+ color: rl.Color,
84
+ scale: float = UI_TEXT_SCALE,
85
+ ) -> None:
86
+ if self._small is not None:
87
+ draw_small_text(self._small, text, x, y, scale, color)
88
+ else:
89
+ rl.draw_text(text, int(x), int(y), int(20 * scale), color)
90
+
91
+ def _spawn_creature(self, *, world_x: float, world_y: float, type_id: CreatureTypeId, hp: float) -> None:
92
+ init = CreatureInit(
93
+ origin_template_id=0,
94
+ pos_x=_clamp(world_x, 64.0, WORLD_SIZE - 64.0),
95
+ pos_y=_clamp(world_y, 64.0, WORLD_SIZE - 64.0),
96
+ heading=math.pi,
97
+ phase_seed=0.0,
98
+ type_id=type_id,
99
+ health=float(hp),
100
+ max_health=float(hp),
101
+ move_speed=0.0,
102
+ reward_value=0.0,
103
+ size=50.0,
104
+ contact_damage=0.0,
105
+ )
106
+ self._world.creatures.spawn_init(init, rand=self._world.state.rng.rand)
107
+
108
+ def _reset_scene(self) -> None:
109
+ self._world.reset(seed=0xBEEF, player_count=1)
110
+ self._world.state.camera_shake_offset_x = 0.0
111
+ self._world.state.camera_shake_offset_y = 0.0
112
+ self._world.state.camera_shake_timer = 0.0
113
+ self._world.state.camera_shake_pulses = 0
114
+
115
+ player = self._world.players[0]
116
+ player.pos_x = WORLD_SIZE * 0.5
117
+ player.pos_y = WORLD_SIZE * 0.5
118
+
119
+ spawn = [
120
+ _SpawnSpec(r=140.0, angle_rad=0.0, type_id=CreatureTypeId.ZOMBIE, hp=50.0),
121
+ _SpawnSpec(r=160.0, angle_rad=math.pi * 0.5, type_id=CreatureTypeId.LIZARD, hp=60.0),
122
+ _SpawnSpec(r=180.0, angle_rad=math.pi, type_id=CreatureTypeId.ALIEN, hp=70.0),
123
+ _SpawnSpec(r=200.0, angle_rad=math.pi * 1.5, type_id=CreatureTypeId.SPIDER_SP1, hp=80.0),
124
+ _SpawnSpec(r=320.0, angle_rad=math.pi * 0.25, type_id=CreatureTypeId.SPIDER_SP2, hp=90.0),
125
+ _SpawnSpec(r=460.0, angle_rad=math.pi * 1.25, type_id=CreatureTypeId.ZOMBIE, hp=100.0),
126
+ ]
127
+ for entry in spawn:
128
+ x = player.pos_x + math.cos(entry.angle_rad) * entry.r
129
+ y = player.pos_y + math.sin(entry.angle_rad) * entry.r
130
+ self._spawn_creature(world_x=x, world_y=y, type_id=entry.type_id, hp=entry.hp)
131
+
132
+ def open(self) -> None:
133
+ self._missing_assets.clear()
134
+ try:
135
+ self._small = load_small_font(self._assets_root, self._missing_assets)
136
+ except Exception:
137
+ self._small = None
138
+ self._world.open()
139
+
140
+ def close(self) -> None:
141
+ self._world.close()
142
+ if self._small is not None:
143
+ rl.unload_texture(self._small.texture)
144
+ self._small = None
145
+
146
+ def _handle_input(self) -> None:
147
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
148
+ self.close_requested = True
149
+ return
150
+
151
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
152
+ self._reset_scene()
153
+
154
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_N):
155
+ player = self._world.players[0]
156
+ bonus_apply(
157
+ self._world.state,
158
+ player,
159
+ BonusId.NUKE,
160
+ origin=player,
161
+ creatures=self._world.creatures.entries,
162
+ )
163
+
164
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_T):
165
+ self._reflex_boost_locked = not self._reflex_boost_locked
166
+ self._world.state.bonuses.reflex_boost = 9999.0 if self._reflex_boost_locked else 0.0
167
+
168
+ def _build_input(self) -> PlayerInput:
169
+ move_x = 0.0
170
+ move_y = 0.0
171
+ if rl.is_key_down(rl.KeyboardKey.KEY_A):
172
+ move_x -= 1.0
173
+ if rl.is_key_down(rl.KeyboardKey.KEY_D):
174
+ move_x += 1.0
175
+ if rl.is_key_down(rl.KeyboardKey.KEY_W):
176
+ move_y -= 1.0
177
+ if rl.is_key_down(rl.KeyboardKey.KEY_S):
178
+ move_y += 1.0
179
+
180
+ mouse = rl.get_mouse_position()
181
+ aim_x, aim_y = self._world.screen_to_world(float(mouse.x), float(mouse.y))
182
+
183
+ fire_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
184
+ fire_pressed = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
185
+ reload_pressed = rl.is_key_pressed(rl.KeyboardKey.KEY_R)
186
+
187
+ return PlayerInput(
188
+ move_x=move_x,
189
+ move_y=move_y,
190
+ aim_x=float(aim_x),
191
+ aim_y=float(aim_y),
192
+ fire_down=bool(fire_down),
193
+ fire_pressed=bool(fire_pressed),
194
+ reload_pressed=bool(reload_pressed),
195
+ )
196
+
197
+ def update(self, dt: float) -> None:
198
+ self._handle_input()
199
+ if self._reflex_boost_locked:
200
+ self._world.state.bonuses.reflex_boost = 9999.0
201
+ self._world.update(dt, inputs=[self._build_input()], auto_pick_perks=True, perk_progression_enabled=False)
202
+
203
+ def draw(self) -> None:
204
+ self._world.draw()
205
+
206
+ if self._missing_assets:
207
+ message = "Missing assets: " + ", ".join(self._missing_assets)
208
+ self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
209
+
210
+ state = self._world.state
211
+ cam_x, cam_y, _sx, _sy = self._world._world_params()
212
+ lines = [
213
+ "WASD move N: nuke shake T: toggle reflex-boost shake-rate R: reset Esc: exit",
214
+ f"camera_offset=({cam_x:.1f},{cam_y:.1f}) camera_raw=({self._world.camera_x:.1f},{self._world.camera_y:.1f})",
215
+ f"shake_offset=({state.camera_shake_offset_x:.1f},{state.camera_shake_offset_y:.1f}) "
216
+ f"shake_timer={state.camera_shake_timer:.3f} pulses={state.camera_shake_pulses}",
217
+ f"reflex_boost={state.bonuses.reflex_boost:.2f} creatures_alive={len(self._world.creatures.iter_active())}",
218
+ ]
219
+ x = 24.0
220
+ y = 24.0 + float(self._ui_line_height()) + 12.0
221
+ for idx, line in enumerate(lines):
222
+ color = UI_HINT_COLOR if idx == 0 else UI_TEXT_COLOR
223
+ self._draw_ui_text(line, x, y, color)
224
+ y += float(self._ui_line_height())
225
+
226
+
227
+ @register_view("camera-shake", "Camera shake")
228
+ def build_camera_shake_view(*, ctx: ViewContext) -> View:
229
+ return CameraShakeView(ctx)
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ import pyray as rl
7
+
8
+ from crimson.creatures.anim import creature_corpse_frame_for_type
9
+ from crimson.creatures.spawn import CreatureTypeId
10
+ from grim.assets import resolve_asset_path
11
+ from grim.config import ensure_crimson_cfg
12
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
13
+ from grim.terrain_render import GroundCorpseDecal, GroundRenderer, _maybe_alpha_test
14
+ from grim.view import View, ViewContext
15
+
16
+ from ..paths import default_runtime_dir
17
+ from .registry import register_view
18
+
19
+
20
+ WORLD_SIZE = 1024.0
21
+ WINDOW_W = 1024
22
+ WINDOW_H = 768
23
+
24
+ BG = rl.Color(235, 235, 235, 255)
25
+ UI_TEXT = rl.Color(20, 20, 20, 255)
26
+ UI_HINT = rl.Color(70, 70, 70, 255)
27
+ UI_ERROR = rl.Color(240, 80, 80, 255)
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class _Step:
32
+ name: str
33
+ description: str
34
+
35
+
36
+ _STEPS: tuple[_Step, ...] = (
37
+ _Step(name="clear", description="Clear the ground render target"),
38
+ _Step(name="shadow", description="Bake shadow pass only (correct order)"),
39
+ _Step(name="color", description="Bake color pass only (correct order)"),
40
+ _Step(name="clear", description="Clear the ground render target"),
41
+ _Step(name="color", description="Bake color pass only (wrong order)"),
42
+ _Step(name="shadow", description="Bake shadow pass only (wrong order)"),
43
+ )
44
+
45
+
46
+ class CorpseStampDebugView:
47
+ def __init__(self, ctx: ViewContext) -> None:
48
+ self._assets_root = ctx.assets_dir
49
+ self._missing_assets: list[str] = []
50
+ self._small: SmallFontData | None = None
51
+
52
+ self._owned_textures: list[rl.Texture] = []
53
+ self._ground: GroundRenderer | None = None
54
+ self._bodyset: rl.Texture | None = None
55
+
56
+ self.close_requested = False
57
+ self._step_index = 0
58
+ self._corpse_size = 256.0
59
+ self._corpse_rotation = 0.0
60
+ self._screenshot_requested = False
61
+ self._dump_requested = False
62
+ self._dump_index = 0
63
+
64
+ def _ui_line_height(self) -> int:
65
+ if self._small is not None:
66
+ return int(self._small.cell_size)
67
+ return 20
68
+
69
+ def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color) -> None:
70
+ if self._small is not None:
71
+ draw_small_text(self._small, text, x, y, 1.0, color)
72
+ else:
73
+ rl.draw_text(text, int(x), int(y), 20, color)
74
+
75
+ def _load_runtime_config(self) -> tuple[float, float | None, float | None]:
76
+ runtime_dir = default_runtime_dir()
77
+ if runtime_dir.is_dir():
78
+ try:
79
+ cfg = ensure_crimson_cfg(runtime_dir)
80
+ return float(cfg.texture_scale), float(cfg.screen_width), float(cfg.screen_height)
81
+ except Exception:
82
+ return 1.0, None, None
83
+ return 1.0, None, None
84
+
85
+ def _dump_render_target(self) -> Path | None:
86
+ ground = self._ground
87
+ if ground is None or ground.render_target is None:
88
+ return None
89
+
90
+ log_dir = Path("artifacts") / "debug"
91
+ try:
92
+ log_dir.mkdir(parents=True, exist_ok=True)
93
+ except Exception:
94
+ log_dir = Path("artifacts")
95
+
96
+ step = _STEPS[self._step_index]
97
+ alpha_test = "a1" if bool(getattr(ground, "alpha_test", True)) else "a0"
98
+ filename = f"corpse_stamp_rt_{self._dump_index:03d}_step{self._step_index + 1:02d}_{step.name}_{alpha_test}.png"
99
+ out_path = log_dir / filename
100
+
101
+ image = rl.load_image_from_texture(ground.render_target.texture)
102
+ # Render textures are vertically flipped in raylib.
103
+ rl.image_flip_vertical(image)
104
+ try:
105
+ rl.export_image(image, str(out_path))
106
+ finally:
107
+ rl.unload_image(image)
108
+ self._dump_index += 1
109
+ return out_path
110
+
111
+ def _corpse_decal(self) -> GroundCorpseDecal:
112
+ size = float(self._corpse_size)
113
+ frame = creature_corpse_frame_for_type(int(CreatureTypeId.SPIDER_SP1))
114
+ cx = WORLD_SIZE * 0.5
115
+ cy = WORLD_SIZE * 0.5
116
+ return GroundCorpseDecal(
117
+ bodyset_frame=int(frame),
118
+ top_left_x=cx - size * 0.5,
119
+ top_left_y=cy - size * 0.5,
120
+ size=size,
121
+ rotation_rad=float(self._corpse_rotation),
122
+ tint=rl.Color(255, 255, 255, 255),
123
+ )
124
+
125
+ def _clear_ground(self) -> None:
126
+ ground = self._ground
127
+ if ground is None:
128
+ return
129
+ ground.create_render_target()
130
+ if ground.render_target is None:
131
+ return
132
+ rl.begin_texture_mode(ground.render_target)
133
+ rl.clear_background(BG)
134
+ rl.end_texture_mode()
135
+ # GroundRenderer treats this as an internal invariant; set it for debug fills.
136
+ ground._render_target_ready = True # type: ignore[attr-defined]
137
+
138
+ def _bake_shadow_only(self) -> None:
139
+ ground = self._ground
140
+ bodyset = self._bodyset
141
+ if ground is None or bodyset is None:
142
+ return
143
+ ground.create_render_target()
144
+ if ground.render_target is None:
145
+ return
146
+
147
+ scale = ground._normalized_texture_scale()
148
+ inv_scale = 1.0 / scale
149
+ offset = 2.0 * scale / float(ground.width)
150
+ ground._set_texture_filters((bodyset,), point=True)
151
+
152
+ rl.begin_texture_mode(ground.render_target)
153
+ with _maybe_alpha_test(ground.alpha_test):
154
+ ground._draw_corpse_shadow_pass(bodyset, [self._corpse_decal()], inv_scale, offset)
155
+ rl.end_texture_mode()
156
+
157
+ ground._set_texture_filters((bodyset,), point=False)
158
+ ground._render_target_ready = True # type: ignore[attr-defined]
159
+
160
+ def _bake_color_only(self) -> None:
161
+ ground = self._ground
162
+ bodyset = self._bodyset
163
+ if ground is None or bodyset is None:
164
+ return
165
+ ground.bake_corpse_decals(bodyset, [self._corpse_decal()], shadow=False)
166
+
167
+ def _apply_step(self) -> None:
168
+ step = _STEPS[self._step_index]
169
+ if step.name == "clear":
170
+ self._clear_ground()
171
+ elif step.name == "shadow":
172
+ self._bake_shadow_only()
173
+ elif step.name == "color":
174
+ self._bake_color_only()
175
+
176
+ def open(self) -> None:
177
+ rl.set_window_size(WINDOW_W, WINDOW_H)
178
+ self._missing_assets.clear()
179
+ self._owned_textures.clear()
180
+ self._ground = None
181
+ self._bodyset = None
182
+ self.close_requested = False
183
+ self._step_index = 0
184
+
185
+ try:
186
+ self._small = load_small_font(self._assets_root, self._missing_assets)
187
+ except Exception:
188
+ self._small = None
189
+
190
+ base_path = resolve_asset_path(self._assets_root, "ter/ter_q1_base.png")
191
+ bodyset_path = resolve_asset_path(self._assets_root, "game/bodyset.png")
192
+ if base_path is None:
193
+ self._missing_assets.append("ter/ter_q1_base.png")
194
+ if bodyset_path is None:
195
+ self._missing_assets.append("game/bodyset.png")
196
+ if self._missing_assets:
197
+ raise FileNotFoundError("Missing assets: " + ", ".join(self._missing_assets))
198
+
199
+ base = rl.load_texture(str(base_path))
200
+ bodyset = rl.load_texture(str(bodyset_path))
201
+ self._owned_textures.extend([base, bodyset])
202
+ self._bodyset = bodyset
203
+
204
+ texture_scale, screen_w, screen_h = self._load_runtime_config()
205
+ self._ground = GroundRenderer(
206
+ texture=base,
207
+ width=int(WORLD_SIZE),
208
+ height=int(WORLD_SIZE),
209
+ texture_scale=float(texture_scale),
210
+ screen_width=screen_w,
211
+ screen_height=screen_h,
212
+ )
213
+ self._ground.alpha_test = True
214
+ self._clear_ground()
215
+
216
+ def close(self) -> None:
217
+ if self._ground is not None and self._ground.render_target is not None:
218
+ rl.unload_render_texture(self._ground.render_target)
219
+ self._ground.render_target = None
220
+ self._ground = None
221
+ self._bodyset = None
222
+
223
+ for texture in self._owned_textures:
224
+ rl.unload_texture(texture)
225
+ self._owned_textures.clear()
226
+
227
+ if self._small is not None:
228
+ rl.unload_texture(self._small.texture)
229
+ self._small = None
230
+
231
+ def update(self, dt: float) -> None:
232
+ del dt
233
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
234
+ self.close_requested = True
235
+ return
236
+
237
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
238
+ self._step_index = 0
239
+ self._clear_ground()
240
+
241
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_A):
242
+ if self._ground is not None:
243
+ self._ground.alpha_test = not bool(self._ground.alpha_test)
244
+
245
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_N) or rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
246
+ self._step_index = (self._step_index + 1) % len(_STEPS)
247
+ self._apply_step()
248
+
249
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_P):
250
+ self._screenshot_requested = True
251
+
252
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_D):
253
+ self._dump_requested = True
254
+
255
+ if rl.is_key_down(rl.KeyboardKey.KEY_Q):
256
+ self._corpse_rotation -= 0.04
257
+ if rl.is_key_down(rl.KeyboardKey.KEY_E):
258
+ self._corpse_rotation += 0.04
259
+
260
+ def consume_screenshot_request(self) -> bool:
261
+ requested = self._screenshot_requested
262
+ self._screenshot_requested = False
263
+ return requested
264
+
265
+ def draw(self) -> None:
266
+ rl.clear_background(BG)
267
+
268
+ if self._missing_assets:
269
+ self._draw_ui_text("Missing assets: " + ", ".join(self._missing_assets), 24, 24, UI_ERROR)
270
+ return
271
+
272
+ ground = self._ground
273
+ if ground is None:
274
+ self._draw_ui_text("Ground renderer not initialized.", 24, 24, UI_ERROR)
275
+ return
276
+
277
+ if self._dump_requested:
278
+ self._dump_requested = False
279
+ self._dump_render_target()
280
+
281
+ screen_w = float(rl.get_screen_width())
282
+ screen_h = float(rl.get_screen_height())
283
+ cam_x = screen_w * 0.5 - WORLD_SIZE * 0.5
284
+ cam_y = screen_h * 0.5 - WORLD_SIZE * 0.5
285
+ ground.draw(cam_x, cam_y, screen_w=screen_w, screen_h=screen_h)
286
+
287
+ # UI
288
+ x = 24.0
289
+ y = 20.0
290
+ line = float(self._ui_line_height())
291
+ step = _STEPS[self._step_index]
292
+ alpha_test = bool(getattr(ground, "alpha_test", True))
293
+ self._draw_ui_text("Corpse stamp debug (SPIDER)", x, y, UI_TEXT)
294
+ y += line
295
+ self._draw_ui_text(
296
+ "N/Space: next step R: reset A: toggle alpha test Q/E: rotate P: screenshot D: dump RT",
297
+ x,
298
+ y,
299
+ UI_HINT,
300
+ )
301
+ y += line
302
+ self._draw_ui_text(f"step {self._step_index + 1}/{len(_STEPS)}: {step.description}", x, y, UI_HINT)
303
+ y += line
304
+ self._draw_ui_text(
305
+ f"alpha_test={'on' if alpha_test else 'off'} size={self._corpse_size:.1f} dump_index={self._dump_index}",
306
+ x,
307
+ y,
308
+ UI_HINT,
309
+ )
310
+
311
+ # Source preview (bodyset frame) in the corner for inspection.
312
+ if self._bodyset is not None:
313
+ frame = creature_corpse_frame_for_type(int(CreatureTypeId.SPIDER_SP1))
314
+ src = ground._corpse_src(self._bodyset, int(frame))
315
+ preview = 256.0
316
+ pad = 18.0
317
+ dst = rl.Rectangle(screen_w - preview - pad, pad, preview, preview)
318
+ rl.draw_rectangle(int(dst.x) - 2, int(dst.y) - 2, int(preview) + 4, int(preview) + 4, rl.Color(0, 0, 0, 30))
319
+ rl.draw_texture_pro(self._bodyset, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
320
+
321
+
322
+ @register_view("corpse-stamp-debug", "Corpse stamp debug")
323
+ def build_corpse_stamp_debug_view(ctx: ViewContext) -> View:
324
+ return CorpseStampDebugView(ctx)