crimsonland 0.1.0.dev7__tar.gz → 0.1.0.dev10__tar.gz

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 (138) hide show
  1. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/pyproject.toml +1 -1
  3. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/creatures/anim.py +1 -0
  4. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/demo.py +77 -35
  5. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/game.py +5 -0
  6. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/gameplay.py +38 -11
  7. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/modes/base_gameplay_mode.py +75 -1
  8. crimsonland-0.1.0.dev10/src/crimson/modes/quest_mode.py +929 -0
  9. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/modes/rush_mode.py +12 -3
  10. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/modes/survival_mode.py +13 -2
  11. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/modes/tutorial_mode.py +12 -1
  12. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/modes/typo_mode.py +12 -4
  13. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/render/world_renderer.py +12 -16
  14. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/ui/hud.py +274 -51
  15. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/arsenal_debug.py +46 -1
  16. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/player.py +2 -2
  17. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/console.py +14 -0
  18. crimsonland-0.1.0.dev7/src/crimson/modes/quest_mode.py +0 -502
  19. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/__init__.py +0 -0
  20. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/assets_fetch.py +0 -0
  21. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/atlas.py +0 -0
  22. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/audio_router.py +0 -0
  23. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/bonuses.py +0 -0
  24. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/camera.py +0 -0
  25. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/cli.py +0 -0
  26. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/creatures/__init__.py +0 -0
  27. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/creatures/ai.py +0 -0
  28. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/creatures/damage.py +0 -0
  29. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/creatures/runtime.py +0 -0
  30. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/creatures/spawn.py +0 -0
  31. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/debug.py +0 -0
  32. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/demo_trial.py +0 -0
  33. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/effects.py +0 -0
  34. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/effects_atlas.py +0 -0
  35. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/__init__.py +0 -0
  36. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/assets.py +0 -0
  37. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/boot.py +0 -0
  38. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/menu.py +0 -0
  39. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/panels/__init__.py +0 -0
  40. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/panels/base.py +0 -0
  41. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/panels/controls.py +0 -0
  42. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/panels/mods.py +0 -0
  43. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/panels/options.py +0 -0
  44. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/panels/play_game.py +0 -0
  45. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/panels/stats.py +0 -0
  46. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/frontend/transitions.py +0 -0
  47. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/game_modes.py +0 -0
  48. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/game_world.py +0 -0
  49. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/input_codes.py +0 -0
  50. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/modes/__init__.py +0 -0
  51. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/paths.py +0 -0
  52. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/perks.py +0 -0
  53. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/persistence/__init__.py +0 -0
  54. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/persistence/highscores.py +0 -0
  55. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/persistence/save_status.py +0 -0
  56. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/player_damage.py +0 -0
  57. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/projectiles.py +0 -0
  58. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/__init__.py +0 -0
  59. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/helpers.py +0 -0
  60. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/registry.py +0 -0
  61. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/results.py +0 -0
  62. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/runtime.py +0 -0
  63. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/tier1.py +0 -0
  64. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/tier2.py +0 -0
  65. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/tier3.py +0 -0
  66. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/tier4.py +0 -0
  67. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/tier5.py +0 -0
  68. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/timeline.py +0 -0
  69. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/quests/types.py +0 -0
  70. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/render/__init__.py +0 -0
  71. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/render/terrain_fx.py +0 -0
  72. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/sim/__init__.py +0 -0
  73. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/sim/world_defs.py +0 -0
  74. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/sim/world_state.py +0 -0
  75. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/terrain_assets.py +0 -0
  76. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/tutorial/__init__.py +0 -0
  77. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/tutorial/timeline.py +0 -0
  78. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/typo/__init__.py +0 -0
  79. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/typo/names.py +0 -0
  80. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/typo/player.py +0 -0
  81. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/typo/spawns.py +0 -0
  82. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/typo/typing.py +0 -0
  83. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/ui/__init__.py +0 -0
  84. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/ui/cursor.py +0 -0
  85. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/ui/demo_trial_overlay.py +0 -0
  86. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/ui/game_over.py +0 -0
  87. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/ui/perk_menu.py +0 -0
  88. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/__init__.py +0 -0
  89. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/aim_debug.py +0 -0
  90. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/animations.py +0 -0
  91. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/audio_bootstrap.py +0 -0
  92. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/bonuses.py +0 -0
  93. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/camera_debug.py +0 -0
  94. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/camera_shake.py +0 -0
  95. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/corpse_stamp_debug.py +0 -0
  96. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/decals_debug.py +0 -0
  97. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/empty.py +0 -0
  98. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/fonts.py +0 -0
  99. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/game_over.py +0 -0
  100. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/ground.py +0 -0
  101. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/lighting_debug.py +0 -0
  102. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/particles.py +0 -0
  103. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/perk_menu_debug.py +0 -0
  104. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/perks.py +0 -0
  105. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/player_sprite_debug.py +0 -0
  106. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/projectile_fx.py +0 -0
  107. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/projectile_render_debug.py +0 -0
  108. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/projectiles.py +0 -0
  109. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/quest_title_overlay.py +0 -0
  110. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/registry.py +0 -0
  111. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/rush.py +0 -0
  112. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/small_font_debug.py +0 -0
  113. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/spawn_plan.py +0 -0
  114. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/sprites.py +0 -0
  115. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/survival.py +0 -0
  116. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/terrain.py +0 -0
  117. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/ui.py +0 -0
  118. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/views/wicons.py +0 -0
  119. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/weapon_sfx.py +0 -0
  120. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/crimson/weapons.py +0 -0
  121. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/__init__.py +0 -0
  122. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/app.py +0 -0
  123. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/assets.py +0 -0
  124. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/audio.py +0 -0
  125. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/config.py +0 -0
  126. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/fonts/__init__.py +0 -0
  127. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/fonts/grim_mono.py +0 -0
  128. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/fonts/small.py +0 -0
  129. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/input.py +0 -0
  130. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/jaz.py +0 -0
  131. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/math.py +0 -0
  132. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/music.py +0 -0
  133. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/paq.py +0 -0
  134. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/rand.py +0 -0
  135. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/sfx.py +0 -0
  136. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/sfx_map.py +0 -0
  137. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/terrain_render.py +0 -0
  138. {crimsonland-0.1.0.dev7 → crimsonland-0.1.0.dev10}/src/grim/view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: crimsonland
