crimsonland 0.1.0.dev1__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 (138) 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 +153 -0
  5. crimson/bonuses.py +167 -0
  6. crimson/camera.py +75 -0
  7. crimson/cli.py +377 -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 +663 -0
  34. crimson/gameplay.py +2450 -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 +1039 -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 +1338 -0
  65. crimson/sim/__init__.py +1 -0
  66. crimson/sim/world_defs.py +56 -0
  67. crimson/sim/world_state.py +421 -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 +414 -0
  86. crimson/views/bonuses.py +201 -0
  87. crimson/views/camera_debug.py +359 -0
  88. crimson/views/camera_shake.py +229 -0
  89. crimson/views/corpse_stamp_debug.py +324 -0
  90. crimson/views/decals_debug.py +739 -0
  91. crimson/views/empty.py +19 -0
  92. crimson/views/fonts.py +114 -0
  93. crimson/views/game_over.py +117 -0
  94. crimson/views/ground.py +259 -0
  95. crimson/views/lighting_debug.py +1166 -0
  96. crimson/views/particles.py +293 -0
  97. crimson/views/perk_menu_debug.py +430 -0
  98. crimson/views/perks.py +398 -0
  99. crimson/views/player.py +433 -0
  100. crimson/views/player_sprite_debug.py +314 -0
  101. crimson/views/projectile_fx.py +608 -0
  102. crimson/views/projectile_render_debug.py +407 -0
  103. crimson/views/projectiles.py +221 -0
  104. crimson/views/quest_title_overlay.py +108 -0
  105. crimson/views/registry.py +34 -0
  106. crimson/views/rush.py +16 -0
  107. crimson/views/small_font_debug.py +204 -0
  108. crimson/views/spawn_plan.py +363 -0
  109. crimson/views/sprites.py +214 -0
  110. crimson/views/survival.py +15 -0
  111. crimson/views/terrain.py +132 -0
  112. crimson/views/ui.py +123 -0
  113. crimson/views/wicons.py +166 -0
  114. crimson/weapon_sfx.py +63 -0
  115. crimson/weapons.py +860 -0
  116. crimsonland-0.1.0.dev1.dist-info/METADATA +9 -0
  117. crimsonland-0.1.0.dev1.dist-info/RECORD +138 -0
  118. crimsonland-0.1.0.dev1.dist-info/WHEEL +4 -0
  119. crimsonland-0.1.0.dev1.dist-info/entry_points.txt +4 -0
  120. grim/__init__.py +20 -0
  121. grim/app.py +92 -0
  122. grim/assets.py +231 -0
  123. grim/audio.py +106 -0
  124. grim/config.py +294 -0
  125. grim/console.py +737 -0
  126. grim/fonts/__init__.py +7 -0
  127. grim/fonts/grim_mono.py +111 -0
  128. grim/fonts/small.py +120 -0
  129. grim/input.py +44 -0
  130. grim/jaz.py +103 -0
  131. grim/math.py +17 -0
  132. grim/music.py +403 -0
  133. grim/paq.py +76 -0
  134. grim/rand.py +37 -0
  135. grim/sfx.py +276 -0
  136. grim/sfx_map.py +103 -0
  137. grim/terrain_render.py +840 -0
  138. grim/view.py +16 -0
