crimsonland 0.1.0.dev15__tar.gz → 0.1.0.dev16__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 (162) hide show
  1. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/pyproject.toml +1 -1
  3. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/cli.py +61 -0
  4. crimsonland-0.1.0.dev16/src/crimson/creatures/damage.py +178 -0
  5. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/creatures/runtime.py +246 -156
  6. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/creatures/spawn.py +7 -3
  7. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/demo.py +38 -45
  8. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/effects.py +7 -13
  9. crimsonland-0.1.0.dev16/src/crimson/frontend/high_scores_layout.py +107 -0
  10. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/base.py +4 -1
  11. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/controls.py +0 -15
  12. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/databases.py +291 -3
  13. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/mods.py +0 -15
  14. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/play_game.py +0 -16
  15. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/game.py +441 -1
  16. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/gameplay.py +905 -569
  17. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/modes/base_gameplay_mode.py +33 -12
  18. crimsonland-0.1.0.dev16/src/crimson/modes/components/__init__.py +2 -0
  19. crimsonland-0.1.0.dev16/src/crimson/modes/components/highscore_record_builder.py +58 -0
  20. crimsonland-0.1.0.dev16/src/crimson/modes/components/perk_menu_controller.py +325 -0
  21. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/modes/quest_mode.py +58 -273
  22. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/modes/rush_mode.py +12 -43
  23. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/modes/survival_mode.py +71 -328
  24. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/modes/tutorial_mode.py +46 -247
  25. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/modes/typo_mode.py +11 -38
  26. crimsonland-0.1.0.dev16/src/crimson/oracle.py +396 -0
  27. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/perks.py +5 -2
  28. crimsonland-0.1.0.dev16/src/crimson/player_damage.py +136 -0
  29. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/projectiles.py +539 -320
  30. crimsonland-0.1.0.dev16/src/crimson/render/projectile_draw_registry.py +637 -0
  31. crimsonland-0.1.0.dev16/src/crimson/render/projectile_render_registry.py +110 -0
  32. crimsonland-0.1.0.dev16/src/crimson/render/secondary_projectile_draw_registry.py +206 -0
  33. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/render/world_renderer.py +58 -707
  34. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/sim/world_state.py +118 -61
  35. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/typo/spawns.py +5 -12
  36. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/demo_trial_overlay.py +3 -11
  37. crimsonland-0.1.0.dev16/src/crimson/ui/formatting.py +24 -0
  38. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/game_over.py +12 -58
  39. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/hud.py +72 -39
  40. crimsonland-0.1.0.dev16/src/crimson/ui/layout.py +20 -0
  41. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/perk_menu.py +9 -34
  42. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/quest_results.py +12 -64
  43. crimsonland-0.1.0.dev16/src/crimson/ui/text_input.py +20 -0
  44. crimsonland-0.1.0.dev16/src/crimson/views/_ui_helpers.py +27 -0
  45. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/aim_debug.py +15 -32
  46. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/animations.py +18 -28
  47. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/arsenal_debug.py +22 -32
  48. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/bonuses.py +23 -36
  49. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/camera_debug.py +16 -29
  50. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/camera_shake.py +9 -33
  51. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/corpse_stamp_debug.py +13 -21
  52. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/decals_debug.py +36 -23
  53. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/fonts.py +8 -25
  54. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/ground.py +4 -21
  55. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/lighting_debug.py +42 -45
  56. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/particles.py +33 -42
  57. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/perk_menu_debug.py +3 -10
  58. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/player.py +50 -44
  59. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/player_sprite_debug.py +24 -31
  60. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/projectile_fx.py +57 -52
  61. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/projectile_render_debug.py +24 -33
  62. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/projectiles.py +24 -37
  63. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/spawn_plan.py +13 -29
  64. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/sprites.py +14 -29
  65. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/terrain.py +6 -23
  66. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/ui.py +7 -24
  67. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/wicons.py +28 -33
  68. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/config.py +29 -1
  69. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/console.py +7 -10
  70. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/math.py +12 -0
  71. crimsonland-0.1.0.dev15/src/crimson/.DS_Store +0 -0
  72. crimsonland-0.1.0.dev15/src/crimson/creatures/.DS_Store +0 -0
  73. crimsonland-0.1.0.dev15/src/crimson/creatures/damage.py +0 -103
  74. crimsonland-0.1.0.dev15/src/crimson/frontend/high_scores_layout.py +0 -26
  75. crimsonland-0.1.0.dev15/src/crimson/player_damage.py +0 -79
  76. crimsonland-0.1.0.dev15/src/grim/.DS_Store +0 -0
  77. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/readme.md +0 -0
  78. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/__init__.py +0 -0
  79. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/assets_fetch.py +0 -0
  80. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/atlas.py +0 -0
  81. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/audio_router.py +0 -0
  82. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/bonuses.py +0 -0
  83. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/camera.py +0 -0
  84. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/creatures/__init__.py +0 -0
  85. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/creatures/ai.py +0 -0
  86. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/creatures/anim.py +0 -0
  87. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/debug.py +0 -0
  88. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/demo_trial.py +0 -0
  89. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/effects_atlas.py +0 -0
  90. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/__init__.py +0 -0
  91. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/assets.py +0 -0
  92. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/boot.py +0 -0
  93. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/menu.py +0 -0
  94. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/__init__.py +0 -0
  95. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/credits.py +0 -0
  96. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/options.py +0 -0
  97. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/panels/stats.py +0 -0
  98. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/pause_menu.py +0 -0
  99. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/frontend/transitions.py +0 -0
  100. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/game_modes.py +0 -0
  101. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/game_world.py +0 -0
  102. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/input_codes.py +0 -0
  103. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/modes/__init__.py +0 -0
  104. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/paths.py +0 -0
  105. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/persistence/__init__.py +0 -0
  106. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/persistence/highscores.py +0 -0
  107. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/persistence/save_status.py +0 -0
  108. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/__init__.py +0 -0
  109. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/helpers.py +0 -0
  110. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/registry.py +0 -0
  111. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/results.py +0 -0
  112. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/runtime.py +0 -0
  113. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/tier1.py +0 -0
  114. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/tier2.py +0 -0
  115. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/tier3.py +0 -0
  116. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/tier4.py +0 -0
  117. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/tier5.py +0 -0
  118. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/timeline.py +0 -0
  119. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/quests/types.py +0 -0
  120. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/render/__init__.py +0 -0
  121. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/render/terrain_fx.py +0 -0
  122. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/sim/__init__.py +0 -0
  123. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/sim/world_defs.py +0 -0
  124. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/terrain_assets.py +0 -0
  125. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/tutorial/__init__.py +0 -0
  126. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/tutorial/timeline.py +0 -0
  127. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/typo/__init__.py +0 -0
  128. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/typo/names.py +0 -0
  129. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/typo/player.py +0 -0
  130. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/typo/typing.py +0 -0
  131. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/__init__.py +0 -0
  132. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/cursor.py +0 -0
  133. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/menu_panel.py +0 -0
  134. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/ui/shadow.py +0 -0
  135. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/__init__.py +0 -0
  136. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/audio_bootstrap.py +0 -0
  137. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/empty.py +0 -0
  138. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/game_over.py +0 -0
  139. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/perks.py +0 -0
  140. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/quest_title_overlay.py +0 -0
  141. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/registry.py +0 -0
  142. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/rush.py +0 -0
  143. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/small_font_debug.py +0 -0
  144. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/views/survival.py +0 -0
  145. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/weapon_sfx.py +0 -0
  146. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/crimson/weapons.py +0 -0
  147. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/__init__.py +0 -0
  148. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/app.py +0 -0
  149. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/assets.py +0 -0
  150. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/audio.py +0 -0
  151. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/fonts/__init__.py +0 -0
  152. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/fonts/grim_mono.py +0 -0
  153. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/fonts/small.py +0 -0
  154. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/input.py +0 -0
  155. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/jaz.py +0 -0
  156. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/music.py +0 -0
  157. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/paq.py +0 -0
  158. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/rand.py +0 -0
  159. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/sfx.py +0 -0
  160. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/sfx_map.py +0 -0
  161. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/src/grim/terrain_render.py +0 -0
  162. {crimsonland-0.1.0.dev15 → crimsonland-0.1.0.dev16}/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.dev15
3
+ Version: 0.1.0.dev16
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.dev15"
3
+ version = "0.1.0.dev16"
4
4
  readme = { file = "readme.md", content-type = "text/markdown" }
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
@@ -374,6 +374,67 @@ def cmd_spawn_plan(
374
374
  typer.echo(f"burst x={fx.x:.1f} y={fx.y:.1f} count={fx.count}")
375
375
 
376
376
 
377
+ @app.command("oracle")
378
+ def cmd_oracle(
379
+ seed: int = typer.Option(0xBEEF, help="RNG seed for deterministic runs"),
380
+ input_file: Path | None = typer.Option(None, "--input-file", "-i", help="JSON file with input sequence"),
381
+ max_frames: int = typer.Option(36000, help="Maximum frames to run (default: 10 min at 60fps)"),
382
+ frame_rate: int = typer.Option(60, help="Frame rate for simulation"),
383
+ sample_rate: int = typer.Option(60, "--sample-rate", "-s", help="Emit state every N frames (1=every frame, 60=1/sec)"),
384
+ output_mode: str = typer.Option(
385
+ "summary",
386
+ "--output", "-o",
387
+ help="Output mode: full (all entities), summary (fast), hash (ultra-fast), checkpoints (on events only)",
388
+ ),
389
+ ) -> None:
390
+ """Run headless oracle mode for differential testing.
391
+
392
+ Emits JSON game state to stdout. Use with --seed for deterministic runs
393
+ and --input-file for replaying specific input sequences.
394
+
395
+ Output modes:
396
+ - summary: Score, kills, player pos/health (default, fast)
397
+ - full: All entities including creatures, projectiles, bonuses
398
+ - hash: SHA256 hash of full state (ultra-fast comparison)
399
+ - checkpoints: Emit only when score/kills/level/weapon changes
400
+
401
+ Examples:
402
+ # Fast validation at 1 Hz sampling
403
+ crimson oracle --seed 12345 -i replay.json -s 60 -o summary
404
+
405
+ # Full frame-by-frame for debugging divergence
406
+ crimson oracle --seed 12345 -i replay.json -s 1 -o full
407
+
408
+ # Ultra-fast hash comparison
409
+ crimson oracle --seed 12345 -i replay.json -o hash
410
+
411
+ # Event-driven checkpoints only
412
+ crimson oracle --seed 12345 -i replay.json -o checkpoints
413
+ """
414
+ from .oracle import OracleConfig, OutputMode, run_headless
415
+
416
+ # Validate output mode
417
+ mode_map = {
418
+ "full": OutputMode.FULL,
419
+ "summary": OutputMode.SUMMARY,
420
+ "hash": OutputMode.HASH,
421
+ "checkpoints": OutputMode.CHECKPOINTS,
422
+ }
423
+ if output_mode not in mode_map:
424
+ typer.echo(f"Invalid output mode: {output_mode!r}. Choose from: {', '.join(mode_map)}", err=True)
425
+ raise typer.Exit(code=1)
426
+
427
+ config = OracleConfig(
428
+ seed=seed,
429
+ input_file=input_file,
430
+ max_frames=max_frames,
431
+ frame_rate=frame_rate,
432
+ sample_rate=sample_rate,
433
+ output_mode=mode_map[output_mode],
434
+ )
435
+ run_headless(config)
436
+
437
+
377
438
  def main(argv: list[str] | None = None) -> None:
378
439
  app(prog_name="crimson", args=argv)
379
440
 
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+ from typing import Callable
6
+
7
+ from ..gameplay import PlayerState, perk_active
8
+ from ..perks import PerkId
9
+ from .runtime import CREATURE_HITBOX_ALIVE, CreatureState
10
+ from .spawn import CreatureFlags
11
+
12
+
13
+ def _owner_id_to_player_index(owner_id: int) -> int | None:
14
+ if owner_id == -100:
15
+ return 0
16
+ if owner_id < 0:
17
+ return -1 - owner_id
18
+ return None
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class _CreatureDamageCtx:
23
+ creature: CreatureState
24
+ damage: float
25
+ damage_type: int
26
+ impulse_x: float
27
+ impulse_y: float
28
+ owner_id: int
29
+ dt: float
30
+ players: list[PlayerState]
31
+ rand: Callable[[], int]
32
+ attacker: PlayerState | None
33
+
34
+
35
+ _CreatureDamageStep = Callable[[_CreatureDamageCtx], None]
36
+
37
+
38
+ def _damage_type1_uranium_filled_bullets(ctx: _CreatureDamageCtx) -> None:
39
+ if ctx.attacker is None or not perk_active(ctx.attacker, PerkId.URANIUM_FILLED_BULLETS):
40
+ return
41
+ ctx.damage *= 2.0
42
+
43
+
44
+ def _damage_type1_living_fortress(ctx: _CreatureDamageCtx) -> None:
45
+ attacker = ctx.attacker
46
+ if attacker is None or not perk_active(attacker, PerkId.LIVING_FORTRESS):
47
+ return
48
+ for player in ctx.players:
49
+ if float(player.health) <= 0.0:
50
+ continue
51
+ timer = float(player.living_fortress_timer)
52
+ if timer > 0.0:
53
+ ctx.damage *= timer * 0.05 + 1.0
54
+
55
+
56
+ def _damage_type1_barrel_greaser(ctx: _CreatureDamageCtx) -> None:
57
+ if ctx.attacker is None or not perk_active(ctx.attacker, PerkId.BARREL_GREASER):
58
+ return
59
+ ctx.damage *= 1.4
60
+
61
+
62
+ def _damage_type1_doctor(ctx: _CreatureDamageCtx) -> None:
63
+ if ctx.attacker is None or not perk_active(ctx.attacker, PerkId.DOCTOR):
64
+ return
65
+ ctx.damage *= 1.2
66
+
67
+
68
+ def _damage_type1_heading_jitter(ctx: _CreatureDamageCtx) -> None:
69
+ creature = ctx.creature
70
+ if (creature.flags & CreatureFlags.ANIM_PING_PONG) != 0:
71
+ return
72
+ jitter = float((int(ctx.rand()) & 0x7F) - 0x40) * 0.002
73
+ size = max(1e-6, float(creature.size))
74
+ turn = jitter / (size * 0.025)
75
+ turn = max(-math.pi / 2.0, min(math.pi / 2.0, turn))
76
+ creature.heading += turn
77
+
78
+
79
+ def _damage_type7_ion_gun_master(ctx: _CreatureDamageCtx) -> None:
80
+ if ctx.attacker is None or not perk_active(ctx.attacker, PerkId.ION_GUN_MASTER):
81
+ return
82
+ ctx.damage *= 1.2
83
+
84
+
85
+ def _damage_type4_pyromaniac(ctx: _CreatureDamageCtx) -> None:
86
+ if ctx.attacker is None or not perk_active(ctx.attacker, PerkId.PYROMANIAC):
87
+ return
88
+ ctx.damage *= 1.5
89
+ ctx.rand()
90
+
91
+
92
+ _CREATURE_DAMAGE_ATTACKER_PRE_STEPS: dict[int, tuple[_CreatureDamageStep, ...]] = {
93
+ 1: (
94
+ _damage_type1_uranium_filled_bullets,
95
+ _damage_type1_living_fortress,
96
+ _damage_type1_barrel_greaser,
97
+ _damage_type1_doctor,
98
+ _damage_type1_heading_jitter,
99
+ ),
100
+ 7: (_damage_type7_ion_gun_master,),
101
+ }
102
+
103
+
104
+ _CREATURE_DAMAGE_ATTACKER_ALIVE_STEPS: dict[int, tuple[_CreatureDamageStep, ...]] = {
105
+ 4: (_damage_type4_pyromaniac,),
106
+ }
107
+
108
+
109
+ def creature_apply_damage(
110
+ creature: CreatureState,
111
+ *,
112
+ damage_amount: float,
113
+ damage_type: int,
114
+ impulse_x: float,
115
+ impulse_y: float,
116
+ owner_id: int,
117
+ dt: float,
118
+ players: list[PlayerState],
119
+ rand: Callable[[], int],
120
+ ) -> bool:
121
+ """Apply damage to a creature, returning True if the hit killed it.
122
+
123
+ This is a partial port of `creature_apply_damage` (FUN_004207c0).
124
+
125
+ Notes:
126
+ - Death side-effects are handled by the caller (see Phase 2 in `plan.md`).
127
+ - `damage_type` is a native integer category; call sites must supply it.
128
+ """
129
+
130
+ creature.last_hit_owner_id = int(owner_id)
131
+ creature.hit_flash_timer = 0.2
132
+
133
+ player_index = _owner_id_to_player_index(owner_id)
134
+ attacker = players[player_index] if player_index is not None and 0 <= player_index < len(players) else None
135
+
136
+ ctx = _CreatureDamageCtx(
137
+ creature=creature,
138
+ damage=float(damage_amount),
139
+ damage_type=int(damage_type),
140
+ impulse_x=float(impulse_x),
141
+ impulse_y=float(impulse_y),
142
+ owner_id=int(owner_id),
143
+ dt=float(dt),
144
+ players=players,
145
+ rand=rand,
146
+ attacker=attacker,
147
+ )
148
+
149
+ if attacker is not None:
150
+ for step in _CREATURE_DAMAGE_ATTACKER_PRE_STEPS.get(ctx.damage_type, ()):
151
+ step(ctx)
152
+
153
+ if creature.hp <= 0.0:
154
+ if dt > 0.0:
155
+ creature.hitbox_size -= float(dt) * 15.0
156
+ return True
157
+
158
+ if attacker is not None:
159
+ for step in _CREATURE_DAMAGE_ATTACKER_ALIVE_STEPS.get(ctx.damage_type, ()):
160
+ step(ctx)
161
+
162
+ creature.hp -= float(ctx.damage)
163
+ creature.vel_x -= float(ctx.impulse_x)
164
+ creature.vel_y -= float(ctx.impulse_y)
165
+
166
+ if creature.hp <= 0.0:
167
+ if dt > 0.0:
168
+ creature.hitbox_size = float(creature.hitbox_size) - float(dt)
169
+ else:
170
+ creature.hitbox_size = float(creature.hitbox_size) - 0.001
171
+ creature.vel_x -= float(impulse_x) * 2.0
172
+ creature.vel_y -= float(impulse_y) * 2.0
173
+ return True
174
+
175
+ if creature.hitbox_size != CREATURE_HITBOX_ALIVE and dt > 0.0:
176
+ creature.hitbox_size = CREATURE_HITBOX_ALIVE
177
+
178
+ return False