crimsonland 0.1.0.dev14__tar.gz → 0.1.0.dev15__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 (149) hide show
  1. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/pyproject.toml +1 -1
  3. crimsonland-0.1.0.dev15/src/crimson/.DS_Store +0 -0
  4. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/cli.py +2 -0
  5. crimsonland-0.1.0.dev15/src/crimson/creatures/.DS_Store +0 -0
  6. crimsonland-0.1.0.dev15/src/crimson/debug.py +16 -0
  7. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/game.py +248 -2
  8. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/gameplay.py +16 -0
  9. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/modes/quest_mode.py +37 -0
  10. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/modes/survival_mode.py +42 -6
  11. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/player_damage.py +2 -0
  12. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/quest_results.py +16 -6
  13. crimsonland-0.1.0.dev15/src/grim/.DS_Store +0 -0
  14. crimsonland-0.1.0.dev14/src/crimson/debug.py +0 -7
  15. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/readme.md +0 -0
  16. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/__init__.py +0 -0
  17. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/assets_fetch.py +0 -0
  18. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/atlas.py +0 -0
  19. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/audio_router.py +0 -0
  20. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/bonuses.py +0 -0
  21. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/camera.py +0 -0
  22. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/creatures/__init__.py +0 -0
  23. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/creatures/ai.py +0 -0
  24. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/creatures/anim.py +0 -0
  25. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/creatures/damage.py +0 -0
  26. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/creatures/runtime.py +0 -0
  27. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/creatures/spawn.py +0 -0
  28. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/demo.py +0 -0
  29. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/demo_trial.py +0 -0
  30. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/effects.py +0 -0
  31. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/effects_atlas.py +0 -0
  32. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/__init__.py +0 -0
  33. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/assets.py +0 -0
  34. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/boot.py +0 -0
  35. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/high_scores_layout.py +0 -0
  36. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/menu.py +0 -0
  37. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/__init__.py +0 -0
  38. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/base.py +0 -0
  39. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/controls.py +0 -0
  40. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/credits.py +0 -0
  41. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/databases.py +0 -0
  42. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/mods.py +0 -0
  43. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/options.py +0 -0
  44. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/play_game.py +0 -0
  45. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/panels/stats.py +0 -0
  46. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/pause_menu.py +0 -0
  47. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/frontend/transitions.py +0 -0
  48. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/game_modes.py +0 -0
  49. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/game_world.py +0 -0
  50. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/input_codes.py +0 -0
  51. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/modes/__init__.py +0 -0
  52. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/modes/base_gameplay_mode.py +0 -0
  53. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/modes/rush_mode.py +0 -0
  54. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/modes/tutorial_mode.py +0 -0
  55. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/modes/typo_mode.py +0 -0
  56. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/paths.py +0 -0
  57. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/perks.py +0 -0
  58. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/persistence/__init__.py +0 -0
  59. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/persistence/highscores.py +0 -0
  60. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/persistence/save_status.py +0 -0
  61. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/projectiles.py +0 -0
  62. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/__init__.py +0 -0
  63. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/helpers.py +0 -0
  64. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/registry.py +0 -0
  65. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/results.py +0 -0
  66. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/runtime.py +0 -0
  67. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/tier1.py +0 -0
  68. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/tier2.py +0 -0
  69. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/tier3.py +0 -0
  70. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/tier4.py +0 -0
  71. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/tier5.py +0 -0
  72. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/timeline.py +0 -0
  73. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/quests/types.py +0 -0
  74. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/render/__init__.py +0 -0
  75. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/render/terrain_fx.py +0 -0
  76. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/render/world_renderer.py +0 -0
  77. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/sim/__init__.py +0 -0
  78. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/sim/world_defs.py +0 -0
  79. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/sim/world_state.py +0 -0
  80. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/terrain_assets.py +0 -0
  81. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/tutorial/__init__.py +0 -0
  82. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/tutorial/timeline.py +0 -0
  83. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/typo/__init__.py +0 -0
  84. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/typo/names.py +0 -0
  85. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/typo/player.py +0 -0
  86. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/typo/spawns.py +0 -0
  87. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/typo/typing.py +0 -0
  88. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/__init__.py +0 -0
  89. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/cursor.py +0 -0
  90. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/demo_trial_overlay.py +0 -0
  91. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/game_over.py +0 -0
  92. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/hud.py +0 -0
  93. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/menu_panel.py +0 -0
  94. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/perk_menu.py +0 -0
  95. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/ui/shadow.py +0 -0
  96. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/__init__.py +0 -0
  97. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/aim_debug.py +0 -0
  98. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/animations.py +0 -0
  99. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/arsenal_debug.py +0 -0
  100. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/audio_bootstrap.py +0 -0
  101. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/bonuses.py +0 -0
  102. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/camera_debug.py +0 -0
  103. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/camera_shake.py +0 -0
  104. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/corpse_stamp_debug.py +0 -0
  105. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/decals_debug.py +0 -0
  106. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/empty.py +0 -0
  107. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/fonts.py +0 -0
  108. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/game_over.py +0 -0
  109. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/ground.py +0 -0
  110. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/lighting_debug.py +0 -0
  111. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/particles.py +0 -0
  112. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/perk_menu_debug.py +0 -0
  113. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/perks.py +0 -0
  114. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/player.py +0 -0
  115. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/player_sprite_debug.py +0 -0
  116. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/projectile_fx.py +0 -0
  117. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/projectile_render_debug.py +0 -0
  118. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/projectiles.py +0 -0
  119. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/quest_title_overlay.py +0 -0
  120. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/registry.py +0 -0
  121. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/rush.py +0 -0
  122. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/small_font_debug.py +0 -0
  123. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/spawn_plan.py +0 -0
  124. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/sprites.py +0 -0
  125. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/survival.py +0 -0
  126. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/terrain.py +0 -0
  127. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/ui.py +0 -0
  128. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/views/wicons.py +0 -0
  129. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/weapon_sfx.py +0 -0
  130. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/crimson/weapons.py +0 -0
  131. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/__init__.py +0 -0
  132. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/app.py +0 -0
  133. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/assets.py +0 -0
  134. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/audio.py +0 -0
  135. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/config.py +0 -0
  136. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/console.py +0 -0
  137. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/fonts/__init__.py +0 -0
  138. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/fonts/grim_mono.py +0 -0
  139. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/fonts/small.py +0 -0
  140. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/input.py +0 -0
  141. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/jaz.py +0 -0
  142. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/math.py +0 -0
  143. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/music.py +0 -0
  144. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/paq.py +0 -0
  145. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/rand.py +0 -0
  146. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/sfx.py +0 -0
  147. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/sfx_map.py +0 -0
  148. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/src/grim/terrain_render.py +0 -0
  149. {crimsonland-0.1.0.dev14 → crimsonland-0.1.0.dev15}/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.dev14
