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.
- crimson/__init__.py +24 -0
- crimson/assets_fetch.py +60 -0
- crimson/atlas.py +92 -0
- crimson/audio_router.py +155 -0
- crimson/bonuses.py +167 -0
- crimson/camera.py +75 -0
- crimson/cli.py +380 -0
- crimson/creatures/__init__.py +8 -0
- crimson/creatures/ai.py +186 -0
- crimson/creatures/anim.py +173 -0
- crimson/creatures/damage.py +103 -0
- crimson/creatures/runtime.py +1019 -0
- crimson/creatures/spawn.py +2871 -0
- crimson/debug.py +7 -0
- crimson/demo.py +1360 -0
- crimson/demo_trial.py +140 -0
- crimson/effects.py +1086 -0
- crimson/effects_atlas.py +73 -0
- crimson/frontend/__init__.py +1 -0
- crimson/frontend/assets.py +43 -0
- crimson/frontend/boot.py +424 -0
- crimson/frontend/menu.py +700 -0
- crimson/frontend/panels/__init__.py +1 -0
- crimson/frontend/panels/base.py +410 -0
- crimson/frontend/panels/controls.py +132 -0
- crimson/frontend/panels/mods.py +128 -0
- crimson/frontend/panels/options.py +409 -0
- crimson/frontend/panels/play_game.py +627 -0
- crimson/frontend/panels/stats.py +351 -0
- crimson/frontend/transitions.py +31 -0
- crimson/game.py +2533 -0
- crimson/game_modes.py +15 -0
- crimson/game_world.py +652 -0
- crimson/gameplay.py +2467 -0
- crimson/input_codes.py +176 -0
- crimson/modes/__init__.py +1 -0
- crimson/modes/base_gameplay_mode.py +219 -0
- crimson/modes/quest_mode.py +502 -0
- crimson/modes/rush_mode.py +300 -0
- crimson/modes/survival_mode.py +792 -0
- crimson/modes/tutorial_mode.py +648 -0
- crimson/modes/typo_mode.py +472 -0
- crimson/paths.py +23 -0
- crimson/perks.py +828 -0
- crimson/persistence/__init__.py +1 -0
- crimson/persistence/highscores.py +385 -0
- crimson/persistence/save_status.py +245 -0
- crimson/player_damage.py +77 -0
- crimson/projectiles.py +1133 -0
- crimson/quests/__init__.py +18 -0
- crimson/quests/helpers.py +147 -0
- crimson/quests/registry.py +49 -0
- crimson/quests/results.py +164 -0
- crimson/quests/runtime.py +91 -0
- crimson/quests/tier1.py +620 -0
- crimson/quests/tier2.py +652 -0
- crimson/quests/tier3.py +579 -0
- crimson/quests/tier4.py +721 -0
- crimson/quests/tier5.py +886 -0
- crimson/quests/timeline.py +115 -0
- crimson/quests/types.py +70 -0
- crimson/render/__init__.py +1 -0
- crimson/render/terrain_fx.py +88 -0
- crimson/render/world_renderer.py +1941 -0
- crimson/sim/__init__.py +1 -0
- crimson/sim/world_defs.py +67 -0
- crimson/sim/world_state.py +422 -0
- crimson/terrain_assets.py +19 -0
- crimson/tutorial/__init__.py +12 -0
- crimson/tutorial/timeline.py +291 -0
- crimson/typo/__init__.py +2 -0
- crimson/typo/names.py +233 -0
- crimson/typo/player.py +43 -0
- crimson/typo/spawns.py +73 -0
- crimson/typo/typing.py +52 -0
- crimson/ui/__init__.py +3 -0
- crimson/ui/cursor.py +95 -0
- crimson/ui/demo_trial_overlay.py +235 -0
- crimson/ui/game_over.py +660 -0
- crimson/ui/hud.py +601 -0
- crimson/ui/perk_menu.py +388 -0
- crimson/views/__init__.py +40 -0
- crimson/views/aim_debug.py +276 -0
- crimson/views/animations.py +274 -0
- crimson/views/arsenal_debug.py +404 -0
- crimson/views/audio_bootstrap.py +47 -0
- crimson/views/bonuses.py +201 -0
- crimson/views/camera_debug.py +359 -0
- crimson/views/camera_shake.py +229 -0
- crimson/views/corpse_stamp_debug.py +324 -0
- crimson/views/decals_debug.py +739 -0
- crimson/views/empty.py +19 -0
- crimson/views/fonts.py +114 -0
- crimson/views/game_over.py +117 -0
- crimson/views/ground.py +259 -0
- crimson/views/lighting_debug.py +1166 -0
- crimson/views/particles.py +293 -0
- crimson/views/perk_menu_debug.py +430 -0
- crimson/views/perks.py +398 -0
- crimson/views/player.py +434 -0
- crimson/views/player_sprite_debug.py +314 -0
- crimson/views/projectile_fx.py +609 -0
- crimson/views/projectile_render_debug.py +393 -0
- crimson/views/projectiles.py +221 -0
- crimson/views/quest_title_overlay.py +108 -0
- crimson/views/registry.py +34 -0
- crimson/views/rush.py +16 -0
- crimson/views/small_font_debug.py +204 -0
- crimson/views/spawn_plan.py +363 -0
- crimson/views/sprites.py +214 -0
- crimson/views/survival.py +15 -0
- crimson/views/terrain.py +132 -0
- crimson/views/ui.py +123 -0
- crimson/views/wicons.py +166 -0
- crimson/weapon_sfx.py +63 -0
- crimson/weapons.py +860 -0
- crimsonland-0.1.0.dev5.dist-info/METADATA +9 -0
- crimsonland-0.1.0.dev5.dist-info/RECORD +139 -0
- crimsonland-0.1.0.dev5.dist-info/WHEEL +4 -0
- crimsonland-0.1.0.dev5.dist-info/entry_points.txt +4 -0
- grim/__init__.py +20 -0
- grim/app.py +92 -0
- grim/assets.py +231 -0
- grim/audio.py +106 -0
- grim/config.py +294 -0
- grim/console.py +737 -0
- grim/fonts/__init__.py +7 -0
- grim/fonts/grim_mono.py +111 -0
- grim/fonts/small.py +120 -0
- grim/input.py +44 -0
- grim/jaz.py +103 -0
- grim/math.py +17 -0
- grim/music.py +403 -0
- grim/paq.py +76 -0
- grim/rand.py +37 -0
- grim/sfx.py +276 -0
- grim/sfx_map.py +103 -0
- grim/terrain_render.py +840 -0
- 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
|
+
)
|
crimson/typo/__init__.py
ADDED
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