crimsonland 0.1.0.dev2__tar.gz → 0.1.0.dev8__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.dev2 → crimsonland-0.1.0.dev8}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/pyproject.toml +1 -1
  3. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/audio_router.py +15 -3
  4. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/creatures/anim.py +1 -0
  5. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/demo.py +2 -2
  6. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/effects.py +1 -1
  7. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/game.py +5 -0
  8. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/game_world.py +3 -14
  9. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/gameplay.py +138 -63
  10. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/modes/base_gameplay_mode.py +75 -1
  11. crimsonland-0.1.0.dev8/src/crimson/modes/quest_mode.py +929 -0
  12. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/modes/rush_mode.py +12 -3
  13. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/modes/survival_mode.py +13 -2
  14. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/modes/tutorial_mode.py +12 -1
  15. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/modes/typo_mode.py +12 -4
  16. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/projectiles.py +318 -61
  17. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/render/world_renderer.py +789 -46
  18. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/sim/world_defs.py +16 -5
  19. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/sim/world_state.py +1 -0
  20. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/ui/hud.py +274 -51
  21. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/arsenal_debug.py +57 -1
  22. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/player.py +3 -2
  23. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/projectile_fx.py +1 -0
  24. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/projectile_render_debug.py +9 -2
  25. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/console.py +14 -0
  26. crimsonland-0.1.0.dev2/src/crimson/modes/quest_mode.py +0 -502
  27. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/__init__.py +0 -0
  28. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/assets_fetch.py +0 -0
  29. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/atlas.py +0 -0
  30. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/bonuses.py +0 -0
  31. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/camera.py +0 -0
  32. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/cli.py +0 -0
  33. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/creatures/__init__.py +0 -0
  34. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/creatures/ai.py +0 -0
  35. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/creatures/damage.py +0 -0
  36. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/creatures/runtime.py +0 -0
  37. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/creatures/spawn.py +0 -0
  38. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/debug.py +0 -0
  39. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/demo_trial.py +0 -0
  40. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/effects_atlas.py +0 -0
  41. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/__init__.py +0 -0
  42. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/assets.py +0 -0
  43. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/boot.py +0 -0
  44. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/menu.py +0 -0
  45. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/panels/__init__.py +0 -0
  46. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/panels/base.py +0 -0
  47. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/panels/controls.py +0 -0
  48. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/panels/mods.py +0 -0
  49. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/panels/options.py +0 -0
  50. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/panels/play_game.py +0 -0
  51. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/panels/stats.py +0 -0
  52. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/frontend/transitions.py +0 -0
  53. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/game_modes.py +0 -0
  54. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/input_codes.py +0 -0
  55. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/modes/__init__.py +0 -0
  56. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/paths.py +0 -0
  57. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/perks.py +0 -0
  58. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/persistence/__init__.py +0 -0
  59. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/persistence/highscores.py +0 -0
  60. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/persistence/save_status.py +0 -0
  61. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/player_damage.py +0 -0
  62. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/__init__.py +0 -0
  63. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/helpers.py +0 -0
  64. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/registry.py +0 -0
  65. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/results.py +0 -0
  66. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/runtime.py +0 -0
  67. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/tier1.py +0 -0
  68. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/tier2.py +0 -0
  69. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/tier3.py +0 -0
  70. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/tier4.py +0 -0
  71. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/tier5.py +0 -0
  72. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/timeline.py +0 -0
  73. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/quests/types.py +0 -0
  74. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/render/__init__.py +0 -0
  75. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/render/terrain_fx.py +0 -0
  76. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/sim/__init__.py +0 -0
  77. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/terrain_assets.py +0 -0
  78. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/tutorial/__init__.py +0 -0
  79. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/tutorial/timeline.py +0 -0
  80. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/typo/__init__.py +0 -0
  81. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/typo/names.py +0 -0
  82. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/typo/player.py +0 -0
  83. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/typo/spawns.py +0 -0
  84. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/typo/typing.py +0 -0
  85. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/ui/__init__.py +0 -0
  86. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/ui/cursor.py +0 -0
  87. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/ui/demo_trial_overlay.py +0 -0
  88. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/ui/game_over.py +0 -0
  89. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/ui/perk_menu.py +0 -0
  90. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/__init__.py +0 -0
  91. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/aim_debug.py +0 -0
  92. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/animations.py +0 -0
  93. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/audio_bootstrap.py +0 -0
  94. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/bonuses.py +0 -0
  95. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/camera_debug.py +0 -0
  96. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/camera_shake.py +0 -0
  97. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/corpse_stamp_debug.py +0 -0
  98. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/decals_debug.py +0 -0
  99. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/empty.py +0 -0
  100. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/fonts.py +0 -0
  101. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/game_over.py +0 -0
  102. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/ground.py +0 -0
  103. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/lighting_debug.py +0 -0
  104. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/particles.py +0 -0
  105. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/perk_menu_debug.py +0 -0
  106. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/perks.py +0 -0
  107. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/player_sprite_debug.py +0 -0
  108. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/projectiles.py +0 -0
  109. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/quest_title_overlay.py +0 -0
  110. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/registry.py +0 -0
  111. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/rush.py +0 -0
  112. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/small_font_debug.py +0 -0
  113. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/spawn_plan.py +0 -0
  114. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/sprites.py +0 -0
  115. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/survival.py +0 -0
  116. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/terrain.py +0 -0
  117. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/ui.py +0 -0
  118. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/views/wicons.py +0 -0
  119. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/weapon_sfx.py +0 -0
  120. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/crimson/weapons.py +0 -0
  121. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/__init__.py +0 -0
  122. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/app.py +0 -0
  123. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/assets.py +0 -0
  124. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/audio.py +0 -0
  125. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/config.py +0 -0
  126. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/fonts/__init__.py +0 -0
  127. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/fonts/grim_mono.py +0 -0
  128. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/fonts/small.py +0 -0
  129. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/input.py +0 -0
  130. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/jaz.py +0 -0
  131. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/math.py +0 -0
  132. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/music.py +0 -0
  133. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/paq.py +0 -0
  134. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/rand.py +0 -0
  135. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/sfx.py +0 -0
  136. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/sfx_map.py +0 -0
  137. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/src/grim/terrain_render.py +0 -0
  138. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev8}/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.dev2
