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,363 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+
6
+ import pyray as rl
7
+
8
+ from grim.rand import Crand
9
+ from ..creatures.spawn import (
10
+ CreatureTypeId,
11
+ SPAWN_TEMPLATES,
12
+ SpawnEnv,
13
+ SpawnSlotInit,
14
+ build_spawn_plan,
15
+ spawn_id_label,
16
+ tick_spawn_slot,
17
+ )
18
+ from .registry import register_view
19
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
20
+ from grim.view import View, ViewContext
21
+
22
+
23
+ BASE_POS = (512.0, 512.0)
24
+
25
+ UI_TEXT_SCALE = 1
26
+ UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
27
+ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
28
+ UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
29
+
30
+ BG_COLOR = rl.Color(12, 12, 14, 255)
31
+ GRID_COLOR = rl.Color(40, 40, 48, 255)
32
+ LINK_COLOR = rl.Color(80, 160, 255, 120)
33
+ OFFSET_COLOR = rl.Color(255, 200, 80, 140)
34
+
35
+
36
+ def _type_color(type_id: CreatureTypeId | None) -> rl.Color:
37
+ if type_id == CreatureTypeId.ZOMBIE:
38
+ return rl.Color(120, 220, 120, 255)
39
+ if type_id == CreatureTypeId.LIZARD:
40
+ return rl.Color(120, 160, 255, 255)
41
+ if type_id == CreatureTypeId.ALIEN:
42
+ return rl.Color(200, 140, 255, 255)
43
+ if type_id == CreatureTypeId.SPIDER_SP1:
44
+ return rl.Color(255, 120, 120, 255)
45
+ if type_id == CreatureTypeId.SPIDER_SP2:
46
+ return rl.Color(255, 160, 120, 255)
47
+ return rl.Color(200, 200, 200, 255)
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class _PlanSummary:
52
+ creature_count: int
53
+ spawn_slot_count: int
54
+ effect_count: int
55
+ primary_idx: int
56
+
57
+
58
+ class SpawnPlanView:
59
+ def __init__(self, ctx: ViewContext) -> None:
60
+ self._assets_root = ctx.assets_dir
61
+ self._missing_assets: list[str] = []
62
+ self._small: SmallFontData | None = None
63
+
64
+ self._template_ids = [t.spawn_id for t in sorted(SPAWN_TEMPLATES, key=lambda t: t.spawn_id)]
65
+ self._index = 0
66
+
67
+ self._seed = 0xBEEF
68
+ self._world_scale = 1.0
69
+ self._hardcore = False
70
+ self._difficulty = 0
71
+ self._demo_mode_active = True
72
+
73
+ self._plan = None
74
+ self._plan_summary = None
75
+ self._error = None
76
+
77
+ self._sim_running = False
78
+ self._sim_time = 0.0
79
+ self._sim_slots: list[SpawnSlotInit] = []
80
+ self._sim_events: list[str] = []
81
+
82
+ self._rebuild_plan()
83
+
84
+ def open(self) -> None:
85
+ self._missing_assets.clear()
86
+ try:
87
+ self._small = load_small_font(self._assets_root, self._missing_assets)
88
+ except FileNotFoundError:
89
+ self._small = None
90
+
91
+ def close(self) -> None:
92
+ if self._small is not None:
93
+ rl.unload_texture(self._small.texture)
94
+ self._small = None
95
+
96
+ def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
97
+ if self._small is not None:
98
+ return int(self._small.cell_size * scale)
99
+ return int(20 * scale)
100
+
101
+ def _draw_ui_text(
102
+ self,
103
+ text: str,
104
+ x: float,
105
+ y: float,
106
+ color: rl.Color,
107
+ scale: float = UI_TEXT_SCALE,
108
+ ) -> None:
109
+ if self._small is not None:
110
+ draw_small_text(self._small, text, x, y, scale, color)
111
+ else:
112
+ rl.draw_text(text, int(x), int(y), int(20 * scale), color)
113
+
114
+ def _draw_ui_label(self, label: str, value: str, x: float, y: float) -> None:
115
+ label_text = f"{label}: "
116
+ self._draw_ui_text(label_text, x, y, UI_HINT_COLOR)
117
+ label_w = measure_small_text_width(self._small, label_text, UI_TEXT_SCALE) if self._small else 0.0
118
+ self._draw_ui_text(value, x + label_w, y, UI_TEXT_COLOR)
119
+
120
+ def _rebuild_plan(self) -> None:
121
+ spawn_id = self._template_ids[self._index]
122
+ rng = Crand(self._seed)
123
+ env = SpawnEnv(
124
+ terrain_width=1024.0,
125
+ terrain_height=1024.0,
126
+ demo_mode_active=self._demo_mode_active,
127
+ hardcore=self._hardcore,
128
+ difficulty_level=self._difficulty,
129
+ )
130
+ try:
131
+ self._plan = build_spawn_plan(spawn_id, BASE_POS, 0.0, rng, env)
132
+ self._plan_summary = _PlanSummary(
133
+ creature_count=len(self._plan.creatures),
134
+ spawn_slot_count=len(self._plan.spawn_slots),
135
+ effect_count=len(self._plan.effects),
136
+ primary_idx=self._plan.primary,
137
+ )
138
+ self._reset_sim()
139
+ self._error = None
140
+ except Exception as exc:
141
+ self._plan = None
142
+ self._plan_summary = None
143
+ self._error = str(exc)
144
+ self._reset_sim()
145
+
146
+ def _reset_sim(self) -> None:
147
+ self._sim_running = False
148
+ self._sim_time = 0.0
149
+ self._sim_events.clear()
150
+ self._sim_slots = []
151
+ if self._plan is None:
152
+ return
153
+ for slot in self._plan.spawn_slots:
154
+ self._sim_slots.append(
155
+ SpawnSlotInit(
156
+ owner_creature=slot.owner_creature,
157
+ timer=slot.timer,
158
+ count=slot.count,
159
+ limit=slot.limit,
160
+ interval=slot.interval,
161
+ child_template_id=slot.child_template_id,
162
+ )
163
+ )
164
+
165
+ def _advance_template(self, delta: int) -> None:
166
+ if not self._template_ids:
167
+ return
168
+ self._index = (self._index + delta) % len(self._template_ids)
169
+ self._rebuild_plan()
170
+
171
+ def _adjust_seed(self, delta: int) -> None:
172
+ self._seed = (self._seed + delta) & 0xFFFFFFFF
173
+ self._rebuild_plan()
174
+
175
+ def _adjust_scale(self, delta: float) -> None:
176
+ self._world_scale = max(0.1, min(4.0, self._world_scale + delta))
177
+
178
+ def _toggle_hardcore(self) -> None:
179
+ self._hardcore = not self._hardcore
180
+ self._rebuild_plan()
181
+
182
+ def _toggle_demo_mode(self) -> None:
183
+ self._demo_mode_active = not self._demo_mode_active
184
+ self._rebuild_plan()
185
+
186
+ def _adjust_difficulty(self, delta: int) -> None:
187
+ self._difficulty = max(0, min(5, self._difficulty + delta))
188
+ self._rebuild_plan()
189
+
190
+ def update(self, dt: float) -> None:
191
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
192
+ self._advance_template(1)
193
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
194
+ self._advance_template(-1)
195
+
196
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
197
+ self._adjust_seed(1)
198
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
199
+ self._adjust_seed(-1)
200
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
201
+ self._seed = rl.get_random_value(0, 0x7FFFFFFF)
202
+ self._rebuild_plan()
203
+
204
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
205
+ self._adjust_scale(-0.1)
206
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
207
+ self._adjust_scale(0.1)
208
+
209
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_H):
210
+ self._toggle_hardcore()
211
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_D):
212
+ self._toggle_demo_mode()
213
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_COMMA):
214
+ self._adjust_difficulty(-1)
215
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_PERIOD):
216
+ self._adjust_difficulty(1)
217
+
218
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
219
+ self._sim_running = not self._sim_running
220
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
221
+ self._reset_sim()
222
+
223
+ if self._sim_running and self._sim_slots:
224
+ sim_dt = min(max(0.0, float(dt)), 0.1)
225
+ self._sim_time += sim_dt
226
+ for idx, slot in enumerate(self._sim_slots):
227
+ child_template_id = tick_spawn_slot(slot, sim_dt)
228
+ if child_template_id is None:
229
+ continue
230
+ self._sim_events.append(
231
+ f"t={self._sim_time:6.2f} slot={idx:02d} spawn=0x{child_template_id:02x} ({spawn_id_label(child_template_id)})"
232
+ )
233
+ if len(self._sim_events) > 12:
234
+ self._sim_events = self._sim_events[-12:]
235
+
236
+ def _world_to_screen(self, x: float, y: float) -> tuple[float, float]:
237
+ base_x, base_y = BASE_POS
238
+ screen_w = float(rl.get_screen_width())
239
+ screen_h = float(rl.get_screen_height())
240
+ cx = screen_w * 0.5 + (x - base_x) * self._world_scale
241
+ cy = screen_h * 0.5 + (y - base_y) * self._world_scale
242
+ return cx, cy
243
+
244
+ def _draw_grid(self) -> None:
245
+ screen_w = rl.get_screen_width()
246
+ screen_h = rl.get_screen_height()
247
+ step = int(64 * self._world_scale)
248
+ if step < 24:
249
+ return
250
+ x = 0
251
+ while x < screen_w:
252
+ rl.draw_line(x, 0, x, screen_h, GRID_COLOR)
253
+ x += step
254
+ y = 0
255
+ while y < screen_h:
256
+ rl.draw_line(0, y, screen_w, y, GRID_COLOR)
257
+ y += step
258
+
259
+ def draw(self) -> None:
260
+ rl.clear_background(BG_COLOR)
261
+ self._draw_grid()
262
+
263
+ margin = 16.0
264
+ line_h = float(self._ui_line_height())
265
+
266
+ spawn_id = self._template_ids[self._index] if self._template_ids else 0
267
+ self._draw_ui_text(
268
+ f"spawn-plan view (template 0x{spawn_id:02x})",
269
+ margin,
270
+ margin,
271
+ UI_TEXT_COLOR,
272
+ scale=0.8,
273
+ )
274
+ hints = "Left/Right: id Up/Down: seed R: random seed [,]: scale H: hardcore D: demo-mode ,/.: difficulty Space: sim Backspace: reset"
275
+ self._draw_ui_text(hints, margin, margin + line_h, UI_HINT_COLOR)
276
+
277
+ y = margin + line_h * 2.0 + 4.0
278
+ self._draw_ui_label("seed", f"0x{self._seed:08x}", margin, y)
279
+ y += line_h
280
+ self._draw_ui_label("world_scale", f"{self._world_scale:.2f}", margin, y)
281
+ y += line_h
282
+ self._draw_ui_label("hardcore", str(self._hardcore), margin, y)
283
+ y += line_h
284
+ self._draw_ui_label("difficulty", str(self._difficulty), margin, y)
285
+ y += line_h
286
+ self._draw_ui_label("demo_mode_active", str(self._demo_mode_active), margin, y)
287
+ y += line_h
288
+
289
+ if self._error is not None:
290
+ self._draw_ui_text(self._error, margin, y + 6.0, UI_ERROR_COLOR)
291
+ return
292
+ if self._plan is None or self._plan_summary is None:
293
+ self._draw_ui_text("No plan.", margin, y + 6.0, UI_ERROR_COLOR)
294
+ return
295
+
296
+ summary = self._plan_summary
297
+ self._draw_ui_label(
298
+ "plan",
299
+ f"creatures={summary.creature_count} slots={summary.spawn_slot_count} effects={summary.effect_count} primary={summary.primary_idx}",
300
+ margin,
301
+ y,
302
+ )
303
+ y += line_h
304
+ sim_state = "running" if self._sim_running else "paused"
305
+ self._draw_ui_label("sim", f"{sim_state} t={self._sim_time:.2f}s", margin, y)
306
+ y += line_h
307
+ for idx, slot in enumerate(self._sim_slots[:3]):
308
+ self._draw_ui_label(
309
+ f"slot{idx:02d}",
310
+ f"timer={slot.timer:5.2f} count={slot.count:3d}/{slot.limit:<3d} interval={slot.interval:5.2f} child=0x{slot.child_template_id:02x}",
311
+ margin,
312
+ y,
313
+ )
314
+ y += line_h
315
+ if self._sim_events:
316
+ self._draw_ui_text("events:", margin, y + 2.0, UI_HINT_COLOR)
317
+ y += line_h
318
+ for ev in self._sim_events[-5:]:
319
+ self._draw_ui_text(ev, margin, y, UI_TEXT_COLOR)
320
+ y += line_h
321
+
322
+ # Link lines.
323
+ for idx, c in enumerate(self._plan.creatures):
324
+ if c.ai_link_parent is None:
325
+ continue
326
+ if not (0 <= c.ai_link_parent < len(self._plan.creatures)):
327
+ continue
328
+ p = self._plan.creatures[c.ai_link_parent]
329
+ x0, y0 = self._world_to_screen(c.pos_x, c.pos_y)
330
+ x1, y1 = self._world_to_screen(p.pos_x, p.pos_y)
331
+ rl.draw_line_ex(rl.Vector2(x0, y0), rl.Vector2(x1, y1), 2.0, LINK_COLOR)
332
+
333
+ # Offset hints.
334
+ for c in self._plan.creatures:
335
+ if c.target_offset_x is None or c.target_offset_y is None:
336
+ continue
337
+ x0, y0 = self._world_to_screen(c.pos_x, c.pos_y)
338
+ x1, y1 = self._world_to_screen(c.pos_x + c.target_offset_x, c.pos_y + c.target_offset_y)
339
+ rl.draw_line_ex(rl.Vector2(x0, y0), rl.Vector2(x1, y1), 2.0, OFFSET_COLOR)
340
+ rl.draw_circle_lines(int(x1), int(y1), max(2.0, 4.0 * self._world_scale), OFFSET_COLOR)
341
+
342
+ # Creature dots.
343
+ for idx, c in enumerate(self._plan.creatures):
344
+ x, y = self._world_to_screen(c.pos_x, c.pos_y)
345
+ radius = max(3.0, 6.0 * math.sqrt(max(1.0, (c.size or 50.0) / 50.0)))
346
+ radius = min(radius, 24.0)
347
+ color = _type_color(c.type_id)
348
+ rl.draw_circle(int(x), int(y), radius, color)
349
+ if idx == summary.primary_idx:
350
+ rl.draw_circle_lines(int(x), int(y), radius + 2.0, rl.Color(255, 255, 255, 200))
351
+
352
+ # Spawn-slot owners.
353
+ for slot in self._plan.spawn_slots:
354
+ if not (0 <= slot.owner_creature < len(self._plan.creatures)):
355
+ continue
356
+ owner = self._plan.creatures[slot.owner_creature]
357
+ x, y = self._world_to_screen(owner.pos_x, owner.pos_y)
358
+ rl.draw_circle_lines(int(x), int(y), max(8.0, 12.0 * self._world_scale), rl.Color(120, 255, 180, 200))
359
+
360
+
361
+ @register_view("spawn-plan", "Spawn plan")
362
+ def view_spawn_plan(*, ctx: ViewContext) -> View:
363
+ return SpawnPlanView(ctx)
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import pyray as rl
6
+
7
+ from .registry import register_view
8
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
9
+ from grim.view import View, ViewContext
10
+
11
+ UI_TEXT_SCALE = 1.0
12
+ UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
13
+ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
14
+ UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class SpriteSheetSpec:
19
+ name: str
20
+ rel_path: str
21
+ grids: tuple[int, ...]
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class SpriteSheet:
26
+ name: str
27
+ texture: rl.Texture
28
+ grids: tuple[int, ...]
29
+ grid_index: int = 0
30
+
31
+ @property
32
+ def grid(self) -> int:
33
+ return self.grids[self.grid_index]
34
+
35
+
36
+ SPRITE_SHEETS: list[SpriteSheetSpec] = [
37
+ SpriteSheetSpec("projs", "game/projs.png", (4, 2)),
38
+ SpriteSheetSpec("particles", "game/particles.png", (8, 4)),
39
+ SpriteSheetSpec("bonuses", "game/bonuses.png", (4,)),
40
+ SpriteSheetSpec("bodyset", "game/bodyset.png", (8,)),
41
+ SpriteSheetSpec("muzzleFlash", "game/muzzleFlash.png", (4, 2)),
42
+ SpriteSheetSpec("arrow", "game/arrow.png", (1,)),
43
+ ]
44
+
45
+
46
+ class SpriteSheetView:
47
+ def __init__(self, ctx: ViewContext) -> None:
48
+ self._assets_root = ctx.assets_dir
49
+ self._missing_assets: list[str] = []
50
+ self._sheets: list[SpriteSheet] = []
51
+ self._index = 0
52
+ self._small: SmallFontData | None = None
53
+
54
+ def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
55
+ if self._small is not None:
56
+ return int(self._small.cell_size * scale)
57
+ return int(20 * scale)
58
+
59
+ def _draw_ui_text(
60
+ self,
61
+ text: str,
62
+ x: float,
63
+ y: float,
64
+ color: rl.Color,
65
+ scale: float = UI_TEXT_SCALE,
66
+ ) -> None:
67
+ if self._small is not None:
68
+ draw_small_text(self._small, text, x, y, scale, color)
69
+ else:
70
+ rl.draw_text(text, int(x), int(y), int(20 * scale), color)
71
+
72
+ def open(self) -> None:
73
+ self._missing_assets.clear()
74
+ self._sheets.clear()
75
+ self._small = load_small_font(self._assets_root, self._missing_assets)
76
+ for spec in SPRITE_SHEETS:
77
+ path = self._assets_root / "crimson" / spec.rel_path
78
+ if not path.is_file():
79
+ self._missing_assets.append(spec.rel_path)
80
+ continue
81
+ texture = rl.load_texture(str(path))
82
+ self._sheets.append(SpriteSheet(name=spec.name, texture=texture, grids=spec.grids))
83
+ if self._missing_assets:
84
+ raise FileNotFoundError(f"Missing sprite assets: {', '.join(self._missing_assets)}")
85
+
86
+ def close(self) -> None:
87
+ for sheet in self._sheets:
88
+ rl.unload_texture(sheet.texture)
89
+ self._sheets.clear()
90
+ if self._small is not None:
91
+ rl.unload_texture(self._small.texture)
92
+ self._small = None
93
+
94
+ def update(self, dt: float) -> None:
95
+ del dt
96
+
97
+ def _advance_sheet(self, delta: int) -> None:
98
+ if not self._sheets:
99
+ return
100
+ self._index = (self._index + delta) % len(self._sheets)
101
+
102
+ def _set_grid(self, grid: int) -> None:
103
+ if not self._sheets:
104
+ return
105
+ sheet = self._sheets[self._index]
106
+ if grid not in sheet.grids:
107
+ return
108
+ sheet.grid_index = sheet.grids.index(grid)
109
+
110
+ def _cycle_grid(self, delta: int) -> None:
111
+ if not self._sheets:
112
+ return
113
+ sheet = self._sheets[self._index]
114
+ sheet.grid_index = (sheet.grid_index + delta) % len(sheet.grids)
115
+
116
+ def _handle_input(self) -> None:
117
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
118
+ self._advance_sheet(1)
119
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
120
+ self._advance_sheet(-1)
121
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
122
+ self._cycle_grid(1)
123
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
124
+ self._cycle_grid(-1)
125
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ONE):
126
+ self._set_grid(1)
127
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_TWO):
128
+ self._set_grid(2)
129
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_FOUR):
130
+ self._set_grid(4)
131
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_EIGHT):
132
+ self._set_grid(8)
133
+
134
+ def draw(self) -> None:
135
+ rl.clear_background(rl.Color(12, 12, 14, 255))
136
+ if self._missing_assets:
137
+ message = "Missing assets: " + ", ".join(self._missing_assets)
138
+ self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
139
+ return
140
+ if not self._sheets:
141
+ self._draw_ui_text("No sprite sheets loaded.", 24, 24, UI_TEXT_COLOR)
142
+ return
143
+
144
+ self._handle_input()
145
+ sheet = self._sheets[self._index]
146
+ grid = sheet.grid
147
+
148
+ margin = 24
149
+ info = f"{sheet.name} (grid {grid}x{grid})"
150
+ self._draw_ui_text(info, margin, margin, UI_TEXT_COLOR)
151
+ hint = "Left/Right: sheet Up/Down: grid 1/2/4/8: grid"
152
+ self._draw_ui_text(hint, margin, margin + self._ui_line_height() + 6, UI_HINT_COLOR)
153
+
154
+ available_width = rl.get_screen_width() - margin * 2
155
+ available_height = rl.get_screen_height() - margin * 2 - 60
156
+ scale = min(
157
+ 1.0,
158
+ available_width / sheet.texture.width,
159
+ available_height / sheet.texture.height,
160
+ )
161
+ draw_w = sheet.texture.width * scale
162
+ draw_h = sheet.texture.height * scale
163
+ x = margin
164
+ y = margin + 60
165
+
166
+ src = rl.Rectangle(0.0, 0.0, float(sheet.texture.width), float(sheet.texture.height))
167
+ dst = rl.Rectangle(float(x), float(y), float(draw_w), float(draw_h))
168
+ rl.draw_texture_pro(sheet.texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
169
+
170
+ if grid > 1:
171
+ cell_w = draw_w / grid
172
+ cell_h = draw_h / grid
173
+ for i in range(1, grid):
174
+ rl.draw_line(
175
+ int(x + i * cell_w),
176
+ int(y),
177
+ int(x + i * cell_w),
178
+ int(y + draw_h),
179
+ rl.Color(60, 60, 70, 255),
180
+ )
181
+ rl.draw_line(
182
+ int(x),
183
+ int(y + i * cell_h),
184
+ int(x + draw_w),
185
+ int(y + i * cell_h),
186
+ rl.Color(60, 60, 70, 255),
187
+ )
188
+
189
+ mouse = rl.get_mouse_position()
190
+ if x <= mouse.x <= x + draw_w and y <= mouse.y <= y + draw_h:
191
+ cell_w = draw_w / grid
192
+ cell_h = draw_h / grid
193
+ col = int((mouse.x - x) // cell_w)
194
+ row = int((mouse.y - y) // cell_h)
195
+ if 0 <= col < grid and 0 <= row < grid:
196
+ index = row * grid + col
197
+ hl = rl.Rectangle(
198
+ float(x + col * cell_w),
199
+ float(y + row * cell_h),
200
+ float(cell_w),
201
+ float(cell_h),
202
+ )
203
+ rl.draw_rectangle_lines_ex(hl, 2, rl.Color(240, 200, 80, 255))
204
+ self._draw_ui_text(
205
+ f"frame {index:02d}",
206
+ x,
207
+ y + draw_h + 10,
208
+ UI_TEXT_COLOR,
209
+ )
210
+
211
+
212
+ @register_view("sprites", "Sprite atlas preview")
213
+ def build_sprite_view(ctx: ViewContext) -> View:
214
+ return SpriteSheetView(ctx)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from grim.view import ViewContext
4
+
5
+ from ..modes.survival_mode import SurvivalMode
6
+ from .registry import register_view
7
+
8
+
9
+ class SurvivalView(SurvivalMode):
10
+ pass
11
+
12
+
13
+ @register_view("survival", "Survival")
14
+ def _create_survival_view(*, ctx: ViewContext) -> SurvivalView:
15
+ return SurvivalView(ctx)