crimsonland 0.1.0.dev11__tar.gz → 0.1.0.dev13__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 (146) hide show
  1. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/pyproject.toml +1 -1
  3. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/assets_fetch.py +23 -8
  4. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/creatures/runtime.py +15 -0
  5. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/demo.py +47 -38
  6. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/effects.py +46 -1
  7. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/boot.py +2 -1
  8. crimsonland-0.1.0.dev13/src/crimson/frontend/high_scores_layout.py +26 -0
  9. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/menu.py +24 -43
  10. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/panels/base.py +27 -29
  11. crimsonland-0.1.0.dev13/src/crimson/frontend/panels/controls.py +219 -0
  12. crimsonland-0.1.0.dev13/src/crimson/frontend/panels/credits.py +221 -0
  13. crimsonland-0.1.0.dev13/src/crimson/frontend/panels/databases.py +307 -0
  14. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/panels/mods.py +1 -3
  15. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/panels/options.py +36 -42
  16. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/panels/play_game.py +82 -74
  17. crimsonland-0.1.0.dev13/src/crimson/frontend/panels/stats.py +308 -0
  18. crimsonland-0.1.0.dev13/src/crimson/frontend/pause_menu.py +425 -0
  19. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/game.py +512 -505
  20. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/gameplay.py +35 -6
  21. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/modes/base_gameplay_mode.py +3 -0
  22. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/modes/quest_mode.py +54 -44
  23. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/modes/rush_mode.py +4 -1
  24. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/modes/survival_mode.py +15 -10
  25. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/modes/tutorial_mode.py +15 -5
  26. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/modes/typo_mode.py +4 -1
  27. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/persistence/highscores.py +6 -2
  28. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/render/world_renderer.py +1 -1
  29. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/sim/world_state.py +8 -1
  30. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/typo/spawns.py +3 -4
  31. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/ui/demo_trial_overlay.py +3 -3
  32. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/ui/game_over.py +18 -2
  33. crimsonland-0.1.0.dev13/src/crimson/ui/menu_panel.py +127 -0
  34. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/ui/perk_menu.py +101 -44
  35. crimsonland-0.1.0.dev13/src/crimson/ui/quest_results.py +669 -0
  36. crimsonland-0.1.0.dev13/src/crimson/ui/shadow.py +39 -0
  37. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/particles.py +1 -1
  38. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/perk_menu_debug.py +2 -2
  39. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/perks.py +2 -2
  40. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/weapons.py +110 -110
  41. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/app.py +3 -0
  42. crimsonland-0.1.0.dev11/src/crimson/frontend/panels/controls.py +0 -132
  43. crimsonland-0.1.0.dev11/src/crimson/frontend/panels/stats.py +0 -351
  44. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/__init__.py +0 -0
  45. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/atlas.py +0 -0
  46. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/audio_router.py +0 -0
  47. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/bonuses.py +0 -0
  48. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/camera.py +0 -0
  49. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/cli.py +0 -0
  50. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/creatures/__init__.py +0 -0
  51. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/creatures/ai.py +0 -0
  52. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/creatures/anim.py +0 -0
  53. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/creatures/damage.py +0 -0
  54. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/creatures/spawn.py +0 -0
  55. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/debug.py +0 -0
  56. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/demo_trial.py +0 -0
  57. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/effects_atlas.py +0 -0
  58. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/__init__.py +0 -0
  59. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/assets.py +0 -0
  60. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/panels/__init__.py +0 -0
  61. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/frontend/transitions.py +0 -0
  62. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/game_modes.py +0 -0
  63. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/game_world.py +0 -0
  64. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/input_codes.py +0 -0
  65. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/modes/__init__.py +0 -0
  66. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/paths.py +0 -0
  67. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/perks.py +0 -0
  68. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/persistence/__init__.py +0 -0
  69. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/persistence/save_status.py +0 -0
  70. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/player_damage.py +0 -0
  71. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/projectiles.py +0 -0
  72. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/__init__.py +0 -0
  73. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/helpers.py +0 -0
  74. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/registry.py +0 -0
  75. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/results.py +0 -0
  76. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/runtime.py +0 -0
  77. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/tier1.py +0 -0
  78. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/tier2.py +0 -0
  79. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/tier3.py +0 -0
  80. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/tier4.py +0 -0
  81. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/tier5.py +0 -0
  82. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/timeline.py +0 -0
  83. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/quests/types.py +0 -0
  84. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/render/__init__.py +0 -0
  85. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/render/terrain_fx.py +0 -0
  86. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/sim/__init__.py +0 -0
  87. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/sim/world_defs.py +0 -0
  88. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/terrain_assets.py +0 -0
  89. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/tutorial/__init__.py +0 -0
  90. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/tutorial/timeline.py +0 -0
  91. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/typo/__init__.py +0 -0
  92. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/typo/names.py +0 -0
  93. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/typo/player.py +0 -0
  94. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/typo/typing.py +0 -0
  95. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/ui/__init__.py +0 -0
  96. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/ui/cursor.py +0 -0
  97. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/ui/hud.py +0 -0
  98. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/__init__.py +0 -0
  99. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/aim_debug.py +0 -0
  100. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/animations.py +0 -0
  101. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/arsenal_debug.py +0 -0
  102. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/audio_bootstrap.py +0 -0
  103. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/bonuses.py +0 -0
  104. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/camera_debug.py +0 -0
  105. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/camera_shake.py +0 -0
  106. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/corpse_stamp_debug.py +0 -0
  107. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/decals_debug.py +0 -0
  108. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/empty.py +0 -0
  109. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/fonts.py +0 -0
  110. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/game_over.py +0 -0
  111. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/ground.py +0 -0
  112. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/lighting_debug.py +0 -0
  113. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/player.py +0 -0
  114. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/player_sprite_debug.py +0 -0
  115. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/projectile_fx.py +0 -0
  116. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/projectile_render_debug.py +0 -0
  117. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/projectiles.py +0 -0
  118. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/quest_title_overlay.py +0 -0
  119. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/registry.py +0 -0
  120. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/rush.py +0 -0
  121. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/small_font_debug.py +0 -0
  122. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/spawn_plan.py +0 -0
  123. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/sprites.py +0 -0
  124. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/survival.py +0 -0
  125. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/terrain.py +0 -0
  126. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/ui.py +0 -0
  127. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/views/wicons.py +0 -0
  128. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/crimson/weapon_sfx.py +0 -0
  129. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/__init__.py +0 -0
  130. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/assets.py +0 -0
  131. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/audio.py +0 -0
  132. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/config.py +0 -0
  133. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/console.py +0 -0
  134. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/fonts/__init__.py +0 -0
  135. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/fonts/grim_mono.py +0 -0
  136. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/fonts/small.py +0 -0
  137. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/input.py +0 -0
  138. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/jaz.py +0 -0
  139. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/math.py +0 -0
  140. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/music.py +0 -0
  141. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/paq.py +0 -0
  142. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/rand.py +0 -0
  143. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/sfx.py +0 -0
  144. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/sfx_map.py +0 -0
  145. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/src/grim/terrain_render.py +0 -0
  146. {crimsonland-0.1.0.dev11 → crimsonland-0.1.0.dev13}/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.dev11