3
+ Version: 0.1.0.dev8
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.dev2"
3
+ version = "0.1.0.dev8"
4
4
  requires-python = ">=3.13"
5
5
  dependencies = [
6
6
  "construct>=2.10.70",
@@ -9,7 +9,7 @@ from grim.audio import AudioState, play_sfx, trigger_game_tune
9
9
  from .creatures.spawn import CreatureTypeId
10
10
  from .game_modes import GameMode
11
11
  from .weapon_sfx import resolve_weapon_sfx_ref
12
- from .weapons import WEAPON_BY_ID
12
+ from .weapons import WEAPON_BY_ID, WeaponId
13
13
 
14
14
  _MAX_HIT_SFX_PER_FRAME = 4
15
15
  _MAX_DEATH_SFX_PER_FRAME = 3
@@ -96,7 +96,17 @@ class AudioRouter:
96
96
  return
97
97
 
98
98
  if int(getattr(player, "shot_seq", 0)) > int(prev_shot_seq):
99
- self.play_sfx(resolve_weapon_sfx_ref(weapon.fire_sound))
99
+ if float(getattr(player, "fire_bullets_timer", 0.0)) > 0.0:
100
+ # player_update (crimsonland.exe): when Fire Bullets is active, the regular per-weapon
101
+ # shot sfx is suppressed and replaced by Fire Bullets + Plasma Minigun fire sfx.
102
+ fire_bullets = WEAPON_BY_ID.get(int(WeaponId.FIRE_BULLETS))
103
+ plasma_minigun = WEAPON_BY_ID.get(int(WeaponId.PLASMA_MINIGUN))
104
+ if fire_bullets is not None:
105
+ self.play_sfx(resolve_weapon_sfx_ref(fire_bullets.fire_sound))
106
+ if plasma_minigun is not None:
107
+ self.play_sfx(resolve_weapon_sfx_ref(plasma_minigun.fire_sound))
108
+ else:
109
+ self.play_sfx(resolve_weapon_sfx_ref(weapon.fire_sound))
100
110
 
101
111
  reload_active = bool(getattr(player, "reload_active", False))
102
112
  reload_timer = float(getattr(player, "reload_timer", 0.0))
@@ -111,7 +121,9 @@ class AudioRouter:
111
121
  beam_types: frozenset[int],
112
122
  rand: Callable[[], int],
113
123
  ) -> str | None:
