crimsonland 0.1.0.dev14__py3-none-any.whl → 0.1.0.dev16__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 (73) hide show
  1. crimson/cli.py +63 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/debug.py +9 -0
  6. crimson/demo.py +38 -45
  7. crimson/effects.py +7 -13
  8. crimson/frontend/high_scores_layout.py +81 -0
  9. crimson/frontend/panels/base.py +4 -1
  10. crimson/frontend/panels/controls.py +0 -15
  11. crimson/frontend/panels/databases.py +291 -3
  12. crimson/frontend/panels/mods.py +0 -15
  13. crimson/frontend/panels/play_game.py +0 -16
  14. crimson/game.py +689 -3
  15. crimson/gameplay.py +921 -569
  16. crimson/modes/base_gameplay_mode.py +33 -12
  17. crimson/modes/components/__init__.py +2 -0
  18. crimson/modes/components/highscore_record_builder.py +58 -0
  19. crimson/modes/components/perk_menu_controller.py +325 -0
  20. crimson/modes/quest_mode.py +94 -272
  21. crimson/modes/rush_mode.py +12 -43
  22. crimson/modes/survival_mode.py +109 -330
  23. crimson/modes/tutorial_mode.py +46 -247
  24. crimson/modes/typo_mode.py +11 -38
  25. crimson/oracle.py +396 -0
  26. crimson/perks.py +5 -2
  27. crimson/player_damage.py +95 -36
  28. crimson/projectiles.py +539 -320
  29. crimson/render/projectile_draw_registry.py +637 -0
  30. crimson/render/projectile_render_registry.py +110 -0
  31. crimson/render/secondary_projectile_draw_registry.py +206 -0
  32. crimson/render/world_renderer.py +58 -707
  33. crimson/sim/world_state.py +118 -61
  34. crimson/typo/spawns.py +5 -12
  35. crimson/ui/demo_trial_overlay.py +3 -11
  36. crimson/ui/formatting.py +24 -0
  37. crimson/ui/game_over.py +12 -58
  38. crimson/ui/hud.py +72 -39
  39. crimson/ui/layout.py +20 -0
  40. crimson/ui/perk_menu.py +9 -34
  41. crimson/ui/quest_results.py +28 -70
  42. crimson/ui/text_input.py +20 -0
  43. crimson/views/_ui_helpers.py +27 -0
  44. crimson/views/aim_debug.py +15 -32
  45. crimson/views/animations.py +18 -28
  46. crimson/views/arsenal_debug.py +22 -32
  47. crimson/views/bonuses.py +23 -36
  48. crimson/views/camera_debug.py +16 -29
  49. crimson/views/camera_shake.py +9 -33
  50. crimson/views/corpse_stamp_debug.py +13 -21
  51. crimson/views/decals_debug.py +36 -23
  52. crimson/views/fonts.py +8 -25
  53. crimson/views/ground.py +4 -21
  54. crimson/views/lighting_debug.py +42 -45
  55. crimson/views/particles.py +33 -42
  56. crimson/views/perk_menu_debug.py +3 -10
  57. crimson/views/player.py +50 -44
  58. crimson/views/player_sprite_debug.py +24 -31
  59. crimson/views/projectile_fx.py +57 -52
  60. crimson/views/projectile_render_debug.py +24 -33
  61. crimson/views/projectiles.py +24 -37
  62. crimson/views/spawn_plan.py +13 -29
  63. crimson/views/sprites.py +14 -29
  64. crimson/views/terrain.py +6 -23
  65. crimson/views/ui.py +7 -24
  66. crimson/views/wicons.py +28 -33
  67. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  68. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +73 -62
  69. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +1 -1
  70. grim/config.py +29 -1
  71. grim/console.py +7 -10
  72. grim/math.py +12 -0
  73. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
crimson/ui/perk_menu.py CHANGED
@@ -8,13 +8,11 @@ import pyray as rl
8
8
 
9
9
  from grim.assets import TextureLoader
10
10
  from grim.fonts.small import SmallFontData, draw_small_text, measure_small_text_width
11
+ from grim.math import clamp
11
12
 
13
+ from .layout import menu_widescreen_y_shift, ui_origin, ui_scale
12
14
  from .menu_panel import draw_classic_menu_panel
13
15
 
14
-
15
- UI_BASE_WIDTH = 640.0
16
- UI_BASE_HEIGHT = 480.0
17
-
18
16
  # Perk selection screen panel uses ui_element-style timeline animation:
19
17
  # - fully hidden until end_ms
20
18
  # - slides in over (end_ms..start_ms)
@@ -71,21 +69,6 @@ class PerkMenuComputedLayout:
71
69
  cancel_x: float