3
+ Version: 0.1.0.dev15
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.dev14"
3
+ version = "0.1.0.dev15"
4
4
  readme = { file = "readme.md", content-type = "text/markdown" }
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
@@ -211,6 +211,7 @@ def cmd_game(
211
211
  seed: int | None = typer.Option(None, help="rng seed"),
212
212
  demo: bool = typer.Option(False, "--demo", help="enable shareware demo mode"),
213
213
  no_intro: bool = typer.Option(False, "--no-intro", help="skip company splashes and intro music"),
214
+ debug: bool = typer.Option(False, "--debug", help="enable debug cheats and overlays"),
214
215
  base_dir: Path = typer.Option(
215
216
  default_runtime_dir(),
216
217
  "--base-dir",
@@ -236,6 +237,7 @@ def cmd_game(
236
237
  seed=seed,
237
238
  demo_enabled=demo,
238
239
  no_intro=no_intro,
240
+ debug=debug,
239
241
  )
240
242
  run_game(config)
241
243
 
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ _DEBUG_OVERRIDE: bool | None = None
6
+
7
+
8
+ def set_debug_enabled(enabled: bool) -> None:
9
+ global _DEBUG_OVERRIDE
10
+ _DEBUG_OVERRIDE = bool(enabled)
11
+
12
+
13
+ def debug_enabled() -> bool:
14
+ if _DEBUG_OVERRIDE is not None:
15
+ return bool(_DEBUG_OVERRIDE)
16
+ return os.environ.get("CRIMSON_DEBUG") == "1"
@@ -38,7 +38,7 @@ from grim.terrain_render import GroundRenderer
38
38
  from grim.view import View, ViewContext
39
39
  from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
40
40
 
41
- from .debug import debug_enabled
41
+ from .debug import debug_enabled, set_debug_enabled
42
42
  from grim import music
43
43
 
44
44
  from .demo import DemoView
@@ -113,6 +113,7 @@ class GameConfig:
113
113
  seed: int | None = None
114
114
  demo_enabled: bool = False
115
115
  no_intro: bool = False
116
+ debug: bool = False
116
117
 
117
118
 
118
119
  @dataclass(slots=True)
@@ -190,6 +191,25 @@ QUEST_BACK_BUTTON_X_OFFSET = 138.0
190
191
  QUEST_BACK_BUTTON_Y_OFFSET = 212.0
191
192
  QUEST_PANEL_HEIGHT = 378.0
192
193
 
194
+ # game_update_victory_screen (0x00406350): used as the "end note" screen after the final quest.
195
+ END_NOTE_PANEL_POS_X = -45.0
196
+ END_NOTE_PANEL_POS_Y = 110.0
197
+ END_NOTE_PANEL_GEOM_X0 = -63.0
198
+ END_NOTE_PANEL_GEOM_Y0 = -81.0
199
+ END_NOTE_PANEL_W = 510.0
200
+ END_NOTE_PANEL_H = 378.0
201
+
202
+ END_NOTE_HEADER_X_OFFSET = 214.0 # v11 + 44 - 10 in the decompile, relative to panel-left
203
+ END_NOTE_HEADER_Y_OFFSET = 46.0 # (base_y + 40) + 6 in the decompile, relative to panel-top
204
+ END_NOTE_BODY_X_OFFSET = END_NOTE_HEADER_X_OFFSET - 8.0
205
+ END_NOTE_BODY_Y_GAP = 32.0
206
+ END_NOTE_LINE_STEP_Y = 14.0
207
+ END_NOTE_AFTER_BODY_Y_GAP = 22.0 # 14 + 8 in the decompile
208
+
209
+ END_NOTE_BUTTON_X_OFFSET = 266.0 # (v11 + 44 + 20) - 4 + 26, relative to panel-left
210
+ END_NOTE_BUTTON_Y_OFFSET = 210.0 # (base_y + 40) + 170 in the decompile, relative to panel-top
211
+ END_NOTE_BUTTON_STEP_Y = 32.0
212
+
193
213
 
194
214
  class QuestsMenuView:
195
215
  """Quest selection menu.
@@ -282,6 +302,7 @@ class QuestsMenuView:
282
302
  self._cursor_pulse_time += min(dt, 0.1) * 1.1
283
303
 
284
304
  config = self._state.config
305
+ status = self._state.status
285
306
 
286
307
  # The original forcibly clears hardcore in the demo build.
287
308
  if self._state.demo_enabled:
@@ -289,6 +310,14 @@ class QuestsMenuView:
289
310
  config.data["hardcore_flag"] = 0
290
311
  self._dirty = True
291
312
 
313
+ if debug_enabled() and rl.is_key_pressed(rl.KeyboardKey.KEY_F5):
314
+ unlock = 49
315
+ if int(status.quest_unlock_index) < unlock:
316
+ status.quest_unlock_index = unlock
317
+ if int(status.quest_unlock_index_full) < unlock:
318
+ status.quest_unlock_index_full = unlock
319
+ self._state.console.log.log("debug: unlocked all quests")
320
+
292
321
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
293
322
  self._action = "open_play_game"
294
323
  return
@@ -374,7 +403,7 @@ class QuestsMenuView:
374
403
  # `sub_447d40` base sums:
375
404
  # x_sum = <ui_element_x> + <ui_element_offset_x> (x=-5)
376
405
  # y_sum = <ui_element_y> + <ui_element_offset_y> (y=185 + widescreen shift via ui_menu_layout_init)
377
- x_sum = QUEST_MENU_BASE_X + MENU_PANEL_OFFSET_X
406
+ x_sum = QUEST_MENU_BASE_X + QUEST_MENU_PANEL_OFFSET_X
378
407
  y_sum = QUEST_MENU_BASE_Y + MENU_PANEL_OFFSET_Y + self._widescreen_y_shift
379
408
 
380
409
  title_x = x_sum + QUEST_TITLE_X_OFFSET
@@ -1288,6 +1317,9 @@ class QuestResultsView:
1288
1317
  self._action = "start_quest"
1289
1318
  return
1290
1319
  if action == "play_next":
1320
+ if int(self._quest_stage_major) == 5 and int(self._quest_stage_minor) == 10:
1321
+ self._action = "end_note"
1322
+ return
1291
1323
  next_level = _next_quest_level(self._quest_level)
1292
1324
  if next_level is not None:
1293
1325
  self._state.pending_quest_level = next_level
@@ -1336,6 +1368,217 @@ class QuestResultsView:
1336
1368
  self._action = "open_high_scores"
1337
1369
 
1338
1370
 
1371
+ class EndNoteView:
1372
+ """Final quest "Show End Note" flow.
1373
+
1374
+ Classic:
1375
+ - quest_results_screen_update uses "Show End Note" instead of "Play Next" for quest 5.10
1376
+ - clicking it transitions to state 0x15 (game_update_victory_screen @ 0x00406350)
1377
+ """
1378
+
1379
+ def __init__(self, state: GameState) -> None:
1380
+ self._state = state
1381
+ self._ground: GroundRenderer | None = None
1382
+ self._small_font: SmallFontData | None = None
1383
+ self._panel_tex: rl.Texture2D | None = None
1384
+ self._button_textures: UiButtonTextureSet | None = None
1385
+ self._action: str | None = None
1386
+ self._cursor_pulse_time = 0.0
1387
+
1388
+ self._survival_button = UiButtonState("Survival", force_wide=True)
1389
+ self._rush_button = UiButtonState(" Rush ", force_wide=True)
1390
+ self._typo_button = UiButtonState("Typ'o'Shooter", force_wide=True)
1391
+ self._main_menu_button = UiButtonState("Main Menu", force_wide=True)
1392
+
1393
+ def open(self) -> None:
1394
+ self._action = None
1395
+ self._cursor_pulse_time = 0.0
1396
+ self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
1397
+
1398
+ cache = _ensure_texture_cache(self._state)
1399
+ self._panel_tex = cache.get_or_load("ui_menuPanel", "ui/ui_menuPanel.jaz").texture
1400
+ button_md = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
1401
+ button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
1402
+ self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=button_md)
1403
+ self._small_font = None
1404
+
1405
+ def close(self) -> None:
1406
+ self._ground = None
1407
+ self._small_font = None
1408
+ self._panel_tex = None
1409
+ self._button_textures = None
1410
+
1411
+ def update(self, dt: float) -> None:
1412
+ if self._state.audio is not None:
1413
+ update_audio(self._state.audio, dt)
1414
+ if self._ground is not None:
1415
+ self._ground.process_pending()
1416
+ self._cursor_pulse_time += min(float(dt), 0.1) * 1.1
1417
+
1418
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
1419
+ if self._state.audio is not None:
1420
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1421
+ self._action = "back_to_menu"
1422
+ return
1423
+
1424
+ textures = self._button_textures
1425
+ if textures is None or (textures.button_sm is None and textures.button_md is None):
1426
+ return
1427
+
1428
+ screen_w = float(rl.get_screen_width())
1429
+ scale = 1.0
1430
+
1431
+ layout_w = screen_w / scale if scale else screen_w
1432
+ widescreen_shift_y = MenuView._menu_widescreen_y_shift(layout_w)
1433
+
1434
+ panel_left = (END_NOTE_PANEL_GEOM_X0 + END_NOTE_PANEL_POS_X) * scale
1435
+ panel_top = (END_NOTE_PANEL_GEOM_Y0 + END_NOTE_PANEL_POS_Y + widescreen_shift_y) * scale
1436
+
1437
+ button_x = panel_left + END_NOTE_BUTTON_X_OFFSET * scale
1438
+ button_y = panel_top + END_NOTE_BUTTON_Y_OFFSET * scale
1439
+
1440
+ font = self._ensure_small_font()
1441
+ mouse = rl.get_mouse_position()
1442
+ click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
1443
+ dt_ms = min(float(dt), 0.1) * 1000.0
1444
+
1445
+ survival_w = button_width(font, self._survival_button.label, scale=scale, force_wide=self._survival_button.force_wide)
1446
+ if button_update(self._survival_button, x=button_x, y=button_y, width=survival_w, dt_ms=dt_ms, mouse=mouse, click=click):
1447
+ self._state.config.data["game_mode"] = 1
1448
+ if self._state.audio is not None:
1449
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1450
+ self._action = "start_survival"
1451
+ return
1452
+
1453
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1454
+ rush_w = button_width(font, self._rush_button.label, scale=scale, force_wide=self._rush_button.force_wide)
1455
+ if button_update(self._rush_button, x=button_x, y=button_y, width=rush_w, dt_ms=dt_ms, mouse=mouse, click=click):
1456
+ self._state.config.data["game_mode"] = 2
1457
+ if self._state.audio is not None:
1458
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1459
+ self._action = "start_rush"
1460
+ return
1461
+
1462
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1463
+ typo_w = button_width(font, self._typo_button.label, scale=scale, force_wide=self._typo_button.force_wide)
1464
+ if button_update(self._typo_button, x=button_x, y=button_y, width=typo_w, dt_ms=dt_ms, mouse=mouse, click=click):
1465
+ self._state.config.data["game_mode"] = 4
1466
+ self._state.screen_fade_alpha = 0.0
1467
+ self._state.screen_fade_ramp = True
1468
+ if self._state.audio is not None:
1469
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1470
+ self._action = "start_typo"
1471
+ return
1472
+
1473
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1474
+ main_w = button_width(font, self._main_menu_button.label, scale=scale, force_wide=self._main_menu_button.force_wide)
1475
+ if button_update(self._main_menu_button, x=button_x, y=button_y, width=main_w, dt_ms=dt_ms, mouse=mouse, click=click):
1476
+ if self._state.audio is not None:
1477
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1478
+ self._action = "back_to_menu"
1479
+ return
1480
+
1481
+ def draw(self) -> None:
1482
+ rl.clear_background(rl.BLACK)
1483
+ pause_background = self._state.pause_background
1484
+ if pause_background is not None:
1485
+ pause_background.draw_pause_background()
1486
+ elif self._ground is not None:
1487
+ self._ground.draw(0.0, 0.0)
1488
+ _draw_screen_fade(self._state)
1489
+
1490
+ panel_tex = self._panel_tex
1491
+ if panel_tex is None:
1492
+ return
1493
+
1494
+ screen_w = float(rl.get_screen_width())
1495
+ scale = 1.0
1496
+ layout_w = screen_w / scale if scale else screen_w
1497
+ widescreen_shift_y = MenuView._menu_widescreen_y_shift(layout_w)
1498
+
1499
+ panel_left = (END_NOTE_PANEL_GEOM_X0 + END_NOTE_PANEL_POS_X) * scale
1500
+ panel_top = (END_NOTE_PANEL_GEOM_Y0 + END_NOTE_PANEL_POS_Y + widescreen_shift_y) * scale
1501
+ panel = rl.Rectangle(
1502
+ float(panel_left),
1503
+ float(panel_top),
1504
+ float(END_NOTE_PANEL_W * scale),
1505
+ float(END_NOTE_PANEL_H * scale),
1506
+ )
1507
+
1508
+ fx_detail = bool(int(self._state.config.data.get("fx_detail_0", 0) or 0))
1509
+ draw_classic_menu_panel(panel_tex, dst=panel, tint=rl.WHITE, shadow=fx_detail)
1510
+
1511
+ font = self._ensure_small_font()
1512
+ hardcore = bool(int(self._state.config.data.get("hardcore_flag", 0) or 0))
1513
+ header = " Incredible!" if hardcore else "Congratulations!"
1514
+ body_lines = (
1515
+ [
1516
+ "You've done the thing we all thought was",
1517
+ "virtually impossible. To reward your",
1518
+ "efforts a new weapon has been unlocked ",
1519
+ "for you: Splitter Gun.",
1520
+ "",
1521
+ "",
1522
+ ]
1523
+ if hardcore
1524
+ else [
1525
+ "You've completed all the levels but the battle",
1526
+ "isn't over yet! With all of the unlocked perks",
1527
+ "and weapons your Survival is just a bit easier.",
1528
+ "You can also replay the quests in Hardcore.",
1529
+ "As an additional reward for your victorious",
1530
+ "playing, a completely new and different game",
1531
+ "mode is unlocked for you: Typ'o'Shooter.",
1532
+ ]
1533
+ )
1534
+
1535
+ header_x = panel_left + END_NOTE_HEADER_X_OFFSET * scale
1536
+ header_y = panel_top + END_NOTE_HEADER_Y_OFFSET * scale
1537
+ header_color = rl.Color(255, 255, 255, int(255 * 0.8))
1538
+ body_color = rl.Color(255, 255, 255, int(255 * 0.5))
1539
+
1540
+ draw_small_text(font, header, header_x, header_y, 1.5 * scale, header_color)
1541
+
1542
+ body_x = panel_left + END_NOTE_BODY_X_OFFSET * scale
1543
+ body_y = header_y + END_NOTE_BODY_Y_GAP * scale
1544
+ for idx, line in enumerate(body_lines):
1545
+ draw_small_text(font, line, body_x, body_y, 1.0 * scale, body_color)
1546
+ if idx != len(body_lines) - 1:
1547
+ body_y += END_NOTE_LINE_STEP_Y * scale
1548
+ body_y += END_NOTE_AFTER_BODY_Y_GAP * scale
1549
+ draw_small_text(font, "Good luck with your battles, trooper!", body_x, body_y, 1.0 * scale, body_color)
1550
+
1551
+ textures = self._button_textures
1552
+ if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
1553
+ button_x = panel_left + END_NOTE_BUTTON_X_OFFSET * scale
1554
+ button_y = panel_top + END_NOTE_BUTTON_Y_OFFSET * scale
1555
+ survival_w = button_width(font, self._survival_button.label, scale=scale, force_wide=self._survival_button.force_wide)
1556
+ button_draw(textures, font, self._survival_button, x=button_x, y=button_y, width=survival_w, scale=scale)
1557
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1558
+ rush_w = button_width(font, self._rush_button.label, scale=scale, force_wide=self._rush_button.force_wide)
1559
+ button_draw(textures, font, self._rush_button, x=button_x, y=button_y, width=rush_w, scale=scale)
1560
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1561
+ typo_w = button_width(font, self._typo_button.label, scale=scale, force_wide=self._typo_button.force_wide)
1562
+ button_draw(textures, font, self._typo_button, x=button_x, y=button_y, width=typo_w, scale=scale)
1563
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1564
+ main_w = button_width(font, self._main_menu_button.label, scale=scale, force_wide=self._main_menu_button.force_wide)
1565
+ button_draw(textures, font, self._main_menu_button, x=button_x, y=button_y, width=main_w, scale=scale)
1566
+
1567
+ _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
1568
+
1569
+ def take_action(self) -> str | None:
1570
+ action = self._action
1571
+ self._action = None
1572
+ return action
1573
+
1574
+ def _ensure_small_font(self) -> SmallFontData:
1575
+ if self._small_font is not None:
1576
+ return self._small_font
1577
+ missing_assets: list[str] = []
1578
+ self._small_font = load_small_font(self._state.assets_dir, missing_assets)
1579
+ return self._small_font
1580
+
1581
+
1339
1582
  class QuestFailedView:
1340
1583
  def __init__(self, state: GameState) -> None:
1341
1584
  self._state = state
@@ -1944,6 +2187,7 @@ class GameLoopView:
1944
2187
  "start_quest": QuestGameView(state),
1945
2188
  "quest_results": QuestResultsView(state),
1946
2189
  "quest_failed": QuestFailedView(state),
2190
+ "end_note": EndNoteView(state),
1947
2191
  "open_high_scores": HighScoresView(state),
1948
2192
  "start_survival": SurvivalGameView(state),
1949
2193
  "start_rush": RushGameView(state),
@@ -2474,6 +2718,8 @@ def _resolve_assets_dir(config: GameConfig) -> Path:
2474
2718
 
2475
2719
 
2476
2720
  def run_game(config: GameConfig) -> None:
2721
+ if config.debug:
2722
+ set_debug_enabled(True)
2477
2723
  base_dir = config.base_dir
2478
2724
  base_dir.mkdir(parents=True, exist_ok=True)
2479
2725
  crash_path = base_dir / "crash.log"
@@ -534,6 +534,7 @@ class GameplayState:
534
534
  weapon_available: list[bool] = field(default_factory=lambda: [False] * WEAPON_COUNT_SIZE)
535
535
  _weapon_available_game_mode: int = -1
536
536
  _weapon_available_unlock_index: int = -1
537
+ _weapon_available_unlock_index_full: int = -1
537
538
  friendly_fire_enabled: bool = False
538
539
  bonus_spawn_guard: bool = False
539
540
  bonus_hud: BonusHudState = field(default_factory=BonusHudState)
@@ -547,6 +548,7 @@ class GameplayState:
547
548
  shots_fired: list[int] = field(default_factory=lambda: [0] * 4)
548
549
  shots_hit: list[int] = field(default_factory=lambda: [0] * 4)
549
550
  weapon_shots_fired: list[list[int]] = field(default_factory=lambda: [[0] * WEAPON_COUNT_SIZE for _ in range(4)])
551
+ debug_god_mode: bool = False
550
552
 
551
553
  def __post_init__(self) -> None:
552
554
  rand = self.rng.rand
@@ -1236,17 +1238,23 @@ def weapon_refresh_available(state: "GameplayState") -> None:
1236
1238
  """
1237
1239
 
1238
1240
  unlock_index = 0
1241
+ unlock_index_full = 0
1239
1242
  status = state.status
1240
1243
  if status is not None:
1241
1244
  try:
1242
1245
  unlock_index = int(status.quest_unlock_index)
1243
1246
  except Exception:
1244
1247
  unlock_index = 0
1248
+ try:
1249
+ unlock_index_full = int(status.quest_unlock_index_full)
1250
+ except Exception:
1251
+ unlock_index_full = 0
1245
1252
 
1246
1253
  game_mode = int(state.game_mode)
1247
1254
  if (
1248
1255
  int(state._weapon_available_game_mode) == game_mode
1249
1256
  and int(state._weapon_available_unlock_index) == unlock_index
1257
+ and int(state._weapon_available_unlock_index_full) == unlock_index_full
1250
1258
  ):
1251
1259
  return
1252
1260
 
@@ -1281,8 +1289,16 @@ def weapon_refresh_available(state: "GameplayState") -> None:
1281
1289
  if 0 <= idx < len(available):
1282
1290
  available[idx] = True
1283
1291
 
1292
+ # Secret unlock: Splitter Gun (weapon id 29) becomes available once the hardcore
1293
+ # unlock track reaches stage 5 (quest_unlock_index_full >= 40).
1294
+ if (not state.demo_mode_active) and unlock_index_full >= 0x28:
1295
+ splitter_id = int(WeaponId.SPLITTER_GUN)
1296
+ if 0 <= splitter_id < len(available):
1297
+ available[splitter_id] = True
1298
+
1284
1299
  state._weapon_available_game_mode = game_mode
1285
1300
  state._weapon_available_unlock_index = unlock_index
1301
+ state._weapon_available_unlock_index_full = unlock_index_full
1286
1302
 
1287
1303
 
1288
1304
  def weapon_pick_random_available(state: "GameplayState") -> int:
@@ -12,6 +12,7 @@ from grim.config import CrimsonConfig
12
12
  from grim.fonts.grim_mono import GrimMonoFont, load_grim_mono_font
13
13
  from grim.view import ViewContext
14
14
 
15
+ from ..debug import debug_enabled
15
16
  from ..game_modes import GameMode
16
17
  from ..gameplay import most_used_weapon_id_for_player, perk_selection_current_choices, perk_selection_pick, weapon_assign_player
17
18
  from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
@@ -44,6 +45,7 @@ from ..ui.perk_menu import (
44
45
  wrap_ui_text,
45
46
  )
46
47
  from ..views.quest_title_overlay import draw_quest_title_overlay
48
+ from ..weapons import WEAPON_BY_ID
47
49
  from .base_gameplay_mode import BaseGameplayMode, _clamp
48
50
 
49
51
  WORLD_SIZE = 1024.0
@@ -78,6 +80,8 @@ PERK_PROMPT_LEVEL_UP_SHIFT_X = -46.0
78
80
  PERK_PROMPT_LEVEL_UP_SHIFT_Y = -4.0
79
81
 
80
82
  PERK_PROMPT_TEXT_MARGIN_X = 16.0
83
+
84
+ _DEBUG_WEAPON_IDS = tuple(sorted(WEAPON_BY_ID))
81
85
  PERK_PROMPT_TEXT_OFFSET_Y = 8.0
82
86
 
83
87
 
@@ -305,10 +309,35 @@ class QuestMode(BaseGameplayMode):
305
309
  if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
306
310
  self._paused = not self._paused
307
311
 
312
+ if debug_enabled() and (not self._perk_menu_open):
313
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F2):
314
+ self._state.debug_god_mode = not bool(self._state.debug_god_mode)
315
+ self._world.audio_router.play_sfx("sfx_ui_buttonclick")
316
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F3):
317
+ self._state.perk_selection.pending_count += 1
318
+ self._state.perk_selection.choices_dirty = True
319
+ self._world.audio_router.play_sfx("sfx_ui_levelup")
320
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
321
+ self._debug_cycle_weapon(-1)
322
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
323
+ self._debug_cycle_weapon(1)
324
+
308
325
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
309
326
  self._action = "open_pause_menu"