114
- if type_id in beam_types:
124
+ weapon = WEAPON_BY_ID.get(int(type_id))
125
+ ammo_class = weapon.ammo_class if weapon is not None else None
126
+ if ammo_class == 4:
115
127
  return "sfx_shock_hit_01"
116
128
  return self._rand_choice(rand, _BULLET_HIT_SFX)
117
129
 
@@ -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
 
@@ -690,8 +690,8 @@ class DemoView:
690
690
  rl.draw_circle(int(sx), int(sy), radius, rl.Color(200, 120, 255, 255))
691
691
  continue
692
692
  if proj.type_id == 3:
693
- t = _clamp(proj.lifetime, 0.0, 1.0)
694
- radius = proj.speed * t * 80.0
693
+ t = _clamp(proj.vel_x, 0.0, 1.0)
694
+ radius = proj.vel_y * t * 80.0
695
695
  alpha = int((1.0 - t) * 180.0)
696
696
  color = rl.Color(200, 120, 255, alpha)
697
697
  rl.draw_circle_lines(int(sx), int(sy), max(1.0, radius * scale), color)
@@ -261,7 +261,7 @@ class ParticlePool:
261
261
  entry.age = alpha
262
262
  entry.scale_x = shade
263
263
  entry.scale_y = shade
264
- entry.scale_z = shade
264
+ # Native only updates scale_x/scale_y; scale_z stays at its spawn value (1.0).
265
265
 
266
266
  alive = entry.intensity > (0.0 if style == 0 else 0.8)
267
267
  if not alive:
@@ -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,
@@ -31,7 +31,7 @@ from .render.world_renderer import WorldRenderer
31
31
  from .audio_router import AudioRouter
32
32
  from .perks import PerkId
33
33
  from .projectiles import ProjectileTypeId
34
- from .sim.world_defs import BEAM_TYPES, CREATURE_ASSET
34
+ from .sim.world_defs import BEAM_TYPES, CREATURE_ASSET, ION_TYPES
35
35
  from .sim.world_state import ProjectileHit, WorldState
36
36
  from .weapons import WEAPON_TABLE
37
37
  from .game_modes import GameMode
@@ -510,19 +510,8 @@ class GameWorld:
510
510
  pos_y=float(target_y) + dir_y * dist * 20.0,
511
511
  rand=rand,
512
512
  )
513
- elif type_id in BEAM_TYPES:
514
- if self.ground is not None and self.fx_textures is not None:
515
- size = float(int(rand()) % 18 + 18)
516
- rotation = float(int(rand()) % 628) * 0.01
517
- self.fx_queue.add(
518
- effect_id=0x01,
519
- pos_x=float(hit_x),
520
- pos_y=float(hit_y),
521
- width=size,
522
- height=size,
523
- rotation=rotation,
524
- rgba=(0.7, 0.9, 1.0, 1.0),
525
- )
513
+ elif type_id in ION_TYPES:
514
+ pass
526
515
  elif not freeze_active:
527
516
  for _ in range(3):
528
517
  spread = float(int(rand()) % 0x14 - 10) * 0.1
@@ -64,8 +64,8 @@ class PlayerState:
64
64
  health: float = 100.0
65
65
  size: float = 50.0
66
66
 
67
- move_speed_multiplier: float = 2.0
68
- long_distance_runner_timer: float = 0.0
67
+ speed_multiplier: float = 2.0
68
+ move_speed: float = 0.0
69
69
  move_phase: float = 0.0
70
70
  heading: float = 0.0
71
71
  death_timer: float = 16.0
@@ -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)
@@ -358,11 +364,49 @@ class BonusPool:
358
364
  return None
359
365
 
360
366
  rng = state.rng
