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
crimson/input_codes.py ADDED
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ import pyray as rl
6
+
7
+ from grim.config import CrimsonConfig
8
+
9
+
10
+ INPUT_CODE_UNBOUND = 0x17E
11
+
12
+
13
+ def _dik_to_rl_key(dik_code: int) -> int | None:
14
+ dik_code = int(dik_code)
15
+ return {
16
+ 0x01: int(rl.KeyboardKey.KEY_ESCAPE),
17
+ 0x0F: int(rl.KeyboardKey.KEY_TAB),
18
+ 0x10: int(rl.KeyboardKey.KEY_Q),
19
+ 0x11: int(rl.KeyboardKey.KEY_W),
20
+ 0x12: int(rl.KeyboardKey.KEY_E),
21
+ 0x13: int(rl.KeyboardKey.KEY_R),
22
+ 0x1C: int(rl.KeyboardKey.KEY_ENTER),
23
+ 0x1D: int(rl.KeyboardKey.KEY_LEFT_CONTROL),
24
+ 0x1E: int(rl.KeyboardKey.KEY_A),
25
+ 0x1F: int(rl.KeyboardKey.KEY_S),
26
+ 0x20: int(rl.KeyboardKey.KEY_D),
27
+ 0x2A: int(rl.KeyboardKey.KEY_LEFT_SHIFT),
28
+ 0x36: int(rl.KeyboardKey.KEY_RIGHT_SHIFT),
29
+ 0x38: int(rl.KeyboardKey.KEY_LEFT_ALT),
30
+ 0x39: int(rl.KeyboardKey.KEY_SPACE),
31
+ 0x9D: int(rl.KeyboardKey.KEY_RIGHT_CONTROL),
32
+ 0xC8: int(rl.KeyboardKey.KEY_UP),
33
+ 0xC9: int(rl.KeyboardKey.KEY_PAGE_UP),
34
+ 0xCB: int(rl.KeyboardKey.KEY_LEFT),
35
+ 0xCD: int(rl.KeyboardKey.KEY_RIGHT),
36
+ 0xD0: int(rl.KeyboardKey.KEY_DOWN),
37
+ 0xD1: int(rl.KeyboardKey.KEY_PAGE_DOWN),
38
+ 0xD3: int(rl.KeyboardKey.KEY_DELETE),
39
+ }.get(dik_code)
40
+
41
+
42
+ def _mouse_button_for_code(key_code: int) -> int | None:
43
+ key_code = int(key_code)
44
+ return {
45
+ 0x100: int(rl.MouseButton.MOUSE_BUTTON_LEFT),
46
+ 0x101: int(rl.MouseButton.MOUSE_BUTTON_RIGHT),
47
+ 0x102: int(rl.MouseButton.MOUSE_BUTTON_MIDDLE),
48
+ 0x103: int(rl.MouseButton.MOUSE_BUTTON_SIDE),
49
+ 0x104: int(rl.MouseButton.MOUSE_BUTTON_EXTRA),
50
+ }.get(key_code)
51
+
52
+
53
+ def input_code_name(key_code: int) -> str:
54
+ key_code = int(key_code)
55
+ if key_code == INPUT_CODE_UNBOUND:
56
+ return "unbound"
57
+ if key_code == 0x100:
58
+ return "Mouse1"
59
+ if key_code == 0x101:
60
+ return "Mouse2"
61
+ if key_code == 0x102:
62
+ return "Mouse3"
63
+ if key_code == 0x103:
64
+ return "Mouse4"
65
+ if key_code == 0x104:
66
+ return "Mouse5"
67
+ if key_code == 0x109:
68
+ return "MWheelUp"
69
+ if key_code == 0x10A:
70
+ return "MWheelDown"
71
+ if key_code == 0x13F:
72
+ return "JoyAxisX"
73
+ if key_code == 0x140:
74
+ return "JoyAxisY"
75
+ if key_code == 0x141:
76
+ return "JoyAxisZ"
77
+ if key_code == 0x153:
78
+ return "JoyAxisR"
79
+
80
+ if key_code < 0x100:
81
+ name = {
82
+ 0x01: "Escape",
83
+ 0x0F: "Tab",
84
+ 0x10: "Q",
85
+ 0x11: "W",
86
+ 0x12: "E",
87
+ 0x13: "R",
88
+ 0x1C: "Enter",
89
+ 0x1D: "LControl",
90
+ 0x1E: "A",
91
+ 0x1F: "S",
92
+ 0x20: "D",
93
+ 0x2A: "LShift",
94
+ 0x36: "RShift",
95
+ 0x38: "LAlt",
96
+ 0x39: "Space",
97
+ 0x9D: "RControl",
98
+ 0xC8: "Up",
99
+ 0xC9: "PageUp",
100
+ 0xCB: "Left",
101
+ 0xCD: "Right",
102
+ 0xD0: "Down",
103
+ 0xD1: "PageDown",
104
+ 0xD3: "Delete",
105
+ }.get(key_code)
106
+ if name is not None:
107
+ return name
108
+ return f"DIK_{key_code:02X}"
109
+
110
+ return f"KEY_{key_code:04X}"
111
+
112
+
113
+ def input_code_is_down(key_code: int) -> bool:
114
+ key_code = int(key_code)
115
+ if key_code == INPUT_CODE_UNBOUND:
116
+ return False
117
+ mouse_button = _mouse_button_for_code(key_code)
118
+ if mouse_button is not None:
119
+ return bool(rl.is_mouse_button_down(mouse_button))
120
+ if key_code < 0x100:
121
+ rl_key = _dik_to_rl_key(key_code)
122
+ if rl_key is None:
123
+ return False
124
+ return bool(rl.is_key_down(rl_key))
125
+ return False
126
+
127
+
128
+ def input_code_is_pressed(key_code: int) -> bool:
129
+ key_code = int(key_code)
130
+ if key_code == INPUT_CODE_UNBOUND:
131
+ return False
132
+ mouse_button = _mouse_button_for_code(key_code)
133
+ if mouse_button is not None:
134
+ return bool(rl.is_mouse_button_pressed(mouse_button))
135
+ if key_code < 0x100:
136
+ rl_key = _dik_to_rl_key(key_code)
137
+ if rl_key is None:
138
+ return False
139
+ return bool(rl.is_key_pressed(rl_key))
140
+ return False
141
+
142
+
143
+ def _parse_keybinds_blob(blob: bytes | bytearray | None) -> tuple[int, ...]:
144
+ if blob is None:
145
+ return ()
146
+ if not isinstance(blob, (bytes, bytearray)):
147
+ return ()
148
+ if len(blob) != 0x80:
149
+ return ()
150
+ out: list[int] = []
151
+ for offset in range(0, 0x80, 4):
152
+ out.append(int.from_bytes(blob[offset : offset + 4], "little"))
153
+ return tuple(out)
154
+
155
+
156
+ def config_keybinds(config: CrimsonConfig | None) -> tuple[int, ...]:
157
+ if config is None:
158
+ return ()
159
+ return _parse_keybinds_blob(config.data.get("keybinds"))
160
+
161
+
162
+ def player_move_fire_binds(keybinds: Sequence[int], player_index: int) -> tuple[int, int, int, int, int]:
163
+ """Return (up, down, left, right, fire) key codes for a player.
164
+
165
+ The classic config packs keybind blocks in 0x10-int strides; the first five entries
166
+ are used by `ui_render_keybind_help` (Up/Down/Left/Right/Fire).
167
+ """
168
+
169
+ base = int(player_index) * 0x10
170
+ values = [INPUT_CODE_UNBOUND, INPUT_CODE_UNBOUND, INPUT_CODE_UNBOUND, INPUT_CODE_UNBOUND, INPUT_CODE_UNBOUND]
171
+ for idx in range(5):
172
+ src = base + idx
173
+ if 0 <= src < len(keybinds):
174
+ values[idx] = int(keybinds[src])
175
+ return values[0], values[1], values[2], values[3], values[4]
176
+
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import random
5
+ from typing import TYPE_CHECKING, Protocol
6
+
7
+ import pyray as rl
8
+
9
+ from grim.assets import PaqTextureCache
10
+ from grim.audio import AudioState, update_audio
11
+ from grim.config import CrimsonConfig
12
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
13
+ from grim.view import ViewContext
14
+
15
+ from ..game_world import GameWorld
16
+ from ..persistence.highscores import HighScoreRecord
17
+ from ..ui.game_over import GameOverUi
18
+ from ..ui.hud import HudAssets, load_hud_assets
19
+
20
+ if TYPE_CHECKING:
21
+ from ..persistence.save_status import GameStatus
22
+
23
+
24
+ def _clamp(value: float, lo: float, hi: float) -> float:
25
+ if value < lo:
26
+ return lo
27
+ if value > hi:
28
+ return hi
29
+ return value
30
+
31
+
32
+ class _ScreenFade(Protocol):
33
+ screen_fade_alpha: float
34
+
35
+
36
+ class BaseGameplayMode:
37
+ def __init__(
38
+ self,
39
+ ctx: ViewContext,
40
+ *,
41
+ world_size: float,
42
+ default_game_mode_id: int,
43
+ demo_mode_active: bool = False,
44
+ difficulty_level: int = 0,
45
+ hardcore: bool = False,
46
+ texture_cache: PaqTextureCache | None = None,
47
+ config: CrimsonConfig | None = None,
48
+ audio: AudioState | None = None,
49
+ audio_rng: random.Random | None = None,
50
+ ) -> None:
51
+ self._assets_root = ctx.assets_dir
52
+ self._missing_assets: list[str] = []
53
+ self._hud_missing: list[str] = []
54
+ self._small: SmallFontData | None = None
55
+ self._hud_assets: HudAssets | None = None
56
+
57
+ self._config = config
58
+ self._base_dir = config.path.parent if config is not None else Path.cwd()
59
+
60
+ self.close_requested = False
61
+ self._action: str | None = None
62
+ self._paused = False
63
+ self._status: GameStatus | None = None
64
+
65
+ self._world = GameWorld(
66
+ assets_dir=ctx.assets_dir,
67
+ world_size=float(world_size),
68
+ demo_mode_active=bool(demo_mode_active),
69
+ difficulty_level=int(difficulty_level),
70
+ hardcore=bool(hardcore),
71
+ texture_cache=texture_cache,
72
+ config=config,
73
+ audio=audio,
74
+ audio_rng=audio_rng,
75
+ )
76
+ self._bind_world()
77
+
78
+ self._game_over_active = False
79
+ self._game_over_record: HighScoreRecord | None = None
80
+ self._game_over_ui = GameOverUi(
81
+ assets_root=self._assets_root,
82
+ base_dir=self._base_dir,
83
+ config=config or CrimsonConfig(path=self._base_dir / "crimson.cfg", data={"game_mode": int(default_game_mode_id)}),
84
+ )
85
+ self._game_over_banner = "reaper"
86
+
87
+ self._ui_mouse_x = 0.0
88
+ self._ui_mouse_y = 0.0
89
+ self._cursor_pulse_time = 0.0
90
+ self._last_dt_ms = 0.0
91
+ self._screen_fade: _ScreenFade | None = None
92
+
93
+ def _bind_world(self) -> None:
94
+ self._state = self._world.state
95
+ self._creatures = self._world.creatures
96
+ self._player = self._world.players[0]
97
+ self._state.status = self._status
98
+
99
+ def bind_status(self, status: GameStatus | None) -> None:
100
+ self._status = status
101
+ self._state.status = status
102
+
103
+ def bind_screen_fade(self, fade: _ScreenFade | None) -> None:
104
+ self._screen_fade = fade
105
+
106
+ def bind_audio(self, audio: AudioState | None, audio_rng: random.Random | None) -> None:
107
+ self._world.audio = audio
108
+ self._world.audio_rng = audio_rng
109
+
110
+ def _update_audio(self, dt: float) -> None:
111
+ if self._world.audio is not None:
112
+ update_audio(self._world.audio, dt)
113
+
114
+ def _ui_line_height(self, scale: float = 1.0) -> int:
115
+ if self._small is not None:
116
+ return int(self._small.cell_size * scale)
117
+ return int(20 * scale)
118
+
119
+ def _ui_text_width(self, text: str, scale: float = 1.0) -> int:
120
+ if self._small is not None:
121
+ return int(measure_small_text_width(self._small, text, scale))
122
+ return int(rl.measure_text(text, int(20 * scale)))
123
+
124
+ def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color, scale: float = 1.0) -> None:
125
+ if self._small is not None:
126
+ draw_small_text(self._small, text, x, y, scale, color)
127
+ else:
128
+ rl.draw_text(text, int(x), int(y), int(20 * scale), color)
129
+
130
+ def _ui_mouse_pos(self) -> rl.Vector2:
131
+ return rl.Vector2(float(self._ui_mouse_x), float(self._ui_mouse_y))
132
+
133
+ def _update_ui_mouse(self) -> None:
134
+ mouse = rl.get_mouse_position()
135
+ screen_w = float(rl.get_screen_width())
136
+ screen_h = float(rl.get_screen_height())
137
+ self._ui_mouse_x = _clamp(float(mouse.x), 0.0, max(0.0, screen_w - 1.0))
138
+ self._ui_mouse_y = _clamp(float(mouse.y), 0.0, max(0.0, screen_h - 1.0))
139
+
140
+ def _tick_frame(self, dt: float, *, clamp_cursor_pulse: bool = False) -> tuple[float, float]:
141
+ dt_frame = float(dt)
142
+ dt_ui_ms = float(min(dt_frame, 0.1) * 1000.0)
143
+ self._last_dt_ms = dt_ui_ms
144
+
145
+ self._update_ui_mouse()
146
+
147
+ pulse_dt = float(min(dt_frame, 0.1)) if clamp_cursor_pulse else dt_frame
148
+ self._cursor_pulse_time += pulse_dt * 1.1
149
+
150
+ return dt_frame, dt_ui_ms
151
+
152
+ def _player_name_default(self) -> str:
153
+ config = self._config
154
+ if config is None:
155
+ return ""
156
+ raw = config.data.get("player_name")
157
+ if isinstance(raw, (bytes, bytearray)):
158
+ return bytes(raw).split(b"\x00", 1)[0].decode("latin-1", errors="ignore")
159
+ if isinstance(raw, str):
160
+ return raw
161
+ return ""
162
+
163
+ def open(self) -> None:
164
+ self.close_requested = False
165
+ self._action = None
166
+ self._paused = False
167
+ self._missing_assets.clear()
168
+ self._hud_missing.clear()
169
+ try:
170
+ self._small = load_small_font(self._assets_root, self._missing_assets)
171
+ except Exception:
172
+ self._small = None
173
+
174
+ self._hud_assets = load_hud_assets(self._assets_root)
175
+ if self._hud_assets.missing:
176
+ self._hud_missing = list(self._hud_assets.missing)
177
+
178
+ self._game_over_active = False
179
+ self._game_over_record = None
180
+ self._game_over_banner = "reaper"
181
+ self._game_over_ui.close()
182
+
183
+ player_count = 1
184
+ config = self._config
185
+ if config is not None:
186
+ try:
187
+ player_count = int(config.data.get("player_count", 1) or 1)
188
+ except Exception:
189
+ player_count = 1
190
+ seed = random.getrandbits(32)
191
+ self._world.reset(seed=seed, player_count=max(1, min(4, player_count)))
192
+ self._world.open()
193
+ self._bind_world()
194
+
195
+ self._ui_mouse_x = float(rl.get_screen_width()) * 0.5
196
+ self._ui_mouse_y = float(rl.get_screen_height()) * 0.5
197
+ self._cursor_pulse_time = 0.0
198
+
199
+ def close(self) -> None:
200
+ self._game_over_ui.close()
201
+ if self._small is not None:
202
+ rl.unload_texture(self._small.texture)
203
+ self._small = None
204
+ self._hud_assets = None
205
+ self._world.close()
206
+
207
+ def take_action(self) -> str | None:
208
+ action = self._action
209
+ self._action = None
210
+ return action
211
+
212
+ def _draw_screen_fade(self) -> None:
213
+ fade_alpha = 0.0
214
+ if self._screen_fade is not None:
215
+ fade_alpha = float(self._screen_fade.screen_fade_alpha)
216
+ if fade_alpha <= 0.0:
217
+ return
218
+ alpha = int(255 * max(0.0, min(1.0, fade_alpha)))
219
+ rl.draw_rectangle(0, 0, int(rl.get_screen_width()), int(rl.get_screen_height()), rl.Color(0, 0, 0, alpha))