310
327
  return
311
328
 
329
+ def _debug_cycle_weapon(self, delta: int) -> None:
330
+ weapon_ids = _DEBUG_WEAPON_IDS
331
+ if not weapon_ids:
332
+ return
333
+ current = int(self._player.weapon_id)
334
+ try:
335
+ idx = weapon_ids.index(current)
336
+ except ValueError:
337
+ idx = 0
338
+ weapon_id = int(weapon_ids[(idx + int(delta)) % len(weapon_ids)])
339
+ weapon_assign_player(self._player, weapon_id, state=self._state)
340
+
312
341
  def _build_input(self):
313
342
  keybinds = config_keybinds(self._config)
314
343
  if not keybinds:
@@ -875,6 +904,12 @@ class QuestMode(BaseGameplayMode):
875
904
  small_indicators=self._hud_small_indicators(),
876
905
  )
877
906
 
907
+ if debug_enabled() and (not perk_menu_active):
908
+ x = 18.0
909
+ y = max(18.0, hud_bottom + 10.0)
910
+ god = "on" if self._state.debug_god_mode else "off"
911
+ self._draw_ui_text(f"debug: [/] weapon F3 perk+1 F2 god={god}", x, y, UI_HINT_COLOR, scale=0.9)
912
+
878
913
  self._draw_quest_title()