361
- if rng.rand() % 9 != 1:
362
- if not any(perk_active(player, PerkId.BONUS_MAGNET) for player in players):
363
- return None
364
- if rng.rand() % 10 != 2:
365
- return None
367
+ # Native special-case: while any player has Pistol, 3/4 chance to force a Weapon drop.
368
+ if players and any(int(player.weapon_id) == int(WeaponId.PISTOL) for player in players):
369
+ if (int(rng.rand()) & 3) < 3:
370
+ entry = self.spawn_at_pos(
371
+ pos_x,
372
+ pos_y,
373
+ state=state,
374
+ players=players,
375
+ world_width=world_width,
376
+ world_height=world_height,
377
+ )
378
+ if entry is None:
379
+ return None
380
+
381
+ entry.bonus_id = int(BonusId.WEAPON)
382
+ weapon_id = int(weapon_pick_random_available(state))
383
+ entry.amount = int(weapon_id)
384
+ if weapon_id == int(WeaponId.PISTOL):
385
+ weapon_id = int(weapon_pick_random_available(state))
386
+ entry.amount = int(weapon_id)
387
+
388
+ matches = sum(1 for bonus in self._entries if bonus.bonus_id == entry.bonus_id)
389
+ if matches > 1:
390
+ self._clear_entry(entry)
391
+ return None
392
+
393
+ if entry.amount == int(WeaponId.PISTOL) or (players and perk_active(players[0], PerkId.MY_FAVOURITE_WEAPON)):
394
+ self._clear_entry(entry)
395
+ return None
396
+
397
+ return entry
398
+
399
+ base_roll = int(rng.rand())
400
+ if base_roll % 9 != 1:
401
+ allow_without_magnet = False
402
+ if players and int(players[0].weapon_id) == int(WeaponId.PISTOL):
403
+ allow_without_magnet = int(rng.rand()) % 5 == 1
404
+
405
+ if not allow_without_magnet:
406
+ if not (players and perk_active(players[0], PerkId.BONUS_MAGNET)):
407
+ return None
408
+ if int(rng.rand()) % 10 != 2:
409
+ return None
366
410
 
