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
@@ -10,13 +10,15 @@ from dataclasses import dataclass
10
10
  import pyray as rl
11
11
 
12
12
  from grim.config import ensure_crimson_cfg
13
- from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
13
+ from grim.fonts.small import SmallFontData, load_small_font
14
+ from grim.math import clamp
14
15
  from grim.view import ViewContext
15
16
 
16
17
  from ..creatures.spawn import CreatureInit, CreatureTypeId
17
18
  from ..game_world import GameWorld
18
19
  from ..gameplay import PlayerInput
19
20
  from ..paths import default_runtime_dir
21
+ from ._ui_helpers import draw_ui_text, ui_line_height
20
22
  from .registry import register_view
21
23
 
22
24
  WORLD_SIZE = 1024.0
@@ -200,14 +202,6 @@ void main()
200
202
  """
201
203
 
202
204
 
203
- def _clamp(value: float, lo: float, hi: float) -> float:
204
- if value < lo:
205
- return lo
206
- if value > hi:
207
- return hi
208
- return value
209
-
210
-
211
205
  @dataclass
212
206
  class _EmissiveProjectile:
213
207
  x: float
@@ -308,9 +302,9 @@ class LightingDebugView:
308
302
  base = self._ambient_base
309
303
  m = max(0.0, float(self._ambient_mul))
310
304
  self._ambient = rl.Color(
311
- int(_clamp(float(base.r) * m, 0.0, 255.0)),
312
- int(_clamp(float(base.g) * m, 0.0, 255.0)),
313
- int(_clamp(float(base.b) * m, 0.0, 255.0)),
305
+ int(clamp(float(base.r) * m, 0.0, 255.0)),
306
+ int(clamp(float(base.g) * m, 0.0, 255.0)),
307
+ int(clamp(float(base.b) * m, 0.0, 255.0)),
314
308
  255,
315
309
  )
316
310
 
@@ -336,8 +330,8 @@ class LightingDebugView:
336
330
  c = palette[i % len(palette)]
337
331
  if rng.random() < 0.5:
338
332
  c = palette[int(rng.random() * len(palette)) % len(palette)]
339
- x = _clamp(px + math.cos(angle) * radius, 0.0, WORLD_SIZE)
340
- y = _clamp(py + math.sin(angle) * radius, 0.0, WORLD_SIZE)
333
+ x = clamp(px + math.cos(angle) * radius, 0.0, WORLD_SIZE)
334
+ y = clamp(py + math.sin(angle) * radius, 0.0, WORLD_SIZE)
341
335
  r = float(self._fly_light_range) * (0.8 + rng.random() * 0.5)
342
336
  sr = float(self._fly_light_source_radius) * (0.7 + rng.random() * 0.7)
343
337
  self._fly_lights.append(
@@ -353,25 +347,14 @@ class LightingDebugView:
353
347
  )
354
348
  )
355
349
 
356
- def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
357
- if self._small is not None:
358
- return int(self._small.cell_size * scale)
359
- return int(20 * scale)
360
-
361
- def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color, scale: float = UI_TEXT_SCALE) -> None:
362
- if self._small is not None:
363
- draw_small_text(self._small, text, x, y, scale, color)
364
- else:
365
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
366
-
367
350
  def _update_ui_mouse(self) -> None:
368
351
  if self._debug_auto_dump:
369
352
  return
370
353
  mouse = rl.get_mouse_position()
371
354
  screen_w = float(rl.get_screen_width())
372
355
  screen_h = float(rl.get_screen_height())
373
- self._ui_mouse_x = _clamp(float(mouse.x), 0.0, max(0.0, screen_w - 1.0))
374
- self._ui_mouse_y = _clamp(float(mouse.y), 0.0, max(0.0, screen_h - 1.0))
356
+ self._ui_mouse_x = clamp(float(mouse.x), 0.0, max(0.0, screen_w - 1.0))
357
+ self._ui_mouse_y = clamp(float(mouse.y), 0.0, max(0.0, screen_h - 1.0))
375
358
 
376
359
  def _handle_debug_input(self) -> None:
377
360
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
@@ -401,18 +384,18 @@ class LightingDebugView:
401
384
  occ_mul_step = 0.05 if not shift else 0.10
402
385
  occ_pad_step = 1.0 if not shift else 4.0
403
386
  if rl.is_key_pressed(rl.KeyboardKey.KEY_O):
404
- self._occluder_radius_mul = _clamp(self._occluder_radius_mul - occ_mul_step, 0.25, 2.50)
387
+ self._occluder_radius_mul = clamp(self._occluder_radius_mul - occ_mul_step, 0.25, 2.50)
405
388
  if rl.is_key_pressed(rl.KeyboardKey.KEY_P):
406
- self._occluder_radius_mul = _clamp(self._occluder_radius_mul + occ_mul_step, 0.25, 2.50)
389
+ self._occluder_radius_mul = clamp(self._occluder_radius_mul + occ_mul_step, 0.25, 2.50)
407
390
  if rl.is_key_pressed(rl.KeyboardKey.KEY_K):
408
- self._occluder_radius_pad_px = _clamp(self._occluder_radius_pad_px - occ_pad_step, -20.0, 60.0)
391
+ self._occluder_radius_pad_px = clamp(self._occluder_radius_pad_px - occ_pad_step, -20.0, 60.0)
409
392
  if rl.is_key_pressed(rl.KeyboardKey.KEY_L):
410
- self._occluder_radius_pad_px = _clamp(self._occluder_radius_pad_px + occ_pad_step, -20.0, 60.0)
393
+ self._occluder_radius_pad_px = clamp(self._occluder_radius_pad_px + occ_pad_step, -20.0, 60.0)
411
394
 
412
395
  if rl.is_key_pressed(rl.KeyboardKey.KEY_MINUS) or rl.is_key_pressed(rl.KeyboardKey.KEY_KP_SUBTRACT):
413
- self._sdf_shadow_floor = _clamp(self._sdf_shadow_floor - 0.05, 0.0, 0.9)
396
+ self._sdf_shadow_floor = clamp(self._sdf_shadow_floor - 0.05, 0.0, 0.9)
414
397
  if rl.is_key_pressed(rl.KeyboardKey.KEY_EQUAL) or rl.is_key_pressed(rl.KeyboardKey.KEY_KP_ADD):
415
- self._sdf_shadow_floor = _clamp(self._sdf_shadow_floor + 0.05, 0.0, 0.9)
398
+ self._sdf_shadow_floor = clamp(self._sdf_shadow_floor + 0.05, 0.0, 0.9)
416
399
 
417
400
  if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
418
401
  if shift:
@@ -435,10 +418,10 @@ class LightingDebugView:
435
418
 
436
419
  amb_step = 0.10 if not shift else 0.25
437
420
  if rl.is_key_pressed(rl.KeyboardKey.KEY_N):
438
- self._ambient_mul = _clamp(self._ambient_mul - amb_step, 0.0, 8.0)
421
+ self._ambient_mul = clamp(self._ambient_mul - amb_step, 0.0, 8.0)
439
422
  self._update_ambient()
440
423
  if rl.is_key_pressed(rl.KeyboardKey.KEY_M):
441
- self._ambient_mul = _clamp(self._ambient_mul + amb_step, 0.0, 8.0)
424
+ self._ambient_mul = clamp(self._ambient_mul + amb_step, 0.0, 8.0)
442
425
  self._update_ambient()
443
426
 
444
427
  def _ensure_sdf_shader(self) -> rl.Shader | None:
@@ -534,8 +517,8 @@ class LightingDebugView:
534
517
  radius = 120.0 + rng.random() * 260.0
535
518
  x = center_x + math.cos(angle) * radius
536
519
  y = center_y + math.sin(angle) * radius
537
- x = _clamp(x, 40.0, WORLD_SIZE - 40.0)
538
- y = _clamp(y, 40.0, WORLD_SIZE - 40.0)
520
+ x = clamp(x, 40.0, WORLD_SIZE - 40.0)
521
+ y = clamp(y, 40.0, WORLD_SIZE - 40.0)
539
522
  init = CreatureInit(
540
523
  origin_template_id=0,
541
524
  pos_x=float(x),
@@ -656,8 +639,8 @@ class LightingDebugView:
656
639
  fl.angle += fl.omega * dt_world
657
640
  wobble = 1.0 + 0.10 * math.sin(fl.angle * 0.7)
658
641
  r = fl.radius * wobble
659
- fl.x = _clamp(px + math.cos(fl.angle) * r, 0.0, WORLD_SIZE)
660
- fl.y = _clamp(py + math.sin(fl.angle) * r, 0.0, WORLD_SIZE)
642
+ fl.x = clamp(px + math.cos(fl.angle) * r, 0.0, WORLD_SIZE)
643
+ fl.y = clamp(py + math.sin(fl.angle) * r, 0.0, WORLD_SIZE)
661
644
 
662
645
  keep: list[_EmissiveProjectile] = []
663
646
  margin = 80.0
@@ -941,7 +924,7 @@ class LightingDebugView:
941
924
 
942
925
  def proj_light(proj: _EmissiveProjectile) -> tuple[float, float, float, float, float, float, float]:
943
926
  sx, sy = self._world.world_to_screen(float(proj.x), float(proj.y))
944
- fade = _clamp(1.0 - float(proj.age) / max(0.001, float(proj.ttl)), 0.0, 1.0)
927
+ fade = clamp(1.0 - float(proj.age) / max(0.001, float(proj.ttl)), 0.0, 1.0)
945
928
  pr = self._proj_light_tint
946
929
  return (
947
930
  float(sx),
@@ -1026,13 +1009,20 @@ class LightingDebugView:
1026
1009
  def draw(self) -> None:
1027
1010
  if self._player is None:
1028
1011
  rl.clear_background(rl.Color(10, 10, 12, 255))
1029
- self._draw_ui_text("Lighting debug view: missing player", 16.0, 16.0, UI_ERROR_COLOR)
1012
+ draw_ui_text(self._small, "Lighting debug view: missing player", 16.0, 16.0, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
1030
1013
  return
1031
1014
 
1032
1015
  self._ensure_render_targets()
1033
1016
  if self._light_rt is None:
1034
1017
  rl.clear_background(rl.Color(10, 10, 12, 255))
1035
- self._draw_ui_text("Lighting debug view: missing render targets", 16.0, 16.0, UI_ERROR_COLOR)
1018
+ draw_ui_text(
1019
+ self._small,
1020
+ "Lighting debug view: missing render targets",
1021
+ 16.0,
1022
+ 16.0,
1023
+ scale=UI_TEXT_SCALE,
1024
+ color=UI_ERROR_COLOR,
1025
+ )
1036
1026
  return
1037
1027
 
1038
1028
  light_x = float(self._ui_mouse_x)
@@ -1057,7 +1047,7 @@ class LightingDebugView:
1057
1047
  rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1058
1048
  for proj in self._projectiles:
1059
1049
  sx, sy = self._world.world_to_screen(float(proj.x), float(proj.y))
1060
- fade = _clamp(1.0 - float(proj.age) / max(0.001, float(proj.ttl)), 0.0, 1.0)
1050
+ fade = clamp(1.0 - float(proj.age) / max(0.001, float(proj.ttl)), 0.0, 1.0)
1061
1051
  c = self._proj_light_tint
1062
1052
  rl.draw_circle(
1063
1053
  int(sx),
@@ -1156,9 +1146,16 @@ class LightingDebugView:
1156
1146
  lines.append("SDF uniforms missing: " + ", ".join(self._sdf_shader_missing))
1157
1147
  x0 = 16.0
1158
1148
  y0 = 16.0
1159
- lh = float(self._ui_line_height())
1149
+ lh = float(ui_line_height(self._small, scale=UI_TEXT_SCALE))
1160
1150
  for idx, line in enumerate(lines):
1161
- self._draw_ui_text(line, x0, y0 + lh * float(idx), UI_TEXT_COLOR if idx < 5 else UI_HINT_COLOR)
1151
+ draw_ui_text(
1152
+ self._small,
1153
+ line,
1154
+ x0,
1155
+ y0 + lh * float(idx),
1156
+ scale=UI_TEXT_SCALE,
1157
+ color=UI_TEXT_COLOR if idx < 5 else UI_HINT_COLOR,
1158
+ )
1162
1159
 
1163
1160
 
1164
1161
  @register_view("lighting-debug", "Lighting (SDF)")
@@ -4,9 +4,10 @@ from dataclasses import dataclass
4
4
 
5
5
  import pyray as rl
6
6
 
7
- from .registry import register_view
8
7
  from ..effects_atlas import EFFECT_ID_ATLAS_TABLE, SIZE_CODE_GRID
9
- from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
8
+ from ._ui_helpers import draw_ui_text, ui_line_height
9
+ from .registry import register_view
10
+ from grim.fonts.small import SmallFontData, load_small_font
10
11
  from grim.view import View, ViewContext
11
12
 
12
13
  UI_TEXT_SCALE = 1.0
@@ -77,24 +78,6 @@ class ParticleView:
77
78
  self._grid = 8
78
79
  self._show_uv_clamp = False
79
80
 
80
- def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
81
- if self._small is not None:
82
- return int(self._small.cell_size * scale)
83
- return int(20 * scale)
84
-
85
- def _draw_ui_text(
86
- self,
87
- text: str,
88
- x: float,
89
- y: float,
90
- color: rl.Color,
91
- scale: float = UI_TEXT_SCALE,
92
- ) -> None:
93
- if self._small is not None:
94
- draw_small_text(self._small, text, x, y, scale, color)
95
- else:
96
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
97
-
98
81
  def open(self) -> None:
99
82
  self._missing_assets.clear()
100
83
  self._small = load_small_font(self._assets_root, self._missing_assets)
@@ -139,10 +122,10 @@ class ParticleView:
139
122
  rl.clear_background(rl.Color(12, 12, 14, 255))
140
123
  if self._missing_assets:
141
124
  message = "Missing assets: " + ", ".join(self._missing_assets)
142
- self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
125
+ draw_ui_text(self._small, message, 24, 24, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
143
126
  return
144
127
  if self._texture is None:
145
- self._draw_ui_text("No particles texture loaded.", 24, 24, UI_TEXT_COLOR)
128
+ draw_ui_text(self._small, "No particles texture loaded.", 24, 24, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
146
129
  return
147
130
 
148
131
  self._handle_input()
@@ -233,59 +216,67 @@ class ParticleView:
233
216
 
234
217
  info_x = x + draw_w + panel_gap
235
218
  info_y = margin
236
- self._draw_ui_text(
219
+ draw_ui_text(
220
+ self._small,
237
221
  f"particles.png (grid {grid}x{grid})",
238
222
  info_x,
239
223
  info_y,
240
- UI_TEXT_COLOR,
224
+ scale=UI_TEXT_SCALE,
225
+ color=UI_TEXT_COLOR,
241
226
  )
242
- info_y += self._ui_line_height() + 6
243
- self._draw_ui_text(
227
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 6
228
+ draw_ui_text(
229
+ self._small,
244
230
  "Up/Down: grid 2/4/8: direct 1: grid16 U: UV clamp",
245
231
  info_x,
246
232
  info_y,
247
- UI_HINT_COLOR,
233
+ scale=UI_TEXT_SCALE,
234
+ color=UI_HINT_COLOR,
248
235
  )
249
- info_y += self._ui_line_height() + 12
236
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 12
250
237
  if self._show_uv_clamp:
251
238
  step_px = int(round(self._texture.width * step))
252
- self._draw_ui_text(
239
+ draw_ui_text(
240
+ self._small,
253
241
  f"UV clamp: {step_px}px of {int(self._texture.width / grid)}px",
254
242
  info_x,
255
243
  info_y,
256
- UI_HINT_COLOR,
244
+ scale=UI_TEXT_SCALE,
245
+ color=UI_HINT_COLOR,
257
246
  )
258
- info_y += self._ui_line_height() + 12
247
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 12
259
248
 
260
249
  if hovered_index is not None:
261
- self._draw_ui_text(f"frame {hovered_index:02d}", info_x, info_y, UI_TEXT_COLOR)
262
- info_y += self._ui_line_height() + 6
250
+ draw_ui_text(self._small, f"frame {hovered_index:02d}", info_x, info_y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
251
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 6
263
252
  entries = known_frames.get(hovered_index, [])
264
253
  if entries:
265
254
  for entry in entries:
266
255
  label = f" {entry.label}" if entry.label else ""
267
- self._draw_ui_text(
256
+ draw_ui_text(
257
+ self._small,
268
258
  f"0x{entry.effect_id:02x}{label}",
269
259
  info_x,
270
260
  info_y,
271
- UI_TEXT_COLOR,
261
+ scale=UI_TEXT_SCALE,
262
+ color=UI_TEXT_COLOR,
272
263
  )
273
- info_y += self._ui_line_height() + 4
264
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 4
274
265
  else:
275
- self._draw_ui_text("no known mapping", info_x, info_y, UI_HINT_COLOR)
276
- info_y += self._ui_line_height() + 4
266
+ draw_ui_text(self._small, "no known mapping", info_x, info_y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
267
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 4
277
268
  info_y += 8
278
269
 
279
- self._draw_ui_text("Effect table", info_x, info_y, UI_TEXT_COLOR)
280
- info_y += self._ui_line_height() + 6
270
+ draw_ui_text(self._small, "Effect table", info_x, info_y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
271
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 6
281
272
  for entry in EFFECT_ENTRIES:
282
273
  grid_label = entry.grid
283
274
  line = f"0x{entry.effect_id:02x} grid{grid_label} frame 0x{entry.frame:02x}"
284
275
  if entry.label:
285
276
  line += f" {entry.label}"
286
277
  color = UI_TEXT_COLOR if entry.grid == grid else UI_HINT_COLOR
287
- self._draw_ui_text(line, info_x, info_y, color)
288
- info_y += self._ui_line_height() + 3
278
+ draw_ui_text(self._small, line, info_x, info_y, scale=UI_TEXT_SCALE, color=color)
279
+ info_y += ui_line_height(self._small, scale=UI_TEXT_SCALE) + 3
289
280
 
290
281
 
291
282
  @register_view("particles", "Particle atlas preview")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import pyray as rl
4
4
 
5
5
  from grim.fonts.small import SmallFontData, load_small_font, measure_small_text_width
6
+ from grim.math import clamp
6
7
  from grim.view import View, ViewContext
7
8
 
8
9
  from ..perks import PERK_BY_ID, PerkId, perk_display_description, perk_display_name
@@ -237,7 +238,7 @@ class PerkMenuDebugView:
237
238
  if rl.is_key_down(rl.KeyboardKey.KEY_RIGHT):
238
239
  self._panel_slide_x += step
239
240
 
240
- self._panel_slide_x = _clamp(self._panel_slide_x, -self._layout.panel_w, 0.0)
241
+ self._panel_slide_x = clamp(self._panel_slide_x, -self._layout.panel_w, 0.0)
241
242
 
242
243
  self._prompt_hover = False
243
244
  self._prompt_rect = None
@@ -250,7 +251,7 @@ class PerkMenuDebugView:
250
251
  self._prompt_hover = rl.check_collision_point_rec(mouse, rect)
251
252
 
252
253
  pulse_delta = dt_ms * (6.0 if self._prompt_hover else -2.0)
253
- self._prompt_pulse = _clamp(self._prompt_pulse + pulse_delta, 0.0, 1000.0)
254
+ self._prompt_pulse = clamp(self._prompt_pulse + pulse_delta, 0.0, 1000.0)
254
255
 
255
256
  if not self._show_menu or self._assets is None:
256
257
  return
@@ -411,14 +412,6 @@ class PerkMenuDebugView:
411
412
  y += line_h
412
413
 
413
414
 
414
- def _clamp(value: float, lo: float, hi: float) -> float:
415
- if value < lo:
416
- return lo
417
- if value > hi:
418
- return hi
419
- return value
420
-
421
-
422
415
  def _ui_text_width(font: SmallFontData | None, text: str, scale: float) -> float:
423
416
  if font is None:
424
417
  return float(rl.measure_text(text, int(20 * scale)))
crimson/views/player.py CHANGED
@@ -5,7 +5,8 @@ from dataclasses import dataclass
5
5
  import pyray as rl
6
6
 
7
7
  from .registry import register_view
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 View, ViewContext
10
11
 
11
12
  from ..bonuses import BonusId
@@ -19,8 +20,9 @@ from ..gameplay import (
19
20
  weapon_assign_player,
20
21
  )
21
22
  from ..perks import PerkId
22
- from ..ui.hud import HudAssets, draw_hud_overlay, hud_ui_scale, load_hud_assets
23
+ from ..ui.hud import HudAssets, HudState, draw_hud_overlay, hud_ui_scale, load_hud_assets
23
24
  from ..weapons import WEAPON_TABLE
25
+ from ._ui_helpers import draw_ui_text, ui_line_height
24
26
 
25
27
  WORLD_SIZE = 1024.0
26
28
 
@@ -38,14 +40,6 @@ class DummyCreature:
38
40
  size: float = 32.0
39
41
 
40
42
 
41
- def _clamp(value: float, lo: float, hi: float) -> float:
42
- if value < lo:
43
- return lo
44
- if value > hi:
45
- return hi
46
- return value
47
-
48
-
49
43
  def _lerp(a: float, b: float, t: float) -> float:
50
44
  return a + (b - a) * t
51
45
 
@@ -66,6 +60,7 @@ class PlayerSandboxView:
66
60
 
67
61
  self._hud_assets: HudAssets | None = None
68
62
  self._hud_missing: list[str] = []
63
+ self._hud_state = HudState()
69
64
  self._elapsed_ms = 0.0
70
65
  self._last_dt_ms = 0.0
71
66
 
@@ -81,24 +76,6 @@ class PlayerSandboxView:
81
76
  continue
82
77
  self._damage_scale_by_type[int(entry.weapon_id)] = float(entry.damage_scale or 1.0)
83
78
 
84
- def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
85
- if self._small is not None:
86
- return int(self._small.cell_size * scale)
87
- return int(20 * scale)
88
-
89
- def _draw_ui_text(
90
- self,
91
- text: str,
92
- x: float,
93
- y: float,
94
- color: rl.Color,
95
- scale: float = UI_TEXT_SCALE,
96
- ) -> None:
97
- if self._small is not None:
98
- draw_small_text(self._small, text, x, y, scale, color)
99
- else:
100
- rl.draw_text(text, int(x), int(y), int(20 * scale), color)
101
-
102
79
  def _ensure_creatures(self, target_count: int) -> None:
103
80
  while len(self._creatures) < target_count:
104
81
  margin = 40.0
@@ -142,6 +119,7 @@ class PlayerSandboxView:
142
119
  self._hud_assets = load_hud_assets(self._assets_root)
143
120
  if self._hud_assets.missing:
144
121
  self._hud_missing = list(self._hud_assets.missing)
122
+ self._hud_state = HudState()
145
123
 
146
124
  self._state.rng.srand(0xBEEF)
147
125
  self._creatures.clear()
@@ -239,7 +217,7 @@ class PlayerSandboxView:
239
217
  if desired_y < min_y:
240
218
  desired_y = min_y
241
219
 
242
- t = _clamp(dt * 6.0, 0.0, 1.0)
220
+ t = clamp(dt * 6.0, 0.0, 1.0)
243
221
  self._camera_x = _lerp(self._camera_x, desired_x, t)
244
222
  self._camera_y = _lerp(self._camera_y, desired_y, t)
245
223
 
@@ -315,7 +293,7 @@ class PlayerSandboxView:
315
293
  rl.clear_background(rl.Color(10, 10, 12, 255))
316
294
  if self._missing_assets:
317
295
  message = "Missing assets: " + ", ".join(self._missing_assets)
318
- self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
296
+ draw_ui_text(self._small, message, 24, 24, scale=UI_TEXT_SCALE, color=UI_ERROR_COLOR)
319
297
  return
320
298
 
321
299
  # World bounds.
@@ -353,6 +331,7 @@ class PlayerSandboxView:
353
331
  if self._hud_assets is not None:
354
332
  hud_bottom = draw_hud_overlay(
355
333
  self._hud_assets,
334
+ state=self._hud_state,
356
335
  player=self._player,
357
336
  bonus_hud=self._state.bonus_hud,
358
337
  elapsed_ms=self._elapsed_ms,
@@ -363,44 +342,64 @@ class PlayerSandboxView:
363
342
 
364
343
  if self._hud_missing:
365
344
  warn = "Missing HUD assets: " + ", ".join(self._hud_missing)
366
- self._draw_ui_text(warn, 24, rl.get_screen_height() - 28, UI_ERROR_COLOR, scale=0.8)
345
+ draw_ui_text(self._small, warn, 24, rl.get_screen_height() - 28, scale=0.8, color=UI_ERROR_COLOR)
367
346
 
368
347
  # UI.
369
348
  scale = hud_ui_scale(float(rl.get_screen_width()), float(rl.get_screen_height()))
370
349
  margin = 18
371
350
  x = float(margin)
372
351
  y = max(float(margin) + 110.0 * scale, hud_bottom + 12.0 * scale)
373
- line = self._ui_line_height()
352
+ line = ui_line_height(self._small, scale=UI_TEXT_SCALE)
374
353
 
375
354
  weapon_id = self._player.weapon_id
376
355
  weapon_name = next((w.name for w in WEAPON_TABLE if w.weapon_id == weapon_id), None) or f"weapon_{weapon_id}"
377
- self._draw_ui_text(f"{weapon_name} (id {weapon_id})", x, y, UI_TEXT_COLOR)
356
+ draw_ui_text(self._small, f"{weapon_name} (id {weapon_id})", x, y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
378
357
  y += line + 4
379
- self._draw_ui_text(
358
+ draw_ui_text(
359
+ self._small,
380
360
  f"ammo {self._player.ammo}/{self._player.clip_size} reload {self._player.reload_timer:.2f}/{self._player.reload_timer_max:.2f}",
381
361
  x,
382
362
  y,
383
- UI_TEXT_COLOR,
363
+ scale=UI_TEXT_SCALE,
364
+ color=UI_TEXT_COLOR,
384
365
  )
385
366
  y += line + 4
386
- self._draw_ui_text(
367
+ draw_ui_text(
368
+ self._small,
387
369
  f"cooldown {self._player.shot_cooldown:.3f} spread {self._player.spread_heat:.3f}",
388
370
  x,
389
371
  y,
390
- UI_TEXT_COLOR,
372
+ scale=UI_TEXT_SCALE,
373
+ color=UI_TEXT_COLOR,
391
374
  )
392
375
  y += line + 8
393
376
 
394
- self._draw_ui_text("WASD move Mouse aim LMB fire R reload/swap Q/E weapon Tab pause", x, y, UI_HINT_COLOR)
377
+ draw_ui_text(
378
+ self._small,
379
+ "WASD move Mouse aim LMB fire R reload/swap Q/E weapon Tab pause",
380
+ x,
381
+ y,
382
+ scale=UI_TEXT_SCALE,
383
+ color=UI_HINT_COLOR,
384
+ )
395
385
  y += line + 4
396
- self._draw_ui_text(
386
+ draw_ui_text(
387
+ self._small,
397
388
  "1 Sharpshooter 2 Anxious 3 Stationary 4 Angry 5 Man Bomb 6 Hot Tempered 7 Fire Cough T Alt Weapon",
398
389
  x,
399
390
  y,
400
- UI_HINT_COLOR,
391
+ scale=UI_TEXT_SCALE,
392
+ color=UI_HINT_COLOR,
401
393
  )
402
394
  y += line + 4
403
- self._draw_ui_text("Z PowerUp X Shield C Speed V FireBullets B Fireblast Backspace clear bonuses", x, y, UI_HINT_COLOR)
395
+ draw_ui_text(
396
+ self._small,
397
+ "Z PowerUp X Shield C Speed V FireBullets B Fireblast Backspace clear bonuses",
398
+ x,
399
+ y,
400
+ scale=UI_TEXT_SCALE,
401
+ color=UI_HINT_COLOR,
402
+ )
404
403
  y += line + 10
405
404
 
406
405
  active_perks = []
@@ -416,16 +415,23 @@ class PlayerSandboxView:
416
415
  ):
417
416
  if self._player.perk_counts[int(perk)]:
418
417
  active_perks.append(perk.name.lower())
419
- self._draw_ui_text("perks: " + (", ".join(active_perks) if active_perks else "none"), x, y, UI_TEXT_COLOR)
418
+ draw_ui_text(
419
+ self._small,
420
+ "perks: " + (", ".join(active_perks) if active_perks else "none"),
421
+ x,
422
+ y,
423
+ scale=UI_TEXT_SCALE,
424
+ color=UI_TEXT_COLOR,
425
+ )
420
426
  y += line + 8
421
427
 
422
428
  # Bonus HUD slots (text-only).
423
429
  slots = [slot for slot in self._state.bonus_hud.slots if slot.active]
424
430
  if slots:
425
- self._draw_ui_text("bonuses:", x, y, UI_TEXT_COLOR)
431
+ draw_ui_text(self._small, "bonuses:", x, y, scale=UI_TEXT_SCALE, color=UI_TEXT_COLOR)
426
432
  y += line + 4
427
433
  for slot in slots:
428
- self._draw_ui_text(f"- {slot.label}", x, y, UI_HINT_COLOR)
434
+ draw_ui_text(self._small, f"- {slot.label}", x, y, scale=UI_TEXT_SCALE, color=UI_HINT_COLOR)
429
435
  y += line + 2
430
436
 
431
437