879
914
 
880
915
  warn_y = float(rl.get_screen_height()) - 28.0
@@ -895,6 +930,8 @@ class QuestMode(BaseGameplayMode):
895
930
  self._draw_game_cursor()
896
931
  x = 18.0
897
932
  y = max(18.0, hud_bottom + 10.0)
933
+ if debug_enabled() and (not perk_menu_active):
934
+ y += float(self._ui_line_height(scale=0.9))
898
935
  self._draw_ui_text("paused (TAB)", x, y, UI_HINT_COLOR)
899
936
  else:
900
937
  self._draw_aim_cursor()
@@ -14,7 +14,14 @@ from grim.view import ViewContext
14
14
  from ..creatures.spawn import advance_survival_spawn_stage, tick_survival_wave_spawns
15
15
  from ..debug import debug_enabled
16
16
  from ..game_modes import GameMode
17
- from ..gameplay import PlayerInput, most_used_weapon_id_for_player, perk_selection_current_choices, perk_selection_pick, survival_check_level_up
17
+ from ..gameplay import (
18
+ PlayerInput,
19
+ most_used_weapon_id_for_player,
20
+ perk_selection_current_choices,
21
+ perk_selection_pick,
22
+ survival_check_level_up,
23
+ weapon_assign_player,
24
+ )
18
25
  from ..persistence.highscores import HighScoreRecord
19
26
  from ..perks import PerkId, perk_display_description, perk_display_name
20
27
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
@@ -38,6 +45,7 @@ from ..ui.perk_menu import (
38
45
  ui_scale,
39
46
  wrap_ui_text,
40
47
  )
48
+ from ..weapons import WEAPON_BY_ID
41
49
  from .base_gameplay_mode import BaseGameplayMode, _clamp
42
50
 
43
51
  WORLD_SIZE = 1024.0
@@ -71,6 +79,8 @@ PERK_PROMPT_LEVEL_UP_SHIFT_Y = -4.0
71
79
  PERK_PROMPT_TEXT_MARGIN_X = 16.0
72
80
  PERK_PROMPT_TEXT_OFFSET_Y = 8.0
73
81
 
82
+ _DEBUG_WEAPON_IDS = tuple(sorted(WEAPON_BY_ID))
83
+
74
84
 
75
85
  @dataclass(slots=True)
76
86
  class _SurvivalState:
@@ -181,14 +191,38 @@ class SurvivalMode(BaseGameplayMode):
181
191
  if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
182
192
  self._paused = not self._paused
183
193
 
184
- if debug_enabled() and rl.is_key_pressed(rl.KeyboardKey.KEY_X):
185
- self._player.experience += 5000
186
- survival_check_level_up(self._player, self._state.perk_selection)
194
+ if debug_enabled() and (not self._perk_menu_open):
195
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F2):
196
+ self._state.debug_god_mode = not bool(self._state.debug_god_mode)
197
+ self._world.audio_router.play_sfx("sfx_ui_buttonclick")
198
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F3):
199
+ self._state.perk_selection.pending_count += 1
200
+ self._state.perk_selection.choices_dirty = True
201
+ self._world.audio_router.play_sfx("sfx_ui_levelup")
202
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
203
+ self._debug_cycle_weapon(-1)
204
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
205
+ self._debug_cycle_weapon(1)
206
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_X):
207
+ self._player.experience += 5000
208
+ survival_check_level_up(self._player, self._state.perk_selection)
187
209
 