3
- Version: 0.1.0.dev7
3
+ Version: 0.1.0.dev10
4
4
  Requires-Dist: construct>=2.10.70
5
5
  Requires-Dist: pillow>=12.1.0
6
6
  Requires-Dist: platformdirs>=4.5.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crimsonland"
3
- version = "0.1.0.dev7"
3
+ version = "0.1.0.dev10"
4
4
  requires-python = ">=3.13"
5
5
  dependencies = [
6
6
  "construct>=2.10.70",
@@ -28,6 +28,7 @@ _CREATURE_CORPSE_FRAMES: dict[int, int] = {
28
28
  3: 1, # spider sp1
29
29
  4: 2, # spider sp2
30
30
  5: 7, # trooper
31
+ 7: 6, # ping-pong strip corpse fallback
31
32
  }
32
33
 
33
34
 
@@ -475,6 +475,9 @@ class DemoView:
475
475
  player = self._world.players[idx]
476
476
  player.pos_x = float(x)
477
477
  player.pos_y = float(y)
478
+ # Keep aim anchored to the spawn position so demo aim starts stable.
479
+ player.aim_x = float(x)
480
+ player.aim_y = float(y)
478
481
  weapon_assign_player(player, int(weapon_id))
479
482
  self._demo_targets = [None] * len(self._world.players)
480
483
 
@@ -549,7 +552,8 @@ class DemoView:
549
552
 
550
553
  def _setup_variant_0(self) -> None:
551
554
  self._demo_time_limit_ms = 4000
552
- weapon_id = 12
555
+ # demo_setup_variant_0 uses weapon_id=0x0B.
556
+ weapon_id = 11
553
557
  self._setup_world_players(
554
558
  [
555
559
  (448.0, 384.0, weapon_id),
@@ -567,7 +571,8 @@ class DemoView:
567
571
 
568
572
  def _setup_variant_1(self) -> None:
569
573
  self._demo_time_limit_ms = 5000
570
- weapon_id = 6
574
+ # demo_setup_variant_1 uses weapon_id=0x05.
575
+ weapon_id = 5
571
576
  self._setup_world_players(
572
577
  [
573
578
  (490.0, 448.0, weapon_id),
@@ -586,7 +591,8 @@ class DemoView:
586
591
 
587
592
  def _setup_variant_2(self) -> None:
588
593
  self._demo_time_limit_ms = 5000
589
- weapon_id = 22
594
+ # demo_setup_variant_2 uses weapon_id=0x15.
595
+ weapon_id = 21
590
596
  self._setup_world_players([(512.0, 512.0, weapon_id)])
591
597
  y = 128
592
598
  i = 0
@@ -601,7 +607,8 @@ class DemoView:
601
607
 
602
608
  def _setup_variant_3(self) -> None:
603
609
  self._demo_time_limit_ms = 4000
604
- weapon_id = 19
610
+ # demo_setup_variant_3 uses weapon_id=0x12.
611
+ weapon_id = 18
605
612
  self._setup_world_players([(512.0, 512.0, weapon_id)])
606
613
  for idx in range(20):
607
614
  x = float(self._crand_mod(200) + 32)
@@ -881,10 +888,10 @@ class DemoView:
881
888
  def _update_world(self, dt: float) -> None:
882
889
  if not self._world.players:
883
890
  return
884
- inputs = self._build_demo_inputs()
891
+ inputs = self._build_demo_inputs(dt)
885
892
  self._world.update(dt, inputs=inputs, auto_pick_perks=False, game_mode=0, perk_progression_enabled=False)
886
893
 
887
- def _build_demo_inputs(self) -> list[PlayerInput]:
894
+ def _build_demo_inputs(self, dt: float) -> list[PlayerInput]:
888
895
  players = self._world.players
889
896
  creatures = self._world.creatures.entries
890
897
  if len(self._demo_targets) != len(players):
@@ -892,42 +899,77 @@ class DemoView:
892
899
  center_x = float(self._world.world_size) * 0.5
893
900
  center_y = float(self._world.world_size) * 0.5
894
901
 
902
+ dt = float(dt)
903
+
904
+ def _turn_towards_heading(cur: float, target: float) -> tuple[float, float]:
905
+ cur = cur % math.tau
906
+ target = target % math.tau
907
+ delta = (target - cur + math.pi) % math.tau - math.pi
908
+ diff = abs(delta)
909
+ if diff <= 1e-9:
910
+ return cur, 0.0
911
+ step = dt * diff * 5.0
912
+ cur = (cur + step) % math.tau if delta > 0.0 else (cur - step) % math.tau
913
+ return cur, diff
914
+
895
915
  inputs: list[PlayerInput] = []
896
916
  for idx, player in enumerate(players):
897
917
  target_idx = self._select_demo_target(idx, player, creatures)
898
- aim_x = center_x
899
- aim_y = center_y
900
918
  target = None
901
919
  if target_idx is not None and 0 <= target_idx < len(creatures):
902
920
  candidate = creatures[target_idx]
903
- if candidate.hp > 0.0:
921
+ if candidate.active and candidate.hp > 0.0:
904
922
  target = candidate
905
- aim_x = candidate.x
906
- aim_y = candidate.y
907
-
908
- move_x, move_y = 0.0, 0.0
909
- to_cx = center_x - player.pos_x
910
- to_cy = center_y - player.pos_y
911
- nx, ny, d = _normalize(to_cx, to_cy)
912
- if d > 120.0:
913
- move_x += nx
914
- move_y += ny
915
923
 
924
+ # Aim: ease the aim point toward the target.
925
+ aim_x = float(player.aim_x)
926
+ aim_y = float(player.aim_y)
927
+ auto_fire = False
916
928
  if target is not None:
917
- rx = player.pos_x - target.x
918
- ry = player.pos_y - target.y
919
- rnx, rny, rd = _normalize(rx, ry)
920
- if 0.0 < rd < 160.0:
921
- strength = (160.0 - rd) / 160.0
922
- move_x += rnx * (1.5 * strength)
923
- move_y += rny * (1.5 * strength)
924
-
925
- orbit_dir = -1.0 if (player.index % 2) else 1.0
926
- ox, oy, _ = _normalize(-(player.pos_y - center_y), player.pos_x - center_x)
927
- move_x += ox * 0.55 * orbit_dir
928
- move_y += oy * 0.55 * orbit_dir
929
-
930
- fire_down = target is not None
929
+ aim_dx = float(target.x) - aim_x
930
+ aim_dy = float(target.y) - aim_y
931
+ aim_dir_x, aim_dir_y, aim_dist = _normalize(aim_dx, aim_dy)
932
+ if aim_dist >= 4.0:
933
+ step = aim_dist * 6.0 * dt
934
+ aim_x += aim_dir_x * step
935
+ aim_y += aim_dir_y * step
936
+ else:
937
+ aim_x = float(target.x)
938
+ aim_y = float(target.y)
939
+ auto_fire = aim_dist < 128.0
940
+ else:
941
+ ax, ay, amag = _normalize(float(player.pos_x) - center_x, float(player.pos_y) - center_y)
942
+ if amag <= 1e-6:
943
+ ax, ay = 0.0, -1.0
944
+ aim_x = float(player.pos_x) + ax * 60.0
945
+ aim_y = float(player.pos_y) + ay * 60.0
946
+
947
+ # Movement:
948
+ # - orbit center if no target
949
+ # - chase target when near center
950
+ # - return to center when too far
951
+ if target is None:
952
+ move_dx = -(float(player.pos_y) - center_y)
953
+ move_dy = float(player.pos_x) - center_x
954
+ else:
955
+ center_dist = math.hypot(float(player.pos_x) - center_x, float(player.pos_y) - center_y)
956
+ if center_dist <= 300.0:
957
+ move_dx = float(target.x) - float(player.pos_x)
958
+ move_dy = float(target.y) - float(player.pos_y)
959
+ else:
960
+ move_dx = center_x - float(player.pos_x)
961
+ move_dy = center_y - float(player.pos_y)
962
+
963
+ desired_x, desired_y, desired_mag = _normalize(move_dx, move_dy)
964
+ if desired_mag <= 1e-6:
965
+ move_x = 0.0
966
+ move_y = 0.0
967
+ else:
968
+ desired_heading = math.atan2(desired_y, desired_x) + math.pi / 2.0
969
+ smoothed_heading, angle_diff = _turn_towards_heading(float(player.heading), desired_heading)
970
+ move_mag = max(0.001, (math.pi - angle_diff) / math.pi)
971
+ move_x = math.cos(smoothed_heading - math.pi / 2.0) * move_mag
972
+ move_y = math.sin(smoothed_heading - math.pi / 2.0) * move_mag
931
973
 
932
974
  inputs.append(
933
975
  PlayerInput(
@@ -935,8 +977,8 @@ class DemoView:
935
977
  move_y=move_y,
936
978
  aim_x=aim_x,
937
979
  aim_y=aim_y,
938
- fire_down=fire_down,
939
- fire_pressed=fire_down,
980
+ fire_down=auto_fire,
981
+ fire_pressed=auto_fire,
940
982
  reload_pressed=False,
941
983
  )
942
984
  )
@@ -789,6 +789,7 @@ class SurvivalGameView:
789
789
  ViewContext(assets_dir=state.assets_dir),
790
790
  texture_cache=state.texture_cache,
791
791
  config=state.config,
792
+ console=state.console,
792
793
  audio=state.audio,
793
794
  audio_rng=state.rng,
794
795
  )
@@ -848,6 +849,7 @@ class RushGameView:
848
849
  ViewContext(assets_dir=state.assets_dir),
849
850
  texture_cache=state.texture_cache,
850
851
  config=state.config,
852
+ console=state.console,
851
853
  audio=state.audio,
852
854
  audio_rng=state.rng,
853
855
  )
@@ -905,6 +907,7 @@ class TypoShooterGameView:
905
907
  ViewContext(assets_dir=state.assets_dir),
906
908
  texture_cache=state.texture_cache,
907
909
  config=state.config,
910
+ console=state.console,
908
911
  audio=state.audio,
909
912
  audio_rng=state.rng,
910
913
  )
@@ -962,6 +965,7 @@ class TutorialGameView:
962
965
  ViewContext(assets_dir=state.assets_dir),
963
966
  texture_cache=state.texture_cache,
964
967
  config=state.config,
968
+ console=state.console,
965
969
  audio=state.audio,
966
970
  audio_rng=state.rng,
967
971
  demo_mode_active=state.demo_enabled,
@@ -1011,6 +1015,7 @@ class QuestGameView:
1011
1015
  ViewContext(assets_dir=state.assets_dir),
1012
1016
  texture_cache=state.texture_cache,
1013
1017
  config=state.config,
1018
+ console=state.console,
1014
1019
  audio=state.audio,
1015
1020
  audio_rng=state.rng,
1016
1021
  demo_mode_active=state.demo_enabled,
@@ -159,8 +159,11 @@ class BonusHudSlot:
159
159
  bonus_id: int = 0
160
160
  label: str = ""
161
161
  icon_id: int = -1
162
+ slide_x: float = -184.0
162
163
  timer_ref: _TimerRef | None = None
163
164
  timer_ref_alt: _TimerRef | None = None
165
+ timer_value: float = 0.0
166
+ timer_value_alt: float = 0.0
164
167
 
165
168
 
166
169
  BONUS_HUD_SLOT_COUNT = 16
@@ -199,8 +202,11 @@ class BonusHudState:
199
202
  slot.bonus_id = int(bonus_id)
200
203
  slot.label = label
201
204
  slot.icon_id = int(icon_id)
205
+ slot.slide_x = -184.0
202
206
  slot.timer_ref = timer_ref
203
207
  slot.timer_ref_alt = timer_ref_alt
208
+ slot.timer_value = 0.0
209
+ slot.timer_value_alt = 0.0
204
210
 
205
211
 
206
212
  @dataclass(slots=True)
@@ -968,8 +974,8 @@ def perk_apply(
968
974
  weapon_id = int(current)
969
975
  for _ in range(100):
970
976
  candidate = int(weapon_pick_random_available(state))
971
- if candidate != 0 and candidate != current:
972
- weapon_id = candidate
977
+ weapon_id = candidate
978
+ if candidate != int(WeaponId.PISTOL) and candidate != current:
973
979
  break
974
980
  weapon_assign_player(owner, weapon_id, state=state)
975
981
  return
@@ -1923,7 +1929,9 @@ def player_update(player: PlayerState, input_state: PlayerInput, dt: float, stat
1923
1929
  raw_move_x = float(input_state.move_x)
1924
1930
  raw_move_y = float(input_state.move_y)
1925
1931
  raw_mag = math.hypot(raw_move_x, raw_move_y)
1926
- moving_input = raw_mag > 0.2
1932
+ # Demo/autoplay uses very small analog magnitudes to represent turn-in-place and
1933
+ # heading alignment slowdown; don't apply a deadzone there.
1934
+ moving_input = raw_mag > (0.0 if state.demo_mode_active else 0.2)
1927
1935
 
1928
1936
  if moving_input:
1929
1937
  inv = 1.0 / raw_mag if raw_mag > 1e-9 else 0.0
@@ -2356,8 +2364,8 @@ def bonus_apply(
2356
2364
  return
2357
2365
 
2358
2366
 
2359
- def bonus_hud_update(state: GameplayState, players: list[PlayerState]) -> None:
2360
- """Refresh HUD slots based on current timer values."""
2367
+ def bonus_hud_update(state: GameplayState, players: list[PlayerState], *, dt: float = 0.0) -> None:
2368
+ """Refresh HUD slots based on current timer values + advance slide animation."""
2361
2369
 
2362
2370
  def _timer_value(ref: _TimerRef | None) -> float:
2363
2371
  if ref is None:
@@ -2371,16 +2379,35 @@ def bonus_hud_update(state: GameplayState, players: list[PlayerState]) -> None:
2371
2379
  return float(getattr(players[idx], ref.key, 0.0) or 0.0)
2372
2380
  return 0.0
2373
2381
 
2374
- for slot in state.bonus_hud.slots:
2382
+ player_count = len(players)
2383
+ dt = max(0.0, float(dt))
2384
+
2385
+ for slot_index, slot in enumerate(state.bonus_hud.slots):
2375
2386
  if not slot.active:
2376
2387
  continue
2377
- timer = _timer_value(slot.timer_ref)
2378
- if slot.timer_ref_alt is not None:
2379
- timer = max(timer, _timer_value(slot.timer_ref_alt))
2380
- if timer <= 0.0:
2388
+ timer = max(0.0, _timer_value(slot.timer_ref))
2389
+ timer_alt = max(0.0, _timer_value(slot.timer_ref_alt)) if (slot.timer_ref_alt is not None and player_count > 1) else 0.0
2390
+ slot.timer_value = float(timer)
2391
+ slot.timer_value_alt = float(timer_alt)
2392
+
2393
+ if timer > 0.0 or timer_alt > 0.0:
2394
+ slot.slide_x += dt * 350.0
2395
+ else:
2396
+ slot.slide_x -= dt * 320.0
2397
+
2398
+ if slot.slide_x > -2.0:
2399
+ slot.slide_x = -2.0
2400
+
2401
+ if slot.slide_x < -184.0 and not any(other.active for other in state.bonus_hud.slots[slot_index + 1 :]):
2381
2402
  slot.active = False
2403
+ slot.bonus_id = 0
2404
+ slot.label = ""
2405
+ slot.icon_id = -1
2406
+ slot.slide_x = -184.0
2382
2407
  slot.timer_ref = None
2383
2408
  slot.timer_ref_alt = None
2409
+ slot.timer_value = 0.0
2410
+ slot.timer_value_alt = 0.0
2384
2411
 
2385
2412
 
2386
2413
  def bonus_telekinetic_update(
@@ -2495,6 +2522,6 @@ def bonus_update(
2495
2522
  state.bonuses.freeze = max(0.0, state.bonuses.freeze - dt)
2496
2523
 
2497
2524
  if update_hud:
2498
- bonus_hud_update(state, players)
2525
+ bonus_hud_update(state, players, dt=dt)
2499
2526
 
2500
2527
  return pickups
@@ -8,14 +8,17 @@ import pyray as rl
8
8
 
9
9
  from grim.assets import PaqTextureCache
10
10
  from grim.audio import AudioState, update_audio
11
+ from grim.console import ConsoleState
11
12
  from grim.config import CrimsonConfig
12
13
  from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
13
14
  from grim.view import ViewContext
14
15
 
16
+ from ..gameplay import _creature_find_in_radius, perk_count_get
15
17
  from ..game_world import GameWorld
16
18
  from ..persistence.highscores import HighScoreRecord
19
+ from ..perks import PerkId
17
20
  from ..ui.game_over import GameOverUi
18
- from ..ui.hud import HudAssets, load_hud_assets
21
+ from ..ui.hud import HudAssets, draw_target_health_bar, load_hud_assets
19
22
 
20
23
  if TYPE_CHECKING:
21
24
  from ..persistence.save_status import GameStatus
@@ -45,6 +48,7 @@ class BaseGameplayMode:
45
48
  hardcore: bool = False,
46
49
  texture_cache: PaqTextureCache | None = None,
47
50
  config: CrimsonConfig | None = None,
51
+ console: ConsoleState | None = None,
48
52
  audio: AudioState | None = None,
49
53
  audio_rng: random.Random | None = None,
50
54
  ) -> None:
@@ -54,7 +58,9 @@ class BaseGameplayMode:
54
58
  self._small: SmallFontData | None = None
55
59
  self._hud_assets: HudAssets | None = None
56
60
 
61
+ self._default_game_mode_id = int(default_game_mode_id)
57
62
  self._config = config
63
+ self._console = console
58
64
  self._base_dir = config.path.parent if config is not None else Path.cwd()
59
65
 
60
66
  self.close_requested = False
@@ -90,6 +96,74 @@ class BaseGameplayMode:
90
96
  self._last_dt_ms = 0.0
91
97
  self._screen_fade: _ScreenFade | None = None
92
98
 
99
+ def _cvar_float(self, name: str, default: float = 0.0) -> float:
100
+ console = self._console
101
+ if console is None:
102
+ return float(default)
103
+ cvar = console.cvars.get(name)
104
+ if cvar is None:
105
+ return float(default)
106
+ return float(cvar.value_f)
107
+
108
+ def _hud_small_indicators(self) -> bool:
109
+ return self._cvar_float("cv_uiSmallIndicators", 0.0) != 0.0
110
+
111
+ def _config_game_mode_id(self) -> int:
112
+ config = self._config
113
+ if config is None:
114
+ return int(self._default_game_mode_id)
115
+ try:
116
+ value = config.data.get("game_mode", self._default_game_mode_id)
117
+ return int(value or self._default_game_mode_id)
118
+ except Exception:
119
+ return int(self._default_game_mode_id)
120
+
121
+ def _draw_target_health_bar(self, *, alpha: float = 1.0) -> None:
122
+ creatures = getattr(self._creatures, "entries", [])
123
+ if not creatures:
124
+ return
125
+
126
+ if perk_count_get(self._player, PerkId.DOCTOR) <= 0:
127
+ return
128
+
129
+ target_idx = _creature_find_in_radius(
130
+ creatures,
131
+ pos_x=float(getattr(self._player, "aim_x", 0.0)),
132
+ pos_y=float(getattr(self._player, "aim_y", 0.0)),
133
+ radius=12.0,
134
+ start_index=0,
135
+ )
136
+ if target_idx == -1:
137
+ return
138
+
139
+ creature = creatures[target_idx]
140
+ if not bool(getattr(creature, "active", False)):
141
+ return
142
+ hp = float(getattr(creature, "hp", 0.0))
143
+ max_hp = float(getattr(creature, "max_hp", 0.0))
144
+ if hp <= 0.0 or max_hp <= 0.0:
145
+ return
146
+
147
+ ratio = hp / max_hp
148
+ if ratio < 0.0:
149
+ ratio = 0.0
150
+ if ratio > 1.0:
151
+ ratio = 1.0
152
+
153
+ x0, y0 = self._world.world_to_screen(float(creature.x) - 32.0, float(creature.y) + 32.0)
154
+ x1, _y1 = self._world.world_to_screen(float(creature.x) + 32.0, float(creature.y) + 32.0)
155
+ width = float(x1) - float(x0)
156
+ if width <= 1e-3:
157
+ return
158
+ draw_target_health_bar(
159
+ x=float(x0),
160
+ y=float(y0),
161
+ width=width,
162
+ ratio=ratio,
163
+ alpha=float(alpha),
164
+ scale=width / 64.0,
165
+ )
166
+
93
167
  def _bind_world(self) -> None:
94
168
  self._state = self._world.state
95
169
  self._creatures = self._world.creatures