crimsonland 0.1.0.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crimson/__init__.py +24 -0
- crimson/assets_fetch.py +60 -0
- crimson/atlas.py +92 -0
- crimson/audio_router.py +155 -0
- crimson/bonuses.py +167 -0
- crimson/camera.py +75 -0
- crimson/cli.py +380 -0
- crimson/creatures/__init__.py +8 -0
- crimson/creatures/ai.py +186 -0
- crimson/creatures/anim.py +173 -0
- crimson/creatures/damage.py +103 -0
- crimson/creatures/runtime.py +1019 -0
- crimson/creatures/spawn.py +2871 -0
- crimson/debug.py +7 -0
- crimson/demo.py +1360 -0
- crimson/demo_trial.py +140 -0
- crimson/effects.py +1086 -0
- crimson/effects_atlas.py +73 -0
- crimson/frontend/__init__.py +1 -0
- crimson/frontend/assets.py +43 -0
- crimson/frontend/boot.py +424 -0
- crimson/frontend/menu.py +700 -0
- crimson/frontend/panels/__init__.py +1 -0
- crimson/frontend/panels/base.py +410 -0
- crimson/frontend/panels/controls.py +132 -0
- crimson/frontend/panels/mods.py +128 -0
- crimson/frontend/panels/options.py +409 -0
- crimson/frontend/panels/play_game.py +627 -0
- crimson/frontend/panels/stats.py +351 -0
- crimson/frontend/transitions.py +31 -0
- crimson/game.py +2533 -0
- crimson/game_modes.py +15 -0
- crimson/game_world.py +652 -0
- crimson/gameplay.py +2467 -0
- crimson/input_codes.py +176 -0
- crimson/modes/__init__.py +1 -0
- crimson/modes/base_gameplay_mode.py +219 -0
- crimson/modes/quest_mode.py +502 -0
- crimson/modes/rush_mode.py +300 -0
- crimson/modes/survival_mode.py +792 -0
- crimson/modes/tutorial_mode.py +648 -0
- crimson/modes/typo_mode.py +472 -0
- crimson/paths.py +23 -0
- crimson/perks.py +828 -0
- crimson/persistence/__init__.py +1 -0
- crimson/persistence/highscores.py +385 -0
- crimson/persistence/save_status.py +245 -0
- crimson/player_damage.py +77 -0
- crimson/projectiles.py +1133 -0
- crimson/quests/__init__.py +18 -0
- crimson/quests/helpers.py +147 -0
- crimson/quests/registry.py +49 -0
- crimson/quests/results.py +164 -0
- crimson/quests/runtime.py +91 -0
- crimson/quests/tier1.py +620 -0
- crimson/quests/tier2.py +652 -0
- crimson/quests/tier3.py +579 -0
- crimson/quests/tier4.py +721 -0
- crimson/quests/tier5.py +886 -0
- crimson/quests/timeline.py +115 -0
- crimson/quests/types.py +70 -0
- crimson/render/__init__.py +1 -0
- crimson/render/terrain_fx.py +88 -0
- crimson/render/world_renderer.py +1941 -0
- crimson/sim/__init__.py +1 -0
- crimson/sim/world_defs.py +67 -0
- crimson/sim/world_state.py +422 -0
- crimson/terrain_assets.py +19 -0
- crimson/tutorial/__init__.py +12 -0
- crimson/tutorial/timeline.py +291 -0
- crimson/typo/__init__.py +2 -0
- crimson/typo/names.py +233 -0
- crimson/typo/player.py +43 -0
- crimson/typo/spawns.py +73 -0
- crimson/typo/typing.py +52 -0
- crimson/ui/__init__.py +3 -0
- crimson/ui/cursor.py +95 -0
- crimson/ui/demo_trial_overlay.py +235 -0
- crimson/ui/game_over.py +660 -0
- crimson/ui/hud.py +601 -0
- crimson/ui/perk_menu.py +388 -0
- crimson/views/__init__.py +40 -0
- crimson/views/aim_debug.py +276 -0
- crimson/views/animations.py +274 -0
- crimson/views/arsenal_debug.py +404 -0
- crimson/views/audio_bootstrap.py +47 -0
- crimson/views/bonuses.py +201 -0
- crimson/views/camera_debug.py +359 -0
- crimson/views/camera_shake.py +229 -0
- crimson/views/corpse_stamp_debug.py +324 -0
- crimson/views/decals_debug.py +739 -0
- crimson/views/empty.py +19 -0
- crimson/views/fonts.py +114 -0
- crimson/views/game_over.py +117 -0
- crimson/views/ground.py +259 -0
- crimson/views/lighting_debug.py +1166 -0
- crimson/views/particles.py +293 -0
- crimson/views/perk_menu_debug.py +430 -0
- crimson/views/perks.py +398 -0
- crimson/views/player.py +434 -0
- crimson/views/player_sprite_debug.py +314 -0
- crimson/views/projectile_fx.py +609 -0
- crimson/views/projectile_render_debug.py +393 -0
- crimson/views/projectiles.py +221 -0
- crimson/views/quest_title_overlay.py +108 -0
- crimson/views/registry.py +34 -0
- crimson/views/rush.py +16 -0
- crimson/views/small_font_debug.py +204 -0
- crimson/views/spawn_plan.py +363 -0
- crimson/views/sprites.py +214 -0
- crimson/views/survival.py +15 -0
- crimson/views/terrain.py +132 -0
- crimson/views/ui.py +123 -0
- crimson/views/wicons.py +166 -0
- crimson/weapon_sfx.py +63 -0
- crimson/weapons.py +860 -0
- crimsonland-0.1.0.dev5.dist-info/METADATA +9 -0
- crimsonland-0.1.0.dev5.dist-info/RECORD +139 -0
- crimsonland-0.1.0.dev5.dist-info/WHEEL +4 -0
- crimsonland-0.1.0.dev5.dist-info/entry_points.txt +4 -0
- grim/__init__.py +20 -0
- grim/app.py +92 -0
- grim/assets.py +231 -0
- grim/audio.py +106 -0
- grim/config.py +294 -0
- grim/console.py +737 -0
- grim/fonts/__init__.py +7 -0
- grim/fonts/grim_mono.py +111 -0
- grim/fonts/small.py +120 -0
- grim/input.py +44 -0
- grim/jaz.py +103 -0
- grim/math.py +17 -0
- grim/music.py +403 -0
- grim/paq.py +76 -0
- grim/rand.py +37 -0
- grim/sfx.py +276 -0
- grim/sfx_map.py +103 -0
- grim/terrain_render.py +840 -0
- grim/view.py +16 -0
crimson/views/terrain.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pyray as rl
|
|
6
|
+
|
|
7
|
+
from .registry import register_view
|
|
8
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
9
|
+
from grim.view import View, ViewContext
|
|
10
|
+
|
|
11
|
+
UI_TEXT_SCALE = 1.0
|
|
12
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
13
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class TerrainTexture:
|
|
18
|
+
terrain_id: int
|
|
19
|
+
name: str
|
|
20
|
+
texture: rl.Texture
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TERRAIN_TEXTURES: list[tuple[int, str, str]] = [
|
|
24
|
+
(0, "ter_q1_base", "ter/ter_q1_base.png"),
|
|
25
|
+
(1, "ter_q1_tex1", "ter/ter_q1_tex1.png"),
|
|
26
|
+
(2, "ter_q2_base", "ter/ter_q2_base.png"),
|
|
27
|
+
(3, "ter_q2_tex1", "ter/ter_q2_tex1.png"),
|
|
28
|
+
(4, "ter_q3_base", "ter/ter_q3_base.png"),
|
|
29
|
+
(5, "ter_q3_tex1", "ter/ter_q3_tex1.png"),
|
|
30
|
+
(6, "ter_q4_base", "ter/ter_q4_base.png"),
|
|
31
|
+
(7, "ter_q4_tex1", "ter/ter_q4_tex1.png"),
|
|
32
|
+
(8, "fb_q1", "ter/fb_q1.png"),
|
|
33
|
+
(9, "fb_q2", "ter/fb_q2.png"),
|
|
34
|
+
(10, "fb_q3", "ter/fb_q3.png"),
|
|
35
|
+
(11, "fb_q4", "ter/fb_q4.png"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TerrainView:
|
|
40
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
41
|
+
self._assets_root = ctx.assets_dir
|
|
42
|
+
self._missing_assets: list[str] = []
|
|
43
|
+
self._textures: list[TerrainTexture] = []
|
|
44
|
+
self._small: SmallFontData | None = None
|
|
45
|
+
|
|
46
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
47
|
+
if self._small is not None:
|
|
48
|
+
return int(self._small.cell_size * scale)
|
|
49
|
+
return int(20 * scale)
|
|
50
|
+
|
|
51
|
+
def _draw_ui_text(
|
|
52
|
+
self,
|
|
53
|
+
text: str,
|
|
54
|
+
x: float,
|
|
55
|
+
y: float,
|
|
56
|
+
color: rl.Color,
|
|
57
|
+
scale: float = UI_TEXT_SCALE,
|
|
58
|
+
) -> None:
|
|
59
|
+
if self._small is not None:
|
|
60
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
61
|
+
else:
|
|
62
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
63
|
+
|
|
64
|
+
def open(self) -> None:
|
|
65
|
+
self._missing_assets.clear()
|
|
66
|
+
self._textures.clear()
|
|
67
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
68
|
+
for terrain_id, name, rel_path in TERRAIN_TEXTURES:
|
|
69
|
+
path = self._assets_root / "crimson" / rel_path
|
|
70
|
+
if not path.is_file():
|
|
71
|
+
self._missing_assets.append(rel_path)
|
|
72
|
+
continue
|
|
73
|
+
texture = rl.load_texture(str(path))
|
|
74
|
+
self._textures.append(TerrainTexture(terrain_id=terrain_id, name=name, texture=texture))
|
|
75
|
+
if self._missing_assets:
|
|
76
|
+
raise FileNotFoundError(f"Missing terrain assets: {', '.join(self._missing_assets)}")
|
|
77
|
+
|
|
78
|
+
def close(self) -> None:
|
|
79
|
+
for entry in self._textures:
|
|
80
|
+
rl.unload_texture(entry.texture)
|
|
81
|
+
self._textures.clear()
|
|
82
|
+
if self._small is not None:
|
|
83
|
+
rl.unload_texture(self._small.texture)
|
|
84
|
+
self._small = None
|
|
85
|
+
|
|
86
|
+
def update(self, dt: float) -> None:
|
|
87
|
+
del dt
|
|
88
|
+
|
|
89
|
+
def draw(self) -> None:
|
|
90
|
+
rl.clear_background(rl.Color(12, 12, 14, 255))
|
|
91
|
+
if self._missing_assets:
|
|
92
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
93
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
94
|
+
return
|
|
95
|
+
if not self._textures:
|
|
96
|
+
self._draw_ui_text("No terrain textures loaded.", 24, 24, UI_TEXT_COLOR)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
cols = 4
|
|
100
|
+
rows = (len(self._textures) + cols - 1) // cols
|
|
101
|
+
margin = 24
|
|
102
|
+
gap_x = 16
|
|
103
|
+
gap_y = 20
|
|
104
|
+
label_height = self._ui_line_height()
|
|
105
|
+
|
|
106
|
+
cell_w = max(entry.texture.width for entry in self._textures)
|
|
107
|
+
cell_h = max(entry.texture.height for entry in self._textures)
|
|
108
|
+
max_width = rl.get_screen_width() - margin * 2 - gap_x * (cols - 1)
|
|
109
|
+
max_height = rl.get_screen_height() - margin * 2 - gap_y * (rows - 1)
|
|
110
|
+
max_height -= rows * label_height
|
|
111
|
+
scale = min(1.0, max_width / (cols * cell_w), max_height / (rows * cell_h))
|
|
112
|
+
|
|
113
|
+
for idx, entry in enumerate(self._textures):
|
|
114
|
+
row = idx // cols
|
|
115
|
+
col = idx % cols
|
|
116
|
+
x = margin + col * (cell_w * scale + gap_x)
|
|
117
|
+
y = margin + row * (cell_h * scale + gap_y + label_height)
|
|
118
|
+
label = f"{entry.terrain_id:02d} {entry.name}"
|
|
119
|
+
self._draw_ui_text(label, x, y, UI_TEXT_COLOR)
|
|
120
|
+
dst = rl.Rectangle(
|
|
121
|
+
float(x),
|
|
122
|
+
float(y + label_height),
|
|
123
|
+
float(entry.texture.width * scale),
|
|
124
|
+
float(entry.texture.height * scale),
|
|
125
|
+
)
|
|
126
|
+
src = rl.Rectangle(0.0, 0.0, float(entry.texture.width), float(entry.texture.height))
|
|
127
|
+
rl.draw_texture_pro(entry.texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@register_view("terrain", "Terrain textures")
|
|
131
|
+
def build_terrain_view(ctx: ViewContext) -> View:
|
|
132
|
+
return TerrainView(ctx)
|
crimson/views/ui.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pyray as rl
|
|
6
|
+
|
|
7
|
+
from .registry import register_view
|
|
8
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
9
|
+
from grim.view import View, ViewContext
|
|
10
|
+
|
|
11
|
+
UI_TEXT_SCALE = 1.0
|
|
12
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
13
|
+
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
|
|
14
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class UiTexture:
|
|
19
|
+
name: str
|
|
20
|
+
texture: rl.Texture
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UiTextureView:
|
|
24
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
25
|
+
self._assets_root = ctx.assets_dir
|
|
26
|
+
self._missing_assets: list[str] = []
|
|
27
|
+
self._textures: list[UiTexture] = []
|
|
28
|
+
self._index = 0
|
|
29
|
+
self._small: SmallFontData | None = None
|
|
30
|
+
|
|
31
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
32
|
+
if self._small is not None:
|
|
33
|
+
return int(self._small.cell_size * scale)
|
|
34
|
+
return int(20 * scale)
|
|
35
|
+
|
|
36
|
+
def _draw_ui_text(
|
|
37
|
+
self,
|
|
38
|
+
text: str,
|
|
39
|
+
x: float,
|
|
40
|
+
y: float,
|
|
41
|
+
color: rl.Color,
|
|
42
|
+
scale: float = UI_TEXT_SCALE,
|
|
43
|
+
) -> None:
|
|
44
|
+
if self._small is not None:
|
|
45
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
46
|
+
else:
|
|
47
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
48
|
+
|
|
49
|
+
def open(self) -> None:
|
|
50
|
+
self._missing_assets.clear()
|
|
51
|
+
self._textures.clear()
|
|
52
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
53
|
+
ui_dir = self._assets_root / "crimson" / "ui"
|
|
54
|
+
if not ui_dir.is_dir():
|
|
55
|
+
self._missing_assets.append("ui/")
|
|
56
|
+
raise FileNotFoundError(f"Missing UI assets directory: {ui_dir}")
|
|
57
|
+
for path in sorted(ui_dir.glob("*.png")):
|
|
58
|
+
texture = rl.load_texture(str(path))
|
|
59
|
+
self._textures.append(UiTexture(name=path.name, texture=texture))
|
|
60
|
+
|
|
61
|
+
def close(self) -> None:
|
|
62
|
+
for entry in self._textures:
|
|
63
|
+
rl.unload_texture(entry.texture)
|
|
64
|
+
self._textures.clear()
|
|
65
|
+
if self._small is not None:
|
|
66
|
+
rl.unload_texture(self._small.texture)
|
|
67
|
+
self._small = None
|
|
68
|
+
|
|
69
|
+
def update(self, dt: float) -> None:
|
|
70
|
+
del dt
|
|
71
|
+
|
|
72
|
+
def _advance(self, delta: int) -> None:
|
|
73
|
+
if not self._textures:
|
|
74
|
+
return
|
|
75
|
+
self._index = (self._index + delta) % len(self._textures)
|
|
76
|
+
|
|
77
|
+
def _handle_input(self) -> None:
|
|
78
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
|
|
79
|
+
self._advance(1)
|
|
80
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
|
|
81
|
+
self._advance(-1)
|
|
82
|
+
|
|
83
|
+
def draw(self) -> None:
|
|
84
|
+
rl.clear_background(rl.Color(12, 12, 14, 255))
|
|
85
|
+
if self._missing_assets:
|
|
86
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
87
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
88
|
+
return
|
|
89
|
+
if not self._textures:
|
|
90
|
+
self._draw_ui_text("No UI textures loaded.", 24, 24, UI_TEXT_COLOR)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
self._handle_input()
|
|
94
|
+
entry = self._textures[self._index]
|
|
95
|
+
|
|
96
|
+
margin = 24
|
|
97
|
+
header_y = margin
|
|
98
|
+
line_height = self._ui_line_height()
|
|
99
|
+
title = f"{self._index + 1}/{len(self._textures)} {entry.name}"
|
|
100
|
+
self._draw_ui_text(title, margin, header_y, UI_TEXT_COLOR)
|
|
101
|
+
header_y += line_height + 6
|
|
102
|
+
self._draw_ui_text("Left/Right: texture", margin, header_y, UI_HINT_COLOR)
|
|
103
|
+
|
|
104
|
+
available_width = rl.get_screen_width() - margin * 2
|
|
105
|
+
available_height = rl.get_screen_height() - (header_y + line_height + margin)
|
|
106
|
+
scale = min(
|
|
107
|
+
2.0,
|
|
108
|
+
available_width / entry.texture.width,
|
|
109
|
+
available_height / entry.texture.height,
|
|
110
|
+
)
|
|
111
|
+
draw_w = entry.texture.width * scale
|
|
112
|
+
draw_h = entry.texture.height * scale
|
|
113
|
+
x = margin + (available_width - draw_w) / 2
|
|
114
|
+
y = header_y + line_height + (available_height - draw_h) / 2
|
|
115
|
+
|
|
116
|
+
src = rl.Rectangle(0.0, 0.0, float(entry.texture.width), float(entry.texture.height))
|
|
117
|
+
dst = rl.Rectangle(float(x), float(y), float(draw_w), float(draw_h))
|
|
118
|
+
rl.draw_texture_pro(entry.texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@register_view("ui", "UI texture preview")
|
|
122
|
+
def build_ui_view(ctx: ViewContext) -> View:
|
|
123
|
+
return UiTextureView(ctx)
|
crimson/views/wicons.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pyray as rl
|
|
6
|
+
|
|
7
|
+
from ..weapons import WEAPON_TABLE, Weapon
|
|
8
|
+
from .registry import register_view
|
|
9
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
10
|
+
from grim.view import View, ViewContext
|
|
11
|
+
|
|
12
|
+
UI_TEXT_SCALE = 1.0
|
|
13
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
14
|
+
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
|
|
15
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
16
|
+
UI_HOVER_COLOR = rl.Color(240, 200, 80, 255)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class WeaponIconGroup:
|
|
21
|
+
icon_index: int
|
|
22
|
+
weapons: tuple[Weapon, ...]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_icon_groups() -> dict[int, WeaponIconGroup]:
|
|
26
|
+
grouped: dict[int, list[Weapon]] = {}
|
|
27
|
+
for entry in WEAPON_TABLE:
|
|
28
|
+
icon_index = entry.icon_index
|
|
29
|
+
if icon_index is None or icon_index < 0 or icon_index > 31:
|
|
30
|
+
continue
|
|
31
|
+
grouped.setdefault(icon_index, []).append(entry)
|
|
32
|
+
return {
|
|
33
|
+
icon_index: WeaponIconGroup(icon_index=icon_index, weapons=tuple(entries))
|
|
34
|
+
for icon_index, entries in grouped.items()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
WEAPON_ICON_GROUPS = _build_icon_groups()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WeaponIconView:
|
|
42
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
43
|
+
self._assets_root = ctx.assets_dir
|
|
44
|
+
self._missing_assets: list[str] = []
|
|
45
|
+
self._texture: rl.Texture | None = None
|
|
46
|
+
self._small: SmallFontData | None = None
|
|
47
|
+
|
|
48
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
49
|
+
if self._small is not None:
|
|
50
|
+
return int(self._small.cell_size * scale)
|
|
51
|
+
return int(20 * scale)
|
|
52
|
+
|
|
53
|
+
def _draw_ui_text(
|
|
54
|
+
self,
|
|
55
|
+
text: str,
|
|
56
|
+
x: float,
|
|
57
|
+
y: float,
|
|
58
|
+
color: rl.Color,
|
|
59
|
+
scale: float = UI_TEXT_SCALE,
|
|
60
|
+
) -> None:
|
|
61
|
+
if self._small is not None:
|
|
62
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
63
|
+
else:
|
|
64
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
65
|
+
|
|
66
|
+
def open(self) -> None:
|
|
67
|
+
self._missing_assets.clear()
|
|
68
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
69
|
+
path = self._assets_root / "crimson" / "ui" / "ui_wicons.png"
|
|
70
|
+
if not path.is_file():
|
|
71
|
+
self._missing_assets.append("ui/ui_wicons.png")
|
|
72
|
+
raise FileNotFoundError(f"Missing asset: {path}")
|
|
73
|
+
self._texture = rl.load_texture(str(path))
|
|
74
|
+
|
|
75
|
+
def close(self) -> None:
|
|
76
|
+
if self._texture is not None:
|
|
77
|
+
rl.unload_texture(self._texture)
|
|
78
|
+
self._texture = None
|
|
79
|
+
if self._small is not None:
|
|
80
|
+
rl.unload_texture(self._small.texture)
|
|
81
|
+
self._small = None
|
|
82
|
+
|
|
83
|
+
def update(self, dt: float) -> None:
|
|
84
|
+
del dt
|
|
85
|
+
|
|
86
|
+
def draw(self) -> None:
|
|
87
|
+
rl.clear_background(rl.Color(12, 12, 14, 255))
|
|
88
|
+
if self._missing_assets:
|
|
89
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
90
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
91
|
+
return
|
|
92
|
+
if self._texture is None:
|
|
93
|
+
self._draw_ui_text("No weapon icon texture loaded.", 24, 24, UI_TEXT_COLOR)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
margin = 24
|
|
97
|
+
panel_gap = 32
|
|
98
|
+
panel_width = min(420, int(rl.get_screen_width() * 0.4))
|
|
99
|
+
available_width = rl.get_screen_width() - margin * 2 - panel_gap - panel_width
|
|
100
|
+
available_height = rl.get_screen_height() - margin * 2 - 60
|
|
101
|
+
|
|
102
|
+
cols = 4
|
|
103
|
+
rows = 8
|
|
104
|
+
icon_w = self._texture.width / cols
|
|
105
|
+
icon_h = self._texture.height / rows
|
|
106
|
+
scale = min(2.5, available_width / (cols * icon_w), available_height / (rows * icon_h))
|
|
107
|
+
|
|
108
|
+
x = margin
|
|
109
|
+
y = margin + 60
|
|
110
|
+
hovered_index = None
|
|
111
|
+
mouse = rl.get_mouse_position()
|
|
112
|
+
|
|
113
|
+
for idx in range(cols * rows):
|
|
114
|
+
row = idx // cols
|
|
115
|
+
col = idx % cols
|
|
116
|
+
dst_x = x + col * icon_w * scale
|
|
117
|
+
dst_y = y + row * icon_h * scale
|
|
118
|
+
dst = rl.Rectangle(float(dst_x), float(dst_y), float(icon_w * scale), float(icon_h * scale))
|
|
119
|
+
src = rl.Rectangle(float(col * icon_w), float(row * icon_h), float(icon_w), float(icon_h))
|
|
120
|
+
rl.draw_texture_pro(self._texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
121
|
+
|
|
122
|
+
if dst_x <= mouse.x <= dst_x + dst.width and dst_y <= mouse.y <= dst_y + dst.height:
|
|
123
|
+
hovered_index = idx
|
|
124
|
+
rl.draw_rectangle_lines_ex(dst, 3, UI_HOVER_COLOR)
|
|
125
|
+
|
|
126
|
+
self._draw_ui_text(
|
|
127
|
+
f"{idx:02d}",
|
|
128
|
+
dst_x + 4,
|
|
129
|
+
dst_y + 4,
|
|
130
|
+
UI_HINT_COLOR,
|
|
131
|
+
scale=0.75,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
info_x = x + cols * icon_w * scale + panel_gap
|
|
135
|
+
info_y = margin
|
|
136
|
+
self._draw_ui_text(
|
|
137
|
+
"ui_wicons.png (8x8 grid, 2x1 subrects)",
|
|
138
|
+
info_x,
|
|
139
|
+
info_y,
|
|
140
|
+
UI_TEXT_COLOR,
|
|
141
|
+
)
|
|
142
|
+
info_y += self._ui_line_height() + 12
|
|
143
|
+
|
|
144
|
+
if hovered_index is not None:
|
|
145
|
+
frame = hovered_index * 2
|
|
146
|
+
self._draw_ui_text(
|
|
147
|
+
f"icon_index {hovered_index} frame {frame}",
|
|
148
|
+
info_x,
|
|
149
|
+
info_y,
|
|
150
|
+
UI_TEXT_COLOR,
|
|
151
|
+
)
|
|
152
|
+
info_y += self._ui_line_height() + 6
|
|
153
|
+
group = WEAPON_ICON_GROUPS.get(hovered_index)
|
|
154
|
+
if group is None:
|
|
155
|
+
self._draw_ui_text("no weapon mapping", info_x, info_y, UI_HINT_COLOR)
|
|
156
|
+
info_y += self._ui_line_height() + 6
|
|
157
|
+
else:
|
|
158
|
+
for weapon in group.weapons:
|
|
159
|
+
name = weapon.name or f"weapon_{weapon.weapon_id}"
|
|
160
|
+
self._draw_ui_text(name, info_x, info_y, UI_TEXT_COLOR)
|
|
161
|
+
info_y += self._ui_line_height() + 4
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@register_view("wicons", "Weapon icon preview")
|
|
165
|
+
def build_weapon_icon_view(ctx: ViewContext) -> View:
|
|
166
|
+
return WeaponIconView(ctx)
|
crimson/weapon_sfx.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .weapons import WEAPON_BY_ID
|
|
4
|
+
|
|
5
|
+
WEAPON_TABLE_BASE_ADDR = 0x4D7A2C
|
|
6
|
+
WEAPON_TABLE_STRIDE_BYTES = 0x7C
|
|
7
|
+
WEAPON_TABLE_FIRE_SFX_OFFSET = 0x58
|
|
8
|
+
WEAPON_TABLE_RELOAD_SFX_OFFSET = 0x60
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_dat_ref(value: str) -> int | None:
|
|
12
|
+
raw = value.strip()
|
|
13
|
+
if raw.startswith("&"):
|
|
14
|
+
raw = raw[1:]
|
|
15
|
+
raw = raw.lstrip("_")
|
|
16
|
+
if not raw.startswith("DAT_"):
|
|
17
|
+
return None
|
|
18
|
+
try:
|
|
19
|
+
return int(raw.removeprefix("DAT_"), 16)
|
|
20
|
+
except ValueError:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_weapon_sfx_ref(value: str | None, *, max_depth: int = 16) -> str | None:
|
|
25
|
+
"""
|
|
26
|
+
Resolve weapon-table references like `_DAT_004d93bc` into a concrete sfx key (e.g. `sfx_shotgun_reload`).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
current = value
|
|
30
|
+
seen_addrs: set[int] = set()
|
|
31
|
+
|
|
32
|
+
for _ in range(max(1, int(max_depth))):
|
|
33
|
+
if current is None:
|
|
34
|
+
return None
|
|
35
|
+
if current.startswith("sfx_"):
|
|
36
|
+
return current
|
|
37
|
+
|
|
38
|
+
addr = _parse_dat_ref(current)
|
|
39
|
+
if addr is None:
|
|
40
|
+
return current
|
|
41
|
+
if addr in seen_addrs:
|
|
42
|
+
return current
|
|
43
|
+
seen_addrs.add(addr)
|
|
44
|
+
|
|
45
|
+
offset = addr - WEAPON_TABLE_BASE_ADDR
|
|
46
|
+
if offset < 0:
|
|
47
|
+
return current
|
|
48
|
+
entry_index, field_offset = divmod(offset, WEAPON_TABLE_STRIDE_BYTES)
|
|
49
|
+
weapon_id = entry_index
|
|
50
|
+
weapon = WEAPON_BY_ID.get(weapon_id)
|
|
51
|
+
if weapon is None:
|
|
52
|
+
return current
|
|
53
|
+
|
|
54
|
+
if field_offset == WEAPON_TABLE_FIRE_SFX_OFFSET:
|
|
55
|
+
current = weapon.fire_sound
|
|
56
|
+
continue
|
|
57
|
+
if field_offset == WEAPON_TABLE_RELOAD_SFX_OFFSET:
|
|
58
|
+
current = weapon.reload_sound
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
return current
|
|
62
|
+
|
|
63
|
+
return current
|