crimsonland 0.1.0.dev14__py3-none-any.whl → 0.1.0.dev16__py3-none-any.whl

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 (73) hide show
  1. crimson/cli.py +63 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/debug.py +9 -0
  6. crimson/demo.py +38 -45
  7. crimson/effects.py +7 -13
  8. crimson/frontend/high_scores_layout.py +81 -0
  9. crimson/frontend/panels/base.py +4 -1
  10. crimson/frontend/panels/controls.py +0 -15
  11. crimson/frontend/panels/databases.py +291 -3
  12. crimson/frontend/panels/mods.py +0 -15
  13. crimson/frontend/panels/play_game.py +0 -16
  14. crimson/game.py +689 -3
  15. crimson/gameplay.py +921 -569
  16. crimson/modes/base_gameplay_mode.py +33 -12
  17. crimson/modes/components/__init__.py +2 -0
  18. crimson/modes/components/highscore_record_builder.py +58 -0
  19. crimson/modes/components/perk_menu_controller.py +325 -0
  20. crimson/modes/quest_mode.py +94 -272
  21. crimson/modes/rush_mode.py +12 -43
  22. crimson/modes/survival_mode.py +109 -330
  23. crimson/modes/tutorial_mode.py +46 -247
  24. crimson/modes/typo_mode.py +11 -38
  25. crimson/oracle.py +396 -0
  26. crimson/perks.py +5 -2
  27. crimson/player_damage.py +95 -36
  28. crimson/projectiles.py +539 -320
  29. crimson/render/projectile_draw_registry.py +637 -0
  30. crimson/render/projectile_render_registry.py +110 -0
  31. crimson/render/secondary_projectile_draw_registry.py +206 -0
  32. crimson/render/world_renderer.py +58 -707
  33. crimson/sim/world_state.py +118 -61
  34. crimson/typo/spawns.py +5 -12
  35. crimson/ui/demo_trial_overlay.py +3 -11
  36. crimson/ui/formatting.py +24 -0
  37. crimson/ui/game_over.py +12 -58
  38. crimson/ui/hud.py +72 -39
  39. crimson/ui/layout.py +20 -0
  40. crimson/ui/perk_menu.py +9 -34
  41. crimson/ui/quest_results.py +28 -70
  42. crimson/ui/text_input.py +20 -0
  43. crimson/views/_ui_helpers.py +27 -0
  44. crimson/views/aim_debug.py +15 -32
  45. crimson/views/animations.py +18 -28
  46. crimson/views/arsenal_debug.py +22 -32
  47. crimson/views/bonuses.py +23 -36
  48. crimson/views/camera_debug.py +16 -29
  49. crimson/views/camera_shake.py +9 -33
  50. crimson/views/corpse_stamp_debug.py +13 -21
  51. crimson/views/decals_debug.py +36 -23
  52. crimson/views/fonts.py +8 -25
  53. crimson/views/ground.py +4 -21
  54. crimson/views/lighting_debug.py +42 -45
  55. crimson/views/particles.py +33 -42
  56. crimson/views/perk_menu_debug.py +3 -10
  57. crimson/views/player.py +50 -44
  58. crimson/views/player_sprite_debug.py +24 -31
  59. crimson/views/projectile_fx.py +57 -52
  60. crimson/views/projectile_render_debug.py +24 -33
  61. crimson/views/projectiles.py +24 -37
  62. crimson/views/spawn_plan.py +13 -29
  63. crimson/views/sprites.py +14 -29
  64. crimson/views/terrain.py +6 -23
  65. crimson/views/ui.py +7 -24
  66. crimson/views/wicons.py +28 -33
  67. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  68. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +73 -62
  69. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +1 -1
  70. grim/config.py +29 -1
  71. grim/console.py +7 -10
  72. grim/math.py +12 -0
  73. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
crimson/cli.py CHANGED
@@ -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
 
@@ -372,6 +374,67 @@ def cmd_spawn_plan(
372
374
  typer.echo(f"burst x={fx.x:.1f} y={fx.y:.1f} count={fx.count}")
373
375
 
374
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
+
375
438
  def main(argv: list[str] | None = None) -> None:
376
439
  app(prog_name="crimson", args=argv)
377
440
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
3
4
  import math
4
5
  from typing import Callable
5
6
 
@@ -17,6 +18,94 @@ def _owner_id_to_player_index(owner_id: int) -> int | None:
17
18
  return None
18
19
 
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
+
20
109
  def creature_apply_damage(
21
110
  creature: CreatureState,
22
111
  *,
@@ -44,49 +133,35 @@ def creature_apply_damage(
44
133
  player_index = _owner_id_to_player_index(owner_id)
45
134
  attacker = players[player_index] if player_index is not None and 0 <= player_index < len(players) else None
46
135
 
47
- damage = float(damage_amount)
48
-
49
- if int(damage_type) == 1 and attacker is not None:
50
- if perk_active(attacker, PerkId.URANIUM_FILLED_BULLETS):
51
- damage *= 2.0
52
-
53
- if perk_active(attacker, PerkId.LIVING_FORTRESS):
54
- for player in players:
55
- if float(player.health) <= 0.0:
56
- continue
57
- timer = float(player.living_fortress_timer)
58
- if timer > 0.0:
59
- damage *= timer * 0.05 + 1.0
60
-
61
- if perk_active(attacker, PerkId.BARREL_GREASER):
62
- damage *= 1.4
63
- if perk_active(attacker, PerkId.DOCTOR):
64
- damage *= 1.2
65
-
66
- if (creature.flags & CreatureFlags.ANIM_PING_PONG) == 0:
67
- jitter = float((int(rand()) & 0x7F) - 0x40) * 0.002
68
- size = max(1e-6, float(creature.size))
69
- turn = jitter / (size * 0.025)
70
- turn = max(-math.pi / 2.0, min(math.pi / 2.0, turn))
71
- creature.heading += turn
72
-
73
- if int(damage_type) == 7 and attacker is not None:
74
- if perk_active(attacker, PerkId.ION_GUN_MASTER):
75
- damage *= 1.2
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)
76
152
 
77
153
  if creature.hp <= 0.0:
78
154
  if dt > 0.0:
79
155
  creature.hitbox_size -= float(dt) * 15.0
80
156
  return True
81
157
 
82
- if int(damage_type) == 4 and attacker is not None:
83
- if perk_active(attacker, PerkId.PYROMANIAC):
84
- damage *= 1.5
85
- rand()
158
+ if attacker is not None:
159
+ for step in _CREATURE_DAMAGE_ATTACKER_ALIVE_STEPS.get(ctx.damage_type, ()):
160
+ step(ctx)
86
161
 
87
- creature.hp -= damage
88
- creature.vel_x -= float(impulse_x)
89
- creature.vel_y -= float(impulse_y)
162
+ creature.hp -= float(ctx.damage)
163
+ creature.vel_x -= float(ctx.impulse_x)
164
+ creature.vel_y -= float(ctx.impulse_y)
90
165
 
91
166
  if creature.hp <= 0.0:
92
167
  if dt > 0.0: