crimsonland 0.1.0.dev13__py3-none-any.whl → 0.1.0.dev15__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/.DS_Store ADDED
Binary file
crimson/cli.py CHANGED
@@ -211,6 +211,7 @@ def cmd_game(
211
211
  seed: int | None = typer.Option(None, help="rng seed"),
212
212
  demo: bool = typer.Option(False, "--demo", help="enable shareware demo mode"),
213
213
  no_intro: bool = typer.Option(False, "--no-intro", help="skip company splashes and intro music"),
214
+ debug: bool = typer.Option(False, "--debug", help="enable debug cheats and overlays"),
214
215
  base_dir: Path = typer.Option(
215
216
  default_runtime_dir(),
216
217
  "--base-dir",
@@ -236,6 +237,7 @@ def cmd_game(
236
237
  seed=seed,
237
238
  demo_enabled=demo,
238
239
  no_intro=no_intro,
240
+ debug=debug,
239
241
  )
240
242
  run_game(config)
241
243
 
Binary file
crimson/debug.py CHANGED
@@ -2,6 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
 
5
+ _DEBUG_OVERRIDE: bool | None = None
6
+
7
+
8
+ def set_debug_enabled(enabled: bool) -> None:
9
+ global _DEBUG_OVERRIDE
10
+ _DEBUG_OVERRIDE = bool(enabled)
11
+
5
12
 
6
13
  def debug_enabled() -> bool:
14
+ if _DEBUG_OVERRIDE is not None:
15
+ return bool(_DEBUG_OVERRIDE)
7
16
  return os.environ.get("CRIMSON_DEBUG") == "1"
crimson/game.py CHANGED
@@ -38,7 +38,7 @@ from grim.terrain_render import GroundRenderer
38
38
  from grim.view import View, ViewContext
39
39
  from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
40
40
 
41
- from .debug import debug_enabled
41
+ from .debug import debug_enabled, set_debug_enabled
42
42
  from grim import music
43
43
 
44
44
  from .demo import DemoView
@@ -113,6 +113,7 @@ class GameConfig:
113
113
  seed: int | None = None
114
114
  demo_enabled: bool = False
115
115
  no_intro: bool = False
116
+ debug: bool = False
116
117
 
117
118
 
118
119
  @dataclass(slots=True)
@@ -190,6 +191,25 @@ QUEST_BACK_BUTTON_X_OFFSET = 138.0
190
191
  QUEST_BACK_BUTTON_Y_OFFSET = 212.0
191
192
  QUEST_PANEL_HEIGHT = 378.0
192
193
 
194
+ # game_update_victory_screen (0x00406350): used as the "end note" screen after the final quest.
195
+ END_NOTE_PANEL_POS_X = -45.0
196
+ END_NOTE_PANEL_POS_Y = 110.0
197
+ END_NOTE_PANEL_GEOM_X0 = -63.0
198
+ END_NOTE_PANEL_GEOM_Y0 = -81.0
199
+ END_NOTE_PANEL_W = 510.0
200
+ END_NOTE_PANEL_H = 378.0
201
+
202
+ END_NOTE_HEADER_X_OFFSET = 214.0 # v11 + 44 - 10 in the decompile, relative to panel-left
203
+ END_NOTE_HEADER_Y_OFFSET = 46.0 # (base_y + 40) + 6 in the decompile, relative to panel-top
204
+ END_NOTE_BODY_X_OFFSET = END_NOTE_HEADER_X_OFFSET - 8.0
205
+ END_NOTE_BODY_Y_GAP = 32.0
206
+ END_NOTE_LINE_STEP_Y = 14.0
207
+ END_NOTE_AFTER_BODY_Y_GAP = 22.0 # 14 + 8 in the decompile
208
+
209
+ END_NOTE_BUTTON_X_OFFSET = 266.0 # (v11 + 44 + 20) - 4 + 26, relative to panel-left
210
+ END_NOTE_BUTTON_Y_OFFSET = 210.0 # (base_y + 40) + 170 in the decompile, relative to panel-top
211
+ END_NOTE_BUTTON_STEP_Y = 32.0
212
+
193
213
 
194
214
  class QuestsMenuView:
195
215
  """Quest selection menu.
@@ -282,6 +302,7 @@ class QuestsMenuView:
282
302
  self._cursor_pulse_time += min(dt, 0.1) * 1.1
283
303
 
284
304
  config = self._state.config
305
+ status = self._state.status
285
306
 
286
307
  # The original forcibly clears hardcore in the demo build.
287
308
  if self._state.demo_enabled:
@@ -289,6 +310,14 @@ class QuestsMenuView:
289
310
  config.data["hardcore_flag"] = 0
290
311
  self._dirty = True
291
312
 
313
+ if debug_enabled() and rl.is_key_pressed(rl.KeyboardKey.KEY_F5):
314
+ unlock = 49
315
+ if int(status.quest_unlock_index) < unlock:
316
+ status.quest_unlock_index = unlock
317
+ if int(status.quest_unlock_index_full) < unlock:
318
+ status.quest_unlock_index_full = unlock
319
+ self._state.console.log.log("debug: unlocked all quests")
320
+
292
321
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
293
322
  self._action = "open_play_game"
294
323
  return
@@ -374,7 +403,7 @@ class QuestsMenuView:
374
403
  # `sub_447d40` base sums:
375
404
  # x_sum = <ui_element_x> + <ui_element_offset_x> (x=-5)
376
405
  # y_sum = <ui_element_y> + <ui_element_offset_y> (y=185 + widescreen shift via ui_menu_layout_init)
377
- x_sum = QUEST_MENU_BASE_X + MENU_PANEL_OFFSET_X
406
+ x_sum = QUEST_MENU_BASE_X + QUEST_MENU_PANEL_OFFSET_X
378
407
  y_sum = QUEST_MENU_BASE_Y + MENU_PANEL_OFFSET_Y + self._widescreen_y_shift
379
408
 
380
409
  title_x = x_sum + QUEST_TITLE_X_OFFSET
@@ -1288,6 +1317,9 @@ class QuestResultsView:
1288
1317
  self._action = "start_quest"
1289
1318
  return
1290
1319
  if action == "play_next":
1320
+ if int(self._quest_stage_major) == 5 and int(self._quest_stage_minor) == 10:
1321
+ self._action = "end_note"
1322
+ return
1291
1323
  next_level = _next_quest_level(self._quest_level)
1292
1324
  if next_level is not None:
1293
1325
  self._state.pending_quest_level = next_level
@@ -1336,6 +1368,217 @@ class QuestResultsView:
1336
1368
  self._action = "open_high_scores"
1337
1369
 
1338
1370
 
1371
+ class EndNoteView:
1372
+ """Final quest "Show End Note" flow.
1373
+
1374
+ Classic:
1375
+ - quest_results_screen_update uses "Show End Note" instead of "Play Next" for quest 5.10
1376
+ - clicking it transitions to state 0x15 (game_update_victory_screen @ 0x00406350)
1377
+ """
1378
+
1379
+ def __init__(self, state: GameState) -> None:
1380
+ self._state = state
1381
+ self._ground: GroundRenderer | None = None
1382
+ self._small_font: SmallFontData | None = None
1383
+ self._panel_tex: rl.Texture2D | None = None
1384
+ self._button_textures: UiButtonTextureSet | None = None
1385
+ self._action: str | None = None
1386
+ self._cursor_pulse_time = 0.0
1387
+
1388
+ self._survival_button = UiButtonState("Survival", force_wide=True)
1389
+ self._rush_button = UiButtonState(" Rush ", force_wide=True)
1390
+ self._typo_button = UiButtonState("Typ'o'Shooter", force_wide=True)
1391
+ self._main_menu_button = UiButtonState("Main Menu", force_wide=True)
1392
+
1393
+ def open(self) -> None:
1394
+ self._action = None
1395
+ self._cursor_pulse_time = 0.0
1396
+ self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
1397
+
1398
+ cache = _ensure_texture_cache(self._state)
1399
+ self._panel_tex = cache.get_or_load("ui_menuPanel", "ui/ui_menuPanel.jaz").texture
1400
+ button_md = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
1401
+ button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
1402
+ self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=button_md)
1403
+ self._small_font = None
1404
+
1405
+ def close(self) -> None:
1406
+ self._ground = None
1407
+ self._small_font = None
1408
+ self._panel_tex = None
1409
+ self._button_textures = None
1410
+
1411
+ def update(self, dt: float) -> None:
1412
+ if self._state.audio is not None:
1413
+ update_audio(self._state.audio, dt)
1414
+ if self._ground is not None:
1415
+ self._ground.process_pending()
1416
+ self._cursor_pulse_time += min(float(dt), 0.1) * 1.1
1417
+
1418
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
1419
+ if self._state.audio is not None:
1420
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1421
+ self._action = "back_to_menu"
1422
+ return
1423
+
1424
+ textures = self._button_textures
1425
+ if textures is None or (textures.button_sm is None and textures.button_md is None):
1426
+ return
1427
+
1428
+ screen_w = float(rl.get_screen_width())
1429
+ scale = 1.0
1430
+
1431
+ layout_w = screen_w / scale if scale else screen_w
1432
+ widescreen_shift_y = MenuView._menu_widescreen_y_shift(layout_w)
1433
+
1434
+ panel_left = (END_NOTE_PANEL_GEOM_X0 + END_NOTE_PANEL_POS_X) * scale
1435
+ panel_top = (END_NOTE_PANEL_GEOM_Y0 + END_NOTE_PANEL_POS_Y + widescreen_shift_y) * scale
1436
+
1437
+ button_x = panel_left + END_NOTE_BUTTON_X_OFFSET * scale
1438
+ button_y = panel_top + END_NOTE_BUTTON_Y_OFFSET * scale
1439
+
1440
+ font = self._ensure_small_font()
1441
+ mouse = rl.get_mouse_position()
1442
+ click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
1443
+ dt_ms = min(float(dt), 0.1) * 1000.0
1444
+
1445
+ survival_w = button_width(font, self._survival_button.label, scale=scale, force_wide=self._survival_button.force_wide)
1446
+ if button_update(self._survival_button, x=button_x, y=button_y, width=survival_w, dt_ms=dt_ms, mouse=mouse, click=click):
1447
+ self._state.config.data["game_mode"] = 1
1448
+ if self._state.audio is not None:
1449
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1450
+ self._action = "start_survival"
1451
+ return
1452
+
1453
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1454
+ rush_w = button_width(font, self._rush_button.label, scale=scale, force_wide=self._rush_button.force_wide)
1455
+ if button_update(self._rush_button, x=button_x, y=button_y, width=rush_w, dt_ms=dt_ms, mouse=mouse, click=click):
1456
+ self._state.config.data["game_mode"] = 2
1457
+ if self._state.audio is not None:
1458
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1459
+ self._action = "start_rush"
1460
+ return
1461
+
1462
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1463
+ typo_w = button_width(font, self._typo_button.label, scale=scale, force_wide=self._typo_button.force_wide)
1464
+ if button_update(self._typo_button, x=button_x, y=button_y, width=typo_w, dt_ms=dt_ms, mouse=mouse, click=click):
1465
+ self._state.config.data["game_mode"] = 4
1466
+ self._state.screen_fade_alpha = 0.0
1467
+ self._state.screen_fade_ramp = True
1468
+ if self._state.audio is not None:
1469
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1470
+ self._action = "start_typo"
1471
+ return
1472
+
1473
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1474
+ main_w = button_width(font, self._main_menu_button.label, scale=scale, force_wide=self._main_menu_button.force_wide)
1475
+ if button_update(self._main_menu_button, x=button_x, y=button_y, width=main_w, dt_ms=dt_ms, mouse=mouse, click=click):
1476
+ if self._state.audio is not None:
1477
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1478
+ self._action = "back_to_menu"
1479
+ return
1480
+
1481
+ def draw(self) -> None:
1482
+ rl.clear_background(rl.BLACK)
1483
+ pause_background = self._state.pause_background
1484
+ if pause_background is not None:
1485
+ pause_background.draw_pause_background()
1486
+ elif self._ground is not None:
1487
+ self._ground.draw(0.0, 0.0)
1488
+ _draw_screen_fade(self._state)
1489
+
1490
+ panel_tex = self._panel_tex
1491
+ if panel_tex is None:
1492
+ return
1493
+
1494
+ screen_w = float(rl.get_screen_width())
1495
+ scale = 1.0
1496
+ layout_w = screen_w / scale if scale else screen_w
1497
+ widescreen_shift_y = MenuView._menu_widescreen_y_shift(layout_w)
1498
+
1499
+ panel_left = (END_NOTE_PANEL_GEOM_X0 + END_NOTE_PANEL_POS_X) * scale
1500
+ panel_top = (END_NOTE_PANEL_GEOM_Y0 + END_NOTE_PANEL_POS_Y + widescreen_shift_y) * scale
1501
+ panel = rl.Rectangle(
1502
+ float(panel_left),
1503
+ float(panel_top),
1504
+ float(END_NOTE_PANEL_W * scale),
1505
+ float(END_NOTE_PANEL_H * scale),
1506
+ )
1507
+
1508
+ fx_detail = bool(int(self._state.config.data.get("fx_detail_0", 0) or 0))
1509
+ draw_classic_menu_panel(panel_tex, dst=panel, tint=rl.WHITE, shadow=fx_detail)
1510
+
1511
+ font = self._ensure_small_font()
1512
+ hardcore = bool(int(self._state.config.data.get("hardcore_flag", 0) or 0))
1513
+ header = " Incredible!" if hardcore else "Congratulations!"
1514
+ body_lines = (
1515
+ [
1516
+ "You've done the thing we all thought was",
1517
+ "virtually impossible. To reward your",
1518
+ "efforts a new weapon has been unlocked ",
1519
+ "for you: Splitter Gun.",
1520
+ "",
1521
+ "",
1522
+ ]
1523
+ if hardcore
1524
+ else [
1525
+ "You've completed all the levels but the battle",
1526
+ "isn't over yet! With all of the unlocked perks",
1527
+ "and weapons your Survival is just a bit easier.",
1528
+ "You can also replay the quests in Hardcore.",
1529
+ "As an additional reward for your victorious",
1530
+ "playing, a completely new and different game",
1531
+ "mode is unlocked for you: Typ'o'Shooter.",
1532
+ ]
1533
+ )
1534
+
1535
+ header_x = panel_left + END_NOTE_HEADER_X_OFFSET * scale
1536
+ header_y = panel_top + END_NOTE_HEADER_Y_OFFSET * scale
1537
+ header_color = rl.Color(255, 255, 255, int(255 * 0.8))
1538
+ body_color = rl.Color(255, 255, 255, int(255 * 0.5))
1539
+
1540
+ draw_small_text(font, header, header_x, header_y, 1.5 * scale, header_color)
1541
+
1542
+ body_x = panel_left + END_NOTE_BODY_X_OFFSET * scale
1543
+ body_y = header_y + END_NOTE_BODY_Y_GAP * scale
1544
+ for idx, line in enumerate(body_lines):
1545
+ draw_small_text(font, line, body_x, body_y, 1.0 * scale, body_color)
1546
+ if idx != len(body_lines) - 1:
1547
+ body_y += END_NOTE_LINE_STEP_Y * scale
1548
+ body_y += END_NOTE_AFTER_BODY_Y_GAP * scale
1549
+ draw_small_text(font, "Good luck with your battles, trooper!", body_x, body_y, 1.0 * scale, body_color)
1550
+
1551
+ textures = self._button_textures
1552
+ if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
1553
+ button_x = panel_left + END_NOTE_BUTTON_X_OFFSET * scale
1554
+ button_y = panel_top + END_NOTE_BUTTON_Y_OFFSET * scale
1555
+ survival_w = button_width(font, self._survival_button.label, scale=scale, force_wide=self._survival_button.force_wide)
1556
+ button_draw(textures, font, self._survival_button, x=button_x, y=button_y, width=survival_w, scale=scale)
1557
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1558
+ rush_w = button_width(font, self._rush_button.label, scale=scale, force_wide=self._rush_button.force_wide)
1559
+ button_draw(textures, font, self._rush_button, x=button_x, y=button_y, width=rush_w, scale=scale)
1560
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1561
+ typo_w = button_width(font, self._typo_button.label, scale=scale, force_wide=self._typo_button.force_wide)
1562
+ button_draw(textures, font, self._typo_button, x=button_x, y=button_y, width=typo_w, scale=scale)
1563
+ button_y += END_NOTE_BUTTON_STEP_Y * scale
1564
+ main_w = button_width(font, self._main_menu_button.label, scale=scale, force_wide=self._main_menu_button.force_wide)
1565
+ button_draw(textures, font, self._main_menu_button, x=button_x, y=button_y, width=main_w, scale=scale)
1566
+
1567
+ _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
1568
+
1569
+ def take_action(self) -> str | None:
1570
+ action = self._action
1571
+ self._action = None
1572
+ return action
1573
+
1574
+ def _ensure_small_font(self) -> SmallFontData:
1575
+ if self._small_font is not None:
1576
+ return self._small_font
1577
+ missing_assets: list[str] = []
1578
+ self._small_font = load_small_font(self._state.assets_dir, missing_assets)
1579
+ return self._small_font
1580
+
1581
+
1339
1582
  class QuestFailedView:
1340
1583
  def __init__(self, state: GameState) -> None:
1341
1584
  self._state = state
@@ -1944,6 +2187,7 @@ class GameLoopView:
1944
2187
  "start_quest": QuestGameView(state),
1945
2188
  "quest_results": QuestResultsView(state),
1946
2189
  "quest_failed": QuestFailedView(state),
2190
+ "end_note": EndNoteView(state),
1947
2191
  "open_high_scores": HighScoresView(state),
1948
2192
  "start_survival": SurvivalGameView(state),
1949
2193
  "start_rush": RushGameView(state),
@@ -2474,6 +2718,8 @@ def _resolve_assets_dir(config: GameConfig) -> Path:
2474
2718
 
2475
2719
 
2476
2720
  def run_game(config: GameConfig) -> None:
2721
+ if config.debug:
2722
+ set_debug_enabled(True)
2477
2723
  base_dir = config.base_dir
2478
2724
  base_dir.mkdir(parents=True, exist_ok=True)
2479
2725
  crash_path = base_dir / "crash.log"
crimson/gameplay.py CHANGED
@@ -534,6 +534,7 @@ class GameplayState:
534
534
  weapon_available: list[bool] = field(default_factory=lambda: [False] * WEAPON_COUNT_SIZE)
535
535
  _weapon_available_game_mode: int = -1
536
536
  _weapon_available_unlock_index: int = -1
537
+ _weapon_available_unlock_index_full: int = -1
537
538
  friendly_fire_enabled: bool = False
538
539
  bonus_spawn_guard: bool = False
539
540
  bonus_hud: BonusHudState = field(default_factory=BonusHudState)
@@ -547,6 +548,7 @@ class GameplayState:
547
548
  shots_fired: list[int] = field(default_factory=lambda: [0] * 4)
548
549
  shots_hit: list[int] = field(default_factory=lambda: [0] * 4)
549
550
  weapon_shots_fired: list[list[int]] = field(default_factory=lambda: [[0] * WEAPON_COUNT_SIZE for _ in range(4)])
551
+ debug_god_mode: bool = False
550
552
 
551
553
  def __post_init__(self) -> None:
552
554
  rand = self.rng.rand
@@ -1236,17 +1238,23 @@ def weapon_refresh_available(state: "GameplayState") -> None:
1236
1238
  """
1237
1239
 
1238
1240
  unlock_index = 0
1241
+ unlock_index_full = 0
1239
1242
  status = state.status
1240
1243
  if status is not None:
1241
1244
  try:
1242
1245
  unlock_index = int(status.quest_unlock_index)
1243
1246
  except Exception:
1244
1247
  unlock_index = 0
1248
+ try:
1249
+ unlock_index_full = int(status.quest_unlock_index_full)
1250
+ except Exception:
1251
+ unlock_index_full = 0
1245
1252
 
1246
1253
  game_mode = int(state.game_mode)
1247
1254
  if (
1248
1255
  int(state._weapon_available_game_mode) == game_mode
1249
1256
  and int(state._weapon_available_unlock_index) == unlock_index
1257
+ and int(state._weapon_available_unlock_index_full) == unlock_index_full
1250
1258
  ):
1251
1259
  return
1252
1260
 
@@ -1281,8 +1289,16 @@ def weapon_refresh_available(state: "GameplayState") -> None:
1281
1289
  if 0 <= idx < len(available):
1282
1290
  available[idx] = True
1283
1291
 
1292
+ # Secret unlock: Splitter Gun (weapon id 29) becomes available once the hardcore
1293
+ # unlock track reaches stage 5 (quest_unlock_index_full >= 40).
1294
+ if (not state.demo_mode_active) and unlock_index_full >= 0x28:
1295
+ splitter_id = int(WeaponId.SPLITTER_GUN)
1296
+ if 0 <= splitter_id < len(available):
1297
+ available[splitter_id] = True
1298
+
1284
1299
  state._weapon_available_game_mode = game_mode
1285
1300
  state._weapon_available_unlock_index = unlock_index
1301
+ state._weapon_available_unlock_index_full = unlock_index_full
1286
1302
 
1287
1303
 
1288
1304
  def weapon_pick_random_available(state: "GameplayState") -> int:
@@ -12,6 +12,7 @@ from grim.config import CrimsonConfig
12
12
  from grim.fonts.grim_mono import GrimMonoFont, load_grim_mono_font
13
13
  from grim.view import ViewContext
14
14
 
15
+ from ..debug import debug_enabled
15
16
  from ..game_modes import GameMode
16
17
  from ..gameplay import most_used_weapon_id_for_player, perk_selection_current_choices, perk_selection_pick, weapon_assign_player
17
18
  from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
@@ -44,6 +45,7 @@ from ..ui.perk_menu import (
44
45
  wrap_ui_text,
45
46
  )
46
47
  from ..views.quest_title_overlay import draw_quest_title_overlay
48
+ from ..weapons import WEAPON_BY_ID
47
49
  from .base_gameplay_mode import BaseGameplayMode, _clamp
48
50
 
49
51
  WORLD_SIZE = 1024.0
@@ -78,6 +80,8 @@ PERK_PROMPT_LEVEL_UP_SHIFT_X = -46.0
78
80
  PERK_PROMPT_LEVEL_UP_SHIFT_Y = -4.0
79
81
 
80
82
  PERK_PROMPT_TEXT_MARGIN_X = 16.0
83
+
84
+ _DEBUG_WEAPON_IDS = tuple(sorted(WEAPON_BY_ID))
81
85
  PERK_PROMPT_TEXT_OFFSET_Y = 8.0
82
86
 
83
87
 
@@ -305,10 +309,35 @@ class QuestMode(BaseGameplayMode):
305
309
  if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
306
310
  self._paused = not self._paused
307
311
 
312
+ if debug_enabled() and (not self._perk_menu_open):
313
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F2):
314
+ self._state.debug_god_mode = not bool(self._state.debug_god_mode)
315
+ self._world.audio_router.play_sfx("sfx_ui_buttonclick")
316
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F3):
317
+ self._state.perk_selection.pending_count += 1
318
+ self._state.perk_selection.choices_dirty = True
319
+ self._world.audio_router.play_sfx("sfx_ui_levelup")
320
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
321
+ self._debug_cycle_weapon(-1)
322
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
323
+ self._debug_cycle_weapon(1)
324
+
308
325
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
309
326
  self._action = "open_pause_menu"
310
327
  return
311
328
 
329
+ def _debug_cycle_weapon(self, delta: int) -> None:
330
+ weapon_ids = _DEBUG_WEAPON_IDS
331
+ if not weapon_ids:
332
+ return
333
+ current = int(self._player.weapon_id)
334
+ try:
335
+ idx = weapon_ids.index(current)
336
+ except ValueError:
337
+ idx = 0
338
+ weapon_id = int(weapon_ids[(idx + int(delta)) % len(weapon_ids)])
339
+ weapon_assign_player(self._player, weapon_id, state=self._state)
340
+
312
341
  def _build_input(self):
313
342
  keybinds = config_keybinds(self._config)
314
343
  if not keybinds:
@@ -875,6 +904,12 @@ class QuestMode(BaseGameplayMode):
875
904
  small_indicators=self._hud_small_indicators(),
876
905
  )
877
906
 
907
+ if debug_enabled() and (not perk_menu_active):
908
+ x = 18.0
909
+ y = max(18.0, hud_bottom + 10.0)
910
+ god = "on" if self._state.debug_god_mode else "off"
911
+ self._draw_ui_text(f"debug: [/] weapon F3 perk+1 F2 god={god}", x, y, UI_HINT_COLOR, scale=0.9)
912
+
878
913
  self._draw_quest_title()
879
914
 
880
915
  warn_y = float(rl.get_screen_height()) - 28.0
@@ -895,6 +930,8 @@ class QuestMode(BaseGameplayMode):
895
930
  self._draw_game_cursor()
896
931
  x = 18.0
897
932
  y = max(18.0, hud_bottom + 10.0)
933
+ if debug_enabled() and (not perk_menu_active):
934
+ y += float(self._ui_line_height(scale=0.9))
898
935
  self._draw_ui_text("paused (TAB)", x, y, UI_HINT_COLOR)
899
936
  else:
900
937
  self._draw_aim_cursor()
@@ -14,7 +14,14 @@ from grim.view import ViewContext
14
14
  from ..creatures.spawn import advance_survival_spawn_stage, tick_survival_wave_spawns
15
15
  from ..debug import debug_enabled
16
16
  from ..game_modes import GameMode
17
- from ..gameplay import PlayerInput, most_used_weapon_id_for_player, perk_selection_current_choices, perk_selection_pick, survival_check_level_up
17
+ from ..gameplay import (
18
+ PlayerInput,
19
+ most_used_weapon_id_for_player,
20
+ perk_selection_current_choices,
21
+ perk_selection_pick,
22
+ survival_check_level_up,
23
+ weapon_assign_player,
24
+ )
18
25
  from ..persistence.highscores import HighScoreRecord
19
26
  from ..perks import PerkId, perk_display_description, perk_display_name
20
27
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
@@ -38,6 +45,7 @@ from ..ui.perk_menu import (
38
45
  ui_scale,
39
46
  wrap_ui_text,
40
47
  )
48
+ from ..weapons import WEAPON_BY_ID
41
49
  from .base_gameplay_mode import BaseGameplayMode, _clamp
42
50
 
43
51
  WORLD_SIZE = 1024.0
@@ -71,6 +79,8 @@ PERK_PROMPT_LEVEL_UP_SHIFT_Y = -4.0
71
79
  PERK_PROMPT_TEXT_MARGIN_X = 16.0
72
80
  PERK_PROMPT_TEXT_OFFSET_Y = 8.0
73
81
 
82
+ _DEBUG_WEAPON_IDS = tuple(sorted(WEAPON_BY_ID))
83
+
74
84
 
75
85
  @dataclass(slots=True)
76
86
  class _SurvivalState:
@@ -181,14 +191,38 @@ class SurvivalMode(BaseGameplayMode):
181
191
  if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
182
192
  self._paused = not self._paused
183
193
 
184
- if debug_enabled() and rl.is_key_pressed(rl.KeyboardKey.KEY_X):
185
- self._player.experience += 5000
186
- survival_check_level_up(self._player, self._state.perk_selection)
194
+ if debug_enabled() and (not self._perk_menu_open):
195
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F2):
196
+ self._state.debug_god_mode = not bool(self._state.debug_god_mode)
197
+ self._world.audio_router.play_sfx("sfx_ui_buttonclick")
198
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F3):
199
+ self._state.perk_selection.pending_count += 1
200
+ self._state.perk_selection.choices_dirty = True
201
+ self._world.audio_router.play_sfx("sfx_ui_levelup")
202
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
203
+ self._debug_cycle_weapon(-1)
204
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
205
+ self._debug_cycle_weapon(1)
206
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_X):
207
+ self._player.experience += 5000
208
+ survival_check_level_up(self._player, self._state.perk_selection)
187
209
 
