crimsonland 0.1.0.dev10__tar.gz → 0.1.0.dev12__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 (140) hide show
  1. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/pyproject.toml +1 -1
  3. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/creatures/runtime.py +15 -0
  4. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/demo.py +47 -38
  5. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/effects.py +46 -1
  6. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/boot.py +2 -1
  7. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/menu.py +6 -27
  8. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/panels/base.py +13 -3
  9. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/panels/controls.py +1 -3
  10. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/panels/mods.py +1 -3
  11. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/panels/options.py +35 -42
  12. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/panels/play_game.py +78 -70
  13. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/panels/stats.py +1 -3
  14. crimsonland-0.1.0.dev12/src/crimson/frontend/pause_menu.py +425 -0
  15. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/game.py +315 -446
  16. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/gameplay.py +37 -8
  17. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/modes/base_gameplay_mode.py +3 -0
  18. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/modes/quest_mode.py +45 -36
  19. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/modes/rush_mode.py +4 -1
  20. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/modes/survival_mode.py +6 -2
  21. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/modes/tutorial_mode.py +6 -2
  22. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/modes/typo_mode.py +4 -1
  23. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/persistence/highscores.py +6 -2
  24. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/render/world_renderer.py +1 -1
  25. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/sim/world_state.py +8 -1
  26. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/typo/spawns.py +3 -4
  27. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/ui/demo_trial_overlay.py +3 -3
  28. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/ui/game_over.py +18 -2
  29. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/ui/perk_menu.py +106 -14
  30. crimsonland-0.1.0.dev12/src/crimson/ui/quest_results.py +663 -0
  31. crimsonland-0.1.0.dev12/src/crimson/ui/shadow.py +39 -0
  32. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/particles.py +1 -1
  33. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/weapons.py +110 -110
  34. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/app.py +3 -0
  35. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/__init__.py +0 -0
  36. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/assets_fetch.py +0 -0
  37. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/atlas.py +0 -0
  38. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/audio_router.py +0 -0
  39. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/bonuses.py +0 -0
  40. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/camera.py +0 -0
  41. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/cli.py +0 -0
  42. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/creatures/__init__.py +0 -0
  43. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/creatures/ai.py +0 -0
  44. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/creatures/anim.py +0 -0
  45. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/creatures/damage.py +0 -0
  46. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/creatures/spawn.py +0 -0
  47. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/debug.py +0 -0
  48. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/demo_trial.py +0 -0
  49. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/effects_atlas.py +0 -0
  50. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/__init__.py +0 -0
  51. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/assets.py +0 -0
  52. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/panels/__init__.py +0 -0
  53. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/frontend/transitions.py +0 -0
  54. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/game_modes.py +0 -0
  55. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/game_world.py +0 -0
  56. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/input_codes.py +0 -0
  57. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/modes/__init__.py +0 -0
  58. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/paths.py +0 -0
  59. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/perks.py +0 -0
  60. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/persistence/__init__.py +0 -0
  61. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/persistence/save_status.py +0 -0
  62. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/player_damage.py +0 -0
  63. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/projectiles.py +0 -0
  64. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/__init__.py +0 -0
  65. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/helpers.py +0 -0
  66. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/registry.py +0 -0
  67. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/results.py +0 -0
  68. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/runtime.py +0 -0
  69. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/tier1.py +0 -0
  70. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/tier2.py +0 -0
  71. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/tier3.py +0 -0
  72. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/tier4.py +0 -0
  73. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/tier5.py +0 -0
  74. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/timeline.py +0 -0
  75. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/quests/types.py +0 -0
  76. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/render/__init__.py +0 -0
  77. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/render/terrain_fx.py +0 -0
  78. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/sim/__init__.py +0 -0
  79. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/sim/world_defs.py +0 -0
  80. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/terrain_assets.py +0 -0
  81. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/tutorial/__init__.py +0 -0
  82. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/tutorial/timeline.py +0 -0
  83. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/typo/__init__.py +0 -0
  84. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/typo/names.py +0 -0
  85. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/typo/player.py +0 -0
  86. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/typo/typing.py +0 -0
  87. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/ui/__init__.py +0 -0
  88. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/ui/cursor.py +0 -0
  89. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/ui/hud.py +0 -0
  90. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/__init__.py +0 -0
  91. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/aim_debug.py +0 -0
  92. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/animations.py +0 -0
  93. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/arsenal_debug.py +0 -0
  94. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/audio_bootstrap.py +0 -0
  95. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/bonuses.py +0 -0
  96. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/camera_debug.py +0 -0
  97. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/camera_shake.py +0 -0
  98. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/corpse_stamp_debug.py +0 -0
  99. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/decals_debug.py +0 -0
  100. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/empty.py +0 -0
  101. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/fonts.py +0 -0
  102. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/game_over.py +0 -0
  103. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/ground.py +0 -0
  104. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/lighting_debug.py +0 -0
  105. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/perk_menu_debug.py +0 -0
  106. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/perks.py +0 -0
  107. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/player.py +0 -0
  108. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/player_sprite_debug.py +0 -0
  109. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/projectile_fx.py +0 -0
  110. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/projectile_render_debug.py +0 -0
  111. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/projectiles.py +0 -0
  112. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/quest_title_overlay.py +0 -0
  113. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/registry.py +0 -0
  114. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/rush.py +0 -0
  115. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/small_font_debug.py +0 -0
  116. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/spawn_plan.py +0 -0
  117. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/sprites.py +0 -0
  118. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/survival.py +0 -0
  119. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/terrain.py +0 -0
  120. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/ui.py +0 -0
  121. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/views/wicons.py +0 -0
  122. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/crimson/weapon_sfx.py +0 -0
  123. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/__init__.py +0 -0
  124. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/assets.py +0 -0
  125. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/audio.py +0 -0
  126. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/config.py +0 -0
  127. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/console.py +0 -0
  128. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/fonts/__init__.py +0 -0
  129. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/fonts/grim_mono.py +0 -0
  130. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/fonts/small.py +0 -0
  131. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/input.py +0 -0
  132. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/jaz.py +0 -0
  133. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/math.py +0 -0
  134. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/music.py +0 -0
  135. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/paq.py +0 -0
  136. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/rand.py +0 -0
  137. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/sfx.py +0 -0
  138. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/sfx_map.py +0 -0
  139. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/src/grim/terrain_render.py +0 -0
  140. {crimsonland-0.1.0.dev10 → crimsonland-0.1.0.dev12}/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.dev10
