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,291 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, replace
4
+
5
+ from ..bonuses import BonusId
6
+ from ..creatures.spawn import (
7
+ SpawnTemplateCall,
8
+ build_tutorial_stage3_fire_spawns,
9
+ build_tutorial_stage4_clear_spawns,
10
+ build_tutorial_stage5_repeat_spawns,
11
+ build_tutorial_stage6_perks_done_spawns,
12
+ )
13
+
14
+ _TUTORIAL_STAGE_TEXT: tuple[str, ...] = (
15
+ "In this tutorial you'll learn how to play Crimsonland",
16
+ "First learn to move by pushing the arrow keys.",
17
+ "Now pick up the bonuses by walking over them",
18
+ "Now learn to shoot and move at the same time.\nClick the left Mouse button to shoot.",
19
+ "Now, move the mouse to aim at the monsters",
20
+ "It will help you to move and shoot at the same time. Just keep moving!",
21
+ "Now let's learn about Perks. You'll receive a perk when you gain enough experience points.",
22
+ "Perks can give you extra abilities, or boost your skills. Choose wisely!",
23
+ "Great! Now you are ready to start playing Crimsonland",
24
+ )
25
+
26
+ _TUTORIAL_HINT_TEXT: tuple[str, ...] = (
27
+ "This is the speed powerup, it makes you move faster!",
28
+ "This is a weapon powerup. Picking it up gives you a new weapon.",
29
+ "This powerup doubles all experience points you gain while it's active.",
30
+ "This is the nuke powerup, picking it up causes a huge\nexposion harming all monsters nearby!",
31
+ "Reflex Boost powerup slows down time giving you a chance to react better",
32
+ "",
33
+ "",
34
+ )
35
+
36
+
37
+ @dataclass(slots=True)
38
+ class TutorialState:
39
+ stage_index: int = -1
40
+ stage_timer_ms: int = 0
41
+ stage_transition_timer_ms: int = -1000
42
+ hint_index: int = -1
43
+ hint_alpha: int = 0
44
+ hint_fade_in: bool = False
45
+ repeat_spawn_count: int = 0
46
+ hint_bonus_creature_ref: int | None = None
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class BonusSpawnCall:
51
+ bonus_id: int
52
+ amount: int
53
+ pos: tuple[float, float]
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class TutorialFrameActions:
58
+ prompt_text: str = ""
59
+ prompt_alpha: float = 0.0
60
+ hint_text: str = ""
61
+ hint_alpha: float = 0.0
62
+ spawn_templates: tuple[SpawnTemplateCall, ...] = ()
63
+ spawn_bonuses: tuple[BonusSpawnCall, ...] = ()
64
+ stage5_bonus_carrier_drop: tuple[int, int] | None = None
65
+ play_levelup_sfx: bool = False
66
+ force_player_health: float = 100.0
67
+ force_player_experience: int | None = None
68
+
69
+
70
+ def tutorial_stage5_bonus_carrier_config(repeat_spawn_count: int) -> tuple[int, int] | None:
71
+ """Return the (bonus_id, amount_override) applied to the stage-5 bonus carrier for this repeat count.
72
+
73
+ This reproduces the packed bonus-arg writes to `tutorial_hint_bonus_ptr` in `tutorial_timeline_update`.
74
+
75
+ - amount_override == -1 means "use the bonus meta default".
76
+ - For weapon bonuses, amount_override is the weapon id.
77
+ """
78
+ n = int(repeat_spawn_count)
79
+ if n == 1:
80
+ return int(BonusId.SPEED), -1
81
+ if n == 2:
82
+ return int(BonusId.WEAPON), 5
83
+ if n == 3:
84
+ return int(BonusId.DOUBLE_EXPERIENCE), -1
85
+ if n == 4:
86
+ return int(BonusId.NUKE), -1
87
+ if n == 5:
88
+ return int(BonusId.REFLEX_BOOST), -1
89
+ return None
90
+
91
+
92
+ def _clamp01(value: float) -> float:
93
+ if value <= 0.0:
94
+ return 0.0
95
+ if value >= 1.0:
96
+ return 1.0
97
+ return float(value)
98
+
99
+
100
+ def _tick_stage_transition(stage_index: int, transition_timer_ms: int, *, frame_dt_ms: int) -> tuple[int, int]:
101
+ stage_index = int(stage_index)
102
+ transition_timer_ms = int(transition_timer_ms)
103
+ dt_ms = int(frame_dt_ms)
104
+
105
+ if transition_timer_ms < -1:
106
+ transition_timer_ms += dt_ms
107
+ if transition_timer_ms < -1:
108
+ return stage_index, transition_timer_ms
109
+ stage_index += 1
110
+ if stage_index == 9:
111
+ stage_index = 0
112
+ transition_timer_ms = 0
113
+ return stage_index, transition_timer_ms
114
+
115
+ if -1 < transition_timer_ms:
116
+ transition_timer_ms += dt_ms
117
+ if 1000 < transition_timer_ms:
118
+ transition_timer_ms = -1
119
+ return stage_index, transition_timer_ms
120
+
121
+
122
+ def _prompt_alpha(*, stage_index: int, stage_timer_ms: int, transition_timer_ms: int) -> float:
123
+ stage_index = int(stage_index)
124
+ stage_timer_ms = int(stage_timer_ms)
125
+ transition_timer_ms = int(transition_timer_ms)
126
+
127
+ if stage_index < 0:
128
+ return 0.0
129
+
130
+ if transition_timer_ms < -1:
131
+ alpha = float(-transition_timer_ms) * 0.001
132
+ elif transition_timer_ms < 0:
133
+ alpha = 1.0
134
+ else:
135
+ alpha = float(transition_timer_ms) * 0.001
136
+
137
+ if stage_index == 5:
138
+ if stage_timer_ms > 5000 and transition_timer_ms > -2:
139
+ alpha = 1.0 - float(stage_timer_ms - 5000) * 0.001
140
+ if stage_timer_ms >= 0x1771:
141
+ alpha = 0.0
142
+
143
+ return _clamp01(alpha)
144
+
145
+
146
+ def _tick_hint(state: TutorialState, *, frame_dt_ms: int, hint_bonus_died: bool) -> tuple[tuple[SpawnTemplateCall, ...], str, float]:
147
+ hint_spawns: list[SpawnTemplateCall] = []
148
+
149
+ if (not state.hint_fade_in) and bool(hint_bonus_died):
150
+ state.hint_fade_in = True
151
+ state.hint_index = int(state.hint_index) + 1
152
+ hint_spawns.extend(
153
+ (
154
+ SpawnTemplateCall(template_id=0x24, pos=(128.0, 128.0), heading=3.1415927),
155
+ SpawnTemplateCall(template_id=0x26, pos=(152.0, 160.0), heading=3.1415927),
156
+ )
157
+ )
158
+
159
+ delta = int(frame_dt_ms) * 3
160
+ state.hint_alpha = int(state.hint_alpha) + (delta if state.hint_fade_in else -delta)
161
+ if state.hint_alpha < 0:
162
+ state.hint_alpha = 0
163
+ elif state.hint_alpha > 1000:
164
+ state.hint_alpha = 1000
165
+
166
+ idx = int(state.hint_index)
167
+ text = _TUTORIAL_HINT_TEXT[idx] if 0 <= idx < len(_TUTORIAL_HINT_TEXT) else ""
168
+ alpha = float(state.hint_alpha) * 0.001 if text else 0.0
169
+ return tuple(hint_spawns), text, _clamp01(alpha)
170
+
171
+
172
+ def tick_tutorial_timeline(
173
+ state: TutorialState,
174
+ *,
175
+ frame_dt_ms: float,
176
+ any_move_active: bool,
177
+ any_fire_active: bool,
178
+ creatures_none_active: bool,
179
+ bonus_pool_empty: bool,
180
+ perk_pending_count: int,
181
+ hint_bonus_died: bool = False,
182
+ ) -> tuple[TutorialState, TutorialFrameActions]:
183
+ """Pure model of the tutorial director (`tutorial_timeline_update` / 0x00408990).
184
+
185
+ Notes:
186
+ - The returned UI model (prompt/hint text+alpha) reflects the state *before* any stage triggers
187
+ applied by this tick. The returned state reflects the post-trigger values for the next frame.
188
+ """
189
+ dt_ms = int(float(frame_dt_ms))
190
+ state = replace(state)
191
+ state.stage_timer_ms = int(state.stage_timer_ms) + dt_ms
192
+
193
+ stage_index, transition_timer_ms = _tick_stage_transition(state.stage_index, state.stage_transition_timer_ms, frame_dt_ms=dt_ms)
194
+ state.stage_index = int(stage_index)
195
+ state.stage_transition_timer_ms = int(transition_timer_ms)
196
+
197
+ prompt_text = _TUTORIAL_STAGE_TEXT[stage_index] if 0 <= stage_index < len(_TUTORIAL_STAGE_TEXT) else ""
198
+ prompt_alpha = _prompt_alpha(stage_index=stage_index, stage_timer_ms=state.stage_timer_ms, transition_timer_ms=transition_timer_ms)
199
+ if stage_index == 6 and int(perk_pending_count) < 1:
200
+ prompt_text = ""
201
+ prompt_alpha = 0.0
202
+
203
+ hint_spawns, hint_text, hint_alpha = _tick_hint(state, frame_dt_ms=dt_ms, hint_bonus_died=bool(hint_bonus_died))
204
+
205
+ actions = TutorialFrameActions(
206
+ prompt_text=prompt_text,
207
+ prompt_alpha=prompt_alpha,
208
+ hint_text=hint_text,
209
+ hint_alpha=hint_alpha,
210
+ spawn_templates=hint_spawns,
211
+ spawn_bonuses=(),
212
+ stage5_bonus_carrier_drop=None,
213
+ play_levelup_sfx=False,
214
+ force_player_health=100.0,
215
+ force_player_experience=0 if stage_index != 6 else None,
216
+ )
217
+
218
+ spawn_templates: list[SpawnTemplateCall] = list(actions.spawn_templates)
219
+ spawn_bonuses: list[BonusSpawnCall] = []
220
+ play_levelup_sfx = False
221
+ stage5_bonus_carrier_drop: tuple[int, int] | None = None
222
+ force_experience = actions.force_player_experience
223
+
224
+ if stage_index == 0:
225
+ if state.stage_timer_ms > 6000 and state.stage_transition_timer_ms == -1:
226
+ state.repeat_spawn_count = 0
227
+ state.hint_index = int(state.stage_transition_timer_ms)
228
+ state.hint_fade_in = False
229
+ state.stage_transition_timer_ms = -1000
230
+ elif stage_index == 1:
231
+ if bool(any_move_active) and state.stage_transition_timer_ms == -1:
232
+ state.stage_transition_timer_ms = -1000
233
+ play_levelup_sfx = True
234
+ spawn_bonuses.extend(
235
+ (
236
+ BonusSpawnCall(bonus_id=int(BonusId.POINTS), amount=500, pos=(260.0, 260.0)),
237
+ BonusSpawnCall(bonus_id=int(BonusId.POINTS), amount=1000, pos=(600.0, 400.0)),
238
+ BonusSpawnCall(bonus_id=int(BonusId.POINTS), amount=500, pos=(300.0, 400.0)),
239
+ )
240
+ )
241
+ elif stage_index == 2:
242
+ if bool(bonus_pool_empty) and state.stage_transition_timer_ms == -1:
243
+ state.stage_transition_timer_ms = -1000
244
+ play_levelup_sfx = True
245
+ elif stage_index == 3:
246
+ if bool(any_fire_active) and state.stage_transition_timer_ms == -1:
247
+ state.stage_transition_timer_ms = -1000
248
+ play_levelup_sfx = True
249
+ spawn_templates.extend(build_tutorial_stage3_fire_spawns())
250
+ elif stage_index == 4:
251
+ if bool(creatures_none_active) and state.stage_transition_timer_ms == -1:
252
+ state.stage_timer_ms = 1000
253
+ state.stage_transition_timer_ms = -1000
254
+ play_levelup_sfx = True
255
+ state.repeat_spawn_count = 0
256
+ spawn_templates.extend(build_tutorial_stage4_clear_spawns())
257
+ elif stage_index == 5:
258
+ if bool(bonus_pool_empty) and bool(creatures_none_active):
259
+ state.repeat_spawn_count = int(state.repeat_spawn_count) + 1
260
+ if int(state.repeat_spawn_count) < 8:
261
+ state.hint_fade_in = False
262
+ state.hint_bonus_creature_ref = None
263
+ spawn_templates.extend(build_tutorial_stage5_repeat_spawns(int(state.repeat_spawn_count)))
264
+ stage5_bonus_carrier_drop = tutorial_stage5_bonus_carrier_config(int(state.repeat_spawn_count))
265
+ elif state.stage_transition_timer_ms == -1:
266
+ state.stage_transition_timer_ms = -1000
267
+ play_levelup_sfx = True
268
+ force_experience = 3000
269
+ elif stage_index == 6:
270
+ if int(perk_pending_count) < 1 and state.stage_transition_timer_ms == -1:
271
+ state.stage_transition_timer_ms = -1000
272
+ spawn_templates.extend(build_tutorial_stage6_perks_done_spawns())
273
+ elif stage_index == 7:
274
+ if bool(bonus_pool_empty) and bool(creatures_none_active) and state.stage_transition_timer_ms == -1:
275
+ state.stage_transition_timer_ms = -1000
276
+
277
+ return (
278
+ state,
279
+ TutorialFrameActions(
280
+ prompt_text=actions.prompt_text,
281
+ prompt_alpha=actions.prompt_alpha,
282
+ hint_text=actions.hint_text,
283
+ hint_alpha=actions.hint_alpha,
284
+ spawn_templates=tuple(spawn_templates),
285
+ spawn_bonuses=tuple(spawn_bonuses),
286
+ stage5_bonus_carrier_drop=stage5_bonus_carrier_drop,
287
+ play_levelup_sfx=bool(play_levelup_sfx),
288
+ force_player_health=actions.force_player_health,
289
+ force_player_experience=force_experience,
290
+ ),
291
+ )
@@ -0,0 +1,2 @@
1
+ from __future__ import annotations
2
+
crimson/typo/names.py ADDED
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Sequence
6
+
7
+ from grim.rand import Crand
8
+
9
+
10
+ NAME_MAX_CHARS = 16 # creature_name_assign_random enforces strlen < 0x10.
11
+
12
+
13
+ _NAME_PARTS: tuple[str, ...] = (
14
+ "lamb",
15
+ "gun",
16
+ "head",
17
+ "tail",
18
+ "leg",
19
+ "nose",
20
+ "road",
21
+ "stab",
22
+ "high",
23
+ "low",
24
+ "hat",
25
+ "pie",
26
+ "hand",
27
+ "jack",
28
+ "cube",
29
+ "ice",
30
+ "cow",
31
+ "king",
32
+ "lord",
33
+ "mate",
34
+ "mary",
35
+ "dick",
36
+ "bill",
37
+ "cat",
38
+ "harry",
39
+ "tom",
40
+ "fly",
41
+ "call",
42
+ "shot",
43
+ "gate",
44
+ "quick",
45
+ "brown",
46
+ "fox",
47
+ "jumper",
48
+ "over",
49
+ "lazy",
50
+ "dog",
51
+ "zeta",
52
+ "unique",
53
+ "nerd",
54
+ "earl",
55
+ "sleep",
56
+ "onyx",
57
+ "mill",
58
+ "blue",
59
+ "below",
60
+ "scape",
61
+ "reap",
62
+ "damo",
63
+ "break",
64
+ "boom",
65
+ "the",
66
+ )
67
+
68
+
69
+ def typo_name_part(rng: Crand, *, allow_the: bool) -> str:
70
+ mod = 52 if allow_the else 51
71
+ idx = int(rng.rand() % mod)
72
+ if idx == 39:
73
+ return "nerd"
74
+ return _NAME_PARTS[idx]
75
+
76
+
77
+ def typo_build_name(rng: Crand, *, score_xp: int, unique_words: Sequence[str] | None = None) -> str:
78
+ score_xp = int(score_xp)
79
+ if unique_words:
80
+ return _typo_build_custom_name(rng, score_xp=score_xp, unique_words=unique_words)
81
+ if score_xp > 120:
82
+ if int(rng.rand() % 100) < 10 and unique_words:
83
+ return str(unique_words[int(rng.rand() % len(unique_words))])
84
+ if int(rng.rand() % 100) < 80:
85
+ return "".join(
86
+ [
87
+ typo_name_part(rng, allow_the=True),
88
+ typo_name_part(rng, allow_the=False),
89
+ typo_name_part(rng, allow_the=False),
90
+ typo_name_part(rng, allow_the=False),
91
+ ]
92
+ )
93
+
94
+ if (score_xp > 80 and int(rng.rand() % 100) < 80) or (score_xp > 60 and int(rng.rand() % 100) < 40):
95
+ return "".join(
96
+ [
97
+ typo_name_part(rng, allow_the=True),
98
+ typo_name_part(rng, allow_the=False),
99
+ typo_name_part(rng, allow_the=False),
100
+ ]
101
+ )
102
+
103
+ if (score_xp > 40 and int(rng.rand() % 100) < 80) or (score_xp > 20 and int(rng.rand() % 100) < 40):
104
+ return "".join(
105
+ [
106
+ typo_name_part(rng, allow_the=True),
107
+ typo_name_part(rng, allow_the=False),
108
+ ]
109
+ )
110
+
111
+ return typo_name_part(rng, allow_the=False)
112
+
113
+
114
+ def _pick_word(rng: Crand, words: Sequence[str]) -> str:
115
+ return str(words[int(rng.rand() % len(words))])
116
+
117
+
118
+ def _pick_unique_words(rng: Crand, words: Sequence[str], count: int) -> list[str]:
119
+ if count <= 1:
120
+ return [_pick_word(rng, words)]
121
+ if len(words) <= count:
122
+ return [_pick_word(rng, words) for _ in range(count)]
123
+
124
+ picked: list[str] = []
125
+ used: set[int] = set()
126
+ while len(picked) < count:
127
+ idx = int(rng.rand() % len(words))
128
+ if idx in used:
129
+ continue
130
+ used.add(idx)
131
+ picked.append(str(words[idx]))
132
+ return picked
133
+
134
+
135
+ def _typo_build_custom_name(rng: Crand, *, score_xp: int, unique_words: Sequence[str]) -> str:
136
+ score_xp = int(score_xp)
137
+ if score_xp > 120:
138
+ if int(rng.rand() % 100) < 10:
139
+ return _pick_word(rng, unique_words)
140
+ if int(rng.rand() % 100) < 80:
141
+ return "".join(_pick_unique_words(rng, unique_words, 4))
142
+
143
+ if (score_xp > 80 and int(rng.rand() % 100) < 80) or (score_xp > 60 and int(rng.rand() % 100) < 40):
144
+ return "".join(_pick_unique_words(rng, unique_words, 3))
145
+
146
+ if (score_xp > 40 and int(rng.rand() % 100) < 80) or (score_xp > 20 and int(rng.rand() % 100) < 40):
147
+ return "".join(_pick_unique_words(rng, unique_words, 2))
148
+
149
+ return _pick_word(rng, unique_words)
150
+
151
+
152
+ def load_typo_dictionary(path: Path) -> list[str]:
153
+ try:
154
+ raw = path.read_text(encoding="utf-8", errors="ignore")
155
+ except OSError:
156
+ return []
157
+
158
+ words: list[str] = []
159
+ seen: set[str] = set()
160
+ for line in raw.splitlines():
161
+ text = line.split("#", 1)[0].strip()
162
+ if not text:
163
+ continue
164
+ if len(text) >= NAME_MAX_CHARS:
165
+ continue
166
+ if text in seen:
167
+ continue
168
+ words.append(text)
169
+ seen.add(text)
170
+ return words
171
+
172
+
173
+ @dataclass(slots=True)
174
+ class CreatureNameTable:
175
+ names: list[str]
176
+
177
+ @classmethod
178
+ def sized(cls, size: int) -> CreatureNameTable:
179
+ return cls(names=[""] * int(size))
180
+
181
+ def clear(self, idx: int) -> None:
182
+ if 0 <= int(idx) < len(self.names):
183
+ self.names[int(idx)] = ""
184
+
185
+ def find_by_name(self, name: str, *, active_mask: Sequence[bool]) -> int | None:
186
+ for idx, existing in enumerate(self.names):
187
+ if not (0 <= idx < len(active_mask) and bool(active_mask[idx])):
188
+ continue
189
+ if existing == name:
190
+ return idx
191
+ return None
192
+
193
+ def is_unique(self, name: str, *, exclude_idx: int, active_mask: Sequence[bool]) -> bool:
194
+ exclude = int(exclude_idx)
195
+ for idx, existing in enumerate(self.names):
196
+ if idx == exclude:
197
+ continue
198
+ if not (0 <= idx < len(active_mask) and bool(active_mask[idx])):
199
+ continue
200
+ if existing == name:
201
+ return False
202
+ return True
203
+
204
+ def assign_random(
205
+ self,
206
+ creature_idx: int,
207
+ rng: Crand,
208
+ *,
209
+ score_xp: int,
210
+ active_mask: Sequence[bool],
211
+ unique_words: Sequence[str] | None = None,
212
+ ) -> str:
213
+ idx = int(creature_idx)
214
+ if not (0 <= idx < len(self.names)):
215
+ raise IndexError(f"creature_idx out of range: {idx}")
216
+
217
+ too_long_attempts = 0
218
+ attempts = 0
219
+ while True:
220
+ name = typo_build_name(rng, score_xp=score_xp, unique_words=unique_words)
221
+ if not self.is_unique(name, exclude_idx=idx, active_mask=active_mask):
222
+ attempts += 1
223
+ if attempts < 200:
224
+ continue
225
+
226
+ if len(name) < NAME_MAX_CHARS:
227
+ self.names[idx] = name
228
+ return name
229
+
230
+ too_long_attempts += 1
231
+ if too_long_attempts > 99:
232
+ self.names[idx] = name
233
+ return name
crimson/typo/player.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from ..gameplay import PlayerInput, PlayerState, weapon_assign_player
4
+
5
+ TYPO_WEAPON_ID = 4
6
+
7
+
8
+ def enforce_typo_player_frame(player: PlayerState) -> None:
9
+ """Match Typ-o Shooter's bespoke player loop (`player_fire_weapon @ 0x00444980`).
10
+
11
+ Typ-o resets timers and tops up ammo each frame, so typing speed (not weapon
12
+ cooldown) controls rate of fire.
13
+ """
14
+
15
+ if int(player.weapon_id) != TYPO_WEAPON_ID:
16
+ weapon_assign_player(player, TYPO_WEAPON_ID)
17
+
18
+ player.shot_cooldown = 0.0
19
+ player.spread_heat = 0.0
20
+ player.ammo = float(max(0, int(player.clip_size)))
21
+
22
+ player.reload_active = False
23
+ player.reload_timer = 0.0
24
+ player.reload_timer_max = 0.0
25
+
26
+
27
+ def build_typo_player_input(
28
+ *,
29
+ aim_x: float,
30
+ aim_y: float,
31
+ fire_requested: bool,
32
+ reload_requested: bool,
33
+ ) -> PlayerInput:
34
+ fire = bool(fire_requested)
35
+ return PlayerInput(
36
+ move_x=0.0,
37
+ move_y=0.0,
38
+ aim_x=float(aim_x),
39
+ aim_y=float(aim_y),
40
+ fire_down=fire,
41
+ fire_pressed=fire,
42
+ reload_pressed=bool(reload_requested),
43
+ )
crimson/typo/spawns.py ADDED
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+
6
+ from ..creatures.spawn import CreatureTypeId
7
+
8
+
9
+ def _clamp(value: float, lo: float, hi: float) -> float:
10
+ if value < lo:
11
+ return lo
12
+ if value > hi:
13
+ return hi
14
+ return value
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class TypoSpawnCall:
19
+ pos_x: float
20
+ pos_y: float
21
+ type_id: CreatureTypeId
22
+ tint_rgba: tuple[float, float, float, float]
23
+
24
+
25
+ def tick_typo_spawns(
26
+ *,
27
+ elapsed_ms: int,
28
+ spawn_cooldown_ms: int,
29
+ frame_dt_ms: int,
30
+ player_count: int,
31
+ world_width: float,
32
+ world_height: float,
33
+ ) -> tuple[int, list[TypoSpawnCall]]:
34
+ elapsed_ms = int(elapsed_ms)
35
+ cooldown = int(spawn_cooldown_ms)
36
+ dt_ms = int(frame_dt_ms)
37
+ player_count = max(1, int(player_count))
38
+
39
+ cooldown -= dt_ms * player_count
40
+
41
+ spawns: list[TypoSpawnCall] = []
42
+ while cooldown < 0:
43
+ cooldown += 3500 - elapsed_ms // 800
44
+ cooldown = max(100, cooldown)
45
+
46
+ t = float(elapsed_ms) * 0.001
47
+ y = math.cos(t) * 256.0 + float(world_height) * 0.5
48
+
49
+ tint_t = float(elapsed_ms + 1)
50
+ tint_r = _clamp(tint_t * 0.0000083333334 + 0.30000001, 0.0, 1.0)
51
+ tint_g = _clamp(tint_t * 10000.0 + 0.30000001, 0.0, 1.0)
52
+ tint_b = _clamp(math.sin(tint_t * 0.0001) + 0.30000001, 0.0, 1.0)
53
+ tint = (tint_r, tint_g, tint_b, 1.0)
54
+
55
+ spawns.append(
56
+ TypoSpawnCall(
57
+ pos_x=float(world_width) + 64.0,
58
+ pos_y=y,
59
+ type_id=CreatureTypeId.SPIDER_SP2,
60
+ tint_rgba=tint,
61
+ )
62
+ )
63
+ spawns.append(
64
+ TypoSpawnCall(
65
+ pos_x=-64.0,
66
+ pos_y=y,
67
+ type_id=CreatureTypeId.ALIEN,
68
+ tint_rgba=tint,
69
+ )
70
+ )
71
+
72
+ return cooldown, spawns
73
+
crimson/typo/typing.py ADDED
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable
5
+
6
+
7
+ TYPING_MAX_CHARS = 17
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class TypingEnterResult:
12
+ fire_requested: bool = False
13
+ reload_requested: bool = False
14
+ target_creature_idx: int | None = None
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class TypingBuffer:
19
+ text: str = ""
20
+ shots_fired: int = 0
21
+ shots_hit: int = 0
22
+
23
+ def clear(self) -> None:
24
+ self.text = ""
25
+
26
+ def backspace(self) -> None:
27
+ if self.text:
28
+ self.text = self.text[:-1]
29
+
30
+ def push_char(self, ch: str) -> None:
31
+ if not ch:
32
+ return
33
+ if len(self.text) >= TYPING_MAX_CHARS:
34
+ return
35
+ self.text += ch[0]
36
+
37
+ def enter(self, *, find_target: Callable[[str], int | None]) -> TypingEnterResult:
38
+ if not self.text:
39
+ return TypingEnterResult()
40
+
41
+ entered = self.text
42
+ self.shots_fired += 1
43
+ self.clear()
44
+
45
+ target = find_target(entered)
46
+ if target is not None:
47
+ self.shots_hit += 1
48
+ return TypingEnterResult(fire_requested=True, target_creature_idx=int(target))
49
+ if entered == "reload":
50
+ return TypingEnterResult(reload_requested=True)
51
+ return TypingEnterResult()
52
+
crimson/ui/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ["cursor", "hud", "perk_menu"]