crimsonland 0.1.0.dev15__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 (75) hide show
  1. crimson/cli.py +61 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/demo.py +38 -45
  6. crimson/effects.py +7 -13
  7. crimson/frontend/high_scores_layout.py +81 -0
  8. crimson/frontend/panels/base.py +4 -1
  9. crimson/frontend/panels/controls.py +0 -15
  10. crimson/frontend/panels/databases.py +291 -3
  11. crimson/frontend/panels/mods.py +0 -15
  12. crimson/frontend/panels/play_game.py +0 -16
  13. crimson/game.py +441 -1
  14. crimson/gameplay.py +905 -569
  15. crimson/modes/base_gameplay_mode.py +33 -12
  16. crimson/modes/components/__init__.py +2 -0
  17. crimson/modes/components/highscore_record_builder.py +58 -0
  18. crimson/modes/components/perk_menu_controller.py +325 -0
  19. crimson/modes/quest_mode.py +58 -273
  20. crimson/modes/rush_mode.py +12 -43
  21. crimson/modes/survival_mode.py +71 -328
  22. crimson/modes/tutorial_mode.py +46 -247
  23. crimson/modes/typo_mode.py +11 -38
  24. crimson/oracle.py +396 -0
  25. crimson/perks.py +5 -2
  26. crimson/player_damage.py +94 -37
  27. crimson/projectiles.py +539 -320
  28. crimson/render/projectile_draw_registry.py +637 -0
  29. crimson/render/projectile_render_registry.py +110 -0
  30. crimson/render/secondary_projectile_draw_registry.py +206 -0
  31. crimson/render/world_renderer.py +58 -707
  32. crimson/sim/world_state.py +118 -61
  33. crimson/typo/spawns.py +5 -12
  34. crimson/ui/demo_trial_overlay.py +3 -11
  35. crimson/ui/formatting.py +24 -0
  36. crimson/ui/game_over.py +12 -58
  37. crimson/ui/hud.py +72 -39
  38. crimson/ui/layout.py +20 -0
  39. crimson/ui/perk_menu.py +9 -34
  40. crimson/ui/quest_results.py +12 -64
  41. crimson/ui/text_input.py +20 -0
  42. crimson/views/_ui_helpers.py +27 -0
  43. crimson/views/aim_debug.py +15 -32
  44. crimson/views/animations.py +18 -28
  45. crimson/views/arsenal_debug.py +22 -32
  46. crimson/views/bonuses.py +23 -36
  47. crimson/views/camera_debug.py +16 -29
  48. crimson/views/camera_shake.py +9 -33
  49. crimson/views/corpse_stamp_debug.py +13 -21
  50. crimson/views/decals_debug.py +36 -23
  51. crimson/views/fonts.py +8 -25
  52. crimson/views/ground.py +4 -21
  53. crimson/views/lighting_debug.py +42 -45
  54. crimson/views/particles.py +33 -42
  55. crimson/views/perk_menu_debug.py +3 -10
  56. crimson/views/player.py +50 -44
  57. crimson/views/player_sprite_debug.py +24 -31
  58. crimson/views/projectile_fx.py +57 -52
  59. crimson/views/projectile_render_debug.py +24 -33
  60. crimson/views/projectiles.py +24 -37
  61. crimson/views/spawn_plan.py +13 -29
  62. crimson/views/sprites.py +14 -29
  63. crimson/views/terrain.py +6 -23
  64. crimson/views/ui.py +7 -24
  65. crimson/views/wicons.py +28 -33
  66. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  67. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +72 -64
  68. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +2 -2
  69. grim/config.py +29 -1
  70. grim/console.py +7 -10
  71. grim/math.py +12 -0
  72. crimson/.DS_Store +0 -0
  73. crimson/creatures/.DS_Store +0 -0
  74. grim/.DS_Store +0 -0
  75. {crimsonland-0.1.0.dev15.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
@@ -96,44 +80,6 @@ COLOR_TEXT_SUBTLE = rl.Color(255, 255, 255, int(255 * 0.7))
96
80
  COLOR_GREEN = rl.Color(25, 200, 25, 255)
97
81
 
98
82
 
99
- def _poll_text_input(max_len: int, *, allow_space: bool = True) -> str:
100
- out = ""
101
- while True:
102
- value = rl.get_char_pressed()
103
- if value == 0:
104
- break
105
- if value < 0x20 or value > 0xFF:
106
- continue
107
- if not allow_space and value == 0x20:
108
- continue
109
- if len(out) >= max_len:
110
- continue
111
- out += chr(int(value))
112
- return out
113
-
114
-
115
- def _format_ordinal(value_1_based: int) -> str:
116
- value = int(value_1_based)
117
- if value % 100 in (11, 12, 13):
118
- suffix = "th"
119
- elif value % 10 == 1:
120
- suffix = "st"
121
- elif value % 10 == 2:
122
- suffix = "nd"
123
- elif value % 10 == 3:
124
- suffix = "rd"
125
- else:
126
- suffix = "th"
127
- return f"{value}{suffix}"
128
-
129
-
130
- def _format_time_mm_ss(ms: int) -> str:
131
- total_s = max(0, int(ms)) // 1000
132
- minutes = total_s // 60
133
- seconds = total_s % 60
134
- return f"{minutes}:{seconds:02d}"
135
-
136
-
137
83
  @dataclass(slots=True)
138
84
  class QuestResultsAssets:
139
85
  menu_panel: rl.Texture | None
@@ -308,7 +254,7 @@ class QuestResultsUi:
308
254
 
309
255
  left = (QUEST_RESULTS_PANEL_GEOM_X0 + QUEST_RESULTS_PANEL_POS_X + panel_slide_x) * scale
310
256
  layout_w = screen_w / scale if scale else screen_w
311
- widescreen_shift_y = _menu_widescreen_y_shift(layout_w)
257
+ widescreen_shift_y = menu_widescreen_y_shift(layout_w)
312
258
  top = (QUEST_RESULTS_PANEL_GEOM_Y0 + QUEST_RESULTS_PANEL_POS_Y + widescreen_shift_y) * scale
313
259
  panel = rl.Rectangle(float(left), float(top), QUEST_RESULTS_PANEL_W * scale, QUEST_RESULTS_PANEL_H * scale)
314
260
  return panel, left, top
@@ -382,7 +328,7 @@ class QuestResultsUi:
382
328
 
383
329
  if self.phase == 1:
384
330
  click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
385
- 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)
386
332
  if typed:
387
333
  self.input_text = (self.input_text[: self.input_caret] + typed + self.input_text[self.input_caret :])[:NAME_MAX_EDIT]
388
334
  self.input_caret = min(len(self.input_text), self.input_caret + len(typed))
@@ -429,6 +375,8 @@ class QuestResultsUi:
429
375
  except Exception:
430
376
  self.highlight_rank = None
431
377
  self._saved = True
378
+ self.config.set_player_name(self.input_text)
379
+ self.config.save()
432
380
  self.phase = 2
433
381
  return None
434
382
  if play_sfx is not None:
@@ -570,10 +518,10 @@ class QuestResultsUi:
570
518
  return rl.Color(rgb[0], rgb[1], rgb[2], int(255 * max(0.0, min(1.0, alpha))))
571
519
 
572
520
  y = top + 156.0 * scale
573
- base_value = _format_time_mm_ss(base_time_ms)
574
- life_value = _format_time_mm_ss(life_bonus_ms)
575
- perk_value = _format_time_mm_ss(perk_bonus_ms)
576
- 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)
577
525
 
578
526
  self._draw_small("Base Time:", label_x, y, 1.0 * scale, _row_color(0))
579
527
  self._draw_small(base_value, value_x, y, 1.0 * scale, _row_color(0))
@@ -639,7 +587,7 @@ class QuestResultsUi:
639
587
  seconds = float(int(self.record.survival_elapsed_ms)) * 0.001
640
588
  score_value = f"{seconds:.2f} secs"
641
589
  xp_value = f"{int(self.record.score_xp)}"
642
- rank_text = _format_ordinal(int(self.rank) + 1) if qualifies else "--"
590
+ rank_text = format_ordinal(int(self.rank) + 1) if qualifies else "--"
643
591
 
644
592
  col_label = rl.Color(230, 230, 230, 255)
645
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))