3
+ Version: 0.1.0.dev12
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.dev10"
3
+ version = "0.1.0.dev12"
4
4
  requires-python = ">=3.13"
5
5
  dependencies = [
6
6
  "construct>=2.10.70",
@@ -667,6 +667,21 @@ class CreaturePool:
667
667
  elif perk_active(player, PerkId.VEINS_OF_POISON):
668
668
  creature.flags |= CreatureFlags.SELF_DAMAGE_TICK
669
669
  player_take_damage(state, player, float(creature.contact_damage), dt=dt, rand=rand)
670
+ if fx_queue is not None:
671
+ dx = float(player.pos_x) - float(creature.x)
672
+ dy = float(player.pos_y) - float(creature.y)
673
+ dist = math.hypot(dx, dy)
674
+ if dist > 1e-9:
675
+ dx /= dist
676
+ dy /= dist
677
+ else:
678
+ dx = 0.0
679
+ dy = 0.0
680
+ fx_queue.add_random(
681
+ pos_x=float(player.pos_x) + dx * 3.0,
682
+ pos_y=float(player.pos_y) + dy * 3.0,
683
+ rand=rand,
684
+ )
670
685
 
671
686
  if (
672
687
  bool(player.plaguebearer_active)
@@ -17,6 +17,7 @@ from grim.rand import Crand
17
17
  from .game_world import GameWorld
18
18
  from .gameplay import PlayerInput, PlayerState, weapon_assign_player
19
19
  from .ui.cursor import draw_menu_cursor
20
+ from .ui.perk_menu import UiButtonState, UiButtonTextureSet, button_draw, button_update, button_width
20
21
  from .weapons import WEAPON_TABLE, WeaponId, projectile_type_id_from_weapon_id, weapon_entry_for_projectile_type_id
21
22
 
22
23
  WORLD_SIZE = 1024.0
@@ -123,6 +124,8 @@ class DemoView:
123
124
  self._small_font: SmallFontData | None = None
124
125
  self._purchase_active = False
125
126
  self._purchase_url_opened = False
127
+ self._purchase_button = UiButtonState("Purchase", force_wide=True)
128
+ self._maybe_later_button = UiButtonState("Maybe later", force_wide=True)
126
129
  self._spawn_rng = Crand(0)
127
130
 
128
131
  def open(self) -> None:
@@ -131,6 +134,8 @@ class DemoView:
131
134
  self._upsell_pulse_ms = 0
132
135
  self._purchase_active = False
133
136
  self._purchase_url_opened = False
137
+ self._purchase_button = UiButtonState("Purchase", force_wide=True)
138
+ self._maybe_later_button = UiButtonState("Maybe later", force_wide=True)
134
139
  self._variant_index = 0
135
140
  self._demo_variant_index = 0
136
141
  self._quest_spawn_timeline_ms = 0
@@ -166,7 +171,7 @@ class DemoView:
166
171
 
167
172
  if self._purchase_active:
168
173
  self._upsell_pulse_ms += frame_dt_ms
169
- self._update_purchase_screen()
174
+ self._update_purchase_screen(frame_dt_ms)
170
175
  self._quest_spawn_timeline_ms += frame_dt_ms
171
176
  if self._quest_spawn_timeline_ms > self._demo_time_limit_ms:
172
177
  # demo_purchase_screen_update restarts the demo once the purchase screen
@@ -214,6 +219,8 @@ class DemoView:
214
219
  self._quest_spawn_timeline_ms = 0
215
220
  self._demo_time_limit_ms = max(0, int(limit_ms))
216
221
  self._purchase_url_opened = False
222
+ self._purchase_button = UiButtonState("Purchase", force_wide=True)
223
+ self._maybe_later_button = UiButtonState("Maybe later", force_wide=True)
217
224
 
218
225
  def _ensure_small_font(self) -> SmallFontData:
219
226
  if self._small_font is not None:
@@ -230,18 +237,20 @@ class DemoView:
230
237
  return 128.0
231
238
  return 0.0
232
239
 
233
- def _update_purchase_screen(self) -> None:
240
+ def _update_purchase_screen(self, dt_ms: int) -> None:
241
+ dt_ms = max(0, int(dt_ms))
234
242
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
235
243
  self._purchase_active = False
236
244
  self._finished = True
237
245
  return
238
246
 
239
- small = self._ensure_small_font()
240
- # ui_button_update uses the medium (145px wide) button sprite here (the per-button
241
- # "small" flag at +0x14 is 0 for both purchase/maybe-later globals).
242
- button_tex = self._ensure_cache().get_or_load("ui_button_md", "ui/ui_button_145x32.jaz").texture
243
-
244
- if button_tex is None:
247
+ font = self._ensure_small_font()
248
+ cache = self._ensure_cache()
249
+ textures = UiButtonTextureSet(
250
+ button_sm=cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture,
251
+ button_md=cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture,
252
+ )
253
+ if textures.button_sm is None and textures.button_md is None:
245
254
  return
246
255
 
247
256
  w = float(self._state.config.screen_width)
@@ -252,14 +261,18 @@ class DemoView:
252
261
  purchase_y = button_base_y + 50.0
253
262
  maybe_y = button_base_y + 90.0
254
263
 
255
- purchase_rect = rl.Rectangle(button_x, purchase_y, float(button_tex.width), float(button_tex.height))
256
- maybe_rect = rl.Rectangle(button_x, maybe_y, float(button_tex.width), float(button_tex.height))
257
-
258
264
  mouse = rl.get_mouse_position()
259
- if (
260
- purchase_rect.x <= mouse.x <= purchase_rect.x + purchase_rect.width
261
- and purchase_rect.y <= mouse.y <= purchase_rect.y + purchase_rect.height
262
- and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
265
+ click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
266
+ scale = 1.0
267
+ button_w = button_width(font, self._purchase_button.label, scale=scale, force_wide=self._purchase_button.force_wide)
268
+ if button_update(
269
+ self._purchase_button,
270
+ x=float(button_x),
271
+ y=float(purchase_y),
272
+ width=float(button_w),
273
+ dt_ms=float(dt_ms),
274
+ mouse=mouse,
275
+ click=bool(click),
263
276
  ):
264
277
  if not self._purchase_url_opened:
265
278
  self._purchase_url_opened = True
@@ -272,10 +285,14 @@ class DemoView:
272
285
  if hasattr(self._state, "quit_requested"):
273
286
  self._state.quit_requested = True
274
287
 
275
- if (
276
- maybe_rect.x <= mouse.x <= maybe_rect.x + maybe_rect.width
277
- and maybe_rect.y <= mouse.y <= maybe_rect.y + maybe_rect.height
278
- and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
288
+ if button_update(
289
+ self._maybe_later_button,
290
+ x=float(button_x),
291
+ y=float(maybe_y),
292
+ width=float(button_w),
293
+ dt_ms=float(dt_ms),
294
+ mouse=mouse,
295
+ click=bool(click),
279
296
  ):
280
297
  self._purchase_active = False
281
298
  self._finished = True
@@ -294,8 +311,8 @@ class DemoView:
294
311
  if hasattr(self._state, "quit_requested"):
295
312
  self._state.quit_requested = True
296
313
 
297
- # Keep small referenced to avoid unused warnings if this method grows.
298
- _ = small
314
+ # Keep referenced to avoid unused warnings if this method grows.
315
+ _ = textures
299
316
 
300
317
  def _draw_purchase_screen(self) -> None:
301
318
  rl.clear_background(rl.BLACK)
@@ -393,29 +410,21 @@ class DemoView:
393
410
 
394
411
  # Buttons on the right.
395
412
  cache = self._ensure_cache()
396
- button_tex = cache.get_or_load("ui_button_md", "ui/ui_button_145x32.jaz").texture
397
- if button_tex is None:
413
+ textures = UiButtonTextureSet(
414
+ button_sm=cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture,
415
+ button_md=cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture,
416
+ )
417
+ if textures.button_sm is None and textures.button_md is None:
398
418
  return
399
419
 
400
420
  button_x = screen_w / 2.0 + 128.0
401
421
  button_base_y = screen_h / 2.0 + 102.0 + wide_shift * 0.3
402
422
  purchase_y = button_base_y + 50.0
403
423
  maybe_y = button_base_y + 90.0
404
- mouse = rl.get_mouse_position()
405
-
406
- def draw_button(texture: rl.Texture2D, label: str, x: float, y0: float) -> None:
407
- hovered = x <= mouse.x <= x + texture.width and y0 <= mouse.y <= y0 + texture.height
408
- tint = rl.Color(255, 255, 255, 255) if hovered else rl.Color(220, 220, 220, 255)
409
- rl.draw_texture(texture, int(x), int(y0), tint)
410
- label_scale = 1.0
411
- text_w = measure_small_text_width(small, label, label_scale)
412
- text_x = x + float(texture.width) * 0.5 - text_w * 0.5 + 1.0
413
- text_y = y0 + 10.0
414
- alpha = 1.0 if hovered else 0.7
415
- draw_small_text(small, label, text_x, text_y, label_scale, rl.Color(255, 255, 255, int(255 * alpha)))
416
-
417
- draw_button(button_tex, "Purchase", button_x, purchase_y)
418
- draw_button(button_tex, "Maybe later", button_x, maybe_y)
424
+ scale = 1.0
425
+ button_w = button_width(small, self._purchase_button.label, scale=scale, force_wide=self._purchase_button.force_wide)
426
+ button_draw(textures, small, self._purchase_button, x=button_x, y=purchase_y, width=button_w, scale=scale)
427
+ button_draw(textures, small, self._maybe_later_button, x=button_x, y=maybe_y, width=button_w, scale=scale)
419
428
 
420
429
  # Demo purchase screen uses menu-style cursor; draw it explicitly since the OS cursor is hidden.
421
430
  particles = cache.get_or_load("particles", "game/particles.jaz").texture
@@ -209,7 +209,7 @@ class ParticlePool:
209
209
 
210
210
  size = float(getattr(creature, "size", 50.0))
211
211
  dist = math.hypot(float(getattr(creature, "x", 0.0)) - pos_x, float(getattr(creature, "y", 0.0)) - pos_y) - radius
212
- threshold = size * 0.142857149 + 3.0
212
+ threshold = size * 0.14285715 + 3.0
213
213
  if threshold < dist:
214
214
  continue
215
215
  return int(creature_idx)
@@ -721,6 +721,51 @@ class EffectPool:
721
721
 
722
722
  self.free(idx)
723
723
 
724
+ def spawn_shell_casing(
725
+ self,
726
+ *,
727
+ pos_x: float,
728
+ pos_y: float,
729
+ aim_heading: float,
730
+ weapon_flags: int,
731
+ rand: Callable[[], int],
732
+ detail_preset: int,
733
+ ) -> None:
734
+ """Port of the casing spawn in `player_update` (effect_id 0x12)."""
735
+
736
+ if (int(weapon_flags) & 0x1) == 0:
737
+ return
738
+
739
+ angle = float(aim_heading) + float(int(rand()) & 0x3F) * 0.01
740
+ speed = float(int(rand()) & 0x3F) * 0.022727273 + 1.0
741
+ vel_x = math.cos(angle) * speed * 100.0
742
+ vel_y = math.sin(angle) * speed * 100.0
743
+
744
+ rotation = float((int(rand()) & 0x3F) - 0x20) * 0.1
745
+ rotation_step = (float(int(rand()) % 0x14) * 0.1 - 1.0) * 14.0
746
+
747
+ self.spawn(
748
+ effect_id=0x12,
749
+ pos_x=float(pos_x),
750
+ pos_y=float(pos_y),
751
+ vel_x=float(vel_x),
752
+ vel_y=float(vel_y),
753
+ rotation=float(rotation),
754
+ scale=1.0,
755
+ half_width=2.0,
756
+ half_height=2.0,
757
+ age=0.0,
758
+ lifetime=0.15,
759
+ flags=0x1C5,
760
+ color_r=1.0,
761
+ color_g=1.0,
762
+ color_b=1.0,
763
+ color_a=0.6,
764
+ rotation_step=float(rotation_step),
765
+ scale_step=0.0,
766
+ detail_preset=int(detail_preset),
767
+ )
768
+
724
769
  def spawn_blood_splatter(
725
770
  self,
726
771
  *,
@@ -118,7 +118,8 @@ MENU_PREP_TEXTURES: tuple[tuple[str, str], ...] = (
118
118
  ("ui_checkOff", "ui/ui_checkOff.jaz"),
119
119
  ("ui_rectOff", "ui/ui_rectOff.jaz"),
120
120
  ("ui_rectOn", "ui/ui_rectOn.jaz"),
121
- ("ui_button_md", "ui/ui_button_145x32.jaz"),
121
+ ("ui_buttonSm", "ui/ui_button_64x32.jaz"),
122
+ ("ui_buttonMd", "ui/ui_button_128x32.jaz"),
122
123
  )
123
124
 
124
125
 
@@ -11,6 +11,7 @@ from grim.audio import play_music, play_sfx, stop_music, update_audio
11
11
  from grim.terrain_render import GroundRenderer
12
12
 
13
13
  from ..ui.cursor import draw_menu_cursor
14
+ from ..ui.shadow import UI_SHADOW_OFFSET, UI_SHADOW_TINT, draw_ui_quad_shadow # noqa: F401
14
15
  from .assets import MenuAssets, _ensure_texture_cache, load_menu_assets
15
16
  from .transitions import _draw_screen_fade
16
17
 
@@ -37,7 +38,10 @@ MENU_ITEM_OFFSET_X = -72.0
37
38
  MENU_ITEM_OFFSET_Y = -60.0
38
39
  MENU_PANEL_WIDTH = 512.0
39
40
  MENU_PANEL_HEIGHT = 256.0
40
- MENU_PANEL_OFFSET_X = 20.0
41
+ # ui_menu_assets_init:
42
+ # - ui_menuPanel starts with offset_x=+20 (ui_element_set_rect)
43
+ # - the menu-panel layout copy applies data_48fdb4 -= 116, so offset_x becomes -96
44
+ MENU_PANEL_OFFSET_X = -96.0
41
45
  MENU_PANEL_OFFSET_Y = -82.0
42
46
  MENU_PANEL_BASE_X = -45.0
43
47
  MENU_PANEL_BASE_Y = 210.0
@@ -48,11 +52,6 @@ MENU_SCALE_SMALL = 0.8
48
52
  MENU_SCALE_LARGE = 1.2
49
53
  MENU_SCALE_SHIFT = 10.0
50
54
 
51
- # ui_element_render (0x446c40): shadow pass uses offset (7, 7), tint 0x44444444, and
52
- # blend factors (src=ZERO, dst=ONE_MINUS_SRC_ALPHA).
53
- UI_SHADOW_OFFSET = 7.0
54
- UI_SHADOW_TINT = rl.Color(0x44, 0x44, 0x44, 0x44)
55
-
56
55
  MENU_SIGN_WIDTH = 573.44
57
56
  MENU_SIGN_HEIGHT = 143.36
58
57
  MENU_SIGN_OFFSET_X = -577.44
@@ -628,27 +627,7 @@ class MenuView:
628
627
  origin: rl.Vector2,
629
628
  rotation_deg: float,
630
629
  ) -> None:
631
- # NOTE: raylib/rlgl tracks custom blend factors as state; some backends
632
- # only apply them when switching the blend mode.
633
- rl.rl_set_blend_factors_separate(
634
- rl.RL_ZERO,
635
- rl.RL_ONE_MINUS_SRC_ALPHA,
636
- rl.RL_ZERO,
637
- rl.RL_ONE,
638
- rl.RL_FUNC_ADD,
639
- rl.RL_FUNC_ADD,
640
- )
641
- rl.begin_blend_mode(rl.BLEND_CUSTOM_SEPARATE)
642
- rl.rl_set_blend_factors_separate(
643
- rl.RL_ZERO,
644
- rl.RL_ONE_MINUS_SRC_ALPHA,
645
- rl.RL_ZERO,
646
- rl.RL_ONE,
647
- rl.RL_FUNC_ADD,
648
- rl.RL_FUNC_ADD,
649
- )
650
- rl.draw_texture_pro(texture, src, dst, origin, rotation_deg, UI_SHADOW_TINT)
651
- rl.end_blend_mode()
630
+ draw_ui_quad_shadow(texture=texture, src=src, dst=dst, origin=origin, rotation_deg=rotation_deg)
652
631
 
653
632
  def _draw_menu_sign(self) -> None:
654
633
  assets = self._assets
@@ -163,9 +163,7 @@ class PanelMenuView:
163
163
  entry.ready_timer_ms = min(0x100, entry.ready_timer_ms + dt_ms)
164
164
 
165
165
  def draw(self) -> None:
166
- rl.clear_background(rl.BLACK)
167
- if self._ground is not None:
168
- self._ground.draw(0.0, 0.0)
166
+ self._draw_background()
169
167
  _draw_screen_fade(self._state)
170
168
  assets = self._assets
171
169
  entry = self._entry
@@ -206,8 +204,20 @@ class PanelMenuView:
206
204
  return _ensure_texture_cache(self._state)
207
205
 
208
206
  def _init_ground(self) -> None:
207
+ if self._state.pause_background is not None:
208
+ self._ground = None
209
+ return
209
210
  self._ground = ensure_menu_ground(self._state)
210
211
 
212
+ def _draw_background(self) -> None:
213
+ rl.clear_background(rl.BLACK)
214
+ pause_background = self._state.pause_background
215
+ if pause_background is not None:
216
+ pause_background.draw_pause_background()
217
+ return
218
+ if self._ground is not None:
219
+ self._ground.draw(0.0, 0.0)
220
+
211
221
  def _draw_panel(self) -> None:
212
222
  assets = self._assets
213
223
  if assets is None or assets.panel is None:
@@ -32,9 +32,7 @@ class ControlsMenuView(PanelMenuView):
32
32
  self._lines = self._build_lines()
33
33
 
34
34
  def draw(self) -> None:
35
- rl.clear_background(rl.BLACK)
36
- if self._ground is not None:
37
- self._ground.draw(0.0, 0.0)
35
+ self._draw_background()
38
36
  _draw_screen_fade(self._state)
39
37
  assets = self._assets
40
38
  entry = self._entry
@@ -32,9 +32,7 @@ class ModsMenuView(PanelMenuView):
32
32
  self._lines = self._build_lines()
33
33
 
34
34
  def draw(self) -> None:
35
- rl.clear_background(rl.BLACK)
36
- if self._ground is not None:
37
- self._ground.draw(0.0, 0.0)
35
+ self._draw_background()
38
36
  _draw_screen_fade(self._state)
39
37
  assets = self._assets
40
38
  entry = self._entry
@@ -9,6 +9,7 @@ from grim.audio import set_music_volume, set_sfx_volume
9
9
  from grim.config import apply_detail_preset
10
10
  from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
11
11
 
12
+ from ...ui.perk_menu import UiButtonState, UiButtonTextureSet, button_draw, button_update, button_width
12
13
  from ..menu import (
13
14
  MENU_LABEL_ROW_HEIGHT,
14
15
  MENU_LABEL_ROW_OPTIONS,
@@ -42,13 +43,15 @@ class OptionsMenuView(PanelMenuView):
42
43
  )
43
44
 
44
45
  def __init__(self, state: GameState) -> None:
45
- super().__init__(state, title="Options")
46
+ super().__init__(state, title="Options", back_action="open_pause_menu")
46
47
  self._small_font: SmallFontData | None = None
47
48
  self._rect_on: rl.Texture2D | None = None
48
49
  self._rect_off: rl.Texture2D | None = None
49
50
  self._check_on: rl.Texture2D | None = None
50
51
  self._check_off: rl.Texture2D | None = None
51
52
  self._button_tex: rl.Texture2D | None = None
53
+ self._button_textures: UiButtonTextureSet | None = None
54
+ self._controls_button: UiButtonState = UiButtonState("Controls", force_wide=True)
52
55
  self._slider_sfx = SliderState(10, 0, 10)
53
56
  self._slider_music = SliderState(10, 0, 10)
54
57
  self._slider_detail = SliderState(5, 1, 5)
@@ -64,7 +67,10 @@ class OptionsMenuView(PanelMenuView):
64
67
  self._rect_off = cache.get_or_load("ui_rectOff", "ui/ui_rectOff.jaz").texture
65
68
  self._check_on = cache.get_or_load("ui_checkOn", "ui/ui_checkOn.jaz").texture
66
69
  self._check_off = cache.get_or_load("ui_checkOff", "ui/ui_checkOff.jaz").texture
67
- self._button_tex = cache.get_or_load("ui_button_md", "ui/ui_button_145x32.jaz").texture
70
+ self._button_tex = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
71
+ button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
72
+ self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=self._button_tex)
73
+ self._controls_button = UiButtonState("Controls", force_wide=True)
68
74
  self._active_slider = None
69
75
  self._dirty = False
70
76
  self._sync_from_config()
@@ -118,13 +124,25 @@ class OptionsMenuView(PanelMenuView):
118
124
  config.data["ui_info_texts"] = value
119
125
  self._dirty = True
120
126
 
121
- if self._update_controls_button(label_x - 8.0 * scale, base_y + 155.0 * scale, scale):
122
- self._begin_close_transition("open_controls")
127
+ textures = self._button_textures
128
+ if textures is not None and textures.button_md is not None:
129
+ # `sub_4475d0`: controls button shares the slider column.
130
+ x = slider_x
131
+ y = base_y + 155.0 * scale
132
+ dt_ms = min(float(dt), 0.1) * 1000.0
133
+ mouse = rl.get_mouse_position()
134
+ click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
135
+ width = button_width(
136
+ self._ensure_small_font(),
137
+ self._controls_button.label,
138
+ scale=scale,
139
+ force_wide=self._controls_button.force_wide,
140
+ )
141
+ if button_update(self._controls_button, x=x, y=y, width=width, dt_ms=dt_ms, mouse=mouse, click=click):
142
+ self._begin_close_transition("open_controls")
123
143
 
124
144
  def draw(self) -> None:
125
- rl.clear_background(rl.BLACK)
126
- if self._ground is not None:
127
- self._ground.draw(0.0, 0.0)
145
+ self._draw_background()
128
146
  _draw_screen_fade(self._state)
129
147
  assets = self._assets
130
148
  entry = self._entry
@@ -192,7 +210,8 @@ class OptionsMenuView(PanelMenuView):
192
210
  panel_left = panel_x - origin_x
193
211
  panel_top = panel_y - origin_y
194
212
  base_x = panel_left + 212.0 * panel_scale
195
- base_y = panel_top + 32.0 * panel_scale
213
+ # `sub_4475d0`: title label is anchored at panel_top + 40.
214
+ base_y = panel_top + 40.0 * panel_scale
196
215
  label_x = base_x + 8.0 * panel_scale
197
216
  slider_x = label_x + 130.0 * panel_scale
198
217
  return {
@@ -269,18 +288,6 @@ class OptionsMenuView(PanelMenuView):
269
288
  return True
270
289
  return False
271
290
 
272
- def _update_controls_button(self, x: float, y: float, scale: float) -> bool:
273
- tex = self._button_tex
274
- if tex is None:
275
- return False
276
- w = float(tex.width) * scale
277
- h = float(tex.height) * scale
278
- mouse = rl.get_mouse_position()
279
- hovered = x <= mouse.x <= x + w and y <= mouse.y <= y + h
280
- if hovered and rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT):
281
- return True
282
- return False
283
-
284
291
  def _draw_options_contents(self) -> None:
285
292
  assets = self._assets
286
293
  if assets is None:
@@ -298,16 +305,17 @@ class OptionsMenuView(PanelMenuView):
298
305
  text_color = rl.Color(255, 255, 255, int(255 * 0.8))
299
306
 
300
307
  if labels_tex is not None:
308
+ title_w = 128.0
301
309
  src = rl.Rectangle(
302
310
  0.0,
303
311
  float(MENU_LABEL_ROW_OPTIONS) * MENU_LABEL_ROW_HEIGHT,
304
- MENU_LABEL_WIDTH,
312
+ title_w,
305
313
  MENU_LABEL_ROW_HEIGHT,
306
314
  )
307
315
  dst = rl.Rectangle(
308
316
  base_x,
309
317
  base_y,
310
- MENU_LABEL_WIDTH * scale,
318
+ title_w * scale,
311
319
  MENU_LABEL_ROW_HEIGHT * scale,
312
320
  )
313
321
  MenuView._draw_ui_quad(
@@ -363,27 +371,12 @@ class OptionsMenuView(PanelMenuView):
363
371
  )
364
372
 
365
373
  button = self._button_tex
366
- if button is not None:
367
- button_x = label_x - 8.0 * scale
374
+ textures = self._button_textures
375
+ if button is not None and textures is not None:
376
+ button_x = slider_x
368
377
  button_y = base_y + 155.0 * scale
369
- button_w = float(button.width) * scale
370
- button_h = float(button.height) * scale
371
- mouse = rl.get_mouse_position()
372
- hovered = button_x <= mouse.x <= button_x + button_w and button_y <= mouse.y <= button_y + button_h
373
- alpha = 255 if hovered else 220
374
- rl.draw_texture_pro(
375
- button,
376
- rl.Rectangle(0.0, 0.0, float(button.width), float(button.height)),
377
- rl.Rectangle(button_x, button_y, button_w, button_h),
378
- rl.Vector2(0.0, 0.0),
379
- 0.0,
380
- rl.Color(255, 255, 255, alpha),
381
- )
382
- label = "Controls"
383
- label_w = measure_small_text_width(font, label, text_scale)
384
- text_x = button_x + (button_w - label_w) * 0.5
385
- text_y = button_y + (button_h - font.cell_size * text_scale) * 0.5
386
- draw_small_text(font, label, text_x, text_y, text_scale, rl.Color(20, 20, 20, 255))
378
+ button_w = button_width(font, self._controls_button.label, scale=scale, force_wide=self._controls_button.force_wide)
379
+ button_draw(textures, font, self._controls_button, x=button_x, y=button_y, width=button_w, scale=scale)
387
380
 
388
381
  def _draw_slider(
389
382
  self,