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
grim/fonts/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = [
4
+ "grim_mono",
5
+ "small",
6
+ ]
7
+
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ import pyray as rl
7
+
8
+ from grim.assets import PaqTextureCache, find_paq_path, load_paq_entries_from_path
9
+
10
+ GRIM_MONO_ADVANCE = 16.0
11
+ GRIM_MONO_DRAW_SIZE = 32.0
12
+ GRIM_MONO_LINE_HEIGHT = 28.0
13
+ GRIM_MONO_TEXTURE_FILTER = rl.TEXTURE_FILTER_BILINEAR
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class GrimMonoFont:
18
+ texture: rl.Texture
19
+ grid: int = 16
20
+ cell_width: float = 16.0
21
+ cell_height: float = 16.0
22
+ advance: float = GRIM_MONO_ADVANCE
23
+
24
+
25
+ def load_grim_mono_font(assets_root: Path, missing_assets: list[str]) -> GrimMonoFont:
26
+ # Prefer crimson.paq (runtime source-of-truth), but fall back to extracted
27
+ # assets when present for development convenience.
28
+ paq_path = find_paq_path(assets_root)
29
+
30
+ atlas_png = assets_root / "crimson" / "load" / "default_font_courier.png"
31
+ atlas_tga = assets_root / "crimson" / "load" / "default_font_courier.tga"
32
+
33
+ texture: rl.Texture | None = None
34
+ if paq_path is not None:
35
+ try:
36
+ entries = load_paq_entries_from_path(paq_path)
37
+ cache = PaqTextureCache(entries=entries, textures={})
38
+ texture_asset = cache.get_or_load("default_font_courier", "load/default_font_courier.tga")
39
+ texture = texture_asset.texture
40
+ except Exception:
41
+ texture = None
42
+
43
+ if texture is None:
44
+ if atlas_png.is_file():
45
+ texture = rl.load_texture(str(atlas_png))
46
+ elif atlas_tga.is_file():
47
+ texture = rl.load_texture(str(atlas_tga))
48
+ else:
49
+ missing_assets.append("load/default_font_courier.tga")
50
+ raise FileNotFoundError(
51
+ "Missing grim mono font (expected load/default_font_courier.tga in crimson.paq "
52
+ "or extracted crimson/load/default_font_courier.(png|tga))"
53
+ )
54
+
55
+ rl.set_texture_filter(texture, GRIM_MONO_TEXTURE_FILTER)
56
+ grid = 16
57
+ cell_width = texture.width / grid
58
+ cell_height = texture.height / grid
59
+ return GrimMonoFont(
60
+ texture=texture,
61
+ grid=grid,
62
+ cell_width=cell_width,
63
+ cell_height=cell_height,
64
+ advance=GRIM_MONO_ADVANCE,
65
+ )
66
+
67
+
68
+ def draw_grim_mono_text(font: GrimMonoFont, text: str, x: float, y: float, scale: float, color: rl.Color) -> None:
69
+ x_pos = x
70
+ y_pos = y
71
+ advance = font.advance * scale
72
+ draw_size = GRIM_MONO_DRAW_SIZE * scale
73
+ line_height = GRIM_MONO_LINE_HEIGHT * scale
74
+ origin = rl.Vector2(0.0, 0.0)
75
+ skip_advance = False
76
+ for value in text.encode("latin-1", errors="replace"):
77
+ if value == 0x0A:
78
+ x_pos = x
79
+ y_pos += line_height
80
+ continue
81
+ if value == 0x0D:
82
+ continue
83
+ if value == 0xA7:
84
+ skip_advance = True
85
+ continue
86
+
87
+ if skip_advance:
88
+ skip_advance = False
89
+ else:
90
+ x_pos += advance
91
+
92
+ col = value % font.grid
93
+ row = value // font.grid
94
+ src = rl.Rectangle(
95
+ float(col * font.cell_width),
96
+ float(row * font.cell_height),
97
+ float(font.cell_width),
98
+ float(font.cell_height),
99
+ )
100
+ dst = rl.Rectangle(
101
+ float(x_pos),
102
+ float(y_pos + 1.0),
103
+ float(draw_size),
104
+ float(draw_size),
105
+ )
106
+ rl.draw_texture_pro(font.texture, src, dst, origin, 0.0, color)
107
+
108
+
109
+ def measure_grim_mono_text_height(font: GrimMonoFont, text: str, scale: float) -> float:
110
+ line_count = text.count("\n") + 1
111
+ return GRIM_MONO_LINE_HEIGHT * scale * line_count
grim/fonts/small.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ import pyray as rl
7
+
8
+ from grim.assets import PaqTextureCache, find_paq_path, load_paq_entries_from_path
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class SmallFontData:
13
+ widths: list[int]
14
+ texture: rl.Texture
15
+ cell_size: int = 16
16
+ grid: int = 16
17
+
18
+
19
+ SMALL_FONT_UV_BIAS_PX = 0.5
20
+ SMALL_FONT_FILTER = rl.TEXTURE_FILTER_POINT
21
+ SMALL_FONT_RENDER_SCALE = 1.0
22
+
23
+
24
+ def load_small_font(assets_root: Path, missing_assets: list[str]) -> SmallFontData:
25
+ # Prefer crimson.paq (runtime source-of-truth), but fall back to extracted
26
+ # assets when present for development convenience.
27
+ paq_path = find_paq_path(assets_root)
28
+ if paq_path is not None:
29
+ try:
30
+ entries = load_paq_entries_from_path(paq_path)
31
+ widths_data = entries.get("load/smallFnt.dat")
32
+ if widths_data is not None:
33
+ cache = PaqTextureCache(entries=entries, textures={})
34
+ texture_asset = cache.get_or_load("smallWhite", "load/smallWhite.tga")
35
+ if texture_asset.texture is not None:
36
+ rl.set_texture_filter(texture_asset.texture, SMALL_FONT_FILTER)
37
+ return SmallFontData(widths=list(widths_data), texture=texture_asset.texture)
38
+ except Exception:
39
+ pass
40
+
41
+ widths_path = assets_root / "crimson" / "load" / "smallFnt.dat"
42
+ atlas_png = assets_root / "crimson" / "load" / "smallWhite.png"
43
+ atlas_tga = assets_root / "crimson" / "load" / "smallWhite.tga"
44
+ if not widths_path.is_file() or (not atlas_png.is_file() and not atlas_tga.is_file()):
45
+ missing_assets.append("small font assets")
46
+ raise FileNotFoundError(f"Missing small font assets: {widths_path} and {atlas_png} or {atlas_tga}")
47
+ widths = list(widths_path.read_bytes())
48
+ texture = rl.load_texture(str(atlas_png if atlas_png.is_file() else atlas_tga))
49
+ rl.set_texture_filter(texture, SMALL_FONT_FILTER)
50
+ return SmallFontData(widths=widths, texture=texture)
51
+
52
+
53
+ def draw_small_text(font: SmallFontData, text: str, x: float, y: float, scale: float, color: rl.Color) -> None:
54
+ x_pos = x
55
+ y_pos = y
56
+ scale_px = scale * SMALL_FONT_RENDER_SCALE
57
+ line_height = font.cell_size * scale_px
58
+ snap = abs(scale_px - round(scale_px)) < 0.001
59
+ if snap:
60
+ scale_px = float(round(scale_px))
61
+ origin = rl.Vector2(0.0, 0.0)
62
+ bias = 0.0 if SMALL_FONT_FILTER == rl.TEXTURE_FILTER_POINT else SMALL_FONT_UV_BIAS_PX
63
+ for value in text.encode("latin-1", errors="replace"):
64
+ if value == 0x0A:
65
+ x_pos = x
66
+ y_pos += line_height
67
+ continue
68
+ if value == 0x0D:
69
+ continue
70
+ width = font.widths[value]
71
+ if width <= 0:
72
+ continue
73
+ col = value % font.grid
74
+ row = value // font.grid
75
+ src_w = max(float(width) - bias, 0.5)
76
+ src_h = max(float(font.cell_size) - bias, 0.5)
77
+ src = rl.Rectangle(
78
+ float(col * font.cell_size) + bias,
79
+ float(row * font.cell_size) + bias,
80
+ src_w,
81
+ src_h,
82
+ )
83
+ dst_x = float(round(x_pos)) if snap else float(x_pos)
84
+ dst_y = float(round(y_pos)) if snap else float(y_pos)
85
+ dst_w = float(round(width * scale_px)) if snap else float(width * scale_px)
86
+ dst_h = float(round(font.cell_size * scale_px)) if snap else float(font.cell_size * scale_px)
87
+ dst = rl.Rectangle(
88
+ dst_x,
89
+ dst_y,
90
+ dst_w,
91
+ dst_h,
92
+ )
93
+ rl.draw_texture_pro(font.texture, src, dst, origin, 0.0, color)
94
+ x_pos += width * scale_px
95
+
96
+
97
+ def measure_small_text_height(font: SmallFontData, text: str, scale: float) -> float:
98
+ line_count = text.count("\n") + 1
99
+ scale_px = scale * SMALL_FONT_RENDER_SCALE
100
+ return font.cell_size * scale_px * line_count
101
+
102
+
103
+ def measure_small_text_width(font: SmallFontData, text: str, scale: float) -> float:
104
+ """Return the maximum line width for `text` when rendered with `draw_small_text`."""
105
+ scale_px = scale * SMALL_FONT_RENDER_SCALE
106
+ x = 0.0
107
+ best = 0.0
108
+ for value in text.encode("latin-1", errors="replace"):
109
+ if value == 0x0A:
110
+ best = max(best, x)
111
+ x = 0.0
112
+ continue
113
+ if value == 0x0D:
114
+ continue
115
+ width = font.widths[value]
116
+ if width <= 0:
117
+ continue
118
+ x += float(width) * scale_px
119
+ best = max(best, x)
120
+ return best
grim/input.py ADDED
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ import pyray as rl
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class ActionMap:
10
+ bindings: dict[str, tuple[int, ...]] = field(default_factory=dict)
11
+
12
+ def bind(self, action: str, *keys: int) -> None:
13
+ if not keys:
14
+ raise ValueError("bind requires at least one key")
15
+ self.bindings[action] = tuple(int(key) for key in keys)
16
+
17
+ def is_down(self, action: str) -> bool:
18
+ keys = self.bindings.get(action, ())
19
+ return any(rl.is_key_down(key) for key in keys)
20
+
21
+ def was_pressed(self, action: str) -> bool:
22
+ keys = self.bindings.get(action, ())
23
+ return any(rl.is_key_pressed(key) for key in keys)
24
+
25
+
26
+ def is_key_down(key: int) -> bool:
27
+ return rl.is_key_down(key)
28
+
29
+
30
+ def was_key_pressed(key: int) -> bool:
31
+ return rl.is_key_pressed(key)
32
+
33
+
34
+ def is_mouse_button_down(button: int) -> bool:
35
+ return rl.is_mouse_button_down(button)
36
+
37
+
38
+ def was_mouse_button_pressed(button: int) -> bool:
39
+ return rl.is_mouse_button_pressed(button)
40
+
41
+
42
+ def mouse_position() -> tuple[int, int]:
43
+ pos = rl.get_mouse_position()
44
+ return int(pos.x), int(pos.y)
grim/jaz.py ADDED
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ JAZ texture format (Crimsonland).
5
+
6
+ File layout:
7
+ - u8 method: compression method (1 = zlib)
8
+ - u32 comp_size: compressed payload size (bytes)
9
+ - u32 raw_size: uncompressed payload size (bytes)
10
+ - zlib stream (length = comp_size)
11
+
12
+ Decompressed payload:
13
+ - u32 jpeg_len
14
+ - jpeg bytes (length = jpeg_len)
15
+ - alpha_rle: (count, value) byte pairs for alpha channel
16
+
17
+ Notes from assets:
18
+ - alpha runs expand to width*height for most files; one file is short by 1 pixel.
19
+ We pad any remaining pixels with 0 (transparent).
20
+ """
21
+
22
+ import io
23
+ import zlib
24
+ from pathlib import Path
25
+
26
+ from PIL import Image
27
+ from construct import Bytes, Int8ul, Int32ul, Struct, this
28
+
29
+
30
+ JAZ_HEADER = Struct(
31
+ "method" / Int8ul,
32
+ "comp_size" / Int32ul,
33
+ "raw_size" / Int32ul,
34
+ )
35
+
36
+ JAZ_FILE = Struct(
37
+ "header" / JAZ_HEADER,
38
+ "compressed" / Bytes(this.header.comp_size),
39
+ )
40
+
41
+
42
+ def jaz_payload(raw_size: int) -> Struct:
43
+ return Struct(
44
+ "jpeg_len" / Int32ul,
45
+ "jpeg" / Bytes(this.jpeg_len),
46
+ "alpha_rle" / Bytes(raw_size - 4 - this.jpeg_len),
47
+ )
48
+
49
+
50
+ class JazImage:
51
+ def __init__(self, width: int, height: int, jpeg: bytes, alpha: bytes) -> None:
52
+ self.width = width
53
+ self.height = height
54
+ self.jpeg = jpeg
55
+ self.alpha = alpha
56
+
57
+ def rgb_image(self) -> Image.Image:
58
+ img = Image.open(io.BytesIO(self.jpeg))
59
+ return img.convert("RGB")
60
+
61
+ def alpha_image(self) -> Image.Image:
62
+ return Image.frombytes("L", (self.width, self.height), self.alpha)
63
+
64
+ def composite_image(self) -> Image.Image:
65
+ rgb = self.rgb_image()
66
+ alpha = self.alpha_image()
67
+ rgb.putalpha(alpha)
68
+ return rgb
69
+
70
+
71
+ def decode_alpha_rle(data: bytes, expected: int) -> bytes:
72
+ out = bytearray(expected)
73
+ filled = 0
74
+ for i in range(0, len(data) - 1, 2):
75
+ count = data[i]
76
+ value = data[i + 1]
77
+ if count == 0:
78
+ continue
79
+ if filled >= expected:
80
+ break
81
+ end = min(filled + count, expected)
82
+ out[filled:end] = bytes([value]) * (end - filled)
83
+ filled = end
84
+ return bytes(out)
85
+
86
+
87
+ def decode_jaz_bytes(data: bytes) -> JazImage:
88
+ parsed = JAZ_FILE.parse(data)
89
+ header = parsed.header
90
+ if header.method != 1:
91
+ raise ValueError(f"unsupported compression method: {header.method}")
92
+ raw = zlib.decompress(parsed.compressed)
93
+ if len(raw) != header.raw_size:
94
+ raise ValueError(f"raw size mismatch: {len(raw)} != {header.raw_size}")
95
+ payload = jaz_payload(header.raw_size).parse(raw)
96
+ img = Image.open(io.BytesIO(payload.jpeg))
97
+ width, height = img.size
98
+ alpha = decode_alpha_rle(payload.alpha_rle, width * height)
99
+ return JazImage(width, height, payload.jpeg, alpha)
100
+
101
+
102
+ def decode_jaz(path: str | Path) -> JazImage:
103
+ return decode_jaz_bytes(Path(path).read_bytes())
grim/math.py ADDED
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def clamp(value: float, low: float, high: float) -> float:
5
+ if value < low:
6
+ return low
7
+ if value > high:
8
+ return high
9
+ return value
10
+
11
+
12
+ def clamp01(value: float) -> float:
13
+ return clamp(value, 0.0, 1.0)
14
+
15
+
16
+ def lerp(a: float, b: float, t: float) -> float:
17
+ return a + (b - a) * t