@@ -0,0 +1,351 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import pyray as rl
6
+
7
+ from grim.audio import play_music, stop_music
8
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
9
+
10
+ from ..menu import (
11
+ MENU_LABEL_ROW_HEIGHT,
12
+ MENU_LABEL_ROW_STATISTICS,
13
+ MENU_LABEL_WIDTH,
14
+ MENU_PANEL_HEIGHT,
15
+ MENU_PANEL_OFFSET_X,
16
+ MENU_PANEL_OFFSET_Y,
17
+ MENU_PANEL_WIDTH,
18
+ MenuView,
19
+ _draw_menu_cursor,
20
+ )
21
+ from ..transitions import _draw_screen_fade
22
+ from .base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView
23
+
24
+ from ...persistence.save_status import MODE_COUNT_ORDER
25
+ from ...weapons import WEAPON_BY_ID, WeaponId
26
+
27
+ if TYPE_CHECKING:
28
+ from ...game import GameState
29
+
30
+
31
+ class StatisticsMenuView(PanelMenuView):
32
+ _PAGES = ("Summary", "Weapons", "Quests")
33
+
34
+ def __init__(self, state: GameState) -> None:
35
+ super().__init__(state, title="Statistics")
36
+ self._small_font: SmallFontData | None = None
37
+ self._page_index = 0
38
+ self._scroll_index = 0
39
+ self._page_lines: list[list[str]] = []
40
+
41
+ def open(self) -> None:
42
+ super().open()
43
+ self._page_index = 0
44
+ self._scroll_index = 0
45
+ self._page_lines = self._build_pages()
46
+ if self._state.audio is not None:
47
+ if self._state.audio.music.active_track != "shortie_monk":
48
+ stop_music(self._state.audio)
49
+ play_music(self._state.audio, "shortie_monk")
50
+
51
+ def draw(self) -> None:
52
+ rl.clear_background(rl.BLACK)
53
+ if self._ground is not None:
54
+ self._ground.draw(0.0, 0.0)
55
+ _draw_screen_fade(self._state)
56
+ assets = self._assets
57
+ entry = self._entry
58
+ if assets is None or entry is None:
59
+ return
60
+ self._draw_panel()
61
+ self._draw_entry(entry)
62
+ self._draw_sign()
63
+ self._draw_stats_contents()
64
+ _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
65
+
66
+ def update(self, dt: float) -> None:
67
+ super().update(dt)
68
+ if self._closing:
69
+ return
70
+ entry = self._entry
71
+ if entry is None or not self._entry_enabled(entry):
72
+ return
73
+
74
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
75
+ self._switch_page(-1)
76
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
77
+ self._switch_page(1)
78
+
79
+ font = self._ensure_small_font()
80
+ layout = self._content_layout()
81
+ rows = self._visible_rows(font, layout)
82
+ max_scroll = max(0, len(self._active_page_lines()) - rows)
83
+
84
+ wheel = int(rl.get_mouse_wheel_move())
85
+ if wheel:
86
+ self._scroll_index = max(0, min(max_scroll, int(self._scroll_index) - wheel))
87
+
88
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
89
+ self._scroll_index = max(0, int(self._scroll_index) - 1)
90
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
91
+ self._scroll_index = min(max_scroll, int(self._scroll_index) + 1)
92
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_UP):
93
+ self._scroll_index = max(0, int(self._scroll_index) - rows)
94
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_DOWN):
95
+ self._scroll_index = min(max_scroll, int(self._scroll_index) + rows)
96
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_HOME):
97
+ self._scroll_index = 0
98
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_END):
99
+ self._scroll_index = max_scroll
100
+
101
+ def _ensure_small_font(self) -> SmallFontData:
102
+ if self._small_font is not None:
103
+ return self._small_font
104
+ missing_assets: list[str] = []
105
+ self._small_font = load_small_font(self._state.assets_dir, missing_assets)
106
+ return self._small_font
107
+
108
+ def _content_layout(self) -> dict[str, float]:
109
+ panel_scale, _local_shift = self._menu_item_scale(0)
110
+ panel_w = MENU_PANEL_WIDTH * panel_scale
111
+ _angle_rad, slide_x = MenuView._ui_element_anim(
112
+ self,
113
+ index=1,
114
+ start_ms=PANEL_TIMELINE_START_MS,
115
+ end_ms=PANEL_TIMELINE_END_MS,
116
+ width=panel_w,
117
+ )
118
+ panel_x = self._panel_pos_x + slide_x
119
+ panel_y = self._panel_pos_y + self._widescreen_y_shift
120
+ origin_x = -(MENU_PANEL_OFFSET_X * panel_scale)
121
+ origin_y = -(MENU_PANEL_OFFSET_Y * panel_scale)
122
+ panel_left = panel_x - origin_x
123
+ panel_top = panel_y - origin_y
124
+ base_x = panel_left + 212.0 * panel_scale
125
+ base_y = panel_top + 32.0 * panel_scale
126
+ label_x = base_x + 8.0 * panel_scale
127
+ return {
128
+ "panel_left": panel_left,
129
+ "panel_top": panel_top,
130
+ "base_x": base_x,
131
+ "base_y": base_y,
132
+ "label_x": label_x,
133
+ "scale": panel_scale,
134
+ }
135
+
136
+ def _switch_page(self, delta: int) -> None:
137
+ if not self._page_lines:
138
+ return
139
+ count = len(self._page_lines)
140
+ self._page_index = (int(self._page_index) + int(delta)) % count
141
+ self._scroll_index = 0
142
+
143
+ def _active_page_lines(self) -> list[str]:
144
+ if not self._page_lines:
145
+ return []
146
+ idx = int(self._page_index)
147
+ if idx < 0:
148
+ idx = 0
149
+ if idx >= len(self._page_lines):
150
+ idx = len(self._page_lines) - 1
151
+ return self._page_lines[idx]
152
+
153
+ def _build_pages(self) -> list[list[str]]:
154
+ return [
155
+ self._build_summary_lines(),
156
+ self._build_weapon_usage_lines(),
157
+ self._build_quest_progress_lines(),
158
+ ]
159
+
160
+ def _build_summary_lines(self) -> list[str]:
161
+ status = self._state.status
162
+ mode_counts = {name: status.mode_play_count(name) for name, _offset in MODE_COUNT_ORDER}
163
+ quest_counts = status.data.get("quest_play_counts", [])
164
+ if isinstance(quest_counts, list):
165
+ quest_total = int(sum(int(v) for v in quest_counts[:40]))
166
+ else:
167
+ quest_total = 0
168
+
169
+ checksum_text = "unknown"
170
+ try:
171
+ from ...persistence.save_status import load_status
172
+
173
+ blob = load_status(status.path)
174
+ ok = "ok" if blob.checksum_valid else "BAD"
175
+ checksum_text = f"0x{blob.checksum:08x} ({ok})"
176
+ except Exception as exc:
177
+ checksum_text = f"error: {type(exc).__name__}"
178
+
179
+ playtime_ms = int(status.game_sequence_id)
180
+ seconds = max(0, playtime_ms // 1000)
181
+ minutes = seconds // 60
182
+ hours = minutes // 60
183
+ minutes %= 60
184
+ seconds %= 60
185
+
186
+ lines = [
187
+ f"Played for: {hours}h {minutes:02d}m {seconds:02d}s",
188
+ f"Quest unlock: {status.quest_unlock_index} (full {status.quest_unlock_index_full})",
189
+ f"Quest plays (1-40): {quest_total}",
190
+ f"Mode plays: surv {mode_counts['survival']} rush {mode_counts['rush']}",
191
+ f" typo {mode_counts['typo']} other {mode_counts['other']}",
192
+ f"Playtime ms: {int(status.game_sequence_id)}",
193
+ f"Checksum: {checksum_text}",
194
+ ]
195
+
196
+ usage = status.data.get("weapon_usage_counts", [])
197
+ top_weapons: list[tuple[int, int]] = []
198
+ if isinstance(usage, list):
199
+ for idx, count in enumerate(usage):
200
+ count = int(count)
201
+ if count > 0:
202
+ top_weapons.append((idx, count))
203
+ top_weapons.sort(key=lambda item: (-item[1], item[0]))
204
+ top_weapons = top_weapons[:4]
205
+
206
+ if top_weapons:
207
+ lines.append("Top weapons:")
208
+ for idx, count in top_weapons:
209
+ weapon = WEAPON_BY_ID.get(idx)
210
+ name = weapon.name if weapon is not None and weapon.name else f"weapon_{idx}"
211
+ lines.append(f" {name}: {count}")
212
+ else:
213
+ lines.append("Top weapons: none")
214
+
215
+ return lines
216
+
217
+ def _build_weapon_usage_lines(self) -> list[str]:
218
+ status = self._state.status
219
+ usage = status.data.get("weapon_usage_counts", [])
220
+ if not isinstance(usage, list):
221
+ return ["Weapon usage: error (missing weapon_usage_counts)"]
222
+
223
+ items: list[tuple[int, int, str]] = []
224
+ for idx, count in enumerate(usage):
225
+ weapon_id = int(idx)
226
+ if weapon_id == WeaponId.NONE:
227
+ continue
228
+ count = int(count)
229
+ weapon = WEAPON_BY_ID.get(weapon_id)
230
+ name = weapon.name if weapon is not None and weapon.name else f"weapon_{weapon_id}"
231
+ items.append((weapon_id, count, name))
232
+
233
+ items.sort(key=lambda item: (-item[1], item[0]))
234
+ total = sum(count for _weapon_id, count, _name in items)
235
+ max_id_width = max(2, len(str(max((weapon_id for weapon_id, _count, _name in items), default=0))))
236
+
237
+ lines = [
238
+ f"Weapon uses (total {total}):",
239
+ "",
240
+ ]
241
+ for weapon_id, count, name in items:
242
+ lines.append(f"{weapon_id:>{max_id_width}} {count:>8} {name}")
243
+ return lines
244
+
245
+ def _build_quest_progress_lines(self) -> list[str]:
246
+ status = self._state.status
247
+ completed_total = 0
248
+ played_total = 0
249
+
250
+ lines = [
251
+ "Quest progress (stages 1-4):",
252
+ "",
253
+ ]
254
+ for global_index in range(40):
255
+ stage = (global_index // 10) + 1
256
+ row = global_index % 10
257
+ level = f"{stage}.{row + 1}"
258
+ title = "???"
259
+ try:
260
+ from ...quests import quest_by_level
261
+
262
+ quest = quest_by_level(level)
263
+ if quest is not None:
264
+ title = quest.title
265
+ except Exception:
266
+ title = "???"
267
+
268
+ count_index = global_index + 10
269
+ games_idx = 1 + count_index
270
+ completed_idx = 41 + count_index
271
+ games = int(status.quest_play_count(games_idx))
272
+ completed = int(status.quest_play_count(completed_idx))
273
+
274
+ completed_total += completed
275
+ played_total += games
276
+ lines.append(f"{level:>4} {completed:>3}/{games:<3} {title}")
277
+
278
+ lines.extend(
279
+ [
280
+ "",
281
+ f"Completed: {completed_total}",
282
+ f"Played: {played_total}",
283
+ ]
284
+ )
285
+ return lines
286
+
287
+ def _visible_rows(self, font: SmallFontData, layout: dict[str, float]) -> int:
288
+ scale = float(layout["scale"])
289
+ line_step = (float(font.cell_size) + 4.0) * scale
290
+ line_y0 = float(layout["base_y"]) + 66.0 * scale
291
+ panel_bottom = float(layout["panel_top"]) + (MENU_PANEL_HEIGHT * scale)
292
+ available = max(0.0, panel_bottom - line_y0 - 8.0 * scale)
293
+ return max(1, int(available // line_step))
294
+
295
+ def _draw_stats_contents(self) -> None:
296
+ assets = self._assets
297
+ if assets is None:
298
+ return
299
+ labels_tex = assets.labels
300
+ layout = self._content_layout()
301
+ base_x = layout["base_x"]
302
+ base_y = layout["base_y"]
303
+ label_x = layout["label_x"]
304
+ scale = layout["scale"]
305
+
306
+ font = self._ensure_small_font()
307
+ text_scale = 1.0 * scale
308
+ text_color = rl.Color(255, 255, 255, int(255 * 0.8))
309
+
310
+ if labels_tex is not None:
311
+ src = rl.Rectangle(
312
+ 0.0,
313
+ float(MENU_LABEL_ROW_STATISTICS) * MENU_LABEL_ROW_HEIGHT,
314
+ MENU_LABEL_WIDTH,
315
+ MENU_LABEL_ROW_HEIGHT,
316
+ )
317
+ dst = rl.Rectangle(
318
+ base_x,
319
+ base_y,
320
+ MENU_LABEL_WIDTH * scale,
321
+ MENU_LABEL_ROW_HEIGHT * scale,
322
+ )
323
+ MenuView._draw_ui_quad(
324
+ texture=labels_tex,
325
+ src=src,
326
+ dst=dst,
327
+ origin=rl.Vector2(0.0, 0.0),
328
+ rotation_deg=0.0,
329
+ tint=rl.WHITE,
330
+ )
331
+ else:
332
+ rl.draw_text(self._title, int(base_x), int(base_y), int(24 * scale), rl.WHITE)
333
+
334
+ tabs_y = base_y + 44.0 * scale
335
+ x = label_x
336
+ for idx, label in enumerate(self._PAGES):
337
+ active = idx == int(self._page_index)
338
+ color = rl.Color(255, 255, 255, 255 if active else int(255 * 0.55))
339
+ draw_small_text(font, label, x, tabs_y, text_scale, color)
340
+ x += (len(label) * font.cell_size + 18.0) * scale
341
+
342
+ lines = self._active_page_lines()
343
+ rows = self._visible_rows(font, layout)
344
+ start = max(0, int(self._scroll_index))
345
+ end = min(len(lines), start + rows)
346
+
347
+ line_y = base_y + 66.0 * scale
348
+ line_step = (font.cell_size + 4.0) * scale
349
+ for line in lines[start:end]:
350
+ draw_small_text(font, line, label_x, line_y, text_scale, text_color)
351
+ line_y += line_step
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import pyray as rl
6
+
7
+ if TYPE_CHECKING:
8
+ from ..game import GameState
9
+
10
+
11
+ SCREEN_FADE_OUT_RATE = 2.0
12
+ SCREEN_FADE_IN_RATE = 10.0
13
+
14
+
15
+ def _update_screen_fade(state: GameState, dt: float) -> None:
16
+ if state.screen_fade_ramp:
17
+ state.screen_fade_alpha += float(dt) * SCREEN_FADE_IN_RATE
18
+ else:
19
+ state.screen_fade_alpha -= float(dt) * SCREEN_FADE_OUT_RATE
20
+ if state.screen_fade_alpha < 0.0:
21
+ state.screen_fade_alpha = 0.0
22
+ elif state.screen_fade_alpha > 1.0:
23
+ state.screen_fade_alpha = 1.0
24
+
25
+
26
+ def _draw_screen_fade(state: GameState) -> None:
27
+ alpha = float(state.screen_fade_alpha)
28
+ if alpha <= 0.0:
29
+ return
30
+ shade = int(max(0.0, min(1.0, alpha)) * 255.0)
31
+ rl.draw_rectangle(0, 0, int(rl.get_screen_width()), int(rl.get_screen_height()), rl.Color(0, 0, 0, shade))