367
411
  entry = self.spawn_at_pos(
368
412
  pos_x,
@@ -377,11 +421,9 @@ class BonusPool:
377
421
 
378
422
  if entry.bonus_id == int(BonusId.WEAPON):
379
423
  near_sq = BONUS_WEAPON_NEAR_RADIUS * BONUS_WEAPON_NEAR_RADIUS
380
- for player in players:
381
- if _distance_sq(pos_x, pos_y, player.pos_x, player.pos_y) < near_sq:
382
- entry.bonus_id = int(BonusId.POINTS)
383
- entry.amount = 100
384
- break
424
+ if players and _distance_sq(pos_x, pos_y, players[0].pos_x, players[0].pos_y) < near_sq:
425
+ entry.bonus_id = int(BonusId.POINTS)
426
+ entry.amount = 100
385
427
 
386
428
  if entry.bonus_id != int(BonusId.POINTS):
387
429
  matches = sum(1 for bonus in self._entries if bonus.bonus_id == entry.bonus_id)
@@ -390,10 +432,9 @@ class BonusPool:
390
432
  return None
391
433
 
392
434
  if entry.bonus_id == int(BonusId.WEAPON):
393
- for player in players:
394
- if entry.amount == player.weapon_id:
395
- self._clear_entry(entry)
396
- return None
435
+ if players and entry.amount == players[0].weapon_id:
436
+ self._clear_entry(entry)
437
+ return None
397
438
 
398
439
  return entry
399
440
 
@@ -1592,6 +1633,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1592
1633
 
1593
1634
  firing_during_reload = False
1594
1635
  ammo_cost = 1.0
1636
+ is_fire_bullets = float(player.fire_bullets_timer) > 0.0
1595
1637
  if player.reload_timer > 0.0:
1596
1638
  if player.ammo <= 0 and player.experience > 0:
1597
1639
  if perk_active(player, PerkId.REGRESSION_BULLETS):
@@ -1616,7 +1658,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1616
1658
  else:
1617
1659
  return
1618
1660
 
1619
- if player.ammo <= 0 and not firing_during_reload:
1661
+ if player.ammo <= 0 and not firing_during_reload and not is_fire_bullets:
1620
1662
  player_start_reload(player, state)
1621
1663
  return
1622
1664
 
@@ -1624,20 +1666,18 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1624
1666
  fire_bullets_weapon = weapon_entry_for_projectile_type_id(int(ProjectileTypeId.FIRE_BULLETS))
1625
1667
 
1626
1668
  shot_cooldown = float(weapon.shot_cooldown) if weapon.shot_cooldown is not None else 0.0
1627
- spread_inc = float(weapon.spread_heat_inc) if weapon.spread_heat_inc is not None else 0.0
1628
- spread_inc *= 1.3
1629
- if player.fire_bullets_timer > 0.0 and pellet_count == 1 and fire_bullets_weapon is not None:
1669
+ spread_heat_base = float(weapon.spread_heat_inc) if weapon.spread_heat_inc is not None else 0.0
1670
+ if is_fire_bullets and pellet_count == 1 and fire_bullets_weapon is not None and fire_bullets_weapon.spread_heat_inc is not None:
1671
+ spread_heat_base = float(fire_bullets_weapon.spread_heat_inc)
1672
+
1673
+ if is_fire_bullets and pellet_count == 1 and fire_bullets_weapon is not None:
1630
1674
  shot_cooldown = (
1631
1675
  float(fire_bullets_weapon.shot_cooldown)
1632
1676
  if fire_bullets_weapon.shot_cooldown is not None
1633
1677
  else 0.0
1634
1678
  )
1635
- spread_inc = (
1636
- float(fire_bullets_weapon.spread_heat_inc)
1637
- if fire_bullets_weapon.spread_heat_inc is not None
1638
- else 0.0
1639
- )
1640
- spread_inc *= 1.3
1679
+
1680
+ spread_inc = spread_heat_base * 1.3
1641
1681
 
1642
1682
  if perk_active(player, PerkId.FASTSHOT):
1643
1683
  shot_cooldown *= 0.88
@@ -1657,6 +1697,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1657
1697
  aim_jitter_x = aim_x + math.cos(dir_angle) * offset
1658
1698
  aim_jitter_y = aim_y + math.sin(dir_angle) * offset
1659
1699
  shot_angle = math.atan2(aim_jitter_y - float(player.pos_y), aim_jitter_x - float(player.pos_x)) + math.pi / 2.0
1700
+ particle_angle = shot_angle - math.pi / 2.0
1660
1701
 
1661
1702
  muzzle_x = player.pos_x + player.aim_dir_x * 16.0
1662
1703
  muzzle_y = player.pos_y + player.aim_dir_y * 16.0
@@ -1676,14 +1717,12 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1676
1717
  return 0.0013
1677
1718
  return 0.0015
1678
1719
 
1679
- if player.fire_bullets_timer > 0.0:
1720
+ if is_fire_bullets:
1680
1721
  pellets = max(1, int(pellet_count))
1681
1722
  shot_count = pellets
1682
1723
  meta = _projectile_meta_for_type_id(ProjectileTypeId.FIRE_BULLETS)
1683
1724
  for _ in range(pellets):
1684
- angle = shot_angle
1685
- if pellets > 1:
1686
- angle += float(int(state.rng.rand()) % 200 - 100) * 0.0015
1725
+ angle = shot_angle + float(int(state.rng.rand()) % 200 - 100) * 0.0015
1687
1726
  state.projectiles.spawn(
1688
1727
  pos_x=muzzle_x,
1689
1728
  pos_y=muzzle_y,
@@ -1713,16 +1752,16 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1713
1752
  state.secondary_projectiles.spawn(pos_x=muzzle_x, pos_y=muzzle_y, angle=shot_angle, type_id=4, owner_id=owner_id)
1714
1753
  elif weapon_id == WeaponId.FLAMETHROWER:
1715
1754
  # Flamethrower -> fast particle weapon (style 0), fractional ammo drain.
1716
- state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=dir_angle, intensity=1.0, owner_id=owner_id)
1755
+ state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=particle_angle, intensity=1.0, owner_id=owner_id)
1717
1756
  ammo_cost = 0.1
1718
1757
  elif weapon_id == WeaponId.BLOW_TORCH:
1719
1758
  # Blow Torch -> fast particle weapon (style 1), fractional ammo drain.
1720
- particle_id = state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=dir_angle, intensity=1.0, owner_id=owner_id)
1759
+ particle_id = state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=particle_angle, intensity=1.0, owner_id=owner_id)
1721
1760
  state.particles.entries[particle_id].style_id = 1
1722
1761
  ammo_cost = 0.05
1723
1762
  elif weapon_id == WeaponId.HR_FLAMER:
1724
1763
  # HR Flamer -> fast particle weapon (style 2), fractional ammo drain.
1725
- particle_id = state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=dir_angle, intensity=1.0, owner_id=owner_id)
1764
+ particle_id = state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=particle_angle, intensity=1.0, owner_id=owner_id)
1726
1765
  state.particles.entries[particle_id].style_id = 2
1727
1766
  ammo_cost = 0.1
1728
1767
  elif weapon_id == WeaponId.BUBBLEGUN:
@@ -1833,15 +1872,17 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1833
1872
  player.spread_heat = min(0.48, max(0.0, player.spread_heat + spread_inc))
1834
1873
 
