crimsonland 0.1.0.dev11__py3-none-any.whl → 0.1.0.dev13__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 (43) hide show
  1. crimson/assets_fetch.py +23 -8
  2. crimson/creatures/runtime.py +15 -0
  3. crimson/demo.py +47 -38
  4. crimson/effects.py +46 -1
  5. crimson/frontend/boot.py +2 -1
  6. crimson/frontend/high_scores_layout.py +26 -0
  7. crimson/frontend/menu.py +24 -43
  8. crimson/frontend/panels/base.py +27 -29
  9. crimson/frontend/panels/controls.py +152 -65
  10. crimson/frontend/panels/credits.py +221 -0
  11. crimson/frontend/panels/databases.py +307 -0
  12. crimson/frontend/panels/mods.py +1 -3
  13. crimson/frontend/panels/options.py +36 -42
  14. crimson/frontend/panels/play_game.py +82 -74
  15. crimson/frontend/panels/stats.py +255 -298
  16. crimson/frontend/pause_menu.py +425 -0
  17. crimson/game.py +512 -505
  18. crimson/gameplay.py +35 -6
  19. crimson/modes/base_gameplay_mode.py +3 -0
  20. crimson/modes/quest_mode.py +54 -44
  21. crimson/modes/rush_mode.py +4 -1
  22. crimson/modes/survival_mode.py +15 -10
  23. crimson/modes/tutorial_mode.py +15 -5
  24. crimson/modes/typo_mode.py +4 -1
  25. crimson/persistence/highscores.py +6 -2
  26. crimson/render/world_renderer.py +1 -1
  27. crimson/sim/world_state.py +8 -1
  28. crimson/typo/spawns.py +3 -4
  29. crimson/ui/demo_trial_overlay.py +3 -3
  30. crimson/ui/game_over.py +18 -2
  31. crimson/ui/menu_panel.py +127 -0
  32. crimson/ui/perk_menu.py +101 -44
  33. crimson/ui/quest_results.py +669 -0
  34. crimson/ui/shadow.py +39 -0
  35. crimson/views/particles.py +1 -1
  36. crimson/views/perk_menu_debug.py +2 -2
  37. crimson/views/perks.py +2 -2
  38. crimson/weapons.py +110 -110
  39. {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev13.dist-info}/METADATA +1 -1
  40. {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev13.dist-info}/RECORD +43 -36
  41. grim/app.py +3 -0
  42. {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev13.dist-info}/WHEEL +0 -0
  43. {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev13.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import pyray as rl
4
+
5
+ from .shadow import UI_SHADOW_OFFSET, draw_ui_quad_shadow
6
+
7
+
8
+ # Classic menu panel is rendered from the *inset* inner region of ui_menuPanel:
9
+ # - X inset: 1px on each side (uv 1/512 .. 511/512) => 510px wide
10
+ # - Y inset: 1px on each side (uv 1/256 .. 255/256) => 254px tall
11
+ #
12
+ # When a panel is taller than the base height, the original stretches it using a
13
+ # 3-slice: [top][mid][bottom]. The source slice boundaries are at y=130 and y=150
14
+ # in the texture (see grim UVs in ui_render_trace).
15
+ MENU_PANEL_INSET = 1.0
16
+ MENU_PANEL_SRC_SLICE_Y1 = 130.0
17
+ MENU_PANEL_SRC_SLICE_Y2 = 150.0
18
+
19
+ # Destination slice heights observed in the original at scale=1.0 (1024x768).
20
+ MENU_PANEL_DST_TOP_H = 138.0
21
+ MENU_PANEL_DST_BOTTOM_H = 116.0
22
+
23
+
24
+ def draw_classic_menu_panel(
25
+ texture: rl.Texture2D,
26
+ *,
27
+ dst: rl.Rectangle,
28
+ tint: rl.Color = rl.WHITE,
29
+ shadow: bool = False,
30
+ ) -> None:
31
+ """
32
+ Draw a classic menu panel (ui_menuPanel) with the same slicing behavior as the original.
33
+
34
+ - Uses inset source rect (1px border skipped) to match the vertex/UV inset.
35
+ - Uses 3-slice only when dst is taller than (top + bottom); otherwise draws a single quad.
36
+ """
37
+
38
+ tex_w = float(texture.width)
39
+ tex_h = float(texture.height)
40
+ if tex_w <= 0.0 or tex_h <= 0.0:
41
+ return
42
+
43
+ inset = MENU_PANEL_INSET
44
+ src_x = inset
45
+ src_y = inset
46
+ src_w = max(0.0, tex_w - inset * 2.0)
47
+ src_h = max(0.0, tex_h - inset * 2.0)
48
+
49
+ # Scale slice heights with the panel width (menu panel uses the same scale factor).
50
+ # dst.width is already in our "inset" width space (510 at scale=1.0).
51
+ scale = (float(dst.width) / 510.0) if float(dst.width) != 0.0 else 1.0
52
+ top_h = MENU_PANEL_DST_TOP_H * scale
53
+ bottom_h = MENU_PANEL_DST_BOTTOM_H * scale
54
+ mid_h = float(dst.height) - top_h - bottom_h
55
+
56
+ origin = rl.Vector2(0.0, 0.0)
57
+
58
+ if mid_h <= 0.0:
59
+ src = rl.Rectangle(src_x, src_y, src_w, src_h)
60
+ if shadow:
61
+ draw_ui_quad_shadow(
62
+ texture=texture,
63
+ src=src,
64
+ dst=rl.Rectangle(
65
+ float(dst.x + UI_SHADOW_OFFSET),
66
+ float(dst.y + UI_SHADOW_OFFSET),
67
+ float(dst.width),
68
+ float(dst.height),
69
+ ),
70
+ origin=origin,
71
+ rotation_deg=0.0,
72
+ )
73
+ rl.draw_texture_pro(texture, src, dst, origin, 0.0, tint)
74
+ return
75
+
76
+ # Source slice rects (in texture pixels, with 1px inset).
77
+ src_top = rl.Rectangle(src_x, src_y, src_w, max(0.0, MENU_PANEL_SRC_SLICE_Y1 - inset))
78
+ src_mid = rl.Rectangle(src_x, MENU_PANEL_SRC_SLICE_Y1, src_w, max(0.0, MENU_PANEL_SRC_SLICE_Y2 - MENU_PANEL_SRC_SLICE_Y1))
79
+ src_bot = rl.Rectangle(src_x, MENU_PANEL_SRC_SLICE_Y2, src_w, max(0.0, (tex_h - inset) - MENU_PANEL_SRC_SLICE_Y2))
80
+
81
+ # Destination slices.
82
+ dst_top = rl.Rectangle(float(dst.x), float(dst.y), float(dst.width), float(top_h))
83
+ dst_mid = rl.Rectangle(float(dst.x), float(dst.y) + float(top_h), float(dst.width), float(mid_h))
84
+ dst_bot = rl.Rectangle(float(dst.x), float(dst.y) + float(top_h) + float(mid_h), float(dst.width), float(bottom_h))
85
+
86
+ if shadow:
87
+ draw_ui_quad_shadow(
88
+ texture=texture,
89
+ src=src_top,
90
+ dst=rl.Rectangle(
91
+ float(dst_top.x + UI_SHADOW_OFFSET),
92
+ float(dst_top.y + UI_SHADOW_OFFSET),
93
+ float(dst_top.width),
94
+ float(dst_top.height),
95
+ ),
96
+ origin=origin,
97
+ rotation_deg=0.0,
98
+ )
99
+ draw_ui_quad_shadow(
100
+ texture=texture,
101
+ src=src_mid,
102
+ dst=rl.Rectangle(
103
+ float(dst_mid.x + UI_SHADOW_OFFSET),
104
+ float(dst_mid.y + UI_SHADOW_OFFSET),
105
+ float(dst_mid.width),
106
+ float(dst_mid.height),
107
+ ),
108
+ origin=origin,
109
+ rotation_deg=0.0,
110
+ )
111
+ draw_ui_quad_shadow(
112
+ texture=texture,
113
+ src=src_bot,
114
+ dst=rl.Rectangle(
115
+ float(dst_bot.x + UI_SHADOW_OFFSET),
116
+ float(dst_bot.y + UI_SHADOW_OFFSET),
117
+ float(dst_bot.width),
118
+ float(dst_bot.height),
119
+ ),
120
+ origin=origin,
121
+ rotation_deg=0.0,
122
+ )
123
+
124
+ rl.draw_texture_pro(texture, src_top, dst_top, origin, 0.0, tint)
125
+ rl.draw_texture_pro(texture, src_mid, dst_mid, origin, 0.0, tint)
126
+ rl.draw_texture_pro(texture, src_bot, dst_bot, origin, 0.0, tint)
127
+
crimson/ui/perk_menu.py CHANGED
@@ -2,19 +2,26 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
+ from typing import Protocol
5
6
 
6
7
  import pyray as rl
7
8
 
8
9
  from grim.assets import TextureLoader
9
10
  from grim.fonts.small import SmallFontData, draw_small_text, measure_small_text_width
10
11
 
12
+ from .menu_panel import draw_classic_menu_panel
13
+
11
14
 
12
15
  UI_BASE_WIDTH = 640.0
13
16
  UI_BASE_HEIGHT = 480.0
14
17
 
15
-
16
- MENU_PANEL_SLICE_Y1 = 130.0
17
- MENU_PANEL_SLICE_Y2 = 150.0
18
+ # Perk selection screen panel uses ui_element-style timeline animation:
19
+ # - fully hidden until end_ms
20
+ # - slides in over (end_ms..start_ms)
21
+ # - fully visible at start_ms
22
+ PERK_MENU_ANIM_START_MS = 400.0
23
+ PERK_MENU_ANIM_END_MS = 100.0
24
+ PERK_MENU_TRANSITION_MS = PERK_MENU_ANIM_START_MS
18
25
 
19
26
  # Layout offsets from the classic game (perk selection screen), derived from
20
27
  # `perk_selection_screen_update` (see analysis/ghidra + BN).
@@ -42,11 +49,13 @@ MENU_DESC_RIGHT_X = 480.0
42
49
  @dataclass(slots=True)
43
50
  class PerkMenuLayout:
44
51
  # Coordinates live in the original 640x480 UI space.
45
- # Matches the classic menu panel: pos (-45, 110) + offset (20, -82).
46
- panel_x: float = -25.0
47
- panel_y: float = 28.0
48
- panel_w: float = 512.0
49
- panel_h: float = 379.0
52
+ # Capture (1024x768) shows the perk menu panel uses the 3-slice variant:
53
+ # open bbox (-108,119) -> (402,497)
54
+ # which corresponds to ui_element pos (-45,110) + geom (-63,-81) and size 510x378.
55
+ panel_x: float = -108.0
56
+ panel_y: float = 29.0
57
+ panel_w: float = 510.0
58
+ panel_h: float = 378.0
50
59
 
51
60
 
52
61
  @dataclass(slots=True)
@@ -142,32 +151,47 @@ def perk_menu_compute_layout(
142
151
  cancel_y=float(cancel_y),
143
152
  )
144
153
 
154
+ def ui_element_slide_x(
155
+ t_ms: float,
156
+ *,
157
+ start_ms: float,
158
+ end_ms: float,
159
+ width: float,
160
+ direction_flag: int = 0,
161
+ ) -> float:
162
+ """
163
+ Slide offset helper matching ui_element_update semantics (see MenuView._ui_element_anim).
164
+
165
+ direction_flag=0: slide from left (-width -> 0)
166
+ direction_flag=1: slide from right (+width -> 0)
167
+ """
168
+
169
+ if start_ms <= end_ms or width <= 0.0:
170
+ return 0.0
171
+
172
+ width = abs(float(width))
173
+ t = float(t_ms)
174
+ if t < float(end_ms):
175
+ slide = width
176
+ elif t < float(start_ms):
177
+ elapsed = t - float(end_ms)
178
+ span = float(start_ms) - float(end_ms)
179
+ p = elapsed / span if span > 1e-6 else 1.0
180
+ slide = (1.0 - p) * width
181
+ else:
182
+ slide = 0.0
145
183
 
146
- def draw_menu_panel(texture: rl.Texture, *, dst: rl.Rectangle, tint: rl.Color = rl.WHITE) -> None:
147
- scale = float(dst.width) / float(texture.width)
148
- top_h = MENU_PANEL_SLICE_Y1 * scale
149
- bottom_h = (float(texture.height) - MENU_PANEL_SLICE_Y2) * scale
150
- mid_h = float(dst.height) - top_h - bottom_h
151
- if mid_h < 0.0:
152
- src = rl.Rectangle(0.0, 0.0, float(texture.width), float(texture.height))
153
- rl.draw_texture_pro(texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)
154
- return
155
-
156
- src_w = float(texture.width)
157
- src_h = float(texture.height)
158
-
159
- src_top = rl.Rectangle(0.0, 0.0, src_w, MENU_PANEL_SLICE_Y1)
160
- src_mid = rl.Rectangle(0.0, MENU_PANEL_SLICE_Y1, src_w, MENU_PANEL_SLICE_Y2 - MENU_PANEL_SLICE_Y1)
161
- src_bot = rl.Rectangle(0.0, MENU_PANEL_SLICE_Y2, src_w, src_h - MENU_PANEL_SLICE_Y2)
184
+ return slide if int(direction_flag) else -slide
162
185
 
163
- dst_top = rl.Rectangle(float(dst.x), float(dst.y), float(dst.width), top_h)
164
- dst_mid = rl.Rectangle(float(dst.x), float(dst.y) + top_h, float(dst.width), mid_h)
165
- dst_bot = rl.Rectangle(float(dst.x), float(dst.y) + top_h + mid_h, float(dst.width), bottom_h)
166
186
 
167
- origin = rl.Vector2(0.0, 0.0)
168
- rl.draw_texture_pro(texture, src_top, dst_top, origin, 0.0, tint)
169
- rl.draw_texture_pro(texture, src_mid, dst_mid, origin, 0.0, tint)
170
- rl.draw_texture_pro(texture, src_bot, dst_bot, origin, 0.0, tint)
187
+ def perk_menu_panel_slide_x(t_ms: float, *, width: float) -> float:
188
+ return ui_element_slide_x(
189
+ t_ms,
190
+ start_ms=PERK_MENU_ANIM_START_MS,
191
+ end_ms=PERK_MENU_ANIM_END_MS,
192
+ width=width,
193
+ direction_flag=0,
194
+ )
171
195
 
172
196
 
173
197
  @dataclass(slots=True)
@@ -198,11 +222,11 @@ def load_perk_menu_assets(assets_root: Path) -> PerkMenuAssets:
198
222
  fs_rel="ui/ui_textLevelUp.png",
199
223
  ),