188
210
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
189
211
  self._action = "open_pause_menu"
190
212
  return
191
213
 
214
+ def _debug_cycle_weapon(self, delta: int) -> None:
215
+ weapon_ids = _DEBUG_WEAPON_IDS
216
+ if not weapon_ids:
217
+ return
218
+ current = int(self._player.weapon_id)
219
+ try:
220
+ idx = weapon_ids.index(current)
221
+ except ValueError:
222
+ idx = 0
223
+ weapon_id = int(weapon_ids[(idx + int(delta)) % len(weapon_ids)])
224
+ weapon_assign_player(self._player, weapon_id, state=self._state)
225
+
192
226
  def _build_input(self) -> PlayerInput:
193
227
  keybinds = config_keybinds(self._config)
194
228
  if not keybinds:
@@ -779,10 +813,12 @@ class SurvivalMode(BaseGameplayMode):
779
813
  line = float(self._ui_line_height())
780
814
  self._draw_ui_text(f"survival: t={self._survival.elapsed_ms/1000.0:6.1f}s stage={self._survival.stage}", x, y, UI_TEXT_COLOR)
781
815
  self._draw_ui_text(f"xp={self._player.experience} level={self._player.level} kills={self._creatures.kill_count}", x, y + line, UI_HINT_COLOR)
816
+ god = "on" if self._state.debug_god_mode else "off"
817
+ self._draw_ui_text(f"debug: [/] weapon F3 perk+1 F2 god={god} X xp+5000", x, y + line * 2.0, UI_HINT_COLOR, scale=0.9)
782
818
  if self._paused:
783
- self._draw_ui_text("paused (TAB)", x, y + line * 2.0, UI_HINT_COLOR)
819
+ self._draw_ui_text("paused (TAB)", x, y + line * 3.0, UI_HINT_COLOR)
784
820
  if self._player.health <= 0.0:
785
- self._draw_ui_text("game over", x, y + line * 2.0, UI_ERROR_COLOR)
821
+ self._draw_ui_text("game over", x, y + line * 3.0, UI_ERROR_COLOR)
786
822
  warn_y = float(rl.get_screen_height()) - 28.0
787
823
  if self._world.missing_assets:
788
824
  warn = "Missing world assets: " + ", ".join(self._world.missing_assets)
@@ -32,6 +32,8 @@ def player_take_damage(
32
32
  dmg = float(damage)
33
33
  if dmg <= 0.0:
34
34
  return 0.0
35
+ if state.debug_god_mode:
36
+ return 0.0
35
37
 
36
38
  # 1) Death Clock immunity.
37
39
  if perk_active(player, PerkId.DEATH_CLOCK):