3
+ Version: 0.1.0.dev13
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.dev11"
3
+ version = "0.1.0.dev13"
4
4
  requires-python = ">=3.13"
5
5
  dependencies = [
6
6
  "construct>=2.10.70",
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
+ import os
5
6
  import shutil
7
+ import tempfile
6
8
  import urllib.request
7
9
 
8
10
  from grim.console import ConsoleState
@@ -19,17 +21,30 @@ class DownloadResult:
19
21
 
20
22
 
21
23
  def _download_file(url: str, dest: Path) -> None:
22
- tmp = dest.with_suffix(dest.suffix + ".tmp")
23
- if tmp.exists():
24
- tmp.unlink()
24
+ tmp_path: Path | None = None
25
25
  try:
26
26
  req = urllib.request.Request(url, headers={"User-Agent": "crimsonland-decompile"})
27
- with urllib.request.urlopen(req, timeout=30) as resp, tmp.open("wb") as handle:
28
- shutil.copyfileobj(resp, handle)
29
- tmp.replace(dest)
27
+ with urllib.request.urlopen(req, timeout=30) as resp:
28
+ with tempfile.NamedTemporaryFile(
29
+ mode="wb",
30
+ delete=False,
31
+ dir=dest.parent,
32
+ prefix=dest.name + ".",
33
+ suffix=".tmp",
34
+ ) as handle:
35
+ tmp_path = Path(handle.name)
36
+ shutil.copyfileobj(resp, handle)
37
+ handle.flush()
38
+ os.fsync(handle.fileno())
39
+ if tmp_path is None:
40
+ raise RuntimeError("assets: temporary file not created")
41
+ tmp_path.replace(dest)
30
42
  finally:
31
- if tmp.exists():
32
- tmp.unlink()
43
+ if tmp_path is not None:
44
+ try:
45
+ tmp_path.unlink()
46
+ except FileNotFoundError:
47
+ pass
33
48
 
34
49
 
35
50
  def download_missing_paqs(
@@ -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
 
@@ -0,0 +1,26 @@
1
+ """
2
+ Layout constants for the classic high scores screen (state_id=14).
3
+
4
+ Measured from analysis/frida/ui_render_trace_oracle_1024x768.json.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ # Panel positions are expressed in "panel pos" (pre-offset) space, matching other menu panels:
10
+ # panel_top_left = (panel_pos_x + MENU_PANEL_OFFSET_X, panel_pos_y + y_shift + MENU_PANEL_OFFSET_Y)
11
+
12
+ HS_LEFT_PANEL_POS_X = -119.0
13
+ HS_LEFT_PANEL_POS_Y = 185.0
14
+ HS_LEFT_PANEL_HEIGHT = 378.0
15
+
16
+ HS_RIGHT_PANEL_POS_X = 609.0
17
+ HS_RIGHT_PANEL_POS_Y = 200.0
18
+ HS_RIGHT_PANEL_HEIGHT = 254.0
19
+
20
+ # Buttons inside the left panel (relative to the left panel top-left).
21
+ HS_BUTTON_X = 234.0 # x0=136 at 1024x768
22
+ HS_BUTTON_Y0 = 268.0 # y0=462
23
+ HS_BUTTON_STEP_Y = 33.0
24
+
25
+ HS_BACK_BUTTON_X = 400.0 # x0=302
26
+ HS_BACK_BUTTON_Y = 301.0 # y0=495
@@ -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
 
@@ -18,8 +19,8 @@ if TYPE_CHECKING:
18
19
  from ..game import GameState
19
20
 
20
21
 
21
- MENU_LABEL_WIDTH = 124.0
22
- MENU_LABEL_HEIGHT = 30.0
22
+ MENU_LABEL_WIDTH = 122.0
23
+ MENU_LABEL_HEIGHT = 28.0
23
24
  MENU_LABEL_ROW_HEIGHT = 32.0
24
25
  MENU_LABEL_ROW_PLAY_GAME = 1
25
26
  MENU_LABEL_ROW_OPTIONS = 2
@@ -30,15 +31,17 @@ MENU_LABEL_ROW_QUIT = 6
30
31
  MENU_LABEL_ROW_BACK = 7
31
32
  MENU_LABEL_BASE_X = -60.0
32
33
  MENU_LABEL_BASE_Y = 210.0
33
- MENU_LABEL_OFFSET_X = 270.0
34
- MENU_LABEL_OFFSET_Y = -38.0
34
+ MENU_LABEL_OFFSET_X = 271.0
35
+ MENU_LABEL_OFFSET_Y = -37.0
35
36
  MENU_LABEL_STEP = 60.0
36
- MENU_ITEM_OFFSET_X = -72.0
37
- MENU_ITEM_OFFSET_Y = -60.0
38
- MENU_PANEL_WIDTH = 512.0
39
- MENU_PANEL_HEIGHT = 256.0
40
- MENU_PANEL_OFFSET_X = 20.0
41
- MENU_PANEL_OFFSET_Y = -82.0
37
+ MENU_ITEM_OFFSET_X = -71.0
38
+ MENU_ITEM_OFFSET_Y = -59.0
39
+ MENU_PANEL_WIDTH = 510.0
40
+ MENU_PANEL_HEIGHT = 254.0
41
+ # Measured from ui_render_trace at 1024x768 (stable timeline):
42
+ # panel top-left is (pos_x + 21, pos_y - 81) and size is 510x254, plus a shadow pass at +7,+7.
43
+ MENU_PANEL_OFFSET_X = 21.0
44
+ MENU_PANEL_OFFSET_Y = -81.0
42
45
  MENU_PANEL_BASE_X = -45.0
43
46
  MENU_PANEL_BASE_Y = 210.0
44
47
  MENU_SCALE_SMALL_THRESHOLD = 640
@@ -48,15 +51,10 @@ MENU_SCALE_SMALL = 0.8
48
51
  MENU_SCALE_LARGE = 1.2
49
52
  MENU_SCALE_SHIFT = 10.0
50
53
 
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
- MENU_SIGN_WIDTH = 573.44
57
- MENU_SIGN_HEIGHT = 143.36
58
- MENU_SIGN_OFFSET_X = -577.44
59
- MENU_SIGN_OFFSET_Y = -62.0
54
+ MENU_SIGN_WIDTH = 571.44
55
+ MENU_SIGN_HEIGHT = 141.36
56
+ MENU_SIGN_OFFSET_X = -576.44
57
+ MENU_SIGN_OFFSET_Y = -61.0
60
58
  MENU_SIGN_POS_Y = 70.0
61
59
  MENU_SIGN_POS_Y_SMALL = 60.0
62
60
  MENU_SIGN_POS_X_PAD = 4.0
@@ -585,21 +583,24 @@ class MenuView:
585
583
  start_ms: int,
586
584
  end_ms: int,
587
585
  width: float,
586
+ direction_flag: int = 0,
588
587
  ) -> tuple[float, float]:
589
588
  # Matches ui_element_update: angle lerps pi/2 -> 0 over [end_ms, start_ms].
590
- # Direction flag (element+0x314) appears to be 0 for main menu elements.
589
+ # direction_flag=0 slides from left (-width -> 0)
590
+ # direction_flag=1 slides from right (+width -> 0)
591
591
  if start_ms <= end_ms or width <= 0.0:
592
592
  return 0.0, 0.0
593
+ dir_sign = 1.0 if int(direction_flag) else -1.0
593
594
  t = self._timeline_ms
594
595
  if t < end_ms:
595
596
  angle = 1.5707964
596
- offset_x = -abs(width)
597
+ offset_x = dir_sign * abs(width)
597
598
  elif t < start_ms:
598
599
  elapsed = t - end_ms
599
600
  span = float(start_ms - end_ms)
600
601
  p = float(elapsed) / span
601
602
  angle = 1.5707964 * (1.0 - p)
602
- offset_x = -((1.0 - p) * abs(width))
603
+ offset_x = dir_sign * ((1.0 - p) * abs(width))
603
604
  else:
604
605
  angle = 0.0
605
606
  offset_x = 0.0
@@ -628,27 +629,7 @@ class MenuView:
628
629
  origin: rl.Vector2,
629
630
  rotation_deg: float,
630
631
  ) -> 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()
632
+ draw_ui_quad_shadow(texture=texture, src=src, dst=dst, origin=origin, rotation_deg=rotation_deg)
652
633
 
653
634
  def _draw_menu_sign(self) -> None:
654
635
  assets = self._assets
@@ -8,6 +8,7 @@ from grim.assets import PaqTextureCache
8
8
  from grim.audio import play_sfx, update_audio
9
9
  from grim.terrain_render import GroundRenderer
10
10
 
11
+ from ...ui.menu_panel import draw_classic_menu_panel
11
12
  from ..assets import MenuAssets, _ensure_texture_cache, load_menu_assets
12
13
  from ..menu import (
13
14
  MENU_ITEM_OFFSET_X,
@@ -70,6 +71,9 @@ class PanelMenuView:
70
71
  body: str | None = None,
71
72
  panel_pos_x: float = PANEL_POS_X,
72
73
  panel_pos_y: float = PANEL_POS_Y,
74
+ panel_offset_x: float = MENU_PANEL_OFFSET_X,
75
+ panel_offset_y: float = MENU_PANEL_OFFSET_Y,
76
+ panel_height: float = MENU_PANEL_HEIGHT,
73
77
  back_pos_x: float = PANEL_BACK_POS_X,
74
78
  back_pos_y: float = PANEL_BACK_POS_Y,
75
79
  back_action: str = "back_to_menu",
@@ -79,6 +83,9 @@ class PanelMenuView:
79
83
  self._body_lines = (body or "").splitlines()
80
84
  self._panel_pos_x = panel_pos_x
81
85
  self._panel_pos_y = panel_pos_y
86
+ self._panel_offset_x = panel_offset_x
87
+ self._panel_offset_y = panel_offset_y
88
+ self._panel_height = panel_height
82
89
  self._back_pos_x = back_pos_x
83
90
  self._back_pos_y = back_pos_y
84
91
  self._back_action = back_action
@@ -163,9 +170,7 @@ class PanelMenuView:
163
170
  entry.ready_timer_ms = min(0x100, entry.ready_timer_ms + dt_ms)
164
171
 
165
172
  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)
173
+ self._draw_background()
169
174
  _draw_screen_fade(self._state)
170
175
  assets = self._assets
171
176
  entry = self._entry
@@ -206,47 +211,40 @@ class PanelMenuView:
206
211
  return _ensure_texture_cache(self._state)
207
212
 
208
213
  def _init_ground(self) -> None:
214
+ if self._state.pause_background is not None:
215
+ self._ground = None
216
+ return
209
217
  self._ground = ensure_menu_ground(self._state)
210
218
 
219
+ def _draw_background(self) -> None:
220
+ rl.clear_background(rl.BLACK)
221
+ pause_background = self._state.pause_background
222
+ if pause_background is not None:
223
+ pause_background.draw_pause_background()
224
+ return
225
+ if self._ground is not None:
226
+ self._ground.draw(0.0, 0.0)
227
+
211
228
  def _draw_panel(self) -> None:
212
229
  assets = self._assets
213
230
  if assets is None or assets.panel is None:
214
231
  return
215
232
  panel = assets.panel
216
- panel_w = MENU_PANEL_WIDTH
217
- panel_h = MENU_PANEL_HEIGHT
218
233
  _angle_rad, slide_x = MenuView._ui_element_anim(
219
234
  self,
220
235
  index=1,
221
236
  start_ms=PANEL_TIMELINE_START_MS,
222
237
  end_ms=PANEL_TIMELINE_END_MS,
223
- width=panel_w * self._menu_item_scale(0)[0],
238
+ width=MENU_PANEL_WIDTH * self._menu_item_scale(0)[0],
224
239
  )
225
240
  item_scale, _local_y_shift = self._menu_item_scale(0)
226
- dst = rl.Rectangle(
227
- self._panel_pos_x + slide_x,
228
- self._panel_pos_y + self._widescreen_y_shift,
229
- panel_w * item_scale,
230
- panel_h * item_scale,
231
- )
232
- origin = rl.Vector2(-(MENU_PANEL_OFFSET_X * item_scale), -(MENU_PANEL_OFFSET_Y * item_scale))
241
+ panel_w = MENU_PANEL_WIDTH * item_scale
242
+ panel_h = float(self._panel_height) * item_scale
243
+ top_left_x = self._panel_pos_x + slide_x + self._panel_offset_x * item_scale
244
+ top_left_y = self._panel_pos_y + self._widescreen_y_shift + self._panel_offset_y * item_scale
245
+ dst = rl.Rectangle(float(top_left_x), float(top_left_y), float(panel_w), float(panel_h))
233
246
  fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
234
- if fx_detail:
235
- MenuView._draw_ui_quad_shadow(
236
- texture=panel,
237
- src=rl.Rectangle(0.0, 0.0, float(panel.width), float(panel.height)),
238
- dst=rl.Rectangle(dst.x + UI_SHADOW_OFFSET, dst.y + UI_SHADOW_OFFSET, dst.width, dst.height),
239
- origin=origin,
240
- rotation_deg=0.0,
241
- )
242
- MenuView._draw_ui_quad(
243
- texture=panel,
244
- src=rl.Rectangle(0.0, 0.0, float(panel.width), float(panel.height)),
245
- dst=dst,
246
- origin=origin,
247
- rotation_deg=0.0,
248
- tint=rl.WHITE,
249
- )
247
+ draw_classic_menu_panel(panel, dst=dst, tint=rl.WHITE, shadow=fx_detail)
250
248
 
251
249
  def _draw_entry(self, entry: MenuEntry) -> None:
252
250
  assets = self._assets