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
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
5
5
  import pyray as rl
6
6
 
7
7
  from grim.audio import play_sfx, update_audio
8
- from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
8
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
9
9
 
10
10
  from ...ui.menu_panel import draw_classic_menu_panel
11
11
  from ...ui.perk_menu import UiButtonState, UiButtonTextureSet, button_draw, button_update, button_width
@@ -274,34 +274,322 @@ class _DatabaseBaseView:
274
274
 
275
275
 
276
276
  class UnlockedWeaponsDatabaseView(_DatabaseBaseView):
277
+ def __init__(self, state: GameState) -> None:
278
+ super().__init__(state)
279
+ self._wicons_tex: rl.Texture2D | None = None
280
+ self._weapon_ids: list[int] = []
281
+ self._selected_weapon_id: int = 2
282
+
283
+ def open(self) -> None:
284
+ super().open()
285
+ self._weapon_ids = self._build_weapon_database_ids()
286
+ self._selected_weapon_id = 2 if 2 in self._weapon_ids else (self._weapon_ids[0] if self._weapon_ids else 2)
287
+
288
+ if self._wicons_tex is not None:
289
+ rl.unload_texture(self._wicons_tex)
290
+ self._wicons_tex = None
291
+ wicons_path = self._state.assets_dir / "crimson" / "ui" / "ui_wicons.png"
292
+ if wicons_path.is_file():
293
+ self._wicons_tex = rl.load_texture(str(wicons_path))
294
+
295
+ def close(self) -> None:
296
+ if self._wicons_tex is not None:
297
+ rl.unload_texture(self._wicons_tex)
298
+ self._wicons_tex = None
299
+ super().close()
300
+
277
301
  def _back_button_pos(self) -> tuple[float, float]:
278
302
  # state_15: ui_buttonSm bbox [270,507]..[352,539] => relative to left panel (-98,194): (368, 313)
279
303
  return (368.0, 313.0)
280
304
 
281
305
  def _draw_contents(self, left_x0: float, left_y0: float, right_x0: float, right_y0: float, *, scale: float, font: SmallFontData) -> None:
306
+ text_scale = 1.0 * scale
307
+ text_color = rl.Color(255, 255, 255, int(255 * 0.8))
308
+
282
309
  # state_15 title at (153,244) => relative to left panel (-98,194): (251,50)
283
310
  draw_small_text(
284
311
  font,
285
312
  "Unlocked Weapons Database",
286
313
  left_x0 + 251.0 * scale,
287
314
  left_y0 + 50.0 * scale,
288
- 1.0 * scale,
315
+ text_scale,
289
316
  rl.Color(255, 255, 255, 255),
290
317
  )
291
318
 