1835
1874
  muzzle_inc = float(weapon.spread_heat_inc) if weapon.spread_heat_inc is not None else 0.0
1875
+ if is_fire_bullets and pellet_count == 1 and fire_bullets_weapon is not None and fire_bullets_weapon.spread_heat_inc is not None:
1876
+ muzzle_inc = float(fire_bullets_weapon.spread_heat_inc)
1836
1877
  player.muzzle_flash_alpha = min(1.0, player.muzzle_flash_alpha)
1837
1878
  player.muzzle_flash_alpha = min(1.0, player.muzzle_flash_alpha + muzzle_inc)
1838
1879
  player.muzzle_flash_alpha = min(0.8, player.muzzle_flash_alpha)
1839
1880
 
1840
1881
  player.shot_seq += 1
1841
- if not firing_during_reload and state.bonuses.reflex_boost <= 0.0:
1882
+ if (not firing_during_reload) and state.bonuses.reflex_boost <= 0.0 and not is_fire_bullets:
1842
1883
  player.ammo = max(0.0, float(player.ammo) - float(ammo_cost))
1843
- if player.ammo <= 0.0:
1844
- player_start_reload(player, state)
1884
+ if (not firing_during_reload) and player.ammo <= 0.0 and player.reload_timer <= 0.0:
1885
+ player_start_reload(player, state)
1845
1886
 
1846
1887
 
1847
1888
  def player_update(player: PlayerState, input_state: PlayerInput, dt: float, state: GameplayState, *, world_size: float = 1024.0) -> None:
