crimsonland 0.1.0.dev5__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 (139) hide show
  1. crimson/__init__.py +24 -0
  2. crimson/assets_fetch.py +60 -0
  3. crimson/atlas.py +92 -0
  4. crimson/audio_router.py +155 -0
  5. crimson/bonuses.py +167 -0
  6. crimson/camera.py +75 -0
  7. crimson/cli.py +380 -0
  8. crimson/creatures/__init__.py +8 -0
  9. crimson/creatures/ai.py +186 -0
  10. crimson/creatures/anim.py +173 -0
  11. crimson/creatures/damage.py +103 -0
  12. crimson/creatures/runtime.py +1019 -0
  13. crimson/creatures/spawn.py +2871 -0
  14. crimson/debug.py +7 -0
  15. crimson/demo.py +1360 -0
  16. crimson/demo_trial.py +140 -0
  17. crimson/effects.py +1086 -0
  18. crimson/effects_atlas.py +73 -0
  19. crimson/frontend/__init__.py +1 -0
  20. crimson/frontend/assets.py +43 -0
  21. crimson/frontend/boot.py +424 -0
  22. crimson/frontend/menu.py +700 -0
  23. crimson/frontend/panels/__init__.py +1 -0
  24. crimson/frontend/panels/base.py +410 -0
  25. crimson/frontend/panels/controls.py +132 -0
  26. crimson/frontend/panels/mods.py +128 -0
  27. crimson/frontend/panels/options.py +409 -0
  28. crimson/frontend/panels/play_game.py +627 -0
  29. crimson/frontend/panels/stats.py +351 -0
  30. crimson/frontend/transitions.py +31 -0
  31. crimson/game.py +2533 -0
  32. crimson/game_modes.py +15 -0
  33. crimson/game_world.py +652 -0
  34. crimson/gameplay.py +2467 -0
  35. crimson/input_codes.py +176 -0
  36. crimson/modes/__init__.py +1 -0
  37. crimson/modes/base_gameplay_mode.py +219 -0
  38. crimson/modes/quest_mode.py +502 -0
  39. crimson/modes/rush_mode.py +300 -0
  40. crimson/modes/survival_mode.py +792 -0
  41. crimson/modes/tutorial_mode.py +648 -0
  42. crimson/modes/typo_mode.py +472 -0
  43. crimson/paths.py +23 -0
  44. crimson/perks.py +828 -0
  45. crimson/persistence/__init__.py +1 -0
  46. crimson/persistence/highscores.py +385 -0
  47. crimson/persistence/save_status.py +245 -0
  48. crimson/player_damage.py +77 -0
  49. crimson/projectiles.py +1133 -0
  50. crimson/quests/__init__.py +18 -0
  51. crimson/quests/helpers.py +147 -0
  52. crimson/quests/registry.py +49 -0
  53. crimson/quests/results.py +164 -0
  54. crimson/quests/runtime.py +91 -0
  55. crimson/quests/tier1.py +620 -0
  56. crimson/quests/tier2.py +652 -0
  57. crimson/quests/tier3.py +579 -0
  58. crimson/quests/tier4.py +721 -0
  59. crimson/quests/tier5.py +886 -0
  60. crimson/quests/timeline.py +115 -0
  61. crimson/quests/types.py +70 -0
  62. crimson/render/__init__.py +1 -0
  63. crimson/render/terrain_fx.py +88 -0
  64. crimson/render/world_renderer.py +1941 -0
  65. crimson/sim/__init__.py +1 -0
  66. crimson/sim/world_defs.py +67 -0
  67. crimson/sim/world_state.py +422 -0
  68. crimson/terrain_assets.py +19 -0
  69. crimson/tutorial/__init__.py +12 -0
  70. crimson/tutorial/timeline.py +291 -0
  71. crimson/typo/__init__.py +2 -0
  72. crimson/typo/names.py +233 -0
  73. crimson/typo/player.py +43 -0
  74. crimson/typo/spawns.py +73 -0
  75. crimson/typo/typing.py +52 -0
  76. crimson/ui/__init__.py +3 -0
  77. crimson/ui/cursor.py +95 -0
  78. crimson/ui/demo_trial_overlay.py +235 -0
  79. crimson/ui/game_over.py +660 -0
  80. crimson/ui/hud.py +601 -0
  81. crimson/ui/perk_menu.py +388 -0
  82. crimson/views/__init__.py +40 -0
  83. crimson/views/aim_debug.py +276 -0
  84. crimson/views/animations.py +274 -0
  85. crimson/views/arsenal_debug.py +404 -0
  86. crimson/views/audio_bootstrap.py +47 -0
  87. crimson/views/bonuses.py +201 -0
  88. crimson/views/camera_debug.py +359 -0
  89. crimson/views/camera_shake.py +229 -0
  90. crimson/views/corpse_stamp_debug.py +324 -0
  91. crimson/views/decals_debug.py +739 -0
  92. crimson/views/empty.py +19 -0
  93. crimson/views/fonts.py +114 -0
  94. crimson/views/game_over.py +117 -0
  95. crimson/views/ground.py +259 -0
  96. crimson/views/lighting_debug.py +1166 -0
  97. crimson/views/particles.py +293 -0
  98. crimson/views/perk_menu_debug.py +430 -0
  99. crimson/views/perks.py +398 -0
  100. crimson/views/player.py +434 -0
  101. crimson/views/player_sprite_debug.py +314 -0
  102. crimson/views/projectile_fx.py +609 -0
  103. crimson/views/projectile_render_debug.py +393 -0
  104. crimson/views/projectiles.py +221 -0
  105. crimson/views/quest_title_overlay.py +108 -0
  106. crimson/views/registry.py +34 -0
  107. crimson/views/rush.py +16 -0
  108. crimson/views/small_font_debug.py +204 -0
  109. crimson/views/spawn_plan.py +363 -0
  110. crimson/views/sprites.py +214 -0
  111. crimson/views/survival.py +15 -0
  112. crimson/views/terrain.py +132 -0
  113. crimson/views/ui.py +123 -0
  114. crimson/views/wicons.py +166 -0
  115. crimson/weapon_sfx.py +63 -0
  116. crimson/weapons.py +860 -0
  117. crimsonland-0.1.0.dev5.dist-info/METADATA +9 -0
  118. crimsonland-0.1.0.dev5.dist-info/RECORD +139 -0
  119. crimsonland-0.1.0.dev5.dist-info/WHEEL +4 -0
  120. crimsonland-0.1.0.dev5.dist-info/entry_points.txt +4 -0
  121. grim/__init__.py +20 -0
  122. grim/app.py +92 -0
  123. grim/assets.py +231 -0
  124. grim/audio.py +106 -0
  125. grim/config.py +294 -0
  126. grim/console.py +737 -0
  127. grim/fonts/__init__.py +7 -0
  128. grim/fonts/grim_mono.py +111 -0
  129. grim/fonts/small.py +120 -0
  130. grim/input.py +44 -0
  131. grim/jaz.py +103 -0
  132. grim/math.py +17 -0
  133. grim/music.py +403 -0
  134. grim/paq.py +76 -0
  135. grim/rand.py +37 -0
  136. grim/sfx.py +276 -0
  137. grim/sfx_map.py +103 -0
  138. grim/terrain_render.py +840 -0
  139. grim/view.py +16 -0
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from .types import QuestContext, QuestDefinition, SpawnEntry
4
+ from .registry import all_quests, quest_by_level
5
+ from . import tier1, tier2, tier3, tier4, tier5
6
+
7
+ __all__ = [
8
+ "QuestContext",
9
+ "QuestDefinition",
10
+ "SpawnEntry",
11
+ "all_quests",
12
+ "quest_by_level",
13
+ "tier1",
14
+ "tier2",
15
+ "tier3",
16
+ "tier4",
17
+ "tier5",
18
+ ]
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+ import random
6
+ from typing import Iterator
7
+
8
+ from ..creatures.spawn import SpawnId
9
+ from .types import SpawnEntry
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class EdgePoints:
14
+ left: tuple[float, float]
15
+ right: tuple[float, float]
16
+ top: tuple[float, float]
17
+ bottom: tuple[float, float]
18
+
19
+
20
+ def center_point(width: float, height: float | None = None) -> tuple[float, float]:
21
+ if height is None:
22
+ height = width
23
+ return float(width) / 2.0, float(height) / 2.0
24
+
25
+
26
+ def edge_midpoints(width: float, height: float | None = None, offset: float = 64.0) -> EdgePoints:
27
+ if height is None:
28
+ height = width
29
+ cx, cy = center_point(width, height)
30
+ return EdgePoints(
31
+ left=(-offset, cy),
32
+ right=(float(width) + offset, cy),
33
+ top=(cx, -offset),
34
+ bottom=(cx, float(height) + offset),
35
+ )
36
+
37
+
38
+ def corner_points(width: float, height: float | None = None, offset: float = 64.0) -> tuple[tuple[float, float], ...]:
39
+ if height is None:
40
+ height = width
41
+ return (
42
+ (-offset, -offset),
43
+ (float(width) + offset, -offset),
44
+ (-offset, float(height) + offset),
45
+ (float(width) + offset, float(height) + offset),
46
+ )
47
+
48
+
49
+ def iter_angles(count: int, *, step: float | None = None, start: float = 0.0) -> Iterator[float]:
50
+ if count <= 0:
51
+ return iter(())
52
+ if step is None:
53
+ step = math.tau / float(count)
54
+ for idx in range(count):
55
+ yield start + float(idx) * step
56
+
57
+
58
+ def ring_points(
59
+ center_x: float,
60
+ center_y: float,
61
+ radius: float,
62
+ count: int,
63
+ *,
64
+ step: float | None = None,
65
+ start: float = 0.0,
66
+ ) -> Iterator[tuple[float, float, float]]:
67
+ for angle in iter_angles(count, step=step, start=start):
68
+ yield (
69
+ math.cos(angle) * radius + center_x,
70
+ math.sin(angle) * radius + center_y,
71
+ angle,
72
+ )
73
+
74
+
75
+ def random_angle(rng: random.Random) -> float:
76
+ return float(rng.randrange(0x264)) * 0.01
77
+
78
+
79
+ def radial_points(
80
+ center_x: float,
81
+ center_y: float,
82
+ angle: float,
83
+ radius_start: float,
84
+ radius_end: float,
85
+ radius_step: float,
86
+ ) -> Iterator[tuple[float, float]]:
87
+ cos_a = math.cos(angle)
88
+ sin_a = math.sin(angle)
89
+ radius = radius_start
90
+ while radius < radius_end:
91
+ yield (
92
+ cos_a * radius + center_x,
93
+ sin_a * radius + center_y,
94
+ )
95
+ radius += radius_step
96
+
97
+
98
+ def heading_from_center(x: float, y: float, center_x: float, center_y: float) -> float:
99
+ return math.atan2(y - center_y, x - center_x) - (math.pi / 2.0)
100
+
101
+
102
+ def line_points_x(start: float, step: float, count: int, y: float) -> Iterator[tuple[float, float]]:
103
+ for idx in range(count):
104
+ yield start + float(idx) * step, y
105
+
106
+
107
+ def line_points_y(start: float, step: float, count: int, x: float) -> Iterator[tuple[float, float]]:
108
+ for idx in range(count):
109
+ yield x, start + float(idx) * step
110
+
111
+
112
+ def spawn(
113
+ *,
114
+ x: float,
115
+ y: float,
116
+ heading: float = 0.0,
117
+ spawn_id: SpawnId,
118
+ trigger_ms: int,
119
+ count: int,
120
+ ) -> SpawnEntry:
121
+ return SpawnEntry(
122
+ x=x,
123
+ y=y,
124
+ heading=heading,
125
+ spawn_id=spawn_id,
126
+ trigger_ms=trigger_ms,
127
+ count=count,
128
+ )
129
+
130
+
131
+ def spawn_at(
132
+ point: tuple[float, float],
133
+ *,
134
+ heading: float = 0.0,
135
+ spawn_id: SpawnId,
136
+ trigger_ms: int,
137
+ count: int,
138
+ ) -> SpawnEntry:
139
+ x, y = point
140
+ return spawn(
141
+ x=x,
142
+ y=y,
143
+ heading=heading,
144
+ spawn_id=spawn_id,
145
+ trigger_ms=trigger_ms,
146
+ count=count,
147
+ )
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ from .types import QuestBuilder, QuestDefinition
6
+
7
+ _QUESTS: dict[str, QuestDefinition] = {}
8
+
9
+
10
+ def register_quest(
11
+ *,
12
+ level: str,
13
+ title: str,
14
+ time_limit_ms: int,
15
+ start_weapon_id: int,
16
+ unlock_perk_id: int | None = None,
17
+ unlock_weapon_id: int | None = None,
18
+ terrain_id: int | None = None,
19
+ terrain_ids: tuple[int, int, int] | None = None,
20
+ builder_address: int | None = None,
21
+ ) -> Callable[[QuestBuilder], QuestBuilder]:
22
+ def decorator(builder: QuestBuilder) -> QuestBuilder:
23
+ quest = QuestDefinition(
24
+ level=level,
25
+ title=title,
26
+ builder=builder,
27
+ time_limit_ms=time_limit_ms,
28
+ start_weapon_id=start_weapon_id,
29
+ unlock_perk_id=unlock_perk_id,
30
+ unlock_weapon_id=unlock_weapon_id,
31
+ terrain_id=terrain_id,
32
+ terrain_ids=terrain_ids,
33
+ builder_address=builder_address,
34
+ )
35
+ existing = _QUESTS.get(quest.level)
36
+ if existing is not None:
37
+ raise ValueError(f"duplicate quest level {quest.level}: {existing.builder.__name__} vs {builder.__name__}")
38
+ _QUESTS[quest.level] = quest
39
+ return builder
40
+
41
+ return decorator
42
+
43
+
44
+ def all_quests() -> list[QuestDefinition]:
45
+ return sorted(_QUESTS.values(), key=lambda quest: quest.level_key)
46
+
47
+
48
+ def quest_by_level(level: str) -> QuestDefinition | None:
49
+ return _QUESTS.get(level)
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class QuestFinalTime:
8
+ base_time_ms: int
9
+ life_bonus_ms: int
10
+ unpicked_perk_bonus_ms: int
11
+ final_time_ms: int
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class QuestResultsBreakdownAnim:
16
+ """Phase-based breakdown animation modeled after `quest_results_screen_update`.
17
+
18
+ The native flow animates the breakdown in four steps:
19
+ 0) base time counts up to `base_time_ms`
20
+ 1) life bonus counts up to `life_bonus_ms`
21
+ 2) perk bonus counts up (in 1s steps) to `unpicked_perk_bonus_ms`
22
+ 3) final-time highlight blink then completes
23
+ """
24
+
25
+ step: int = 0 # 0=base,1=life,2=perk,3=final blink,4=done
26
+ step_timer_ms: int = 700
27
+
28
+ base_time_ms: int = 0
29
+ life_bonus_ms: int = 0
30
+ unpicked_perk_bonus_s: int = 0
31
+ final_time_ms: int = 0
32
+
33
+ blink_ticks: int = 0
34
+ done: bool = False
35
+
36
+ @classmethod
37
+ def start(cls) -> "QuestResultsBreakdownAnim":
38
+ return cls()
39
+
40
+ def set_final(self, target: QuestFinalTime) -> None:
41
+ self.step = 4
42
+ self.done = True
43
+ self.step_timer_ms = 0
44
+ self.base_time_ms = int(target.base_time_ms)
45
+ self.life_bonus_ms = int(target.life_bonus_ms)
46
+ self.unpicked_perk_bonus_s = max(0, int(target.unpicked_perk_bonus_ms) // 1000)
47
+ self.final_time_ms = int(target.final_time_ms)
48
+ self.blink_ticks = 0
49
+
50
+ def highlight_alpha(self) -> float:
51
+ if self.step != 3:
52
+ return 1.0
53
+ return max(0.0, min(1.0, 1.0 - float(self.blink_ticks) * 0.1))
54
+
55
+
56
+ def compute_quest_final_time(
57
+ *,
58
+ base_time_ms: int,
59
+ player_health: float,
60
+ pending_perk_count: int,
61
+ player2_health: float | None = None,
62
+ ) -> QuestFinalTime:
63
+ """Compute quest final time (ms) and breakdown.
64
+
65
+ Modeled after `quest_results_screen_update`:
66
+ final_time_ms = base_time_ms - round(player_health) - (pending_perk_count * 1000)
67
+ clamped to at least 1ms.
68
+ """
69
+
70
+ base_ms = int(base_time_ms)
71
+ life_bonus_ms = int(round(float(player_health)))
72
+ if player2_health is not None:
73
+ life_bonus_ms += int(round(float(player2_health)))
74
+
75
+ unpicked_perk_bonus_ms = max(0, int(pending_perk_count)) * 1000
76
+ final_ms = base_ms - int(life_bonus_ms) - int(unpicked_perk_bonus_ms)
77
+ if final_ms < 1:
78
+ final_ms = 1
79
+
80
+ return QuestFinalTime(
81
+ base_time_ms=base_ms,
82
+ life_bonus_ms=int(life_bonus_ms),
83
+ unpicked_perk_bonus_ms=int(unpicked_perk_bonus_ms),
84
+ final_time_ms=int(final_ms),
85
+ )
86
+
87
+
88
+ def tick_quest_results_breakdown_anim(
89
+ anim: QuestResultsBreakdownAnim,
90
+ *,
91
+ frame_dt_ms: int,
92
+ target: QuestFinalTime,
93
+ ) -> int:
94
+ """Advance quest results breakdown animation.
95
+
96
+ Returns the number of "clink" ticks to play this frame.
97
+ """
98
+
99
+ if anim.done:
100
+ return 0
101
+
102
+ clinks = 0
103
+ remaining = max(0, int(frame_dt_ms))
104
+ if remaining <= 0:
105
+ return 0
106
+
107
+ base_target_ms = max(0, int(target.base_time_ms))
108
+ life_target_ms = max(0, int(target.life_bonus_ms))
109
+ perk_target_s = max(0, int(target.unpicked_perk_bonus_ms) // 1000)
110
+
111
+ while remaining > 0 and not anim.done:
112
+ step_timer = int(anim.step_timer_ms)
113
+ take = remaining if step_timer <= 0 else min(remaining, step_timer)
114
+ anim.step_timer_ms = int(anim.step_timer_ms) - int(take)
115
+ remaining -= int(take)
116
+
117
+ while anim.step_timer_ms <= 0 and not anim.done:
118
+ step = int(anim.step)
119
+ if step == 0:
120
+ anim.base_time_ms = min(base_target_ms, int(anim.base_time_ms) + 2000)
121
+ anim.final_time_ms = int(anim.base_time_ms)
122
+ anim.step_timer_ms += 40
123
+ clinks += 1
124
+ if int(anim.base_time_ms) >= base_target_ms:
125
+ anim.step = 1
126
+ continue
127
+
128
+ if step == 1:
129
+ anim.life_bonus_ms = min(life_target_ms, int(anim.life_bonus_ms) + 1000)
130
+ anim.final_time_ms = max(
131
+ 1,
132
+ base_target_ms - int(anim.life_bonus_ms) - int(anim.unpicked_perk_bonus_s) * 1000,
133
+ )
134
+ anim.step_timer_ms += 150
135
+ clinks += 1
136
+ if int(anim.life_bonus_ms) >= life_target_ms:
137
+ anim.step = 2
138
+ continue
139
+
140
+ if step == 2:
141
+ anim.unpicked_perk_bonus_s = min(perk_target_s, int(anim.unpicked_perk_bonus_s) + 1)
142
+ anim.final_time_ms = max(
143
+ 1,
144
+ base_target_ms - int(anim.life_bonus_ms) - int(anim.unpicked_perk_bonus_s) * 1000,
145
+ )
146
+ clinks += 1
147
+ if int(anim.unpicked_perk_bonus_s) >= perk_target_s:
148
+ anim.final_time_ms = int(target.final_time_ms)
149
+ anim.step_timer_ms += 1000
150
+ anim.step = 3
151
+ else:
152
+ anim.step_timer_ms += 300
153
+ continue
154
+
155
+ if step == 3:
156
+ anim.blink_ticks += 1
157
+ anim.step_timer_ms += 50
158
+ if int(anim.blink_ticks) > 10:
159
+ anim.set_final(target)
160
+ continue
161
+
162
+ anim.set_final(target)
163
+
164
+ return int(clinks)
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ import inspect
5
+ import random
6
+
7
+ from .types import QuestContext, QuestDefinition, SpawnEntry
8
+
9
+ QUEST_COMPLETION_TRANSITION_MS = 1000.0
10
+
11
+
12
+ def _call_builder(
13
+ builder,
14
+ ctx: QuestContext,
15
+ *,
16
+ rng: random.Random | None,
17
+ full_version: bool,
18
+ ) -> list[SpawnEntry]:
19
+ params = inspect.signature(builder).parameters
20
+ kwargs: dict[str, object] = {}
21
+ if "rng" in params:
22
+ kwargs["rng"] = rng
23
+ if "full_version" in params:
24
+ kwargs["full_version"] = bool(full_version)
25
+ return builder(ctx, **kwargs)
26
+
27
+
28
+ def apply_hardcore_spawn_table_adjustment(entries: list[SpawnEntry]) -> list[SpawnEntry]:
29
+ """Apply quest hardcore spawn-table count adjustment.
30
+
31
+ Modeled after the quest start logic in the classic game, which bumps `SpawnEntry.count`
32
+ for most multi-spawn entries in hardcore mode.
33
+ """
34
+
35
+ adjusted: list[SpawnEntry] = []
36
+ for entry in entries:
37
+ spawn_id = int(entry.spawn_id)
38
+ count = int(entry.count)
39
+ if count > 1 and spawn_id != 0x3C:
40
+ if spawn_id == 0x2B:
41
+ count += 2
42
+ else:
43
+ count += 8
44
+ adjusted.append(entry if count == entry.count else replace(entry, count=count))
45
+ return adjusted
46
+
47
+
48
+ def build_quest_spawn_table(
49
+ quest: QuestDefinition,
50
+ ctx: QuestContext,
51
+ *,
52
+ seed: int | None = None,
53
+ hardcore: bool = False,
54
+ full_version: bool = True,
55
+ ) -> tuple[SpawnEntry, ...]:
56
+ """Build the quest spawn script (with optional hardcore modifications)."""
57
+
58
+ rng = random.Random(seed) if seed is not None else random.Random()
59
+ entries = _call_builder(quest.builder, ctx, rng=rng, full_version=full_version)
60
+ if hardcore:
61
+ entries = apply_hardcore_spawn_table_adjustment(list(entries))
62
+ return tuple(entries)
63
+
64
+
65
+ def tick_quest_completion_transition(
66
+ completion_transition_ms: float,
67
+ frame_dt_ms: float,
68
+ *,
69
+ creatures_none_active: bool,
70
+ spawn_table_empty: bool,
71
+ ) -> tuple[float, bool]:
72
+ """Advance quest completion transition timer.
73
+
74
+ The quest-mode update loop waits for a short delay after the quest is "idle complete"
75
+ (no active creatures + no remaining spawn table entries) before transitioning to the
76
+ results screen.
77
+
78
+ Returns:
79
+ (completion_transition_ms, completed)
80
+ """
81
+
82
+ dt_ms = float(frame_dt_ms)
83
+ timer_ms = float(completion_transition_ms)
84
+
85
+ if creatures_none_active and spawn_table_empty:
86
+ if timer_ms < 0.0:
87
+ timer_ms = 0.0
88
+ timer_ms += dt_ms
89
+ return timer_ms, bool(timer_ms >= QUEST_COMPLETION_TRANSITION_MS)
90
+
91
+ return -1.0, False