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.
- crimson/cli.py +63 -0
- crimson/creatures/damage.py +111 -36
- crimson/creatures/runtime.py +246 -156
- crimson/creatures/spawn.py +7 -3
- crimson/debug.py +9 -0
- crimson/demo.py +38 -45
- crimson/effects.py +7 -13
- crimson/frontend/high_scores_layout.py +81 -0
- crimson/frontend/panels/base.py +4 -1
- crimson/frontend/panels/controls.py +0 -15
- crimson/frontend/panels/databases.py +291 -3
- crimson/frontend/panels/mods.py +0 -15
- crimson/frontend/panels/play_game.py +0 -16
- crimson/game.py +689 -3
- crimson/gameplay.py +921 -569
- crimson/modes/base_gameplay_mode.py +33 -12
- crimson/modes/components/__init__.py +2 -0
- crimson/modes/components/highscore_record_builder.py +58 -0
- crimson/modes/components/perk_menu_controller.py +325 -0
- crimson/modes/quest_mode.py +94 -272
- crimson/modes/rush_mode.py +12 -43
- crimson/modes/survival_mode.py +109 -330
- crimson/modes/tutorial_mode.py +46 -247
- crimson/modes/typo_mode.py +11 -38
- crimson/oracle.py +396 -0
- crimson/perks.py +5 -2
- crimson/player_damage.py +95 -36
- crimson/projectiles.py +539 -320
- crimson/render/projectile_draw_registry.py +637 -0
- crimson/render/projectile_render_registry.py +110 -0
- crimson/render/secondary_projectile_draw_registry.py +206 -0
- crimson/render/world_renderer.py +58 -707
- crimson/sim/world_state.py +118 -61
- crimson/typo/spawns.py +5 -12
- crimson/ui/demo_trial_overlay.py +3 -11
- crimson/ui/formatting.py +24 -0
- crimson/ui/game_over.py +12 -58
- crimson/ui/hud.py +72 -39
- crimson/ui/layout.py +20 -0
- crimson/ui/perk_menu.py +9 -34
- crimson/ui/quest_results.py +28 -70
- crimson/ui/text_input.py +20 -0
- crimson/views/_ui_helpers.py +27 -0
- crimson/views/aim_debug.py +15 -32
- crimson/views/animations.py +18 -28
- crimson/views/arsenal_debug.py +22 -32
- crimson/views/bonuses.py +23 -36
- crimson/views/camera_debug.py +16 -29
- crimson/views/camera_shake.py +9 -33
- crimson/views/corpse_stamp_debug.py +13 -21
- crimson/views/decals_debug.py +36 -23
- crimson/views/fonts.py +8 -25
- crimson/views/ground.py +4 -21
- crimson/views/lighting_debug.py +42 -45
- crimson/views/particles.py +33 -42
- crimson/views/perk_menu_debug.py +3 -10
- crimson/views/player.py +50 -44
- crimson/views/player_sprite_debug.py +24 -31
- crimson/views/projectile_fx.py +57 -52
- crimson/views/projectile_render_debug.py +24 -33
- crimson/views/projectiles.py +24 -37
- crimson/views/spawn_plan.py +13 -29
- crimson/views/sprites.py +14 -29
- crimson/views/terrain.py +6 -23
- crimson/views/ui.py +7 -24
- crimson/views/wicons.py +28 -33
- {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
- {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +73 -62
- {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +1 -1
- grim/config.py +29 -1
- grim/console.py +7 -10
- grim/math.py +12 -0
- {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
|
|
crimson/creatures/damage.py
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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:
|