188
210
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
189
211
  self._action = "open_pause_menu"
190
212
  return
191
213
 
214
+ def _debug_cycle_weapon(self, delta: int) -> None:
215
+ weapon_ids = _DEBUG_WEAPON_IDS
216
+ if not weapon_ids:
217
+ return
218
+ current = int(self._player.weapon_id)
219
+ try:
220
+ idx = weapon_ids.index(current)
221
+ except ValueError:
222
+ idx = 0
223
+ weapon_id = int(weapon_ids[(idx + int(delta)) % len(weapon_ids)])
224
+ weapon_assign_player(self._player, weapon_id, state=self._state)
225
+
192
226
  def _build_input(self) -> PlayerInput:
193
227
  keybinds = config_keybinds(self._config)
194
228
  if not keybinds:
@@ -779,10 +813,12 @@ class SurvivalMode(BaseGameplayMode):
779
813
  line = float(self._ui_line_height())
780
814
  self._draw_ui_text(f"survival: t={self._survival.elapsed_ms/1000.0:6.1f}s stage={self._survival.stage}", x, y, UI_TEXT_COLOR)
781
815
  self._draw_ui_text(f"xp={self._player.experience} level={self._player.level} kills={self._creatures.kill_count}", x, y + line, UI_HINT_COLOR)
816
+ god = "on" if self._state.debug_god_mode else "off"
817
+ self._draw_ui_text(f"debug: [/] weapon F3 perk+1 F2 god={god} X xp+5000", x, y + line * 2.0, UI_HINT_COLOR, scale=0.9)
782
818
  if self._paused:
783
- self._draw_ui_text("paused (TAB)", x, y + line * 2.0, UI_HINT_COLOR)
819
+ self._draw_ui_text("paused (TAB)", x, y + line * 3.0, UI_HINT_COLOR)
784
820
  if self._player.health <= 0.0:
785
- self._draw_ui_text("game over", x, y + line * 2.0, UI_ERROR_COLOR)
821
+ self._draw_ui_text("game over", x, y + line * 3.0, UI_ERROR_COLOR)
786
822
  warn_y = float(rl.get_screen_height()) - 28.0
787
823
  if self._world.missing_assets:
788
824
  warn = "Missing world assets: " + ", ".join(self._world.missing_assets)
crimson/player_damage.py CHANGED
@@ -32,6 +32,8 @@ def player_take_damage(
32
32
  dmg = float(damage)
33
33
  if dmg <= 0.0:
34
34
  return 0.0
35
+ if state.debug_god_mode:
36
+ return 0.0
35
37
 
36
38
  # 1) Death Clock immunity.
37
39
  if perk_active(player, PerkId.DEATH_CLOCK):
@@ -73,6 +73,14 @@ QUEST_RESULTS_PANEL_H = 378.0
73
73
  TEXTURE_TOP_BANNER_W = 256.0
74
74
  TEXTURE_TOP_BANNER_H = 64.0
75
75
 
76
+ # `quest_results_screen_update` uses the classic UI element sums for positioning:
77
+ # content_x = (pos_x + offset_x + slide_x) + 180.0 + 40.0
78
+ # banner_x = content_x - 18.0
79
+ # score_x = content_x + 30.0
80
+ QUEST_RESULTS_CONTENT_X = 220.0
81
+ QUEST_RESULTS_BANNER_X_FROM_CONTENT = -18.0
82
+ QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT = 30.0
83
+
76
84
  INPUT_BOX_W = 166.0
77
85
  INPUT_BOX_H = 18.0
78
86
 
@@ -399,7 +407,7 @@ class QuestResultsUi:
399
407
  screen_h = float(rl.get_screen_height())
400
408
  scale = ui_scale(screen_w, screen_h)
401
409
  _panel, panel_left, panel_top = self._panel_layout(screen_w=screen_w, scale=scale)
402
- anchor_x = panel_left + 40.0 * scale
410
+ anchor_x = panel_left + QUEST_RESULTS_CONTENT_X * scale
403
411
  input_y = panel_top + 150.0 * scale
404
412
  ok_x = anchor_x + 170.0 * scale
405
413
  ok_y = input_y - 8.0 * scale
@@ -451,7 +459,8 @@ class QuestResultsUi:
451
459
  _origin_x, _origin_y = ui_origin(screen_w, screen_h, scale)
452
460
  _panel, left, top = self._panel_layout(screen_w=screen_w, scale=scale)
453
461
  qualifies = int(self.rank) < TABLE_MAX
454
- score_card_x = left + 70.0 * scale
462
+ content_x = left + QUEST_RESULTS_CONTENT_X * scale
463
+ score_card_x = content_x + QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT * scale
455
464
 
456
465
  var_c_12 = top + (96.0 if qualifies else 108.0) * scale
457
466
  var_c_14 = var_c_12 + 84.0 * scale
@@ -515,7 +524,8 @@ class QuestResultsUi:
515
524
  fx_detail = bool(int(self.config.data.get("fx_detail_0", 0) or 0))
516
525
  draw_classic_menu_panel(self.assets.menu_panel, dst=panel, tint=rl.WHITE, shadow=fx_detail)
517
526
 
518
- banner_x = left + 22.0 * scale
527
+ content_x = left + QUEST_RESULTS_CONTENT_X * scale
528
+ banner_x = content_x + QUEST_RESULTS_BANNER_X_FROM_CONTENT * scale
519
529
  banner_y = top + 36.0 * scale
520
530
  if self.assets.text_well_done is not None:
521
531
  src = rl.Rectangle(0.0, 0.0, float(self.assets.text_well_done.width), float(self.assets.text_well_done.height))
@@ -525,7 +535,7 @@ class QuestResultsUi:
525
535
  qualifies = int(self.rank) < TABLE_MAX
526
536
 
527
537
  if self.phase == 0:
528
- anchor_x = left + 40.0 * scale
538
+ anchor_x = content_x
529
539
  label_x = anchor_x + 32.0 * scale
530
540
  value_x = label_x + 132.0 * scale
531
541
 
@@ -587,7 +597,7 @@ class QuestResultsUi:
587
597
  self._draw_small(final_value, value_x, y, 1.0 * scale, _row_color(3, final=True))
588
598
 
589
599
  elif self.phase == 1:
590
- anchor_x = left + 40.0 * scale
600
+ anchor_x = content_x
591
601
  text_y = top + 118.0 * scale
592
602
  self._draw_small("State your name trooper!", anchor_x + 42.0 * scale, text_y, 1.0 * scale, COLOR_TEXT)
593
603
 
@@ -615,7 +625,7 @@ class QuestResultsUi:
615
625
  button_draw(self.assets.perk_menu_assets, self.font, self._ok_button, x=ok_x, y=ok_y, width=ok_w, scale=scale)
616
626
 
617
627
  else:
618
- score_card_x = left + 70.0 * scale
628
+ score_card_x = content_x + QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT * scale
619
629
  var_c_12 = top + (96.0 if qualifies else 108.0) * scale
620
630
  if (not qualifies) and self.font is not None:
621
631
  self._draw_small(
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.3
2
+ Name: crimsonland
3
+ Version: 0.1.0.dev15
4
+ Requires-Dist: construct>=2.10.70
5
+ Requires-Dist: pillow>=12.1.0
6
+ Requires-Dist: platformdirs>=4.5.1
7
+ Requires-Dist: raylib>=5.5.0.4
8
+ Requires-Dist: typer>=0.21.1
9
+ Requires-Python: >=3.13
10
+ Project-URL: Documentation, https://crimson.banteg.xyz/
11
+ Project-URL: Repository, https://github.com/banteg/crimson
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Crimsonland 1.9.93 decompilation + rewrite
15
+
16
+ This repository is a **reverse engineering + high‑fidelity reimplementation** of **Crimsonland 1.9.93 (2003)**.
17
+
18
+ - **Target build:** `v1.9.93` (GOG "Crimsonland Classic") — see [docs/provenance.md](docs/provenance.md) for exact hashes.
19
+ - **Rewrite:** a runnable reference implementation in **Python + raylib** under `src/`.
20
+ - **Analysis:** decompiles, name/type maps, and runtime evidence under `analysis/`.
21
+ - **Docs:** long-form notes and parity tracking under `docs/` (start at [docs/index.md](docs/index.md)).
22
+
23
+ The north star is **behavioral parity** with the original Windows build: timings, RNG, UI/layout quirks, asset decoding, and gameplay rules should match as closely as practical.
24
+
25
+ **[Read the full story](https://banteg.xyz/posts/crimsonland/)** of how this project came together: reverse engineering workflow, custom asset formats, AI-assisted decompilation, and game preservation philosophy.
26
+
27
+ ---
28
+
29
+ ## Quick start
30
+
31
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager.
32
+
33
+ ### Run the latest packaged build
34
+
35
+ If you just want to play the rewrite:
36
+
37
+ ```bash
38
+ uvx crimsonland@latest
39
+ ```
40
+
41
+ ### Run from a checkout
42
+
43
+ ```bash
44
+ gh repo clone banteg/crimson
45
+ cd crimson
46
+ uv run crimson
47
+ ```
48
+
49
+ ### Keep runtime files local to the repo
50
+
51
+ By default, runtime files (e.g. `crimson.cfg`, `game.cfg`, highscores, logs, downloaded PAQs) live in your per-user data dir.
52
+ To keep everything under this checkout:
53
+
54
+ ```bash
55
+ export CRIMSON_RUNTIME_DIR="$PWD/artifacts/runtime"
56
+ mkdir -p artifacts/runtime
57
+ uv run crimson
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Assets + binaries
63
+
64
+ There are two separate “inputs” to this repo:
65
+
66
+ 1. **Runtime assets for the rewrite** (PAQ archives)
67
+ 2. **Original Windows binaries for reverse engineering** (`crimsonland.exe`, `grim.dll`, …)
68
+
69
+ We keep them out of git and expect a local layout like:
70
+
71
+ ```text
72
+ game_bins/
73
+ crimsonland/
74
+ 1.9.93-gog/
75
+ crimsonland.exe
76
+ grim.dll
77
+ crimson.paq
78
+ music.paq
79
+ sfx.paq
80
+ artifacts/
81
+ runtime/ # optional: where you run the rewrite (cfg/status/paqs)
82
+ assets/ # optional: extracted PAQs for inspection/tools
83
+ ```
84
+
85
+ ### Running the rewrite
86
+
87
+ The rewrite loads the assets from original archives:
88
+
89
+ - `crimson.paq`
90
+ - `music.paq`
91
+ - `sfx.paq`
92
+
93
+ ### Extracted assets
94
+
95
+ For inspection/diffs/tools, you can extract PAQs into a filesystem tree:
96
+
97
+ ```bash
98
+ uv run crimson extract crimsonland_1.9.93 artifacts/assets
99
+ ```
100
+
101
+ Same as the original, many loaders can work from either:
102
+
103
+ - **PAQ-backed assets** (preferred when available), or
104
+ - the **extracted filesystem layout** under `artifacts/assets/`.
105
+
106
+ ---
107
+
108
+ ## CLI cheat sheet
109
+
110
+ Everything is exposed via the `crimson` CLI (alias: `crimsonland`):
111
+
112
+ ```bash
113
+ uv run crimson # run the game (default command)
114
+ uv run crimson view ui # debug views / sandboxes
115
+ uv run crimson quests 1.1 # print quest spawn script
116
+ uv run crimson config # inspect crimson.cfg
117
+ uv run crimson extract <game_dir> artifacts/assets
118
+ ```
119
+
120
+ Useful flags:
121
+
122
+ - `--base-dir PATH` / `CRIMSON_RUNTIME_DIR=...` — where saves/config/logs live
123
+ - `--assets-dir PATH` — where `.paq` archives (or extracted assets) are loaded from
124
+ - `--seed N` — deterministic runs for parity testing
125
+ - `--demo` — enable shareware/demo paths
126
+ - `--no-intro` — skip logos/intro music
127
+
128
+ ---
129
+
130
+ ## Docs
131
+
132
+ Docs are authored in `docs/` and built as a static site at https://crimson.banteg.xyz/
133
+
134
+ For development, it's useful to have a live local build:
135
+
136
+ ```
137
+ uv tool install zensical
138
+ zensical serve
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Development
144
+
145
+ ### Tests
146
+
147
+ ```bash
148
+ uv run pytest
149
+ ```
150
+
151
+ ### Lint / checks
152
+
153
+ ```bash
154
+ uv run lint-imports
155
+ uv run python scripts/check_asset_loader_usage.py
156
+ ```
157
+
158
+ ### `justfile` shortcuts
159
+
160
+ If you have `just` installed:
161
+
162
+ ```bash
163
+ just --list
164
+ just test
165
+ just docs-build
166
+ just ghidra-exe
167
+ just ghidra-grim
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Reverse engineering workflow
173
+
174
+ High level:
175
+
176
+ - **Static analysis is the source of truth.**
177
+ - Update names/types in [analysis/ghidra/maps/](analysis/ghidra/maps/).
178
+ - Treat [analysis/ghidra/raw/](analysis/ghidra/raw/) as generated output (regenerate; do not hand-edit).
179
+ - **Runtime tooling** (Frida / WinDbg) validates ambiguous behavior and captures ground truth.
180
+ - Evidence summaries live under [analysis/frida/](analysis/frida/).
181
+
182
+ ---
183
+
184
+ ## Contributing notes
185
+
186
+ - Keep changes small and reviewable (one subsystem/feature at a time).
187
+ - Prefer *measured parity* (captures/logs/deterministic tests) over “looks right”.
188
+ - When porting float constants from decompilation, prefer the intended value
189
+ (e.g. `0.6` instead of `0.6000000238418579` when it’s clearly a float32 artifact).
190
+
191
+ ---
192
+
193
+ ## Legal
194
+
195
+ This project is an independent reverse engineering and reimplementation effort for preservation, research, and compatibility.
196
+
197
+ No original assets or binaries are included. Use your own legally obtained copy.
@@ -1,17 +1,19 @@
1
+ crimson/.DS_Store,sha256=wzeuDdLmWdcG-N9ol7JzTcv0_ctH28Aaeq1C1fe8ZYw,6148
1
2
  crimson/__init__.py,sha256=dij6OQ6Wctqur9P00ojTTZw6IaUNeZagPX4-2Qqr-Kw,367
2
3
  crimson/assets_fetch.py,sha256=ScrxzSkbs0TkaUtIi-WLC4NN1nIL9J8RloSH_Uz4HGo,2356
3
4
  crimson/atlas.py,sha256=hEcCHhPvguXAI6eH_G9Q8rpiX7M5akZ8fgJjMogmYrA,2401
4
5
  crimson/audio_router.py,sha256=4lccGu5044WQ5sMz9yfZd4loSgEMDqXJWGvMmHyMGt0,5449
5
6
  crimson/bonuses.py,sha256=owwYIRHRu1Kymtt4eEvpd62JwWAg8LOe20vDuPFB5SU,5094
6
7
  crimson/camera.py,sha256=VxTNXTh0qHh5VR1OpkiXr-QcgEWPrWw0A3PFyQFqDkY,2278
7
- crimson/cli.py,sha256=Pu4RM_bjGtUgIE_5zZs0uWFY9O8YkhJDseRcVMMPJ8k,14595
8
+ crimson/cli.py,sha256=3OvfDrVJyeiL0hjEYRAxfy2u4Yle36xNAm8ZkbvnkLo,14707
9
+ crimson/creatures/.DS_Store,sha256=sAyNySnK01L93nKof3NyiBn3DTcel6MPhjblBP1p1Uo,6148
8
10
  crimson/creatures/__init__.py,sha256=RJwwR_w98zC_kHF9CwkhzSlf0xjpiP0JU87W4awdAAM,233
9
11
  crimson/creatures/ai.py,sha256=oCuUkDe6Kxya1dThUthEy8GIuObAUjDv8WPfe17HYV4,6353
10
12
  crimson/creatures/anim.py,sha256=k_jEpyfwFcJSC9jY2xuhuxtQ8KvE6fSeuRSBLTTAxWo,5016
11
13
  crimson/creatures/damage.py,sha256=pjKIX32nGDVPnFaWCce0LNZ-UZXZqJpNvwHwq-DCWbE,3211
12
14
  crimson/creatures/runtime.py,sha256=wfSicwqkSovM_mn_7AMQwxWY_-pY1u6Qa1E_qmQ8c94,41490
13
15
  crimson/creatures/spawn.py,sha256=ikgtr4sM2KdA2-eyxYdVmobcuZN6aA7xK7ceaIl7RSw,90059
14
- crimson/debug.py,sha256=vtfr0_HQHpiB5h57jAsl9cWyYxErSbZQ2uazcL1sJhU,127
16
+ crimson/debug.py,sha256=Hcg0I4oHCtbwDMacnhCnY9gRmueorVctAOpuVgFmwcQ,348
15
17
  crimson/demo.py,sha256=5sjQiUGGohm7t0s69-My_S9HwG84T3jDCl682UsffOE,55053
16
18
  crimson/demo_trial.py,sha256=BuoKB1DB-cPZ8jBp4x9gmxWF6tvhXMYRqJ9hMYrhxH4,4651
17
19
  crimson/effects.py,sha256=56YajT1g3MF1UOly1nsjbT0wkKusjAeyalT3eXFBQIE,35384
@@ -32,16 +34,16 @@ crimson/frontend/panels/play_game.py,sha256=BiO8oDhb6YQZqs3nZEJqUvajMeC6VRQ577do
32
34
  crimson/frontend/panels/stats.py,sha256=1-q9TezFVPADkTmPSAg_c_drkrlMOY3jYHjtAm3XGNE,12573
33
35
  crimson/frontend/pause_menu.py,sha256=iM08blMUbgFnV-aGpTHxwieqSQzCnsS66WFMT3lr5vI,16365
34
36
  crimson/frontend/transitions.py,sha256=-sAJUDqNZ943zXlqtvJ6jCg2YH8dSi8k7qK8caAfOKI,883
35
- crimson/game.py,sha256=j8RI1-r67P81wV4_Wy9pKUtqHh8IZTiE-q3yH972z0s,99035
37
+ crimson/game.py,sha256=juwTTwSBB2xBhFprsGpqOYrVSnIkULsOucNOAPwfpIk,110800
36
38
  crimson/game_modes.py,sha256=qW7Tt97lSBmGrt0F17Ni5h8vRyngBzyS9XwWM1TFIEI,255
37
39
  crimson/game_world.py,sha256=nfKGcm3LHChPGLHJsurDFAATrHmhRvTmgxcLzUN9m5I,25440
38
- crimson/gameplay.py,sha256=UHsoxVj6jcdClVLdLM1q1o4NhwSTQqpHSQQ6GBPlT-s,90848
40
+ crimson/gameplay.py,sha256=cghfd7Z3xYDOdI06de6qMPRLlPMVN3wOw2B6wf-cRaE,91602
39
41
  crimson/input_codes.py,sha256=PmSWFZIit8unTBWk3uwifpHWMuk0qMg1ueKX3MGC7D0,5379
40
42
  crimson/modes/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
41
43
  crimson/modes/base_gameplay_mode.py,sha256=0hAtdpXLYuhP304qC1GvbQ2rBAALpJTgScDgOs_nRwU,10190
42
- crimson/modes/quest_mode.py,sha256=DUUH5pfACNmFbcT4F0_yhtyuyOZqx-rizKNx_sL3HDE,38322
44
+ crimson/modes/quest_mode.py,sha256=90SPy0Q4TR8NHZaWfAK0PiAibuIrWwqYSxgvvqAlzv8,40039
43
45
  crimson/modes/rush_mode.py,sha256=l4TAvjOvOA-Z-liMDXp_HGfszfghTmJUyVSkiPkfHQ0,11527
44
- crimson/modes/survival_mode.py,sha256=ZIPjKf2plu_9CDrJp_WLRIGwUCmDo9_imDqa6h7zLD0,33612
46
+ crimson/modes/survival_mode.py,sha256=LmCwkLxCoMPraeywNxF7h14eo9k4mr1awGWDyrbJO_k,35120
45
47
  crimson/modes/tutorial_mode.py,sha256=rWrVSCjM8G-fHn1_0iVWIzGTeLnCFSNktuoRNSX3cg4,27634
46
48
  crimson/modes/typo_mode.py,sha256=G3zpg2J7WafXpIYsVV1LeO1gjCCmb40_cbxa_m2kx0U,17957
47
49
  crimson/paths.py,sha256=JCKauCxHJnP4IIy5ocgs6tvbaSMkMUByQ76Zoz2UGZM,653
@@ -49,7 +51,7 @@ crimson/perks.py,sha256=JHNhKPOUq2w8h23gU7jpkx-erFKTO4Z8l8aHU8vrkO0,37068
49
51
  crimson/persistence/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
50
52
  crimson/persistence/highscores.py,sha256=FyWXTU8faNxYshkbn7Cf3lrxUxiI1VcgGZOq9U9dT8g,13114
51
53
  crimson/persistence/save_status.py,sha256=s-FZONO6JVYRULewtlxoIwATTy3P7rLy-vsVBfSKPk4,7748
52
- crimson/player_damage.py,sha256=cECZO65NJwARoYdtsbvZBM8vQNs_8AVO4xa0omX0pGg,2215
54
+ crimson/player_damage.py,sha256=UyGs6SjJBxB6zECr78jUvI7LlGRg0E4kOBESSLBcyaY,2263
53
55
  crimson/projectiles.py,sha256=56yocC2XYC69bixrmAgHud4YiSIgezMf2blCQw_YekM,51304
54
56
  crimson/quests/__init__.py,sha256=pKCkH0o1XIGDN9h6yNNaqvWek2CybEZRpejYRooULj8,375
55
57
  crimson/quests/helpers.py,sha256=TeZWhIy2x8Zs9S-vYRBOEZ7pkHNMr5YJ-wi4vYQ2q2M,3625
@@ -84,7 +86,7 @@ crimson/ui/game_over.py,sha256=pkHDGE47yBraHY-BTnZzOFdRGUJ4ravF0cL1HBdA300,30267
84
86
  crimson/ui/hud.py,sha256=sVJhh7iAmDISMAcB2AALw1tM0828mlddpAFUXJm8E8c,31123
85
87
  crimson/ui/menu_panel.py,sha256=ko1NErvQ5Ystq4f-AL0_eB6FaWoT_n3uonX-X1Ya_6o,4578
86
88
  crimson/ui/perk_menu.py,sha256=Idz7Ps8JzhBqUZQQH-n1dS_DviX5LDWRrhWFO0Qc9Ac,13787
87
- crimson/ui/quest_results.py,sha256=lbd9uDNyCSNy-KZlNmrCv_S4-JVGioK3DLN65j63PWA,29152
89
+ crimson/ui/quest_results.py,sha256=PLq5XD6hAdQf3qpHWZe2OhiIv7TdC38qr8alPasq4LM,29721
88
90
  crimson/ui/shadow.py,sha256=P6B8lZCu8W0YPhLcqi8H413D5oSNeJYwnroYAw4ZGYI,1090
89
91
  crimson/views/__init__.py,sha256=1UWCzBSsgIeOtcKxqM4Vf12JEfPHKfb9KRZgl2TB84Q,1880
90
92
  crimson/views/aim_debug.py,sha256=2ldQ1MJO8AeG0i9x4ZJ3YLyp1X-HY_nJ0E1mY8jFF7Q,10873
@@ -121,6 +123,7 @@ crimson/views/ui.py,sha256=2gGyCiI_JnlHzG9CWH8nomQIgCthfLwP7bTsDyqAyx4,4294
121
123
  crimson/views/wicons.py,sha256=bGbmE25q_pYJZTiPt6Q5Sy1Ub667KvUIFp1a5rVxhi0,5783
122
124
  crimson/weapon_sfx.py,sha256=_JAFpXi-c7jFNz3hxKygAYwqAR6zC94QnNHQYDCHnOY,1717
123
125
  crimson/weapons.py,sha256=dfKiVrLGw6EaUo1K0-twDowA4S7tFoPy_sRTENrWJGI,22084
126
+ grim/.DS_Store,sha256=QLue9_bBPqaUhApqMqZyfVv_whyhZNPO1nSyW8sjcpo,6148
124
127
  grim/__init__.py,sha256=Cn4f7JY6bscS1gwjDPaWvIvj_vneuUXaiu7wdJB6EVg,262
125
128
  grim/app.py,sha256=WQfJLaCn7-NT1SFQs7Fwm7EgYySiVZkJsf_NPDoiR0o,2798
126
129
  grim/assets.py,sha256=k2rFqNYfcUZfRjFL7r_UBGf1RlcP-urChXCUJ6GzEsI,7661
@@ -140,7 +143,7 @@ grim/sfx.py,sha256=cpn2Mmeio7BSDgbStSft-eZchO9Ot2MrK6iXJqxlLqU,7836
140
143
  grim/sfx_map.py,sha256=FM5iBzKkG30Vtu78SRavVNgXMbGK7ZFcQ8i6lgMlzVw,4697
141
144
  grim/terrain_render.py,sha256=EZ7ySYJyTZwXcrJx1mKbY3ewZtPi7Y270XnZgGJyZG8,31509
142
145
  grim/view.py,sha256=oF4pHZehBqOxPjKMU28TDg3qATh_amMIRJp-vMQnpn4,334
143
- crimsonland-0.1.0.dev13.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
144
- crimsonland-0.1.0.dev13.dist-info/entry_points.txt,sha256=jzzcExxiE9xpt4Iw2nbB1lwTv2Zj4H14WJTIPMkAjoE,77
145
- crimsonland-0.1.0.dev13.dist-info/METADATA,sha256=zmr0z7DJR6Z72vu_5i0IQbCO1qJ-sDd25SGmghwEtWA,244
146
- crimsonland-0.1.0.dev13.dist-info/RECORD,,
146
+ crimsonland-0.1.0.dev15.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
147
+ crimsonland-0.1.0.dev15.dist-info/entry_points.txt,sha256=jzzcExxiE9xpt4Iw2nbB1lwTv2Zj4H14WJTIPMkAjoE,77
148
+ crimsonland-0.1.0.dev15.dist-info/METADATA,sha256=iyGs5qMssJ8szJDMfYv0jRbZnycT3rFPOTs_lI9B7nU,5262
149
+ crimsonland-0.1.0.dev15.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.28
2
+ Generator: uv 0.9.18
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any
grim/.DS_Store ADDED
Binary file
@@ -1,9 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: crimsonland
3
- Version: 0.1.0.dev13
4
- Requires-Dist: construct>=2.10.70
5
- Requires-Dist: pillow>=12.1.0
6
- Requires-Dist: platformdirs>=4.5.1
7
- Requires-Dist: raylib>=5.5.0.4
8
- Requires-Dist: typer>=0.21.1
9
- Requires-Python: >=3.13