@@ -1885,35 +1926,50 @@ def player_update(player: PlayerState, input_state: PlayerInput, dt: float, stat
1885
1926
  player.aim_heading = math.atan2(aim_dir_y, aim_dir_x) + math.pi / 2.0
1886
1927
 
1887
1928
  # Movement.
1888
- move_x, move_y = _normalize(float(input_state.move_x), float(input_state.move_y))
1889
- moving = move_x != 0.0 or move_y != 0.0
1890
- runner_bonus = 0.0
1891
- if perk_active(player, PerkId.LONG_DISTANCE_RUNNER):
1892
- if moving:
1893
- player.long_distance_runner_timer = min(1.2, float(player.long_distance_runner_timer) + dt)
1929
+ raw_move_x = float(input_state.move_x)
1930
+ raw_move_y = float(input_state.move_y)
1931
+ raw_mag = math.hypot(raw_move_x, raw_move_y)
1932
+ moving_input = raw_mag > 0.2
1933
+
1934
+ if moving_input:
1935
+ inv = 1.0 / raw_mag if raw_mag > 1e-9 else 0.0
1936
+ move_x = raw_move_x * inv
1937
+ move_y = raw_move_y * inv
1938
+ player.heading = math.atan2(move_y, move_x) + math.pi / 2.0
1939
+ if perk_active(player, PerkId.LONG_DISTANCE_RUNNER):
1940
+ if player.move_speed < 2.0:
1941
+ player.move_speed = float(player.move_speed + dt * 4.0)
1942
+ player.move_speed = float(player.move_speed + dt)
1943
+ if player.move_speed > 2.8:
1944
+ player.move_speed = 2.8
1894
1945
  else:
1895
- player.long_distance_runner_timer = max(0.0, float(player.long_distance_runner_timer) - dt * 15.0)
1896
- runner_bonus = max(0.0, min(0.8, float(player.long_distance_runner_timer) - 0.4))
1946
+ player.move_speed = float(player.move_speed + dt * 5.0)
1947
+ if player.move_speed > 2.0:
1948
+ player.move_speed = 2.0
1897
1949
  else:
1898
- player.long_distance_runner_timer = 0.0
1950
+ player.move_speed = float(player.move_speed - dt * 15.0)
1951
+ if player.move_speed < 0.0:
1952
+ player.move_speed = 0.0
1953
+ move_x = math.cos(player.heading - math.pi / 2.0)
1954
+ move_y = math.sin(player.heading - math.pi / 2.0)
1899
1955
 
1900
- speed_multiplier = float(player.move_speed_multiplier + runner_bonus)
1956
+ if player.weapon_id == WeaponId.MEAN_MINIGUN and player.move_speed > 0.8:
1957
+ player.move_speed = 0.8
1958
+
1959
+ speed_multiplier = float(player.speed_multiplier)
1901
1960
  if player.speed_bonus_timer > 0.0:
1902
1961
  speed_multiplier += 1.0
1903
- speed = 120.0 * speed_multiplier
1962
+
1963
+ speed = player.move_speed * speed_multiplier * 25.0
1964
+ if moving_input:
1965
+ speed *= min(1.0, raw_mag)
1904
1966
  if perk_active(player, PerkId.ALTERNATE_WEAPON):
1905
1967
  speed *= 0.8
1968
+
1906
1969
  player.pos_x = _clamp(player.pos_x + move_x * speed * dt, 0.0, float(world_size))
1907
1970
  player.pos_y = _clamp(player.pos_y + move_y * speed * dt, 0.0, float(world_size))
1908
1971
 
1909
- if moving:
1910
- player.heading = math.atan2(move_y, move_x) + math.pi / 2.0
1911
-
1912
- move_dist = math.hypot(player.pos_x - prev_x, player.pos_y - prev_y)
1913
- if move_dist > 1e-9:
1914
- # Port of `move_phase += frame_dt * move_speed * 19.0` (player_update).
1915
- move_speed = move_dist / dt / 120.0
1916
- player.move_phase += dt * move_speed * 19.0
1972
+ player.move_phase += dt * player.move_speed * 19.0
1917
1973
 
1918
1974
  stationary = abs(player.pos_x - prev_x) <= 1e-9 and abs(player.pos_y - prev_y) <= 1e-9
1919
1975
  reload_scale = 1.0
@@ -2306,8 +2362,8 @@ def bonus_apply(
2306
2362
  return
2307
2363
 
2308
2364
 
2309
- def bonus_hud_update(state: GameplayState, players: list[PlayerState]) -> None:
2310
- """Refresh HUD slots based on current timer values."""
2365
+ def bonus_hud_update(state: GameplayState, players: list[PlayerState], *, dt: float = 0.0) -> None:
2366
+ """Refresh HUD slots based on current timer values + advance slide animation."""
2311
2367
 
2312
2368
  def _timer_value(ref: _TimerRef | None) -> float:
2313
2369
  if ref is None:
@@ -2321,16 +2377,35 @@ def bonus_hud_update(state: GameplayState, players: list[PlayerState]) -> None:
2321
2377
  return float(getattr(players[idx], ref.key, 0.0) or 0.0)
2322
2378
  return 0.0
2323
2379
 
2324
- for slot in state.bonus_hud.slots:
2380
+ player_count = len(players)
2381
+ dt = max(0.0, float(dt))
2382
+
2383
+ for slot_index, slot in enumerate(state.bonus_hud.slots):
2325
2384
  if not slot.active:
2326
2385
  continue
2327
- timer = _timer_value(slot.timer_ref)
2328
- if slot.timer_ref_alt is not None:
2329
- timer = max(timer, _timer_value(slot.timer_ref_alt))
2330
- if timer <= 0.0:
2386
+ timer = max(0.0, _timer_value(slot.timer_ref))
2387
+ 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
2388
+ slot.timer_value = float(timer)
2389
+ slot.timer_value_alt = float(timer_alt)
2390
+
2391
+ if timer > 0.0 or timer_alt > 0.0:
2392
+ slot.slide_x += dt * 350.0
2393
+ else:
2394
+ slot.slide_x -= dt * 320.0
2395
+
2396
+ if slot.slide_x > -2.0:
2397
+ slot.slide_x = -2.0
2398
+
2399
+ if slot.slide_x < -184.0 and not any(other.active for other in state.bonus_hud.slots[slot_index + 1 :]):
2331
2400
  slot.active = False
2401
+ slot.bonus_id = 0
2402
+ slot.label = ""
2403
+ slot.icon_id = -1
2404
+ slot.slide_x = -184.0
2332
2405
  slot.timer_ref = None
2333
2406
  slot.timer_ref_alt = None
2407
+ slot.timer_value = 0.0
2408
+ slot.timer_value_alt = 0.0
2334
2409
 
2335
2410
 
2336
2411
  def bonus_telekinetic_update(
@@ -2445,6 +2520,6 @@ def bonus_update(
2445
2520
  state.bonuses.freeze = max(0.0, state.bonuses.freeze - dt)
2446
2521
 
2447
2522
  if update_hud:
2448
- bonus_hud_update(state, players)
2523
+ bonus_hud_update(state, players, dt=dt)
2449
2524
 
2450
2525
  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