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,300 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import random
5
+
6
+ import pyray as rl
7
+
8
+ from grim.assets import PaqTextureCache
9
+ from grim.audio import AudioState
10
+ from grim.config import CrimsonConfig
11
+ from grim.view import ViewContext
12
+
13
+ from ..creatures.spawn import tick_rush_mode_spawns
14
+ from ..game_modes import GameMode
15
+ from ..gameplay import PlayerInput, most_used_weapon_id_for_player, weapon_assign_player
16
+ from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
17
+ from ..persistence.highscores import HighScoreRecord
18
+ from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
19
+ from ..ui.hud import draw_hud_overlay
20
+ from ..ui.perk_menu import load_perk_menu_assets
21
+ from .base_gameplay_mode import BaseGameplayMode
22
+
23
+ WORLD_SIZE = 1024.0
24
+ RUSH_WEAPON_ID = 2
25
+
26
+ UI_TEXT_SCALE = 1.0
27
+ UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
28
+ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
29
+ UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class _RushState:
34
+ elapsed_ms: float = 0.0
35
+ spawn_cooldown_ms: float = 0.0
36
+
37
+
38
+ class RushMode(BaseGameplayMode):
39
+ def __init__(
40
+ self,
41
+ ctx: ViewContext,
42
+ *,
43
+ texture_cache: PaqTextureCache | None = None,
44
+ config: CrimsonConfig | None = None,
45
+ audio: AudioState | None = None,
46
+ audio_rng: random.Random | None = None,
47
+ ) -> None:
48
+ super().__init__(
49
+ ctx,
50
+ world_size=WORLD_SIZE,
51
+ default_game_mode_id=int(GameMode.RUSH),
52
+ demo_mode_active=False,
53
+ difficulty_level=0,
54
+ hardcore=False,
55
+ texture_cache=texture_cache,
56
+ config=config,
57
+ audio=audio,
58
+ audio_rng=audio_rng,
59
+ )
60
+ self._rush = _RushState()
61
+
62
+ self._ui_assets = None
63
+
64
+ def _enforce_rush_loadout(self) -> None:
65
+ for player in self._world.players:
66
+ if int(player.weapon_id) != RUSH_WEAPON_ID:
67
+ weapon_assign_player(player, RUSH_WEAPON_ID)
68
+ # `rush_mode_update` forces weapon+ammo every frame; keep ammo topped up.
69
+ player.ammo = float(max(0, int(player.clip_size)))
70
+
71
+ def open(self) -> None:
72
+ super().open()
73
+ self._ui_assets = load_perk_menu_assets(self._assets_root)
74
+ if self._ui_assets.missing:
75
+ self._missing_assets.extend(self._ui_assets.missing)
76
+ self._rush = _RushState()
77
+ self._enforce_rush_loadout()
78
+
79
+ def close(self) -> None:
80
+ if self._ui_assets is not None:
81
+ self._ui_assets = None
82
+ super().close()
83
+
84
+ def _handle_input(self) -> None:
85
+ if self._game_over_active:
86
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
87
+ self._action = "back_to_menu"
88
+ self.close_requested = True
89
+ return
90
+
91
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
92
+ self._paused = not self._paused
93
+
94
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
95
+ self.close_requested = True
96
+
97
+ def _build_input(self) -> PlayerInput:
98
+ keybinds = config_keybinds(self._config)
99
+ if not keybinds:
100
+ keybinds = (0x11, 0x1F, 0x1E, 0x20, 0x100)
101
+ up_key, down_key, left_key, right_key, fire_key = player_move_fire_binds(keybinds, 0)
102
+
103
+ move_x = 0.0
104
+ move_y = 0.0
105
+ if input_code_is_down(left_key):
106
+ move_x -= 1.0
107
+ if input_code_is_down(right_key):
108
+ move_x += 1.0
109
+ if input_code_is_down(up_key):
110
+ move_y -= 1.0
111
+ if input_code_is_down(down_key):
112
+ move_y += 1.0
113
+
114
+ mouse = self._ui_mouse_pos()
115
+ aim_x, aim_y = self._world.screen_to_world(float(mouse.x), float(mouse.y))
116
+
117
+ fire_down = input_code_is_down(fire_key)
118
+ fire_pressed = input_code_is_pressed(fire_key)
119
+
120
+ return PlayerInput(
121
+ move_x=move_x,
122
+ move_y=move_y,
123
+ aim_x=float(aim_x),
124
+ aim_y=float(aim_y),
125
+ fire_down=bool(fire_down),
126
+ fire_pressed=bool(fire_pressed),
127
+ reload_pressed=False,
128
+ )
129
+
130
+ def _player_name_default(self) -> str:
131
+ config = self._config
132
+ if config is None:
133
+ return ""
134
+ raw = config.data.get("player_name")
135
+ if isinstance(raw, (bytes, bytearray)):
136
+ return bytes(raw).split(b"\x00", 1)[0].decode("latin-1", errors="ignore")
137
+ if isinstance(raw, str):
138
+ return raw
139
+ return ""
140
+
141
+ def _enter_game_over(self) -> None:
142
+ if self._game_over_active:
143
+ return
144
+
145
+ record = HighScoreRecord.blank()
146
+ record.score_xp = int(self._player.experience)
147
+ record.survival_elapsed_ms = int(self._rush.elapsed_ms)
148
+ record.creature_kill_count = int(self._creatures.kill_count)
149
+ weapon_id = most_used_weapon_id_for_player(self._state, player_index=int(self._player.index), fallback_weapon_id=int(self._player.weapon_id))
150
+ record.most_used_weapon_id = int(weapon_id)
151
+ fired = 0
152
+ hit = 0
153
+ try:
154
+ fired = int(self._state.shots_fired[int(self._player.index)])
155
+ hit = int(self._state.shots_hit[int(self._player.index)])
156
+ except Exception:
157
+ fired = 0
158
+ hit = 0
159
+ fired = max(0, int(fired))
160
+ hit = max(0, min(int(hit), fired))
161
+ record.shots_fired = fired
162
+ record.shots_hit = hit
163
+ record.game_mode_id = int(self._config.data.get("game_mode", int(GameMode.RUSH))) if self._config is not None else int(GameMode.RUSH)
164
+
165
+ self._game_over_record = record
166
+ self._game_over_ui.open()
167
+ self._game_over_active = True
168
+
169
+ def update(self, dt: float) -> None:
170
+ self._update_audio(dt)
171
+
172
+ dt_frame = self._tick_frame(dt)[0]
173
+ self._handle_input()
174
+
175
+ if self._game_over_active:
176
+ record = self._game_over_record
177
+ if record is None:
178
+ self._enter_game_over()
179
+ record = self._game_over_record
180
+ if record is not None:
181
+ action = self._game_over_ui.update(
182
+ dt,
183
+ record=record,
184
+ player_name_default=self._player_name_default(),
185
+ play_sfx=self._world.audio_router.play_sfx,
186
+ rand=self._state.rng.rand,
187
+ mouse=self._ui_mouse_pos(),
188
+ )
189
+ if action == "play_again":
190
+ self.open()
191
+ return
192
+ if action == "high_scores":
193
+ self._action = "open_high_scores"
194
+ return
195
+ if action == "main_menu":
196
+ self._action = "back_to_menu"
197
+ self.close_requested = True
198
+ return
199
+
200
+ any_alive = any(player.health > 0.0 for player in self._world.players)
201
+ dt_world = 0.0 if self._paused or (not any_alive) else dt_frame
202
+
203
+ self._rush.elapsed_ms += dt_world * 1000.0
204
+ if dt_world <= 0.0:
205
+ if not any_alive:
206
+ self._enter_game_over()
207
+ return
208
+
209
+ self._enforce_rush_loadout()
210
+ input_state = self._build_input()
211
+ self._world.update(
212
+ dt_world,
213
+ inputs=[input_state for _ in self._world.players],
214
+ auto_pick_perks=False,
215
+ game_mode=int(GameMode.RUSH),
216
+ perk_progression_enabled=False,
217
+ )
218
+
219
+ cooldown, spawns = tick_rush_mode_spawns(
220
+ self._rush.spawn_cooldown_ms,
221
+ dt_world * 1000.0,
222
+ self._state.rng,
223
+ player_count=len(self._world.players),
224
+ survival_elapsed_ms=int(self._rush.elapsed_ms),
225
+ terrain_width=float(self._world.world_size),
226
+ terrain_height=float(self._world.world_size),
227
+ )
228
+ self._rush.spawn_cooldown_ms = cooldown
229
+ self._creatures.spawn_inits(spawns)
230
+
231
+ if not any(player.health > 0.0 for player in self._world.players):
232
+ self._enter_game_over()
233
+
234
+ def _draw_game_cursor(self) -> None:
235
+ mouse_x = float(self._ui_mouse_x)
236
+ mouse_y = float(self._ui_mouse_y)
237
+ cursor_tex = self._ui_assets.cursor if self._ui_assets is not None else None
238
+ draw_menu_cursor(
239
+ self._world.particles_texture,
240
+ cursor_tex,
241
+ x=mouse_x,
242
+ y=mouse_y,
243
+ pulse_time=float(self._cursor_pulse_time),
244
+ )
245
+
246
+ def _draw_aim_cursor(self) -> None:
247
+ mouse_x = float(self._ui_mouse_x)
248
+ mouse_y = float(self._ui_mouse_y)
249
+ aim_tex = self._ui_assets.aim if self._ui_assets is not None else None
250
+ draw_aim_cursor(self._world.particles_texture, aim_tex, x=mouse_x, y=mouse_y)
251
+
252
+ def draw(self) -> None:
253
+ self._world.draw(draw_aim_indicators=(not self._game_over_active))
254
+ self._draw_screen_fade()
255
+
256
+ hud_bottom = 0.0
257
+ if (not self._game_over_active) and self._hud_assets is not None:
258
+ hud_bottom = draw_hud_overlay(
259
+ self._hud_assets,
260
+ player=self._player,
261
+ players=self._world.players,
262
+ bonus_hud=self._state.bonus_hud,
263
+ elapsed_ms=self._rush.elapsed_ms,
264
+ font=self._small,
265
+ frame_dt_ms=self._last_dt_ms,
266
+ show_xp=False,
267
+ show_time=True,
268
+ )
269
+
270
+ if not self._game_over_active:
271
+ x = 18.0
272
+ y = max(18.0, hud_bottom + 10.0)
273
+ line = float(self._ui_line_height())
274
+ self._draw_ui_text(f"rush: t={self._rush.elapsed_ms/1000.0:6.1f}s", x, y, UI_TEXT_COLOR)
275
+ self._draw_ui_text(f"kills={self._creatures.kill_count}", x, y + line, UI_HINT_COLOR)
276
+ if self._paused:
277
+ self._draw_ui_text("paused (TAB)", x, y + line * 2.0, UI_HINT_COLOR)
278
+ if self._player.health <= 0.0:
279
+ self._draw_ui_text("game over", x, y + line * 2.0, UI_ERROR_COLOR)
280
+
281
+ warn_y = float(rl.get_screen_height()) - 28.0
282
+ if self._world.missing_assets:
283
+ warn = "Missing world assets: " + ", ".join(self._world.missing_assets)
284
+ self._draw_ui_text(warn, 24.0, warn_y, UI_ERROR_COLOR, scale=0.8)
285
+ warn_y -= float(self._ui_line_height(scale=0.8)) + 2.0
286
+ if self._hud_missing:
287
+ warn = "Missing HUD assets: " + ", ".join(self._hud_missing)
288
+ self._draw_ui_text(warn, 24.0, warn_y, UI_ERROR_COLOR, scale=0.8)
289
+
290
+ if not self._game_over_active:
291
+ self._draw_aim_cursor()
292
+ else:
293
+ self._draw_game_cursor()
294
+ if self._game_over_record is not None:
295
+ self._game_over_ui.draw(
296
+ record=self._game_over_record,
297
+ banner_kind=self._game_over_banner,
298
+ hud_assets=self._hud_assets,
299
+ mouse=self._ui_mouse_pos(),
300
+ )