crimsonland 0.1.0.dev1__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 +153 -0
- crimson/bonuses.py +167 -0
- crimson/camera.py +75 -0
- crimson/cli.py +377 -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 +663 -0
- crimson/gameplay.py +2450 -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 +1039 -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 +1338 -0
- crimson/sim/__init__.py +1 -0
- crimson/sim/world_defs.py +56 -0
- crimson/sim/world_state.py +421 -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 +414 -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 +433 -0
- crimson/views/player_sprite_debug.py +314 -0
- crimson/views/projectile_fx.py +608 -0
- crimson/views/projectile_render_debug.py +407 -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.dev1.dist-info/METADATA +9 -0
- crimsonland-0.1.0.dev1.dist-info/RECORD +138 -0
- crimsonland-0.1.0.dev1.dist-info/WHEEL +4 -0
- crimsonland-0.1.0.dev1.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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pyray as rl
|
|
6
|
+
|
|
7
|
+
from ..creatures.anim import creature_anim_advance_phase, creature_anim_select_frame
|
|
8
|
+
from ..creatures.spawn import CreatureFlags, CreatureTypeId, SPAWN_TEMPLATES, SpawnTemplate, resolve_tint
|
|
9
|
+
from .registry import register_view
|
|
10
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
11
|
+
from grim.view import View, ViewContext
|
|
12
|
+
|
|
13
|
+
UI_TEXT_SCALE = 1.0
|
|
14
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
15
|
+
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
|
|
16
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class TypeAnimInfo:
|
|
21
|
+
base: int
|
|
22
|
+
anim_rate: float
|
|
23
|
+
mirror: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
TYPE_ANIM: dict[CreatureTypeId, TypeAnimInfo] = {
|
|
27
|
+
CreatureTypeId.ZOMBIE: TypeAnimInfo(base=0x20, anim_rate=1.2, mirror=False),
|
|
28
|
+
CreatureTypeId.LIZARD: TypeAnimInfo(base=0x10, anim_rate=1.6, mirror=True),
|
|
29
|
+
CreatureTypeId.ALIEN: TypeAnimInfo(base=0x20, anim_rate=1.35, mirror=False),
|
|
30
|
+
CreatureTypeId.SPIDER_SP1: TypeAnimInfo(base=0x10, anim_rate=1.5, mirror=True),
|
|
31
|
+
CreatureTypeId.SPIDER_SP2: TypeAnimInfo(base=0x10, anim_rate=1.5, mirror=True),
|
|
32
|
+
CreatureTypeId.TROOPER: TypeAnimInfo(base=0x00, anim_rate=1.0, mirror=False),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CreatureAnimationView:
|
|
37
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
38
|
+
self._assets_root = ctx.assets_dir
|
|
39
|
+
self._missing_assets: list[str] = []
|
|
40
|
+
self._textures: dict[str, rl.Texture] = {}
|
|
41
|
+
self._small: SmallFontData | None = None
|
|
42
|
+
self._templates: list[SpawnTemplate] = [
|
|
43
|
+
entry for entry in SPAWN_TEMPLATES if entry.type_id is not None and entry.creature is not None
|
|
44
|
+
]
|
|
45
|
+
self._index = 0
|
|
46
|
+
self._phase = 0.0
|
|
47
|
+
self._move_speed = 2.0
|
|
48
|
+
self._size = 50.0
|
|
49
|
+
self._local_scale = 1.0
|
|
50
|
+
self._paused = False
|
|
51
|
+
self._last_dt = 0.0
|
|
52
|
+
self._last_step = 0.0
|
|
53
|
+
self._apply_template_defaults()
|
|
54
|
+
|
|
55
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
56
|
+
if self._small is not None:
|
|
57
|
+
return int(self._small.cell_size * scale)
|
|
58
|
+
return int(20 * scale)
|
|
59
|
+
|
|
60
|
+
def _draw_ui_text(
|
|
61
|
+
self,
|
|
62
|
+
text: str,
|
|
63
|
+
x: float,
|
|
64
|
+
y: float,
|
|
65
|
+
color: rl.Color,
|
|
66
|
+
scale: float = UI_TEXT_SCALE,
|
|
67
|
+
) -> None:
|
|
68
|
+
if self._small is not None:
|
|
69
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
70
|
+
else:
|
|
71
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
72
|
+
|
|
73
|
+
def open(self) -> None:
|
|
74
|
+
self._missing_assets.clear()
|
|
75
|
+
self._textures.clear()
|
|
76
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
77
|
+
for entry in self._templates:
|
|
78
|
+
if entry.creature is None:
|
|
79
|
+
continue
|
|
80
|
+
if entry.creature in self._textures:
|
|
81
|
+
continue
|
|
82
|
+
path = self._assets_root / "crimson" / "game" / f"{entry.creature}.png"
|
|
83
|
+
if not path.is_file():
|
|
84
|
+
self._missing_assets.append(str(path))
|
|
85
|
+
continue
|
|
86
|
+
self._textures[entry.creature] = rl.load_texture(str(path))
|
|
87
|
+
if self._missing_assets:
|
|
88
|
+
raise FileNotFoundError(f"Missing creature textures: {', '.join(self._missing_assets)}")
|
|
89
|
+
|
|
90
|
+
def close(self) -> None:
|
|
91
|
+
for texture in self._textures.values():
|
|
92
|
+
rl.unload_texture(texture)
|
|
93
|
+
self._textures.clear()
|
|
94
|
+
if self._small is not None:
|
|
95
|
+
rl.unload_texture(self._small.texture)
|
|
96
|
+
self._small = None
|
|
97
|
+
|
|
98
|
+
def update(self, dt: float) -> None:
|
|
99
|
+
self._last_dt = dt
|
|
100
|
+
if self._paused:
|
|
101
|
+
self._last_step = 0.0
|
|
102
|
+
return
|
|
103
|
+
template = self._current_template()
|
|
104
|
+
if template is None or template.type_id is None:
|
|
105
|
+
return
|
|
106
|
+
info = TYPE_ANIM.get(template.type_id)
|
|
107
|
+
if info is None:
|
|
108
|
+
return
|
|
109
|
+
flags = template.flags or CreatureFlags(0)
|
|
110
|
+
self._phase, self._last_step = creature_anim_advance_phase(
|
|
111
|
+
self._phase,
|
|
112
|
+
anim_rate=info.anim_rate,
|
|
113
|
+
move_speed=self._move_speed,
|
|
114
|
+
dt=dt,
|
|
115
|
+
size=self._size,
|
|
116
|
+
local_scale=self._local_scale,
|
|
117
|
+
flags=flags,
|
|
118
|
+
ai_mode=0,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _current_template(self) -> SpawnTemplate | None:
|
|
122
|
+
if not self._templates:
|
|
123
|
+
return None
|
|
124
|
+
return self._templates[self._index]
|
|
125
|
+
|
|
126
|
+
def _advance_template(self, delta: int) -> None:
|
|
127
|
+
if not self._templates:
|
|
128
|
+
return
|
|
129
|
+
self._index = (self._index + delta) % len(self._templates)
|
|
130
|
+
self._phase = 0.0
|
|
131
|
+
self._apply_template_defaults()
|
|
132
|
+
|
|
133
|
+
def _apply_template_defaults(self) -> None:
|
|
134
|
+
template = self._current_template()
|
|
135
|
+
if template is None:
|
|
136
|
+
return
|
|
137
|
+
self._move_speed = template.move_speed if template.move_speed is not None else 2.0
|
|
138
|
+
self._size = template.size if template.size is not None else 50.0
|
|
139
|
+
|
|
140
|
+
def _handle_input(self) -> None:
|
|
141
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
|
|
142
|
+
self._advance_template(1)
|
|
143
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
|
|
144
|
+
self._advance_template(-1)
|
|
145
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
|
|
146
|
+
self._move_speed = min(10.0, self._move_speed + 0.1)
|
|
147
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
|
|
148
|
+
self._move_speed = max(0.0, self._move_speed - 0.1)
|
|
149
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_UP):
|
|
150
|
+
self._size = min(200.0, self._size + 1.0)
|
|
151
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_DOWN):
|
|
152
|
+
self._size = max(1.0, self._size - 1.0)
|
|
153
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
|
|
154
|
+
self._local_scale = min(1.0, self._local_scale + 0.05)
|
|
155
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
|
|
156
|
+
self._local_scale = max(0.0, self._local_scale - 0.05)
|
|
157
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
|
|
158
|
+
self._paused = not self._paused
|
|
159
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
|
|
160
|
+
self._phase = 0.0
|
|
161
|
+
|
|
162
|
+
def draw(self) -> None:
|
|
163
|
+
rl.clear_background(rl.Color(12, 12, 14, 255))
|
|
164
|
+
if self._missing_assets:
|
|
165
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
166
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
167
|
+
return
|
|
168
|
+
if not self._templates:
|
|
169
|
+
self._draw_ui_text("No spawn templates loaded.", 24, 24, UI_TEXT_COLOR)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
self._handle_input()
|
|
173
|
+
template = self._current_template()
|
|
174
|
+
if template is None or template.type_id is None or template.creature is None:
|
|
175
|
+
self._draw_ui_text("Invalid template.", 24, 24, UI_TEXT_COLOR)
|
|
176
|
+
return
|
|
177
|
+
texture = self._textures.get(template.creature)
|
|
178
|
+
if texture is None:
|
|
179
|
+
self._draw_ui_text("Missing texture for creature.", 24, 24, UI_TEXT_COLOR)
|
|
180
|
+
return
|
|
181
|
+
info = TYPE_ANIM.get(template.type_id)
|
|
182
|
+
if info is None:
|
|
183
|
+
self._draw_ui_text("Missing anim info.", 24, 24, UI_TEXT_COLOR)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
frame, mirror_applied, mode = creature_anim_select_frame(
|
|
187
|
+
self._phase,
|
|
188
|
+
base_frame=info.base,
|
|
189
|
+
mirror_long=info.mirror,
|
|
190
|
+
flags=template.flags or CreatureFlags(0),
|
|
191
|
+
)
|
|
192
|
+
grid = 8
|
|
193
|
+
cell = texture.width / grid
|
|
194
|
+
row = frame // grid
|
|
195
|
+
col = frame % grid
|
|
196
|
+
|
|
197
|
+
margin = 24
|
|
198
|
+
title = f"{template.creature} (spawn 0x{template.spawn_id:02x})"
|
|
199
|
+
self._draw_ui_text(title, margin, margin, UI_TEXT_COLOR)
|
|
200
|
+
hint = (
|
|
201
|
+
"Left/Right: spawn template Up/Down: move_speed PgUp/PgDn: size "
|
|
202
|
+
"[/]: local scale Space: pause R: reset"
|
|
203
|
+
)
|
|
204
|
+
self._draw_ui_text(hint, margin, margin + self._ui_line_height() + 6, UI_HINT_COLOR)
|
|
205
|
+
|
|
206
|
+
sheet_scale = min(
|
|
207
|
+
1.0,
|
|
208
|
+
(rl.get_screen_width() * 0.55 - margin * 2) / texture.width,
|
|
209
|
+
(rl.get_screen_height() - margin * 2 - 60) / texture.height,
|
|
210
|
+
)
|
|
211
|
+
sheet_x = margin
|
|
212
|
+
sheet_y = margin + 60
|
|
213
|
+
sheet_w = texture.width * sheet_scale
|
|
214
|
+
sheet_h = texture.height * sheet_scale
|
|
215
|
+
|
|
216
|
+
src = rl.Rectangle(0.0, 0.0, float(texture.width), float(texture.height))
|
|
217
|
+
dst = rl.Rectangle(float(sheet_x), float(sheet_y), float(sheet_w), float(sheet_h))
|
|
218
|
+
rl.draw_texture_pro(texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
219
|
+
|
|
220
|
+
cell_w = sheet_w / grid
|
|
221
|
+
cell_h = sheet_h / grid
|
|
222
|
+
highlight = rl.Rectangle(
|
|
223
|
+
float(sheet_x + col * cell_w),
|
|
224
|
+
float(sheet_y + row * cell_h),
|
|
225
|
+
float(cell_w),
|
|
226
|
+
float(cell_h),
|
|
227
|
+
)
|
|
228
|
+
rl.draw_rectangle_lines_ex(highlight, 2, rl.Color(240, 200, 80, 255))
|
|
229
|
+
|
|
230
|
+
preview_scale = 3.0
|
|
231
|
+
preview_size = cell * preview_scale
|
|
232
|
+
preview_x = sheet_x + sheet_w + 40
|
|
233
|
+
preview_y = sheet_y + 40
|
|
234
|
+
src_frame = rl.Rectangle(
|
|
235
|
+
float(col * cell),
|
|
236
|
+
float(row * cell),
|
|
237
|
+
float(cell),
|
|
238
|
+
float(cell),
|
|
239
|
+
)
|
|
240
|
+
dst_frame = rl.Rectangle(float(preview_x), float(preview_y), float(preview_size), float(preview_size))
|
|
241
|
+
tint_r, tint_g, tint_b, tint_a = resolve_tint(template.tint)
|
|
242
|
+
tint = rl.Color(
|
|
243
|
+
max(0, min(255, int(tint_r * 255))),
|
|
244
|
+
max(0, min(255, int(tint_g * 255))),
|
|
245
|
+
max(0, min(255, int(tint_b * 255))),
|
|
246
|
+
max(0, min(255, int(tint_a * 255))),
|
|
247
|
+
)
|
|
248
|
+
rl.draw_texture_pro(texture, src_frame, dst_frame, rl.Vector2(0.0, 0.0), 0.0, tint)
|
|
249
|
+
|
|
250
|
+
info_lines = [
|
|
251
|
+
f"type_id={template.type_id.name}",
|
|
252
|
+
f"flags={int(template.flags or 0):#x}",
|
|
253
|
+
f"mode={mode}",
|
|
254
|
+
f"frame=0x{frame:02x}",
|
|
255
|
+
f"phase={self._phase:.6f}",
|
|
256
|
+
f"template.move_speed={template.move_speed!r}",
|
|
257
|
+
f"template.size={template.size!r}",
|
|
258
|
+
f"template.tint={template.tint!r}",
|
|
259
|
+
f"move_speed={self._move_speed:.2f}",
|
|
260
|
+
f"size={self._size:.1f}",
|
|
261
|
+
f"local_scale={self._local_scale:.2f}",
|
|
262
|
+
f"dt={self._last_dt:.4f}s step={self._last_step:.6f}",
|
|
263
|
+
f"mirror_applied={'yes' if mirror_applied else 'no'}",
|
|
264
|
+
f"paused={'yes' if self._paused else 'no'}",
|
|
265
|
+
]
|
|
266
|
+
y = int(preview_y + preview_size + 16)
|
|
267
|
+
for line in info_lines:
|
|
268
|
+
self._draw_ui_text(line, preview_x, y, UI_TEXT_COLOR)
|
|
269
|
+
y += self._ui_line_height() + 4
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@register_view("animations", "Creature animation preview")
|
|
273
|
+
def build_animation_view(ctx: ViewContext) -> View:
|
|
274
|
+
return CreatureAnimationView(ctx)
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
import pyray as rl
|
|
7
|
+
|
|
8
|
+
from grim.audio import AudioState, init_audio_state, shutdown_audio, update_audio
|
|
9
|
+
from grim.config import ensure_crimson_cfg
|
|
10
|
+
from grim.console import ConsoleLog, ConsoleState
|
|
11
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
12
|
+
from grim.view import View, ViewContext
|
|
13
|
+
|
|
14
|
+
from ..creatures.spawn import SpawnId
|
|
15
|
+
from ..game_modes import GameMode
|
|
16
|
+
from ..game_world import GameWorld
|
|
17
|
+
from ..gameplay import PlayerInput, weapon_assign_player
|
|
18
|
+
from ..paths import default_runtime_dir
|
|
19
|
+
from ..projectiles import ProjectileTypeId
|
|
20
|
+
from ..ui.cursor import draw_aim_cursor
|
|
21
|
+
from ..weapon_sfx import resolve_weapon_sfx_ref
|
|
22
|
+
from ..weapons import (
|
|
23
|
+
WEAPON_BY_ID,
|
|
24
|
+
WEAPON_TABLE,
|
|
25
|
+
Weapon,
|
|
26
|
+
projectile_type_id_from_weapon_id,
|
|
27
|
+
)
|
|
28
|
+
from .registry import register_view
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
WORLD_SIZE = 1024.0
|
|
32
|
+
|
|
33
|
+
BG = rl.Color(10, 10, 12, 255)
|
|
34
|
+
|
|
35
|
+
UI_TEXT = rl.Color(235, 235, 235, 255)
|
|
36
|
+
UI_HINT = rl.Color(180, 180, 180, 255)
|
|
37
|
+
UI_ERROR = rl.Color(240, 80, 80, 255)
|
|
38
|
+
|
|
39
|
+
DEFAULT_SPAWN_IDS = (
|
|
40
|
+
SpawnId.ZOMBIE_CONST_GREY_42,
|
|
41
|
+
SpawnId.ZOMBIE_CONST_GREEN_BRUTE_43,
|
|
42
|
+
SpawnId.LIZARD_CONST_GREY_2F,
|
|
43
|
+
SpawnId.LIZARD_CONST_YELLOW_BOSS_30,
|
|
44
|
+
SpawnId.ALIEN_CONST_GREEN_24,
|
|
45
|
+
SpawnId.ALIEN_CONST_GREY_BRUTE_29,
|
|
46
|
+
SpawnId.ALIEN_CONST_RED_FAST_2B,
|
|
47
|
+
SpawnId.SPIDER_SP1_CONST_BLUE_40,
|
|
48
|
+
SpawnId.SPIDER_SP1_CONST_WHITE_FAST_3E,
|
|
49
|
+
SpawnId.SPIDER_SP2_RANDOM_35,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
SPECIAL_PROJECTILES: dict[int, str] = {
|
|
53
|
+
9: "particle style 0 (plasma rifle)",
|
|
54
|
+
13: "secondary type 1 (seeker rockets)",
|
|
55
|
+
14: "secondary type 2 (plasma shotgun)",
|
|
56
|
+
16: "particle style 1 (hr flamer)",
|
|
57
|
+
17: "particle style 2 (mini-rocket swarmers)",
|
|
58
|
+
18: "secondary type 2 (rocket minigun)",
|
|
59
|
+
19: "secondary type 4 (pulse gun)",
|
|
60
|
+
43: "particle style 8 (rainbow gun)",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
65
|
+
if value < lo:
|
|
66
|
+
return lo
|
|
67
|
+
if value > hi:
|
|
68
|
+
return hi
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _fmt_float(value: float | None, *, digits: int = 3) -> str:
|
|
73
|
+
if value is None:
|
|
74
|
+
return "—"
|
|
75
|
+
return f"{float(value):.{digits}f}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _fmt_int(value: int | None) -> str:
|
|
79
|
+
if value is None:
|
|
80
|
+
return "—"
|
|
81
|
+
return f"{int(value)}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _fmt_hex(value: int | None) -> str:
|
|
85
|
+
if value is None:
|
|
86
|
+
return "—"
|
|
87
|
+
value = int(value)
|
|
88
|
+
return f"0x{value:02x} ({value})"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _projectile_type_label(type_id: int) -> str:
|
|
92
|
+
try:
|
|
93
|
+
name = ProjectileTypeId(int(type_id)).name.lower().replace("_", " ")
|
|
94
|
+
except ValueError:
|
|
95
|
+
name = "unknown"
|
|
96
|
+
return f"{name} (id {type_id})"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ArsenalDebugView:
|
|
100
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
101
|
+
self._assets_root = ctx.assets_dir
|
|
102
|
+
self._missing_assets: list[str] = []
|
|
103
|
+
self._small: SmallFontData | None = None
|
|
104
|
+
|
|
105
|
+
self._world = GameWorld(
|
|
106
|
+
assets_dir=ctx.assets_dir,
|
|
107
|
+
world_size=WORLD_SIZE,
|
|
108
|
+
demo_mode_active=False,
|
|
109
|
+
difficulty_level=0,
|
|
110
|
+
hardcore=False,
|
|
111
|
+
)
|
|
112
|
+
self._player = self._world.players[0] if self._world.players else None
|
|
113
|
+
self._aim_texture: rl.Texture | None = None
|
|
114
|
+
self._audio: AudioState | None = None
|
|
115
|
+
self._audio_rng: random.Random | None = None
|
|
116
|
+
self._console: ConsoleState | None = None
|
|
117
|
+
|
|
118
|
+
self._weapon_ids = sorted({int(entry.weapon_id) for entry in WEAPON_TABLE})
|
|
119
|
+
self._weapon_index = 0
|
|
120
|
+
self._spawn_ids = [int(spawn_id) for spawn_id in DEFAULT_SPAWN_IDS]
|
|
121
|
+
self._spawn_ring_radius = 280.0
|
|
122
|
+
|
|
123
|
+
self.close_requested = False
|
|
124
|
+
self._paused = False
|
|
125
|
+
self._screenshot_requested = False
|
|
126
|
+
|
|
127
|
+
def _ui_line_height(self, scale: float = 1.0) -> int:
|
|
128
|
+
if self._small is not None:
|
|
129
|
+
return int(self._small.cell_size * scale)
|
|
130
|
+
return int(20 * scale)
|
|
131
|
+
|
|
132
|
+
def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color, scale: float = 1.0) -> None:
|
|
133
|
+
if self._small is not None:
|
|
134
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
135
|
+
else:
|
|
136
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
137
|
+
|
|
138
|
+
def _selected_weapon_id(self) -> int:
|
|
139
|
+
if not self._weapon_ids:
|
|
140
|
+
return 0
|
|
141
|
+
return int(self._weapon_ids[self._weapon_index % len(self._weapon_ids)])
|
|
142
|
+
|
|
143
|
+
def _apply_weapon(self) -> None:
|
|
144
|
+
if self._player is None:
|
|
145
|
+
return
|
|
146
|
+
weapon_assign_player(self._player, self._selected_weapon_id())
|
|
147
|
+
|
|
148
|
+
def _reset_scene(self) -> None:
|
|
149
|
+
self._world.reset(seed=0xBEEF, player_count=1, spawn_x=WORLD_SIZE * 0.5, spawn_y=WORLD_SIZE * 0.5)
|
|
150
|
+
self._player = self._world.players[0] if self._world.players else None
|
|
151
|
+
self._apply_weapon()
|
|
152
|
+
self._reset_creatures()
|
|
153
|
+
self._world.update_camera(0.0)
|
|
154
|
+
|
|
155
|
+
def _reset_creatures(self) -> None:
|
|
156
|
+
self._world.creatures.reset()
|
|
157
|
+
self._world.state.projectiles.reset()
|
|
158
|
+
self._world.state.secondary_projectiles.reset()
|
|
159
|
+
self._world.state.particles.reset()
|
|
160
|
+
self._world.state.sprite_effects.reset()
|
|
161
|
+
self._world.state.effects.reset()
|
|
162
|
+
self._world.state.bonus_pool.reset()
|
|
163
|
+
self._world.fx_queue.clear()
|
|
164
|
+
self._world.fx_queue_rotated.clear()
|
|
165
|
+
|
|
166
|
+
player = self._player
|
|
167
|
+
if player is None:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
count = max(1, len(self._spawn_ids))
|
|
171
|
+
base_x = float(player.pos_x)
|
|
172
|
+
base_y = float(player.pos_y)
|
|
173
|
+
for idx in range(count):
|
|
174
|
+
spawn_id = int(self._spawn_ids[idx % len(self._spawn_ids)])
|
|
175
|
+
angle = float(idx) / float(count) * math.tau
|
|
176
|
+
x = _clamp(base_x + math.cos(angle) * self._spawn_ring_radius, 48.0, WORLD_SIZE - 48.0)
|
|
177
|
+
y = _clamp(base_y + math.sin(angle) * self._spawn_ring_radius, 48.0, WORLD_SIZE - 48.0)
|
|
178
|
+
heading = angle + math.pi
|
|
179
|
+
self._world.creatures.spawn_template(
|
|
180
|
+
spawn_id,
|
|
181
|
+
(x, y),
|
|
182
|
+
heading,
|
|
183
|
+
self._world.state.rng,
|
|
184
|
+
rand=self._world.state.rng.rand,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _handle_debug_input(self) -> None:
|
|
188
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
189
|
+
self.close_requested = True
|
|
190
|
+
|
|
191
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
|
|
192
|
+
self._paused = not self._paused
|
|
193
|
+
|
|
194
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
|
|
195
|
+
self._weapon_index = (self._weapon_index - 1) % max(1, len(self._weapon_ids))
|
|
196
|
+
self._apply_weapon()
|
|
197
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
|
|
198
|
+
self._weapon_index = (self._weapon_index + 1) % max(1, len(self._weapon_ids))
|
|
199
|
+
self._apply_weapon()
|
|
200
|
+
|
|
201
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_T):
|
|
202
|
+
self._reset_creatures()
|
|
203
|
+
|
|
204
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
|
|
205
|
+
self._reset_scene()
|
|
206
|
+
|
|
207
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_P):
|
|
208
|
+
self._screenshot_requested = True
|
|
209
|
+
|
|
210
|
+
def _build_input(self) -> PlayerInput:
|
|
211
|
+
move_x = 0.0
|
|
212
|
+
move_y = 0.0
|
|
213
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_A):
|
|
214
|
+
move_x -= 1.0
|
|
215
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_D):
|
|
216
|
+
move_x += 1.0
|
|
217
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_W):
|
|
218
|
+
move_y -= 1.0
|
|
219
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_S):
|
|
220
|
+
move_y += 1.0
|
|
221
|
+
|
|
222
|
+
mouse = rl.get_mouse_position()
|
|
223
|
+
aim_x, aim_y = self._world.screen_to_world(float(mouse.x), float(mouse.y))
|
|
224
|
+
|
|
225
|
+
fire_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
226
|
+
fire_pressed = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
227
|
+
reload_pressed = rl.is_key_pressed(rl.KeyboardKey.KEY_R)
|
|
228
|
+
|
|
229
|
+
return PlayerInput(
|
|
230
|
+
move_x=move_x,
|
|
231
|
+
move_y=move_y,
|
|
232
|
+
aim_x=float(aim_x),
|
|
233
|
+
aim_y=float(aim_y),
|
|
234
|
+
fire_down=fire_down,
|
|
235
|
+
fire_pressed=fire_pressed,
|
|
236
|
+
reload_pressed=reload_pressed,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _weapon_projectile_desc(self, weapon_id: int) -> str:
|
|
240
|
+
special = SPECIAL_PROJECTILES.get(int(weapon_id))
|
|
241
|
+
if special is not None:
|
|
242
|
+
return special
|
|
243
|
+
type_id = projectile_type_id_from_weapon_id(int(weapon_id))
|
|
244
|
+
if type_id is None:
|
|
245
|
+
return "particle/secondary"
|
|
246
|
+
return _projectile_type_label(type_id)
|
|
247
|
+
|
|
248
|
+
def _weapon_debug_lines(self, weapon: Weapon | None) -> list[str]:
|
|
249
|
+
player = self._player
|
|
250
|
+
if player is None:
|
|
251
|
+
return ["Arsenal debug: missing player"]
|
|
252
|
+
|
|
253
|
+
weapon_id = int(player.weapon_id)
|
|
254
|
+
name = weapon.name if weapon is not None and weapon.name else f"weapon_{weapon_id}"
|
|
255
|
+
index_label = f"{self._weapon_index + 1}/{max(1, len(self._weapon_ids))}"
|
|
256
|
+
|
|
257
|
+
lines = [
|
|
258
|
+
"Arsenal",
|
|
259
|
+
f"{name} (id {weapon_id}) [{index_label}]",
|
|
260
|
+
f"projectile: {self._weapon_projectile_desc(weapon_id)}",
|
|
261
|
+
f"ammo {player.ammo:.1f}/{player.clip_size:.1f} reload {player.reload_timer:.2f}/{player.reload_timer_max:.2f}",
|
|
262
|
+
f"shot_cd {player.shot_cooldown:.3f} spread {player.spread_heat:.3f} muzzle {player.muzzle_flash_alpha:.2f}",
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
if weapon is None:
|
|
266
|
+
return lines
|
|
267
|
+
|
|
268
|
+
fire_sfx = resolve_weapon_sfx_ref(weapon.fire_sound)
|
|
269
|
+
reload_sfx = resolve_weapon_sfx_ref(weapon.reload_sound)
|
|
270
|
+
lines.extend(
|
|
271
|
+
[
|
|
272
|
+
f"clip { _fmt_int(weapon.clip_size) } reload { _fmt_float(weapon.reload_time) } cooldown { _fmt_float(weapon.shot_cooldown) }",
|
|
273
|
+
f"pellets { _fmt_int(weapon.pellet_count) } spread_inc { _fmt_float(weapon.spread_heat_inc) } dmg_scale { _fmt_float(weapon.damage_scale) } meta { _fmt_int(weapon.projectile_meta) }",
|
|
274
|
+
f"ammo_class { _fmt_int(weapon.ammo_class) } flags { _fmt_hex(weapon.flags) } icon { _fmt_int(weapon.icon_index) }",
|
|
275
|
+
f"sfx fire {fire_sfx or '—'} reload {reload_sfx or '—'}",
|
|
276
|
+
]
|
|
277
|
+
)
|
|
278
|
+
return lines
|
|
279
|
+
|
|
280
|
+
def open(self) -> None:
|
|
281
|
+
self._missing_assets.clear()
|
|
282
|
+
try:
|
|
283
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
284
|
+
except Exception:
|
|
285
|
+
self._small = None
|
|
286
|
+
|
|
287
|
+
runtime_dir = default_runtime_dir()
|
|
288
|
+
if runtime_dir.is_dir():
|
|
289
|
+
try:
|
|
290
|
+
self._world.config = ensure_crimson_cfg(runtime_dir)
|
|
291
|
+
except Exception:
|
|
292
|
+
self._world.config = None
|
|
293
|
+
else:
|
|
294
|
+
self._world.config = None
|
|
295
|
+
|
|
296
|
+
if self._world.config is not None:
|
|
297
|
+
try:
|
|
298
|
+
self._console = ConsoleState(
|
|
299
|
+
base_dir=runtime_dir,
|
|
300
|
+
log=ConsoleLog(base_dir=runtime_dir),
|
|
301
|
+
assets_dir=self._assets_root,
|
|
302
|
+
)
|
|
303
|
+
self._audio = init_audio_state(self._world.config, self._assets_root, self._console)
|
|
304
|
+
self._audio_rng = random.Random(0xBEEF)
|
|
305
|
+
self._world.audio = self._audio
|
|
306
|
+
self._world.audio_rng = self._audio_rng
|
|
307
|
+
except Exception:
|
|
308
|
+
self._audio = None
|
|
309
|
+
self._audio_rng = None
|
|
310
|
+
self._console = None
|
|
311
|
+
self._world.audio = None
|
|
312
|
+
self._world.audio_rng = None
|
|
313
|
+
|
|
314
|
+
self._world.open()
|
|
315
|
+
self._aim_texture = self._world._load_texture(
|
|
316
|
+
"ui_aim",
|
|
317
|
+
cache_path="ui/ui_aim.jaz",
|
|
318
|
+
file_path="ui/ui_aim.png",
|
|
319
|
+
)
|
|
320
|
+
self._reset_scene()
|
|
321
|
+
rl.hide_cursor()
|
|
322
|
+
|
|
323
|
+
def close(self) -> None:
|
|
324
|
+
rl.show_cursor()
|
|
325
|
+
if self._small is not None:
|
|
326
|
+
rl.unload_texture(self._small.texture)
|
|
327
|
+
self._small = None
|
|
328
|
+
if self._audio is not None:
|
|
329
|
+
shutdown_audio(self._audio)
|
|
330
|
+
self._audio = None
|
|
331
|
+
self._audio_rng = None
|
|
332
|
+
self._console = None
|
|
333
|
+
self._world.audio = None
|
|
334
|
+
self._world.audio_rng = None
|
|
335
|
+
self._world.close()
|
|
336
|
+
self._aim_texture = None
|
|
337
|
+
|
|
338
|
+
def consume_screenshot_request(self) -> bool:
|
|
339
|
+
requested = self._screenshot_requested
|
|
340
|
+
self._screenshot_requested = False
|
|
341
|
+
return requested
|
|
342
|
+
|
|
343
|
+
def update(self, dt: float) -> None:
|
|
344
|
+
self._handle_debug_input()
|
|
345
|
+
|
|
346
|
+
if self._paused:
|
|
347
|
+
dt = 0.0
|
|
348
|
+
|
|
349
|
+
player = self._player
|
|
350
|
+
if player is None:
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
input_state = self._build_input()
|
|
354
|
+
self._world.update(dt, inputs=[input_state], game_mode=int(GameMode.SURVIVAL))
|
|
355
|
+
|
|
356
|
+
if self._audio is not None:
|
|
357
|
+
update_audio(self._audio, dt)
|
|
358
|
+
|
|
359
|
+
def draw(self) -> None:
|
|
360
|
+
rl.clear_background(BG)
|
|
361
|
+
|
|
362
|
+
if self._world.ground is not None:
|
|
363
|
+
self._world._sync_ground_settings()
|
|
364
|
+
self._world.ground.process_pending()
|
|
365
|
+
|
|
366
|
+
self._world.draw(draw_aim_indicators=True)
|
|
367
|
+
|
|
368
|
+
warn_x = 24.0
|
|
369
|
+
warn_y = 24.0
|
|
370
|
+
warn_line = float(self._ui_line_height())
|
|
371
|
+
if self._missing_assets:
|
|
372
|
+
self._draw_ui_text("Missing assets (ui): " + ", ".join(self._missing_assets), warn_x, warn_y, UI_ERROR)
|
|
373
|
+
warn_y += warn_line
|
|
374
|
+
if self._world.missing_assets:
|
|
375
|
+
self._draw_ui_text(
|
|
376
|
+
"Missing assets (world): " + ", ".join(self._world.missing_assets),
|
|
377
|
+
warn_x,
|
|
378
|
+
warn_y,
|
|
379
|
+
UI_ERROR,
|
|
380
|
+
)
|
|
381
|
+
warn_y += warn_line
|
|
382
|
+
|
|
383
|
+
x = 16.0
|
|
384
|
+
y = 12.0
|
|
385
|
+
line = float(self._ui_line_height())
|
|
386
|
+
|
|
387
|
+
weapon = WEAPON_BY_ID.get(int(self._player.weapon_id)) if self._player is not None else None
|
|
388
|
+
for text in self._weapon_debug_lines(weapon):
|
|
389
|
+
self._draw_ui_text(text, x, y, UI_TEXT)
|
|
390
|
+
y += line
|
|
391
|
+
|
|
392
|
+
if self._player is not None:
|
|
393
|
+
alive = sum(1 for c in self._world.creatures.entries if c.active and c.hp > 0.0)
|
|
394
|
+
total = sum(1 for c in self._world.creatures.entries if c.active)
|
|
395
|
+
self._draw_ui_text(f"creatures alive {alive}/{total}", x, y, UI_TEXT)
|
|
396
|
+
y += line
|
|
397
|
+
|
|
398
|
+
y += 6.0
|
|
399
|
+
self._draw_ui_text(
|
|
400
|
+
"WASD move LMB fire R reload [/] cycle weapons Space pause T respawn Backspace reset Esc quit",
|
|
401
|
+
x,
|
|
402
|
+
y,
|
|
403
|
+
UI_HINT,
|
|
404
|
+
)
|
|
405
|
+
y += line
|
|
406
|
+
self._draw_ui_text("P screenshot", x, y, UI_HINT)
|
|
407
|
+
|
|
408
|
+
mouse = rl.get_mouse_position()
|
|
409
|
+
draw_aim_cursor(self._world.particles_texture, self._aim_texture, x=float(mouse.x), y=float(mouse.y))
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@register_view("arsenal", "Arsenal")
|
|
413
|
+
def build_arsenal_debug_view(ctx: ViewContext) -> View:
|
|
414
|
+
return ArsenalDebugView(ctx)
|