72
70
  cancel_y: float
73
71
 
74
-
75
- def ui_scale(screen_w: float, screen_h: float) -> float:
76
- # Classic UI renders in backbuffer pixels; keep menu scale fixed.
77
- return 1.0
78
-
79
-
80
- def ui_origin(screen_w: float, screen_h: float, scale: float) -> tuple[float, float]:
81
- return 0.0, 0.0
82
-
83
-
84
- def _menu_widescreen_y_shift(layout_w: float) -> float:
85
- # ui_menu_layout_init: pos_y += (screen_width / 640.0) * 150.0 - 150.0
86
- return (layout_w / UI_BASE_WIDTH) * 150.0 - 150.0
87
-
88
-
89
72
  def perk_menu_compute_layout(
90
73
  layout: PerkMenuLayout,
91
74
  *,
@@ -99,7 +82,7 @@ def perk_menu_compute_layout(
99
82
  panel_slide_x: float = 0.0,
100
83
  ) -> PerkMenuComputedLayout:
101
84
  layout_w = screen_w / scale if scale else screen_w
102
- widescreen_shift_y = _menu_widescreen_y_shift(layout_w)
85
+ widescreen_shift_y = menu_widescreen_y_shift(layout_w)
103
86
  panel_x = layout.panel_x + panel_slide_x
104
87
  panel_y = layout.panel_y + widescreen_shift_y
105
88
  panel = rl.Rectangle(
@@ -358,10 +341,10 @@ def button_update(
358
341
  state.hovered = rl.check_collision_point_rec(mouse, button_hit_rect(x=x, y=y, width=width))
359
342
 
360
343
  delta = 6 if (state.enabled and state.hovered) else -4
361
- state.hover_t = int(_clamp(float(state.hover_t + int(dt_ms) * delta), 0.0, 1000.0))
344
+ state.hover_t = int(clamp(float(state.hover_t + int(dt_ms) * delta), 0.0, 1000.0))
362
345
 
363
346
  if state.press_t > 0:
364
- state.press_t = int(_clamp(float(state.press_t - int(dt_ms) * 6), 0.0, 1000.0))
347
+ state.press_t = int(clamp(float(state.press_t - int(dt_ms) * 6), 0.0, 1000.0))
365
348
 
366
349
  state.activated = bool(state.enabled and state.hovered and click)
367
350
  if state.activated:
@@ -369,14 +352,6 @@ def button_update(
369
352
  return state.activated
370
353
 
371
354
 
372
- def _clamp(value: float, lo: float, hi: float) -> float:
373
- if value < lo:
374
- return lo
375
- if value > hi:
376
- return hi
377
- return value
378
-
379
-
380
355
  def button_draw(
381
356
  assets: UiButtonTextures,
382
357
  font: SmallFontData | None,
@@ -409,7 +384,7 @@ def button_draw(
409
384
  int(255 * r),
410
385
  int(255 * g),
411
386
  int(255 * b),
412
- int(255 * _clamp(a, 0.0, 1.0)),
387
+ int(255 * clamp(a, 0.0, 1.0)),
413
388
  )
414
389
  rl.draw_rectangle(
415
390
  int(x + 12.0 * scale),
@@ -419,14 +394,14 @@ def button_draw(
419
394
  hl,
420
395
  )
421
396
 
422
- plate_tint = rl.Color(255, 255, 255, int(255 * _clamp(state.alpha, 0.0, 1.0)))
397
+ plate_tint = rl.Color(255, 255, 255, int(255 * clamp(state.alpha, 0.0, 1.0)))
423
398
 
424
399
  src = rl.Rectangle(0.0, 0.0, float(texture.width), float(texture.height))
425
400
  dst = rl.Rectangle(float(x), float(y), float(width), float(32.0 * scale))
426
401
  rl.draw_texture_pro(texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, plate_tint)
427
402
 
428
403
  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)))
404
+ text_tint = rl.Color(255, 255, 255, int(255 * clamp(text_a, 0.0, 1.0)))
430
405
  text_w = _ui_text_width(font, state.label, scale)
431
406
  text_x = x + width * 0.5 - text_w * 0.5 + 1.0 * scale
432
407
  text_y = y + 10.0 * scale
@@ -437,7 +412,7 @@ def cursor_draw(assets: PerkMenuAssets, *, mouse: rl.Vector2, scale: float, alph
437
412
  tex = assets.cursor
438
413
  if tex is None:
439
414
  return
440
- a = int(255 * _clamp(alpha, 0.0, 1.0))
415
+ a = int(255 * clamp(alpha, 0.0, 1.0))
441
416
  tint = rl.Color(255, 255, 255, a)
442
417
  size = 32.0 * scale
443
418
  src = rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height))
@@ -21,6 +21,8 @@ from ..persistence.highscores import (
21
21
  upsert_highscore_record,
22
22
  )
23
23
  from ..quests.results import QuestFinalTime, QuestResultsBreakdownAnim, tick_quest_results_breakdown_anim
24
+ from .formatting import format_ordinal, format_time_mm_ss
25
+ from .layout import menu_widescreen_y_shift, ui_origin, ui_scale
24
26
  from .menu_panel import draw_classic_menu_panel
25
27
  from .perk_menu import (
26
28
  PerkMenuAssets,
@@ -32,25 +34,7 @@ from .perk_menu import (
32
34
  draw_ui_text,
33
35
  load_perk_menu_assets,
34
36
  )
35
-
36
-
37
- UI_BASE_WIDTH = 640.0
38
- UI_BASE_HEIGHT = 480.0
39
-
40
-
41
- def ui_scale(screen_w: float, screen_h: float) -> float:
42
- # Classic UI-space: draw in backbuffer pixels.
43
- return 1.0
44
-
45
-
46
- def ui_origin(screen_w: float, screen_h: float, scale: float) -> tuple[float, float]:
47
- return 0.0, 0.0
48
-
49
-
50
- def _menu_widescreen_y_shift(layout_w: float) -> float:
51
- # ui_menu_layout_init: pos_y += (screen_width / 640.0) * 150.0 - 150.0
52
- return (layout_w / UI_BASE_WIDTH) * 150.0 - 150.0
53
-
37
+ from .text_input import poll_text_input
54
38
 
55
39
  # `quest_results_screen_update` base layout (Crimsonland classic UI panel).
56
40
  # Values are derived from `ui_menu_assets_init` + `ui_menu_layout_init` and how
@@ -73,6 +57,14 @@ QUEST_RESULTS_PANEL_H = 378.0
73
57
  TEXTURE_TOP_BANNER_W = 256.0
74
58
  TEXTURE_TOP_BANNER_H = 64.0
75
59
 
60
+ # `quest_results_screen_update` uses the classic UI element sums for positioning:
61
+ # content_x = (pos_x + offset_x + slide_x) + 180.0 + 40.0
62
+ # banner_x = content_x - 18.0
63
+ # score_x = content_x + 30.0
64
+ QUEST_RESULTS_CONTENT_X = 220.0
65
+ QUEST_RESULTS_BANNER_X_FROM_CONTENT = -18.0
66
+ QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT = 30.0
67
+
76
68
  INPUT_BOX_W = 166.0
77
69
  INPUT_BOX_H = 18.0
78
70
 
@@ -88,44 +80,6 @@ COLOR_TEXT_SUBTLE = rl.Color(255, 255, 255, int(255 * 0.7))
88
80
  COLOR_GREEN = rl.Color(25, 200, 25, 255)
89
81
 
90
82
 
91
- def _poll_text_input(max_len: int, *, allow_space: bool = True) -> str:
92
- out = ""
93
- while True:
94
- value = rl.get_char_pressed()
95
- if value == 0:
96
- break
97
- if value < 0x20 or value > 0xFF:
98
- continue
99
- if not allow_space and value == 0x20:
100
- continue
101
- if len(out) >= max_len:
102
- continue
103
- out += chr(int(value))
104
- return out
105
-
106
-
107
- def _format_ordinal(value_1_based: int) -> str:
108
- value = int(value_1_based)
109
- if value % 100 in (11, 12, 13):
110
- suffix = "th"
111
- elif value % 10 == 1:
112
- suffix = "st"
113
- elif value % 10 == 2:
114
- suffix = "nd"
115
- elif value % 10 == 3:
116
- suffix = "rd"
117
- else:
118
- suffix = "th"
119
- return f"{value}{suffix}"
120
-
121
-
122
- def _format_time_mm_ss(ms: int) -> str:
123
- total_s = max(0, int(ms)) // 1000
124
- minutes = total_s // 60
125
- seconds = total_s % 60
126
- return f"{minutes}:{seconds:02d}"
127
-
128
-
129
83
  @dataclass(slots=True)
130
84
  class QuestResultsAssets:
131
85
  menu_panel: rl.Texture | None
@@ -300,7 +254,7 @@ class QuestResultsUi:
300
254
 
301
255
  left = (QUEST_RESULTS_PANEL_GEOM_X0 + QUEST_RESULTS_PANEL_POS_X + panel_slide_x) * scale
302
256
  layout_w = screen_w / scale if scale else screen_w
303
- widescreen_shift_y = _menu_widescreen_y_shift(layout_w)
257
+ widescreen_shift_y = menu_widescreen_y_shift(layout_w)
304
258
  top = (QUEST_RESULTS_PANEL_GEOM_Y0 + QUEST_RESULTS_PANEL_POS_Y + widescreen_shift_y) * scale
305
259
  panel = rl.Rectangle(float(left), float(top), QUEST_RESULTS_PANEL_W * scale, QUEST_RESULTS_PANEL_H * scale)
306
260
  return panel, left, top
@@ -374,7 +328,7 @@ class QuestResultsUi:
374
328
 
375
329
  if self.phase == 1:
376
330
  click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
377
- typed = _poll_text_input(NAME_MAX_EDIT - len(self.input_text), allow_space=True)
331
+ typed = poll_text_input(NAME_MAX_EDIT - len(self.input_text), allow_space=True)
378
332
  if typed:
379
333
  self.input_text = (self.input_text[: self.input_caret] + typed + self.input_text[self.input_caret :])[:NAME_MAX_EDIT]
380
334
  self.input_caret = min(len(self.input_text), self.input_caret + len(typed))
@@ -399,7 +353,7 @@ class QuestResultsUi:
399
353
  screen_h = float(rl.get_screen_height())
400
354
  scale = ui_scale(screen_w, screen_h)
401
355
  _panel, panel_left, panel_top = self._panel_layout(screen_w=screen_w, scale=scale)
402
- anchor_x = panel_left + 40.0 * scale
356
+ anchor_x = panel_left + QUEST_RESULTS_CONTENT_X * scale
403
357
  input_y = panel_top + 150.0 * scale
404
358
  ok_x = anchor_x + 170.0 * scale
405
359
  ok_y = input_y - 8.0 * scale
@@ -421,6 +375,8 @@ class QuestResultsUi:
421
375
  except Exception:
422
376
  self.highlight_rank = None
423
377
  self._saved = True
378
+ self.config.set_player_name(self.input_text)
379
+ self.config.save()
424
380
  self.phase = 2
425
381
  return None
426
382
  if play_sfx is not None:
@@ -451,7 +407,8 @@ class QuestResultsUi:
451
407
  _origin_x, _origin_y = ui_origin(screen_w, screen_h, scale)
452
408
  _panel, left, top = self._panel_layout(screen_w=screen_w, scale=scale)
453
409
  qualifies = int(self.rank) < TABLE_MAX
454
- score_card_x = left + 70.0 * scale
410
+ content_x = left + QUEST_RESULTS_CONTENT_X * scale
411
+ score_card_x = content_x + QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT * scale
455
412
 
456
413
  var_c_12 = top + (96.0 if qualifies else 108.0) * scale
457
414
  var_c_14 = var_c_12 + 84.0 * scale
@@ -515,7 +472,8 @@ class QuestResultsUi:
515
472
  fx_detail = bool(int(self.config.data.get("fx_detail_0", 0) or 0))
516
473
  draw_classic_menu_panel(self.assets.menu_panel, dst=panel, tint=rl.WHITE, shadow=fx_detail)
517
474
 
518
- banner_x = left + 22.0 * scale
475
+ content_x = left + QUEST_RESULTS_CONTENT_X * scale
476
+ banner_x = content_x + QUEST_RESULTS_BANNER_X_FROM_CONTENT * scale
519
477
  banner_y = top + 36.0 * scale
520
478
  if self.assets.text_well_done is not None:
521
479
  src = rl.Rectangle(0.0, 0.0, float(self.assets.text_well_done.width), float(self.assets.text_well_done.height))
@@ -525,7 +483,7 @@ class QuestResultsUi:
525
483
  qualifies = int(self.rank) < TABLE_MAX
526
484
 
527
485
  if self.phase == 0:
528
- anchor_x = left + 40.0 * scale
486
+ anchor_x = content_x
529
487
  label_x = anchor_x + 32.0 * scale
530
488
  value_x = label_x + 132.0 * scale
531
489
 
@@ -560,10 +518,10 @@ class QuestResultsUi:
560
518
  return rl.Color(rgb[0], rgb[1], rgb[2], int(255 * max(0.0, min(1.0, alpha))))
561
519
 
562
520
  y = top + 156.0 * scale
563
- base_value = _format_time_mm_ss(base_time_ms)
564
- life_value = _format_time_mm_ss(life_bonus_ms)
565
- perk_value = _format_time_mm_ss(perk_bonus_ms)
566
- final_value = _format_time_mm_ss(final_time_ms)
521
+ base_value = format_time_mm_ss(base_time_ms)
522
+ life_value = format_time_mm_ss(life_bonus_ms)
523
+ perk_value = format_time_mm_ss(perk_bonus_ms)
524
+ final_value = format_time_mm_ss(final_time_ms)
567
525
 
568
526
  self._draw_small("Base Time:", label_x, y, 1.0 * scale, _row_color(0))
569
527
  self._draw_small(base_value, value_x, y, 1.0 * scale, _row_color(0))
@@ -587,7 +545,7 @@ class QuestResultsUi:
587
545
  self._draw_small(final_value, value_x, y, 1.0 * scale, _row_color(3, final=True))
588
546
 
589
547
  elif self.phase == 1:
590
- anchor_x = left + 40.0 * scale
548
+ anchor_x = content_x
591
549
  text_y = top + 118.0 * scale
592
550
  self._draw_small("State your name trooper!", anchor_x + 42.0 * scale, text_y, 1.0 * scale, COLOR_TEXT)
593
551
 
@@ -615,7 +573,7 @@ class QuestResultsUi:
615
573
  button_draw(self.assets.perk_menu_assets, self.font, self._ok_button, x=ok_x, y=ok_y, width=ok_w, scale=scale)
616
574
 
617
575
  else:
618
- score_card_x = left + 70.0 * scale
576
+ score_card_x = content_x + QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT * scale
619
577
  var_c_12 = top + (96.0 if qualifies else 108.0) * scale
620
578
  if (not qualifies) and self.font is not None:
621
579
  self._draw_small(
@@ -629,7 +587,7 @@ class QuestResultsUi:
629
587
  seconds = float(int(self.record.survival_elapsed_ms)) * 0.001
630
588
  score_value = f"{seconds:.2f} secs"
631
589
  xp_value = f"{int(self.record.score_xp)}"
632
- rank_text = _format_ordinal(int(self.rank) + 1) if qualifies else "--"
590
+ rank_text = format_ordinal(int(self.rank) + 1) if qualifies else "--"
633
591
 
634
592
  col_label = rl.Color(230, 230, 230, 255)
635
593
  col_value = rl.Color(230, 230, 255, 255)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import pyray as rl
4
+
5
+
6
+ def poll_text_input(max_len: int, *, allow_space: bool = True) -> str:
7
+ out = ""
8
+ while True:
9
+ value = rl.get_char_pressed()
10
+ if value == 0:
11
+ break
12
+ if value < 0x20 or value > 0xFF:
13
+ continue
14
+ if not allow_space and value == 0x20:
15
+ continue
16
+ if len(out) >= max_len:
17
+ continue
18
+ out += chr(int(value))
19
+ return out
20
+
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import pyray as rl
4
+
5
+ from grim.fonts.small import SmallFontData, draw_small_text
6
+
7
+
8
+ def ui_line_height(font: SmallFontData | None, *, scale: float = 1.0) -> int:
9
+ if font is not None:
10
+ return int(font.cell_size * float(scale))
11
+ return int(20 * float(scale))
12
+
13
+
14
+ def draw_ui_text(
15
+ font: SmallFontData | None,
16
+ text: str,
17
+ x: float,
18
+ y: float,
19
+ *,
20
+ color: rl.Color,
21
+ scale: float = 1.0,
22
+ ) -> None:
23
+ if font is not None:
24
+ draw_small_text(font, text, x, y, float(scale), color)
25
+ else:
26
+ rl.draw_text(text, int(x), int(y), int(20 * float(scale)), color)
27
+
@@ -5,13 +5,15 @@ import math
5
5
  import pyray as rl
6
6
 
7
7
  from grim.config import ensure_crimson_cfg
8
- from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
8
+ from grim.fonts.small import SmallFontData, load_small_font
9
+ from grim.math import clamp
9
10
  from grim.view import ViewContext
10
11
 
11
12
  from ..game_world import GameWorld
12
13
  from ..gameplay import PlayerInput
13
14
  from ..paths import default_runtime_dir
14
15
  from ..ui.cursor import draw_cursor_glow
16
+ from ._ui_helpers import draw_ui_text, ui_line_height
15
17
  from .registry import register_view
16
18
 
17
19
  WORLD_SIZE = 1024.0
@@ -22,14 +24,6 @@ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
22
24
  UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
23
25
 
24
26
 
25
- def _clamp(value: float, lo: float, hi: float) -> float:
26
- if value < lo:
27
- return lo
28
- if value > hi:
29
- return hi
30
- return value
31
-
32
-
33
27
  class AimDebugView:
34
28
  def __init__(self, ctx: ViewContext) -> None:
35
29
  self._assets_root = ctx.assets_dir
@@ -61,30 +55,12 @@ class AimDebugView:
61
55
  self._forced_heat = 0.18
62
56
  self._test_circle_radius = 96.0
63
57
 
64
- def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
65
- if self._small is not None:
66
- return int(self._small.cell_size * scale)
67
- return int(20 * scale)
68
-
69
- def _draw_ui_text(
70
- self,
71
- text: str,
72
- x: float,
73
- y: float,
74
- color: rl.Color,
75
- scale: float = UI_TEXT_SCALE,
76
- ) -> None:
77
- if self._small is not None:
78
- draw_small_text(self._small, text, x, y, scale, color)
79
- else:
80
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
81
-
82
58
  def _update_ui_mouse(self) -> None:
83
59
  mouse = rl.get_mouse_position()
84
60
  screen_w = float(rl.get_screen_width())
85
61
  screen_h = float(rl.get_screen_height())
86
- self._ui_mouse_x = _clamp(float(mouse.x), 0.0, max(0.0, screen_w - 1.0))
87
- self._ui_mouse_y = _clamp(float(mouse.y), 0.0, max(0.0, screen_h - 1.0))
62
+ self._ui_mouse_x = clamp(float(mouse.x), 0.0, max(0.0, screen_w - 1.0))
63
+ self._ui_mouse_y = clamp(float(mouse.y), 0.0, max(0.0, screen_h - 1.0))
88
64
 
89
65
  def _draw_cursor_glow(self, *, x: float, y: float) -> None:
90
66
  draw_cursor_glow(self._world.particles_texture, x=x, y=y)
@@ -264,11 +240,18 @@ class AimDebugView:
264
240
  ]
265
241
  x0 = 16.0
266
242
  y0 = 16.0
267
- lh = float(self._ui_line_height())
243
+ lh = float(ui_line_height(self._small, scale=UI_TEXT_SCALE))
268
244
  for idx, line in enumerate(lines):
269
- self._draw_ui_text(line, x0, y0 + lh * float(idx), UI_TEXT_COLOR if idx < 6 else UI_HINT_COLOR)
245
+ draw_ui_text(
246
+ self._small,
247
+ line,
248
+ x0,
249
+ y0 + lh * float(idx),
250
+ scale=UI_TEXT_SCALE,
251
+ color=UI_TEXT_COLOR if idx < 6 else UI_HINT_COLOR,
252
+ )
270
253
  elif self._draw_expected_overlay and self._player is None:
271
- self._draw_ui_text("Aim debug view: missing player", 16.0, 16.0, UI_ERROR_COLOR)
254
+ draw_ui_text(self._small, "Aim debug view: missing player", 16.0, 16.0, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
272
255
 
273
256
 
274
257
  @register_view("aim-debug", "Aim indicator debug")
@@ -6,8 +6,9 @@ import pyray as rl
6
6
 
7
7
  from ..creatures.anim import creature_anim_advance_phase, creature_anim_select_frame
8
8
  from ..creatures.spawn import CreatureFlags, CreatureTypeId, SPAWN_TEMPLATES, SpawnTemplate, resolve_tint
9
+ from ._ui_helpers import draw_ui_text, ui_line_height
9
10
  from .registry import register_view
10
- from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
11
+ from grim.fonts.small import SmallFontData, load_small_font
11
12
  from grim.view import View, ViewContext
12
13
 
13
14
  UI_TEXT_SCALE = 1.0
@@ -52,24 +53,6 @@ class CreatureAnimationView:
52
53
  self._last_step = 0.0
53
54
  self._apply_template_defaults()
54
55
 
55
- def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
56
- if self._small is not None:
57
- return int(self._small.cell_size * scale)
58
- return int(20 * scale)
59
-
60
- def _draw_ui_text(
61
- self,
62
- text: str,
63
- x: float,
64
- y: float,
65
- color: rl.Color,
66
- scale: float = UI_TEXT_SCALE,
67
- ) -> None:
68
- if self._small is not None:
69
- draw_small_text(self._small, text, x, y, scale, color)
70
- else:
71
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
72
-
73
56
  def open(self) -> None:
74
57
  self._missing_assets.clear()
75
58
  self._textures.clear()
@@ -163,24 +146,24 @@ class CreatureAnimationView:
163
146
  rl.clear_background(rl.Color(12, 12, 14, 255))
164
147
  if self._missing_assets:
165
148
  message = "Missing assets: " + ", ".join(self._missing_assets)
166
- self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
149
+ draw_ui_text(self._small, message, 24, 24, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
167
150
  return
168
151
  if not self._templates:
169
- self._draw_ui_text("No spawn templates loaded.", 24, 24, UI_TEXT_COLOR)
152
+ draw_ui_text(self._small, "No spawn templates loaded.", 24, 24, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
170
153
  return
171
154
 
172
155
  self._handle_input()
173
156
  template = self._current_template()
174
157
  if template is None or template.type_id is None or template.creature is None:
175
- self._draw_ui_text("Invalid template.", 24, 24, UI_TEXT_COLOR)
158
+ draw_ui_text(self._small, "Invalid template.", 24, 24, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
176
159
  return
177
160
  texture = self._textures.get(template.creature)
178
161
  if texture is None:
179
- self._draw_ui_text("Missing texture for creature.", 24, 24, UI_TEXT_COLOR)
162
+ draw_ui_text(self._small, "Missing texture for creature.", 24, 24, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
180
163
  return
181
164
  info = TYPE_ANIM.get(template.type_id)
182
165
  if info is None:
183
- self._draw_ui_text("Missing anim info.", 24, 24, UI_TEXT_COLOR)
166
+ draw_ui_text(self._small, "Missing anim info.", 24, 24, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
184
167
  return
185
168
 
186
169
  frame, mirror_applied, mode = creature_anim_select_frame(
@@ -196,12 +179,19 @@ class CreatureAnimationView:
196
179
 
197
180
  margin = 24
198
181
  title = f"{template.creature} (spawn 0x{template.spawn_id:02x})"
199
- self._draw_ui_text(title, margin, margin, UI_TEXT_COLOR)
182
+ draw_ui_text(self._small, title, margin, margin, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
200
183
  hint = (
201
184
  "Left/Right: spawn template Up/Down: move_speed PgUp/PgDn: size "
202
185
  "[/]: local scale Space: pause R: reset"
203
186
  )
204
- self._draw_ui_text(hint, margin, margin + self._ui_line_height() + 6, UI_HINT_COLOR)
187
+ draw_ui_text(
188
+ self._small,
189
+ hint,
190
+ margin,
191
+ margin + ui_line_height(self._small, scale=UI_TEXT_SCALE) + 6,
192
+ scale=UI_TEXT_SCALE,
193
+ color=UI_HINT_COLOR,
194
+ )
205
195
 
206
196
  sheet_scale = min(
207
197
  1.0,
@@ -265,8 +255,8 @@ class CreatureAnimationView:
265
255
  ]
266
256
  y = int(preview_y + preview_size + 16)
267
257
  for line in info_lines:
268
- self._draw_ui_text(line, preview_x, y, UI_TEXT_COLOR)
269
- y += self._ui_line_height() + 4
258
+ draw_ui_text(self._small, line, preview_x, y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
259
+ y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 4
270
260
 
271
261
 
272
262
  @register_view("animations", "Creature animation preview")
@@ -7,7 +7,8 @@ import pyray as rl
7
7
 
8
8
  from grim.audio import AudioState, shutdown_audio, update_audio
9
9
  from grim.console import ConsoleState
10
- from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
10
+ from grim.fonts.small import SmallFontData, load_small_font
11
+ from grim.math import clamp
11
12
  from grim.view import View, ViewContext
12
13
 
13
14
  from ..bonuses import BONUS_TABLE, BonusId
@@ -24,6 +25,7 @@ from ..weapons import (
24
25
  Weapon,
25
26
  projectile_type_id_from_weapon_id,
26
27
  )
28
+ from ._ui_helpers import draw_ui_text, ui_line_height
27
29
  from .audio_bootstrap import init_view_audio
28
30
  from .registry import register_view
29
31
 
@@ -65,14 +67,6 @@ SPECIAL_PROJECTILES: dict[int, str] = {
65
67
  }
66
68
 
67
69
 
68
- def _clamp(value: float, lo: float, hi: float) -> float:
69
- if value < lo:
70
- return lo
71
- if value > hi:
72
- return hi
73
- return value
74
-
75
-
76
70
  def _fmt_float(value: float | None, *, digits: int = 3) -> str:
77
71
  if value is None:
78
72
  return "—"
@@ -135,17 +129,6 @@ class ArsenalDebugView:
135
129
  player.speed_multiplier = float(ARSENAL_PLAYER_MOVE_SPEED_MULTIPLIER)
136
130
  player.shield_timer = float(ARSENAL_PLAYER_INVULNERABLE_SHIELD_TIMER)
137
131
 
138
- def _ui_line_height(self, scale: float = 1.0) -> int:
139
- if self._small is not None:
140
- return int(self._small.cell_size * scale)
141
- return int(20 * scale)
142
-
143
- def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color, scale: float = 1.0) -> None:
144
- if self._small is not None:
145
- draw_small_text(self._small, text, x, y, scale, color)
146
- else:
147
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
148
-
149
132
  def _selected_weapon_id(self) -> int:
150
133
  if not self._weapon_ids:
151
134
  return 0
@@ -181,11 +164,14 @@ class ArsenalDebugView:
181
164
  count = max(1, len(self._spawn_ids))
182
165
  base_x = float(player.pos_x)
183
166
  base_y = float(player.pos_y)
167
+ detail_preset = 5
168
+ if self._world.config is not None:
169
+ detail_preset = int(self._world.config.data.get("detail_preset", 5) or 5)
184
170
  for idx in range(count):
185
171
  spawn_id = int(self._spawn_ids[idx % len(self._spawn_ids)])
186
172
  angle = float(idx) / float(count) * math.tau
187
- x = _clamp(base_x + math.cos(angle) * self._spawn_ring_radius, 48.0, WORLD_SIZE - 48.0)
188
- y = _clamp(base_y + math.sin(angle) * self._spawn_ring_radius, 48.0, WORLD_SIZE - 48.0)
173
+ x = clamp(base_x + math.cos(angle) * self._spawn_ring_radius, 48.0, WORLD_SIZE - 48.0)
174
+ y = clamp(base_y + math.sin(angle) * self._spawn_ring_radius, 48.0, WORLD_SIZE - 48.0)
189
175
  heading = angle + math.pi
190
176
  self._world.creatures.spawn_template(
191
177
  spawn_id,
@@ -193,6 +179,8 @@ class ArsenalDebugView:
193
179
  heading,
194
180
  self._world.state.rng,
195
181
  rand=self._world.state.rng.rand,
182
+ state=self._world.state,
183
+ detail_preset=detail_preset,
196
184
  )
197
185
 
198
186
  def _spawn_all_bonuses(self) -> None:
@@ -402,43 +390,45 @@ class ArsenalDebugView:
402
390
 
403
391
  warn_x = 24.0
404
392
  warn_y = 24.0
405
- warn_line = float(self._ui_line_height())
393
+ warn_line = float(ui_line_height(self._small))
406
394
  if self._missing_assets:
407
- self._draw_ui_text("Missing assets (ui): " + ", ".join(self._missing_assets), warn_x, warn_y, UI_ERROR)
395
+ draw_ui_text(self._small, "Missing assets (ui): " + ", ".join(self._missing_assets), warn_x, warn_y, color=UI_ERROR)
408
396
  warn_y += warn_line
409
397
  if self._world.missing_assets:
410
- self._draw_ui_text(
398
+ draw_ui_text(
399
+ self._small,
411
400
  "Missing assets (world): " + ", ".join(self._world.missing_assets),
412
401
  warn_x,
413
402
  warn_y,
414
- UI_ERROR,
403
+ color=UI_ERROR,
415
404
  )
416
405
  warn_y += warn_line
417
406
 
418
407
  x = 16.0
419
408
  y = 12.0
420
- line = float(self._ui_line_height())
409
+ line = float(ui_line_height(self._small))
421
410
 
422
411
  weapon = WEAPON_BY_ID.get(int(self._player.weapon_id)) if self._player is not None else None
423
412
  for text in self._weapon_debug_lines(weapon):
424
- self._draw_ui_text(text, x, y, UI_TEXT)
413
+ draw_ui_text(self._small, text, x, y, color=UI_TEXT)
425
414
  y += line
426
415
 
427
416
  if self._player is not None:
428
417
  alive = sum(1 for c in self._world.creatures.entries if c.active and c.hp > 0.0)
429
418
  total = sum(1 for c in self._world.creatures.entries if c.active)
430
- self._draw_ui_text(f"creatures alive {alive}/{total}", x, y, UI_TEXT)
419
+ draw_ui_text(self._small, f"creatures alive {alive}/{total}", x, y, color=UI_TEXT)
431
420
  y += line
432
421
 
433
422
  y += 6.0
434
- self._draw_ui_text(
423
+ draw_ui_text(
424
+ self._small,
435
425
  "WASD move LMB fire R reload [/] cycle weapons Space pause T respawn B spawn all bonuses Backspace reset Esc quit",
436
426
  x,
437
427
  y,
438
- UI_HINT,
428
+ color=UI_HINT,
439
429
  )
440
430
  y += line
441
- self._draw_ui_text("P screenshot", x, y, UI_HINT)
431
+ draw_ui_text(self._small, "P screenshot", x, y, color=UI_HINT)
442
432
 
443
433
  mouse = rl.get_mouse_position()
444
434
  draw_aim_cursor(self._world.particles_texture, self._aim_texture, x=float(mouse.x), y=float(mouse.y))