319
+ weapon_ids = self._weapon_ids
320
+ count = len(weapon_ids)
321
+ weapon_label = "weapon" if count == 1 else "weapons"
322
+ draw_small_text(
323
+ font,
324
+ f"{count} {weapon_label} in database",
325
+ left_x0 + 210.0 * scale,
326
+ left_y0 + 80.0 * scale,
327
+ text_scale,
328
+ text_color,
329
+ )
330
+ draw_small_text(
331
+ font,
332
+ "Weapon",
333
+ left_x0 + 210.0 * scale,
334
+ left_y0 + 108.0 * scale,
335
+ text_scale,
336
+ text_color,
337
+ )
338
+
339
+ # List items (oracle shows 9-row list widget; render the top slice for now).
340
+ list_x = left_x0 + 218.0 * scale
341
+ list_y0 = left_y0 + 130.0 * scale
342
+ row_step = 16.0 * scale
343
+ for row, weapon_id in enumerate(weapon_ids[:9]):
344
+ name, _icon = self._weapon_label_and_icon(weapon_id)
345
+ draw_small_text(font, name, list_x, list_y0 + float(row) * row_step, text_scale, text_color)
346
+
347
+ weapon_id = int(self._selected_weapon_id)
348
+ name, icon_index = self._weapon_label_and_icon(weapon_id)
349
+ weapon = self._weapon_entry(weapon_id)
350
+ draw_small_text(
351
+ font,
352
+ f"wepno #{weapon_id}",
353
+ right_x0 + 240.0 * scale,
354
+ right_y0 + 32.0 * scale,
355
+ text_scale,
356
+ text_color,
357
+ )
358
+ draw_small_text(font, name, right_x0 + 50.0 * scale, right_y0 + 50.0 * scale, text_scale, text_color)
359
+ if icon_index is not None:
360
+ self._draw_wicon(icon_index, x=right_x0 + 82.0 * scale, y=right_y0 + 82.0 * scale, scale=scale)
361
+
362
+ if weapon is not None:
363
+ rpm = self._weapon_rpm(weapon)
364
+ reload_time = weapon.reload_time
365
+ clip_size = weapon.clip_size
366
+ if rpm is not None:
367
+ draw_small_text(
368
+ font,
369
+ f"Firerate: {rpm} rpm",
370
+ right_x0 + 66.0 * scale,
371
+ right_y0 + 128.0 * scale,
372
+ text_scale,
373
+ text_color,
374
+ )
375
+ if reload_time is not None:
376
+ draw_small_text(
377
+ font,
378
+ f"Reload time: {reload_time:g} secs",
379
+ right_x0 + 66.0 * scale,
380
+ right_y0 + 146.0 * scale,
381
+ text_scale,
382
+ text_color,
383
+ )
384
+ if clip_size is not None:
385
+ draw_small_text(
386
+ font,
387
+ f"Clip size: {int(clip_size)}",
388
+ right_x0 + 66.0 * scale,
389
+ right_y0 + 164.0 * scale,
390
+ text_scale,
391
+ text_color,
392
+ )
393
+
394
+ def _build_weapon_database_ids(self) -> list[int]:
395
+ try:
396
+ from ...weapons import WEAPON_TABLE
397
+ except Exception:
398
+ return []
399
+ status = self._state.status
400
+ used: list[int] = []
401
+ for weapon in WEAPON_TABLE:
402
+ if weapon.name is None:
403
+ continue
404
+ weapon_id = int(weapon.weapon_id)
405
+ try:
406
+ if status.weapon_usage_count(weapon_id) != 0:
407
+ used.append(weapon_id)
408
+ except Exception:
409
+ continue
410
+ used.sort()
411
+ return used
412
+
413
+ def _weapon_entry(self, weapon_id: int) -> object | None:
414
+ try:
415
+ from ...weapons import WEAPON_BY_ID
416
+ except Exception:
417
+ return None
418
+ return WEAPON_BY_ID.get(int(weapon_id))
419
+
420
+ def _weapon_rpm(self, weapon: object) -> int | None:
421
+ try:
422
+ cooldown = getattr(weapon, "shot_cooldown", None)
423
+ if cooldown is None:
424
+ return None
425
+ cooldown = float(cooldown)
426
+ except Exception:
427
+ return None
428
+ if cooldown <= 0.0:
429
+ return None
430
+ return int(60.0 / cooldown)
431
+
432
+ def _draw_wicon(self, icon_index: int, *, x: float, y: float, scale: float) -> None:
433
+ tex = self._wicons_tex
434
+ if tex is None:
435
+ return
436
+ idx = int(icon_index)
437
+ if idx < 0 or idx > 31:
438
+ return
439
+ cols = 4
440
+ rows = 8
441
+ icon_w = float(tex.width) / float(cols)
442
+ icon_h = float(tex.height) / float(rows)
443
+ src_x = float(idx % cols) * icon_w
444
+ src_y = float(idx // cols) * icon_h
445
+ rl.draw_texture_pro(
446
+ tex,
447
+ rl.Rectangle(src_x, src_y, icon_w, icon_h),
448
+ rl.Rectangle(float(x), float(y), icon_w * scale, icon_h * scale),
449
+ rl.Vector2(0.0, 0.0),
450
+ 0.0,
451
+ rl.WHITE,
452
+ )
453
+
454
+ @staticmethod
455
+ def _weapon_label_and_icon(weapon_id: int) -> tuple[str, int | None]:
456
+ try:
457
+ from ...weapons import WEAPON_BY_ID
458
+ except Exception:
459
+ WEAPON_BY_ID = {}
460
+ weapon = WEAPON_BY_ID.get(int(weapon_id))
461
+ if weapon is None:
462
+ return f"Weapon {int(weapon_id)}", None
463
+ name = weapon.name or f"weapon_{int(weapon.weapon_id)}"
464
+ return name, weapon.icon_index
465
+
292
466
 
293
467
  class UnlockedPerksDatabaseView(_DatabaseBaseView):
468
+ def __init__(self, state: GameState) -> None:
469
+ super().__init__(state)
470
+ self._perk_ids: list[int] = []
471
+ self._selected_perk_id: int = 4
472
+
473
+ def open(self) -> None:
474
+ super().open()
475
+ self._perk_ids = self._build_perk_database_ids()
476
+ self._selected_perk_id = 4 if 4 in self._perk_ids else (self._perk_ids[0] if self._perk_ids else 4)
477
+
294
478
  def _back_button_pos(self) -> tuple[float, float]:
295
479
  # state_16: ui_buttonSm bbox [258,509]..[340,541] => relative to left panel (-98,194): (356, 315)
296
480
  return (356.0, 315.0)
297
481
 
298
482
  def _draw_contents(self, left_x0: float, left_y0: float, right_x0: float, right_y0: float, *, scale: float, font: SmallFontData) -> None:
483
+ text_scale = 1.0 * scale
484
+ text_color = rl.Color(255, 255, 255, int(255 * 0.8))
485
+
299
486
  # state_16 title at (163,244) => relative to left panel (-98,194): (261,50)
300
487
  draw_small_text(
301
488
  font,
302
489
  "Unlocked Perks Database",
303
490
  left_x0 + 261.0 * scale,
304
491
  left_y0 + 50.0 * scale,
305
- 1.0 * scale,
492
+ text_scale,
306
493
  rl.Color(255, 255, 255, 255),
307
494
  )
495
+
496
+ perk_ids = self._perk_ids
497
+ count = len(perk_ids)
498
+ perk_label = "perk" if count == 1 else "perks"
499
+ draw_small_text(
500
+ font,
501
+ f"{count} {perk_label} in database",
502
+ left_x0 + 210.0 * scale,
503
+ left_y0 + 78.0 * scale,
504
+ text_scale,
505
+ text_color,
506
+ )
507
+ draw_small_text(
508
+ font,
509
+ "Perks",
510
+ left_x0 + 210.0 * scale,
511
+ left_y0 + 106.0 * scale,
512
+ text_scale,
513
+ text_color,
514
+ )
515
+
516
+ list_x = left_x0 + 218.0 * scale
517
+ list_y0 = left_y0 + 128.0 * scale
518
+ row_step = 16.0 * scale
519
+ for row, perk_id in enumerate(perk_ids[:9]):
520
+ draw_small_text(font, self._perk_name(perk_id), list_x, list_y0 + float(row) * row_step, text_scale, text_color)
521
+
522
+ perk_id = int(self._selected_perk_id)
523
+ perk_name = self._perk_name(perk_id)
524
+ draw_small_text(
525
+ font,
526
+ f"perkno #{perk_id}",
527
+ right_x0 + 224.0 * scale,
528
+ right_y0 + 32.0 * scale,
529
+ text_scale,
530
+ text_color,
531
+ )
532
+ draw_small_text(font, perk_name, right_x0 + 93.0 * scale, right_y0 + 50.0 * scale, text_scale, text_color)
533
+
534
+ desc_x = right_x0 + 50.0 * scale
535
+ desc_y = right_y0 + 72.0 * scale
536
+ max_w = float(rl.get_screen_width()) - desc_x - 4.0 * scale
537
+ desc = self._perk_desc(perk_id)
538
+ first_line = self._truncate_small_line(font, desc, max_w, scale=text_scale)
539
+ if first_line:
540
+ draw_small_text(font, first_line, desc_x, desc_y, text_scale, text_color)
541
+
542
+ def _build_perk_database_ids(self) -> list[int]:
543
+ try:
544
+ from ...gameplay import PERK_COUNT_SIZE, perks_rebuild_available
545
+ except Exception:
546
+ return []
547
+
548
+ # Avoid spinning up a full GameplayState; perks_rebuild_available only needs these fields.
549
+ class _Stub:
550
+ status: object | None
551
+ perk_available: list[bool]
552
+ _perk_available_unlock_index: int
553
+
554
+ stub = _Stub()
555
+ stub.status = self._state.status
556
+ stub.perk_available = [False] * int(PERK_COUNT_SIZE)
557
+ stub._perk_available_unlock_index = -1
558
+ perks_rebuild_available(stub) # type: ignore[arg-type]
559
+
560
+ perk_ids = [idx for idx, available in enumerate(stub.perk_available) if available and idx > 0]
561
+ perk_ids.sort()
562
+ return perk_ids
563
+
564
+ @staticmethod
565
+ def _perk_name(perk_id: int) -> str:
566
+ try:
567
+ from ...perks import perk_display_name
568
+
569
+ return perk_display_name(int(perk_id))
570
+ except Exception:
571
+ return f"Perk {int(perk_id)}"
572
+
573
+ @staticmethod
574
+ def _perk_desc(perk_id: int) -> str:
575
+ try:
576
+ from ...perks import perk_display_description
577
+
578
+ return perk_display_description(int(perk_id))
579
+ except Exception:
580
+ return ""
581
+
582
+ @staticmethod
583
+ def _truncate_small_line(font: SmallFontData, text: str, max_width: float, *, scale: float) -> str:
584
+ text = str(text).strip()
585
+ if not text:
586
+ return ""
587
+ words = text.split()
588
+ line = ""
589
+ for word in words:
590
+ candidate = word if not line else f"{line} {word}"
591
+ if measure_small_text_width(font, candidate, float(scale)) <= float(max_width):
592
+ line = candidate
593
+ continue
594
+ break
595
+ return line
@@ -12,9 +12,7 @@ from ..menu import (
12
12
  MENU_PANEL_OFFSET_Y,
13
13
  MENU_PANEL_WIDTH,
14
14
  MenuView,
15
- _draw_menu_cursor,
16
15
  )
17
- from ..transitions import _draw_screen_fade
18
16
  from .base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView
19
17
 
20
18
  if TYPE_CHECKING:
@@ -31,19 +29,6 @@ class ModsMenuView(PanelMenuView):
31
29
  super().open()
32
30
  self._lines = self._build_lines()
33
31
 
34
- def draw(self) -> None:
35
- self._draw_background()
36
- _draw_screen_fade(self._state)
37
- assets = self._assets
38
- entry = self._entry
39
- if assets is None or entry is None:
40
- return
41
- self._draw_panel()
42
- self._draw_entry(entry)
43
- self._draw_sign()
44
- self._draw_contents()
45
- _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
46
-
47
32
  def _ensure_small_font(self) -> SmallFontData:
48
33
  if self._small_font is not None:
49
34
  return self._small_font
@@ -17,9 +17,7 @@ from ..menu import (
17
17
  MENU_LABEL_WIDTH,
18
18
  MENU_PANEL_WIDTH,
19
19
  MenuView,
20
- _draw_menu_cursor,
21
20
  )
22
- from ..transitions import _draw_screen_fade
23
21
  from .base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView
24
22
 
25
23
  if TYPE_CHECKING:
@@ -169,20 +167,6 @@ class PlayGameMenuView(PanelMenuView):
169
167
  continue
170
168
  self._tooltip_ms[key] = max(0, self._tooltip_ms[key] - dt_ms * 2)
171
169
 
172
- def draw(self) -> None:
173
- self._draw_background()
174
- _draw_screen_fade(self._state)
175
- assets = self._assets
176
- entry = self._entry
177
- if assets is None or entry is None:
178
- return
179
-
180
- self._draw_panel()
181
- self._draw_entry(entry)
182
- self._draw_sign()
183
- self._draw_contents()
184
- _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
185
-
186
170
  def _begin_close_transition(self, action: str) -> None:
187
171
  if self._dirty:
188
172
  try: