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
@@ -7,8 +7,9 @@ from pathlib import Path
7
7
  import pyray as rl
8
8
 
9
9
  from grim.assets import resolve_asset_path
10
- from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
10
+ from grim.fonts.small import SmallFontData, load_small_font
11
11
  from grim.view import View, ViewContext
12
+ from ._ui_helpers import draw_ui_text, ui_line_height
12
13
  from .registry import register_view
13
14
 
14
15
 
@@ -54,24 +55,6 @@ class PlayerSpriteDebugView:
54
55
  self._show_shadow = True
55
56
  self._use_torso_offset = True
56
57
 
57
- def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
58
- if self._small is not None:
59
- return int(self._small.cell_size * scale)
60
- return int(20 * scale)
61
-
62
- def _draw_ui_text(
63
- self,
64
- text: str,
65
- x: float,
66
- y: float,
67
- color: rl.Color,
68
- scale: float = UI_TEXT_SCALE,
69
- ) -> None:
70
- if self._small is not None:
71
- draw_small_text(self._small, text, x, y, scale, color)
72
- else:
73
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
74
-
75
58
  def _frame_src(self, texture: rl.Texture, frame_index: int) -> rl.Rectangle:
76
59
  cell = float(texture.width) / float(SPRITE_GRID)
77
60
  pad = SPRITE_PAD_PX
@@ -204,7 +187,7 @@ class PlayerSpriteDebugView:
204
187
  def draw(self) -> None:
205
188
  rl.clear_background(rl.Color(10, 10, 12, 255))
206
189
  if self._assets is None:
207
- self._draw_ui_text("Trooper sprite not loaded.", 16, 16, UI_ERROR_COLOR)
190
+ draw_ui_text(self._small, "Trooper sprite not loaded.", 16, 16, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
208
191
  return
209
192
 
210
193
  cam_x = float(rl.get_screen_width()) * 0.5 - self._player_x
@@ -271,41 +254,51 @@ class PlayerSpriteDebugView:
271
254
 
272
255
  hud_x = 16.0
273
256
  hud_y = 16.0
274
- line = self._ui_line_height()
275
- self._draw_ui_text(
257
+ line = ui_line_height(self._small, scale=UI_TEXT_SCALE)
258
+ draw_ui_text(
259
+ self._small,
276
260
  f"legs frame={leg_frame} (base {self._leg_base}, count {self._frame_count})",
277
261
  hud_x,
278
262
  hud_y,
279
- UI_TEXT_COLOR,
263
+ scale=UI_TEXT_SCALE,
264
+ color=UI_TEXT_COLOR,
280
265
  )
281
266
  hud_y += line
282
267
  torso_label = "offset" if self._use_torso_offset else "match"
283
- self._draw_ui_text(
268
+ draw_ui_text(
269
+ self._small,
284
270
  f"torso frame={torso_frame} (base {self._torso_base}, mode {torso_label})",
285
271
  hud_x,
286
272
  hud_y,
287
- UI_TEXT_COLOR,
273
+ scale=UI_TEXT_SCALE,
274
+ color=UI_TEXT_COLOR,
288
275
  )
289
276
  hud_y += line
290
- self._draw_ui_text(
277
+ draw_ui_text(
278
+ self._small,
291
279
  f"move_heading={self._move_heading:.2f} aim_heading={self._aim_heading:.2f}",
292
280
  hud_x,
293
281
  hud_y,
294
- UI_TEXT_COLOR,
282
+ scale=UI_TEXT_SCALE,
283
+ color=UI_TEXT_COLOR,
295
284
  )
296
285
  hud_y += line
297
- self._draw_ui_text(
286
+ draw_ui_text(
287
+ self._small,
298
288
  "WASD move, mouse aim, LMB recoil, F1 grid, F2 shadow, F3 torso mode",
299
289
  hud_x,
300
290
  hud_y,
301
- UI_HINT_COLOR,
291
+ scale=UI_TEXT_SCALE,
292
+ color=UI_HINT_COLOR,
302
293
  )
303
294
  hud_y += line
304
- self._draw_ui_text(
295
+ draw_ui_text(
296
+ self._small,
305
297
  "[/] torso base, ;/' legs base, ,/. frame count, R reset",
306
298
  hud_x,
307
299
  hud_y,
308
- UI_HINT_COLOR,
300
+ scale=UI_TEXT_SCALE,
301
+ color=UI_HINT_COLOR,
309
302
  )
310
303
 
311
304
 
@@ -5,8 +5,8 @@ import math
5
5
 
6
6
  import pyray as rl
7
7
 
8
- from .registry import register_view
9
- 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
10
10
  from grim.view import View, ViewContext
11
11
 
12
12
  from ..bonuses import BonusId
@@ -18,6 +18,8 @@ from ..weapons import (
18
18
  WEAPON_TABLE,
19
19
  weapon_entry_for_projectile_type_id,
20
20
  )
21
+ from ._ui_helpers import draw_ui_text, ui_line_height
22
+ from .registry import register_view
21
23
 
22
24
  WORLD_SIZE = 1024.0
23
25
 
@@ -34,7 +36,7 @@ class DummyCreature:
34
36
  y: float
35
37
  hp: float
36
38
  size: float = 42.0
37
- collision_flag: int = 0
39
+ plague_infected: bool = False
38
40
 
39
41
 
40
42
  @dataclass(slots=True)
@@ -81,14 +83,6 @@ _BEAM_TYPES = frozenset(
81
83
  )
82
84
 
83
85
 
84
- def _clamp(value: float, lo: float, hi: float) -> float:
85
- if value < lo:
86
- return lo
87
- if value > hi:
88
- return hi
89
- return value
90
-
91
-
92
86
  def _lerp(a: float, b: float, t: float) -> float:
93
87
  return a + (b - a) * t
94
88
 
@@ -134,24 +128,6 @@ class ProjectileFxView:
134
128
  self._beams: list[BeamFx] = []
135
129
  self._effects: list[EffectFx] = []
136
130
 
137
- def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
138
- if self._small is not None:
139
- return int(self._small.cell_size * scale)
140
- return int(20 * scale)
141
-
142
- def _draw_ui_text(
143
- self,
144
- text: str,
145
- x: float,
146
- y: float,
147
- color: rl.Color,
148
- scale: float = UI_TEXT_SCALE,
149
- ) -> None:
150
- if self._small is not None:
151
- draw_small_text(self._small, text, x, y, scale, color)
152
- else:
153
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
154
-
155
131
  def _camera_world_to_screen(self, x: float, y: float) -> tuple[float, float]:
156
132
  return self._camera_x + x, self._camera_y + y
157
133
 
@@ -180,7 +156,7 @@ class ProjectileFxView:
180
156
  if desired_y < min_y:
181
157
  desired_y = min_y
182
158
 
183
- t = _clamp(dt * 6.0, 0.0, 1.0)
159
+ t = clamp(dt * 6.0, 0.0, 1.0)
184
160
  self._camera_x = _lerp(self._camera_x, desired_x, t)
185
161
  self._camera_y = _lerp(self._camera_y, desired_y, t)
186
162
 
@@ -319,8 +295,8 @@ class ProjectileFxView:
319
295
  angle = _angle_to_target(self._origin_x, self._origin_y, aim_x, aim_y)
320
296
 
321
297
  if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_RIGHT):
322
- self._origin_x = _clamp(aim_x, 0.0, WORLD_SIZE)
323
- self._origin_y = _clamp(aim_y, 0.0, WORLD_SIZE)
298
+ self._origin_x = clamp(aim_x, 0.0, WORLD_SIZE)
299
+ self._origin_y = clamp(aim_y, 0.0, WORLD_SIZE)
324
300
 
325
301
  if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
326
302
  self._spawn_projectile(type_id=self._selected_type_id(), angle=angle, owner_id=-1)
@@ -467,7 +443,7 @@ class ProjectileFxView:
467
443
  )
468
444
  return
469
445
 
470
- alpha = int(_clamp(life / 0.4, 0.0, 1.0) * 255)
446
+ alpha = int(clamp(life / 0.4, 0.0, 1.0) * 255)
471
447
  tint = rl.Color(color.r, color.g, color.b, alpha)
472
448
  self._draw_atlas_sprite(texture, grid=grid, frame=frame, x=sx, y=sy, scale=0.6, rotation_rad=angle, tint=tint)
473
449
 
@@ -475,7 +451,7 @@ class ProjectileFxView:
475
451
  rl.clear_background(rl.Color(10, 10, 12, 255))
476
452
  if self._missing_assets and self._projs is None:
477
453
  message = "Missing assets: " + ", ".join(self._missing_assets)
478
- self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
454
+ draw_ui_text(self._small, message, 24, 24, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
479
455
  return
480
456
 
481
457
  # World bounds.
@@ -491,7 +467,11 @@ class ProjectileFxView:
491
467
  # Creatures.
492
468
  for creature in self._creatures:
493
469
  cx, cy = self._camera_world_to_screen(creature.x, creature.y)
494
- color = rl.Color(220, 90, 90, 255) if creature.collision_flag == 0 else rl.Color(240, 180, 90, 255)
470
+ color = (
471
+ rl.Color(220, 90, 90, 255)
472
+ if not creature.plague_infected
473
+ else rl.Color(240, 180, 90, 255)
474
+ )
495
475
  rl.draw_circle(int(cx), int(cy), float(creature.size * 0.5), color)
496
476
  rl.draw_circle_lines(int(cx), int(cy), float(creature.size * 0.5), rl.Color(40, 40, 55, 255))
497
477
 
@@ -517,7 +497,7 @@ class ProjectileFxView:
517
497
 
518
498
  # Beam flashes from hit events.
519
499
  for beam in self._beams:
520
- t = _clamp(beam.life / 0.08, 0.0, 1.0)
500
+ t = clamp(beam.life / 0.08, 0.0, 1.0)
521
501
  alpha = int(200 * t)
522
502
  x0s, y0s = self._camera_world_to_screen(beam.x0, beam.y0)
523
503
  x1s, y1s = self._camera_world_to_screen(beam.x1, beam.y1)
@@ -539,10 +519,10 @@ class ProjectileFxView:
539
519
  if src is None:
540
520
  continue
541
521
  life = max(0.0, fx.life)
542
- alpha = int(_clamp(life / 0.35, 0.0, 1.0) * 220)
522
+ alpha = int(clamp(life / 0.35, 0.0, 1.0) * 220)
543
523
  tint = rl.Color(255, 255, 255, alpha)
544
524
  sx, sy = self._camera_world_to_screen(fx.x, fx.y)
545
- dst_scale = fx.scale * (1.0 + (0.7 - _clamp(life, 0.0, 0.7)) * 0.6)
525
+ dst_scale = fx.scale * (1.0 + (0.7 - clamp(life, 0.0, 0.7)) * 0.6)
546
526
  dst = rl.Rectangle(float(sx), float(sy), src[2] * dst_scale, src[3] * dst_scale)
547
527
  origin = rl.Vector2(dst.width * 0.5, dst.height * 0.5)
548
528
  rl.draw_texture_pro(
@@ -562,46 +542,71 @@ class ProjectileFxView:
562
542
  margin = 18
563
543
  x = float(margin)
564
544
  y = float(margin)
565
- line = self._ui_line_height()
545
+ line = ui_line_height(self._small, scale=UI_TEXT_SCALE)
566
546
 
567
547
  type_id = self._selected_type_id()
568
548
  weapon = WEAPON_BY_ID.get(int(type_id))
569
549
  label = weapon.name if weapon is not None and weapon.name else f"type_{type_id}"
570
- self._draw_ui_text(f"{label} (type_id {type_id} / 0x{type_id:02x})", x, y, UI_TEXT_COLOR)
550
+ draw_ui_text(
551
+ self._small, f"{label} (type_id {type_id} / 0x{type_id:02x})", x, y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR
552
+ )
571
553
  y += line + 4
572
554
 
573
555
  if self._show_debug:
574
556
  meta = self._projectile_meta_for(type_id)
575
557
  dmg = self._damage_scale_by_type.get(type_id, 1.0)
576
558
  pellets = int(weapon.pellet_count) if weapon is not None and weapon.pellet_count is not None else 1
577
- self._draw_ui_text(f"meta {meta:.1f} dmg_scale {dmg:.2f} pellet_count {pellets}", x, y, UI_HINT_COLOR)
559
+ draw_ui_text(
560
+ self._small,
561
+ f"meta {meta:.1f} dmg_scale {dmg:.2f} pellet_count {pellets}",
562
+ x,
563
+ y,
564
+ scale=UI_TEXT_SCALE,
565
+ color=UI_HINT_COLOR,
566
+ )
578
567
  y += line + 4
579
- self._draw_ui_text(
568
+ draw_ui_text(
569
+ self._small,
580
570
  f"shock_chain links {self._state.shock_chain_links_left} proj {self._state.shock_chain_projectile_id}",
581
571
  x,
582
572
  y,
583
- UI_HINT_COLOR,
573
+ scale=UI_TEXT_SCALE,
574
+ color=UI_HINT_COLOR,
584
575
  )
585
576
  y += line + 8
586
577
 
587
578
  if self._show_help:
588
- self._draw_ui_text("controls:", x, y, UI_ACCENT_COLOR)
579
+ draw_ui_text(self._small, "controls:", x, y, scale=UI_TEXT_SCALE, color=UI_ACCENT_COLOR)
589
580
  y += line + 2
590
- self._draw_ui_text("- left/right: select projectile type", x, y, UI_HINT_COLOR)
581
+ draw_ui_text(self._small, "- left/right: select projectile type", x, y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
591
582
  y += line + 2
592
- self._draw_ui_text("- mouse wheel: select type", x, y, UI_HINT_COLOR)
583
+ draw_ui_text(self._small, "- mouse wheel: select type", x, y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
593
584
  y += line + 2
594
- self._draw_ui_text("- LMB: spawn projectile toward mouse", x, y, UI_HINT_COLOR)
585
+ draw_ui_text(self._small, "- LMB: spawn projectile toward mouse", x, y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
595
586
  y += line + 2
596
- self._draw_ui_text("- RMB: move spawn origin", x, y, UI_HINT_COLOR)
587
+ draw_ui_text(self._small, "- RMB: move spawn origin", x, y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
597
588
  y += line + 2
598
- self._draw_ui_text("- space: spawn ring", x, y, UI_HINT_COLOR)
589
+ draw_ui_text(self._small, "- space: spawn ring", x, y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
599
590
  y += line + 2
600
- self._draw_ui_text("- F: fire-bullets volley (uses pellet_count)", x, y, UI_HINT_COLOR)
591
+ draw_ui_text(
592
+ self._small,
593
+ "- F: fire-bullets volley (uses pellet_count)",
594
+ x,
595
+ y,
596
+ scale=UI_TEXT_SCALE,
597
+ color=UI_HINT_COLOR,
598
+ )
601
599
  y += line + 2
602
- self._draw_ui_text("- S: apply Shock Chain bonus", x, y, UI_HINT_COLOR)
600
+ draw_ui_text(self._small, "- S: apply Shock Chain bonus", x, y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
603
601
  y += line + 2
604
- self._draw_ui_text("- R: reset Tab: pause H: hide help F3: toggle debug", x, y, UI_HINT_COLOR, scale=0.9)
602
+ draw_ui_text(
603
+ self._small,
604
+ "- R: reset Tab: pause H: hide help F3: toggle debug",
605
+ x,
606
+ y,
607
+ scale=0.9,
608
+ color=UI_HINT_COLOR,
609
+ )
605
610
 
606
611
 
607
612
  @register_view("projectile_fx", "Projectile FX lab")
@@ -7,13 +7,15 @@ import random
7
7
  import pyray as rl
8
8
 
9
9
  from grim.audio import AudioState, shutdown_audio
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 ..game_world import GameWorld
14
15
  from ..gameplay import PlayerInput, player_update, weapon_assign_player
15
16
  from ..ui.cursor import draw_aim_cursor
16
17
  from ..weapons import WEAPON_TABLE
18
+ from ._ui_helpers import draw_ui_text, ui_line_height
17
19
  from .audio_bootstrap import init_view_audio
18
20
  from .registry import register_view
19
21
 
@@ -39,14 +41,6 @@ class TargetDummy:
39
41
  size: float = 56.0
40
42
 
41
43
 
42
- def _clamp(value: float, lo: float, hi: float) -> float:
43
- if value < lo:
44
- return lo
45
- if value > hi:
46
- return hi
47
- return value
48
-
49
-
50
44
  class ProjectileRenderDebugView:
51
45
  def __init__(self, ctx: ViewContext) -> None:
52
46
  self._assets_root = ctx.assets_dir
@@ -75,17 +69,6 @@ class ProjectileRenderDebugView:
75
69
  self._paused = False
76
70
  self._screenshot_requested = False
77
71
 
78
- def _ui_line_height(self, scale: float = 1.0) -> int:
79
- if self._small is not None:
80
- return int(self._small.cell_size * scale)
81
- return int(20 * scale)
82
-
83
- def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color, scale: float = 1.0) -> None:
84
- if self._small is not None:
85
- draw_small_text(self._small, text, x, y, scale, color)
86
- else:
87
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
88
-
89
72
  def _selected_weapon_id(self) -> int:
90
73
  if not self._weapon_ids:
91
74
  return 0
@@ -103,8 +86,8 @@ class ProjectileRenderDebugView:
103
86
  ring = 260.0
104
87
  for idx in range(10):
105
88
  angle = float(idx) / 10.0 * math.tau
106
- x = _clamp(base_x + math.cos(angle) * ring, 40.0, WORLD_SIZE - 40.0)
107
- y = _clamp(base_y + math.sin(angle) * ring, 40.0, WORLD_SIZE - 40.0)
89
+ x = clamp(base_x + math.cos(angle) * ring, 40.0, WORLD_SIZE - 40.0)
90
+ y = clamp(base_y + math.sin(angle) * ring, 40.0, WORLD_SIZE - 40.0)
108
91
  self._targets.append(TargetDummy(x=x, y=y, hp=260.0, size=64.0))
109
92
 
110
93
  def _reset_scene(self) -> None:
@@ -301,16 +284,17 @@ class ProjectileRenderDebugView:
301
284
 
302
285
  warn_x = 24.0
303
286
  warn_y = 24.0
304
- warn_line = float(self._ui_line_height())
287
+ warn_line = float(ui_line_height(self._small))
305
288
  if self._missing_assets:
306
- self._draw_ui_text("Missing assets (ui): " + ", ".join(self._missing_assets), warn_x, warn_y, UI_ERROR)
289
+ draw_ui_text(self._small, "Missing assets (ui): " + ", ".join(self._missing_assets), warn_x, warn_y, color=UI_ERROR)
307
290
  warn_y += warn_line
308
291
  if self._world.missing_assets:
309
- self._draw_ui_text(
292
+ draw_ui_text(
293
+ self._small,
310
294
  "Missing assets (world): " + ", ".join(self._world.missing_assets),
311
295
  warn_x,
312
296
  warn_y,
313
- UI_ERROR,
297
+ color=UI_ERROR,
314
298
  )
315
299
  warn_y += warn_line
316
300
 
@@ -363,26 +347,33 @@ class ProjectileRenderDebugView:
363
347
  # UI.
364
348
  x = 16.0
365
349
  y = 12.0
366
- line = float(self._ui_line_height())
350
+ line = float(ui_line_height(self._small))
367
351
 
368
352
  weapon_id = int(player.weapon_id) if player is not None else 0
369
353
  weapon_name = next((w.name for w in WEAPON_TABLE if w.weapon_id == weapon_id), None) or f"weapon_{weapon_id}"
370
- self._draw_ui_text("Projectile render debug", x, y, UI_TEXT)
354
+ draw_ui_text(self._small, "Projectile render debug", x, y, color=UI_TEXT)
371
355
  y += line
372
- self._draw_ui_text(f"{weapon_name} (weapon_id={weapon_id})", x, y, UI_TEXT)
356
+ draw_ui_text(self._small, f"{weapon_name} (weapon_id={weapon_id})", x, y, color=UI_TEXT)
373
357
  y += line
374
358
  if player is not None:
375
- self._draw_ui_text(
359
+ draw_ui_text(
360
+ self._small,
376
361
  f"ammo {player.ammo}/{player.clip_size} reload {player.reload_timer:.2f}/{player.reload_timer_max:.2f}",
377
362
  x,
378
363
  y,
379
- UI_TEXT,
364
+ color=UI_TEXT,
380
365
  )
381
366
  y += line
382
367
  y += 6.0
383
- self._draw_ui_text("WASD move LMB fire R reload [/] cycle weapons Space pause P screenshot", x, y, UI_HINT)
368
+ draw_ui_text(
369
+ self._small,
370
+ "WASD move LMB fire R reload [/] cycle weapons Space pause P screenshot",
371
+ x,
372
+ y,
373
+ color=UI_HINT,
374
+ )
384
375
  y += line
385
- self._draw_ui_text("T reset targets Backspace reset scene Esc quit", x, y, UI_HINT)
376
+ draw_ui_text(self._small, "T reset targets Backspace reset scene Esc quit", x, y, color=UI_HINT)
386
377
 
387
378
  mouse = rl.get_mouse_position()
388
379
  draw_aim_cursor(self._world.particles_texture, self._aim_texture, x=float(mouse.x), y=float(mouse.y))
@@ -4,8 +4,9 @@ from dataclasses import dataclass
4
4
 
5
5
  import pyray as rl
6
6
 
7
+ from ._ui_helpers import draw_ui_text, ui_line_height
7
8
  from .registry import register_view
8
- from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
9
+ from grim.fonts.small import SmallFontData, load_small_font
9
10
  from grim.view import View, ViewContext
10
11
 
11
12
  UI_TEXT_SCALE = 1.0
@@ -55,24 +56,6 @@ class ProjectileView:
55
56
  self._small: SmallFontData | None = None
56
57
  self._grid = 4
57
58
 
58
- def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
59
- if self._small is not None:
60
- return int(self._small.cell_size * scale)
61
- return int(20 * scale)
62
-
63
- def _draw_ui_text(
64
- self,
65
- text: str,
66
- x: float,
67
- y: float,
68
- color: rl.Color,
69
- scale: float = UI_TEXT_SCALE,
70
- ) -> None:
71
- if self._small is not None:
72
- draw_small_text(self._small, text, x, y, scale, color)
73
- else:
74
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
75
-
76
59
  def open(self) -> None:
77
60
  self._missing_assets.clear()
78
61
  self._small = load_small_font(self._assets_root, self._missing_assets)
@@ -105,10 +88,10 @@ class ProjectileView:
105
88
  rl.clear_background(rl.Color(12, 12, 14, 255))
106
89
  if self._missing_assets:
107
90
  message = "Missing assets: " + ", ".join(self._missing_assets)
108
- self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
91
+ draw_ui_text(self._small, message, 24, 24, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
109
92
  return
110
93
  if self._texture is None:
111
- self._draw_ui_text("No projectile texture loaded.", 24, 24, UI_TEXT_COLOR)
94
+ draw_ui_text(self._small, "No projectile texture loaded.", 24, 24, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
112
95
  return
113
96
 
114
97
  self._handle_input()
@@ -179,41 +162,45 @@ class ProjectileView:
179
162
 
180
163
  info_x = x + draw_w + panel_gap
181
164
  info_y = margin
182
- self._draw_ui_text(
165
+ draw_ui_text(
166
+ self._small,
183
167
  f"projs.png (grid {self._grid}x{self._grid})",
184
168
  info_x,
185
169
  info_y,
186
- UI_TEXT_COLOR,
170
+ scale=UI_TEXT_SCALE,
171
+ color=UI_TEXT_COLOR,
187
172
  )
188
- info_y += self._ui_line_height() + 6
189
- self._draw_ui_text("2/4: grid G: toggle", info_x, info_y, UI_HINT_COLOR)
190
- info_y += self._ui_line_height() + 12
173
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 6
174
+ draw_ui_text(self._small, "2/4: grid G: toggle", info_x, info_y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
175
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 12
191
176
 
192
177
  if hovered_index is not None:
193
- self._draw_ui_text(f"frame {hovered_index:02d}", info_x, info_y, UI_TEXT_COLOR)
194
- info_y += self._ui_line_height() + 6
178
+ draw_ui_text(self._small, f"frame {hovered_index:02d}", info_x, info_y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
179
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 6
195
180
  entries = known_frames.get(hovered_index, [])
196
181
  if entries:
197
182
  for entry in entries:
198
- self._draw_ui_text(
183
+ draw_ui_text(
184
+ self._small,
199
185
  f"0x{entry.type_id:02x} {entry.label}",
200
186
  info_x,
201
187
  info_y,
202
- UI_TEXT_COLOR,
188
+ scale=UI_TEXT_SCALE,
189
+ color=UI_TEXT_COLOR,
203
190
  )
204
- info_y += self._ui_line_height() + 4
191
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 4
205
192
  else:
206
- self._draw_ui_text("no known mapping", info_x, info_y, UI_HINT_COLOR)
207
- info_y += self._ui_line_height() + 4
193
+ draw_ui_text(self._small, "no known mapping", info_x, info_y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
194
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 4
208
195
  info_y += 8
209
196
 
210
- self._draw_ui_text("Known frames", info_x, info_y, UI_TEXT_COLOR)
211
- info_y += self._ui_line_height() + 6
197
+ draw_ui_text(self._small, "Known frames", info_x, info_y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
198
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 6
212
199
  for frame_index in sorted(known_frames.keys()):
213
200
  entries = known_frames[frame_index]
214
201
  labels = ", ".join(f"0x{entry.type_id:02x} {entry.label}" for entry in entries)
215
- self._draw_ui_text(f"{frame_index:02d}: {labels}", info_x, info_y, UI_HINT_COLOR)
216
- info_y += self._ui_line_height() + 4
202
+ draw_ui_text(self._small, f"{frame_index:02d}: {labels}", info_x, info_y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
203
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 4
217
204
 
218
205
 
219
206
  @register_view("projectiles", "Projectile atlas preview")
@@ -15,8 +15,9 @@ from ..creatures.spawn import (
15
15
  spawn_id_label,
16
16
  tick_spawn_slot,
17
17
  )
18
+ from ._ui_helpers import draw_ui_text, ui_line_height
18
19
  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.fonts.small import SmallFontData, load_small_font, measure_small_text_width
20
21
  from grim.view import View, ViewContext
21
22
 
22
23
 
@@ -93,29 +94,11 @@ class SpawnPlanView:
93
94
  rl.unload_texture(self._small.texture)
94
95
  self._small = None
95
96
 
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
97
  def _draw_ui_label(self, label: str, value: str, x: float, y: float) -> None:
115
98
  label_text = f"{label}: "
116
- self._draw_ui_text(label_text, x, y, UI_HINT_COLOR)
99
+ draw_ui_text(self._small, label_text, x, y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
117
100
  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)
101
+ draw_ui_text(self._small, value, x + label_w, y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
119
102
 
120
103
  def _rebuild_plan(self) -> None:
121
104
  spawn_id = self._template_ids[self._index]
@@ -261,18 +244,19 @@ class SpawnPlanView:
261
244
  self._draw_grid()
262
245
 
263
246
  margin = 16.0
264
- line_h = float(self._ui_line_height())
247
+ line_h = float(ui_line_height(self._small, scale=UI_TEXT_SCALE))
265
248
 
266
249
  spawn_id = self._template_ids[self._index] if self._template_ids else 0
267
- self._draw_ui_text(
250
+ draw_ui_text(
251
+ self._small,
268
252
  f"spawn-plan view (template 0x{spawn_id:02x})",
269
253
  margin,
270
254
  margin,
271
- UI_TEXT_COLOR,
272
255
  scale=0.8,
256
+ color=UI_TEXT_COLOR,
273
257
  )
274
258
  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)
259
+ draw_ui_text(self._small, hints, margin, margin + line_h, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
276
260
 
277
261
  y = margin + line_h * 2.0 + 4.0
278
262
  self._draw_ui_label("seed", f"0x{self._seed:08x}", margin, y)
@@ -287,10 +271,10 @@ class SpawnPlanView:
287
271
  y += line_h
288
272
 
289
273
  if self._error is not None:
290
- self._draw_ui_text(self._error, margin, y + 6.0, UI_ERROR_COLOR)
274
+ draw_ui_text(self._small, self._error, margin, y + 6.0, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
291
275
  return
292
276
  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)
277
+ draw_ui_text(self._small, "No plan.", margin, y + 6.0, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
294
278
  return
295
279
 
296
280
  summary = self._plan_summary
@@ -313,10 +297,10 @@ class SpawnPlanView:
313
297
  )
314
298
  y += line_h
315
299
  if self._sim_events:
316
- self._draw_ui_text("events:", margin, y + 2.0, UI_HINT_COLOR)
300
+ draw_ui_text(self._small, "events:", margin, y + 2.0, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
317
301
  y += line_h
318
302
  for ev in self._sim_events[-5:]:
319
- self._draw_ui_text(ev, margin, y, UI_TEXT_COLOR)
303
+ draw_ui_text(self._small, ev, margin, y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
320
304
  y += line_h
321
305
 
322
306
  # Link lines.