200
224
  menu_item=loader.get(name="ui_menuItem", paq_rel="ui/ui_menuItem.jaz", fs_rel="ui/ui_menuItem.png"),
201
- button_sm=loader.get(name="ui_buttonSm", paq_rel="ui/ui_button_82x32.jaz", fs_rel="ui/ui_button_82x32.png"),
225
+ button_sm=loader.get(name="ui_buttonSm", paq_rel="ui/ui_button_64x32.jaz", fs_rel="ui/ui_button_64x32.png"),
202
226
  button_md=loader.get(
203
227
  name="ui_buttonMd",
204
- paq_rel="ui/ui_button_145x32.jaz",
205
- fs_rel="ui/ui_button_145x32.png",
228
+ paq_rel="ui/ui_button_128x32.jaz",
229
+ fs_rel="ui/ui_button_128x32.png",
206
230
  ),
207
231
  cursor=loader.get(name="ui_cursor", paq_rel="ui/ui_cursor.jaz", fs_rel="ui/ui_cursor.png"),
208
232
  aim=loader.get(name="ui_aim", paq_rel="ui/ui_aim.jaz", fs_rel="ui/ui_aim.png"),
@@ -281,6 +305,17 @@ def draw_menu_item(
281
305
  return float(width)
282
306
 
283
307
 
308
+ class UiButtonTextures(Protocol):
309
+ button_sm: rl.Texture | None
310
+ button_md: rl.Texture | None
311
+
312
+
313
+ @dataclass(slots=True)
314
+ class UiButtonTextureSet:
315
+ button_sm: rl.Texture | None
316
+ button_md: rl.Texture | None
317
+
318
+
284
319
  @dataclass(slots=True)
285
320
  class UiButtonState:
286
321
  label: str
@@ -343,7 +378,7 @@ def _clamp(value: float, lo: float, hi: float) -> float:
343
378
 
344
379
 
345
380
  def button_draw(
346
- assets: PerkMenuAssets,
381
+ assets: UiButtonTextures,
347
382
  font: SmallFontData | None,
348
383
  state: UiButtonState,
349
384
  *,
@@ -357,23 +392,45 @@ def button_draw(
357
392
  return
358
393
 
359
394
  if state.hover_t > 0:
360
- alpha = 0.5
395
+ # ui_button_update: highlight fill uses a hover-scaled alpha and click-biased blue tint.
396
+ # - base: (0.5, 0.5, 0.7)
397
+ # - click_anim: +0.0005 / +0.0007, clamped to 1.0 (towards white)
398
+ # - alpha: hover_anim * 0.001 * button.alpha
399
+ r = 0.5
400
+ g = 0.5
401
+ b = 0.7
361
402
  if state.press_t > 0:
362
- alpha = min(1.0, 0.5 + (float(state.press_t) * 0.0005))
363
- hl = rl.Color(255, 255, 255, int(255 * alpha * 0.25 * state.alpha))
364
- rl.draw_rectangle(int(x + 12.0 * scale), int(y + 5.0 * scale), int(width - 24.0 * scale), int(22.0 * scale), hl)
365
-
366
- tint_a = state.alpha if state.hovered else state.alpha * 0.7
367
- tint = rl.Color(255, 255, 255, int(255 * _clamp(tint_a, 0.0, 1.0)))
403
+ click_t = float(state.press_t)
404
+ g = min(1.0, 0.5 + click_t * 0.0005)
405
+ r = g
406
+ b = min(1.0, 0.7 + click_t * 0.0007)
407
+ a = float(state.hover_t) * 0.001 * state.alpha
408
+ hl = rl.Color(
409
+ int(255 * r),
410
+ int(255 * g),
411
+ int(255 * b),
412
+ int(255 * _clamp(a, 0.0, 1.0)),
413
+ )
414
+ rl.draw_rectangle(
415
+ int(x + 12.0 * scale),
416
+ int(y + 5.0 * scale),
417
+ int(width - 24.0 * scale),
418
+ int(22.0 * scale),
419
+ hl,
420
+ )
421
+
422
+ plate_tint = rl.Color(255, 255, 255, int(255 * _clamp(state.alpha, 0.0, 1.0)))
368
423
 
369
424
  src = rl.Rectangle(0.0, 0.0, float(texture.width), float(texture.height))
370
425
  dst = rl.Rectangle(float(x), float(y), float(width), float(32.0 * scale))
371
- rl.draw_texture_pro(texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)
426
+ rl.draw_texture_pro(texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, plate_tint)
372
427
 
428
+ text_a = state.alpha if state.hovered else state.alpha * 0.7
429
+ text_tint = rl.Color(255, 255, 255, int(255 * _clamp(text_a, 0.0, 1.0)))
373
430
  text_w = _ui_text_width(font, state.label, scale)
374
431
  text_x = x + width * 0.5 - text_w * 0.5 + 1.0 * scale
375
432
  text_y = y + 10.0 * scale
376
- draw_ui_text(font, state.label, text_x, text_y, scale=scale, color=tint)
433
+ draw_ui_text(font, state.label, text_x, text_y, scale=scale, color=text_tint)
377
434
 
378
435
 
379
436
  def cursor_draw(assets: PerkMenuAssets, *, mouse: rl.Vector2, scale: float, alpha: float = 1.0) -> None: