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
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import math
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
import pyray as rl
|
|
8
|
+
|
|
9
|
+
from grim.audio import AudioState, shutdown_audio
|
|
10
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
11
|
+
from grim.view import View, ViewContext
|
|
12
|
+
|
|
13
|
+
from ..game_world import GameWorld
|
|
14
|
+
from ..gameplay import PlayerInput, player_update, weapon_assign_player
|
|
15
|
+
from ..ui.cursor import draw_aim_cursor
|
|
16
|
+
from ..weapons import WEAPON_TABLE
|
|
17
|
+
from .audio_bootstrap import init_view_audio
|
|
18
|
+
from .registry import register_view
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
WORLD_SIZE = 1024.0
|
|
22
|
+
|
|
23
|
+
BG = rl.Color(10, 10, 12, 255)
|
|
24
|
+
GRID_COLOR = rl.Color(255, 255, 255, 14)
|
|
25
|
+
|
|
26
|
+
UI_TEXT = rl.Color(235, 235, 235, 255)
|
|
27
|
+
UI_HINT = rl.Color(180, 180, 180, 255)
|
|
28
|
+
UI_ERROR = rl.Color(240, 80, 80, 255)
|
|
29
|
+
|
|
30
|
+
TARGET_FILL = rl.Color(220, 80, 80, 220)
|
|
31
|
+
TARGET_OUTLINE = rl.Color(140, 40, 40, 255)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True)
|
|
35
|
+
class TargetDummy:
|
|
36
|
+
x: float
|
|
37
|
+
y: float
|
|
38
|
+
hp: float
|
|
39
|
+
size: float = 56.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
43
|
+
if value < lo:
|
|
44
|
+
return lo
|
|
45
|
+
if value > hi:
|
|
46
|
+
return hi
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProjectileRenderDebugView:
|
|
51
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
52
|
+
self._assets_root = ctx.assets_dir
|
|
53
|
+
self._missing_assets: list[str] = []
|
|
54
|
+
self._small: SmallFontData | None = None
|
|
55
|
+
|
|
56
|
+
self._world = GameWorld(
|
|
57
|
+
assets_dir=ctx.assets_dir,
|
|
58
|
+
world_size=WORLD_SIZE,
|
|
59
|
+
demo_mode_active=False,
|
|
60
|
+
difficulty_level=0,
|
|
61
|
+
hardcore=False,
|
|
62
|
+
)
|
|
63
|
+
self._player = self._world.players[0] if self._world.players else None
|
|
64
|
+
self._aim_texture: rl.Texture | None = None
|
|
65
|
+
self._audio: AudioState | None = None
|
|
66
|
+
self._audio_rng: random.Random | None = None
|
|
67
|
+
self._console: ConsoleState | None = None
|
|
68
|
+
|
|
69
|
+
self._weapon_ids = [entry.weapon_id for entry in WEAPON_TABLE if entry.name is not None]
|
|
70
|
+
self._weapon_index = 0
|
|
71
|
+
|
|
72
|
+
self._targets: list[TargetDummy] = []
|
|
73
|
+
|
|
74
|
+
self.close_requested = False
|
|
75
|
+
self._paused = False
|
|
76
|
+
self._screenshot_requested = False
|
|
77
|
+
|
|
78
|
+
def _ui_line_height(self, scale: float = 1.0) -> int:
|
|
79
|
+
if self._small is not None:
|
|
80
|
+
return int(self._small.cell_size * scale)
|
|
81
|
+
return int(20 * scale)
|
|
82
|
+
|
|
83
|
+
def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color, scale: float = 1.0) -> None:
|
|
84
|
+
if self._small is not None:
|
|
85
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
86
|
+
else:
|
|
87
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
88
|
+
|
|
89
|
+
def _selected_weapon_id(self) -> int:
|
|
90
|
+
if not self._weapon_ids:
|
|
91
|
+
return 0
|
|
92
|
+
return int(self._weapon_ids[self._weapon_index % len(self._weapon_ids)])
|
|
93
|
+
|
|
94
|
+
def _apply_weapon(self) -> None:
|
|
95
|
+
if self._player is None:
|
|
96
|
+
return
|
|
97
|
+
weapon_assign_player(self._player, self._selected_weapon_id())
|
|
98
|
+
|
|
99
|
+
def _reset_targets(self) -> None:
|
|
100
|
+
self._targets.clear()
|
|
101
|
+
base_x = WORLD_SIZE * 0.5
|
|
102
|
+
base_y = WORLD_SIZE * 0.5
|
|
103
|
+
ring = 260.0
|
|
104
|
+
for idx in range(10):
|
|
105
|
+
angle = float(idx) / 10.0 * math.tau
|
|
106
|
+
x = _clamp(base_x + math.cos(angle) * ring, 40.0, WORLD_SIZE - 40.0)
|
|
107
|
+
y = _clamp(base_y + math.sin(angle) * ring, 40.0, WORLD_SIZE - 40.0)
|
|
108
|
+
self._targets.append(TargetDummy(x=x, y=y, hp=260.0, size=64.0))
|
|
109
|
+
|
|
110
|
+
def _reset_scene(self) -> None:
|
|
111
|
+
self._world.reset(seed=0xBEEF, player_count=1, spawn_x=WORLD_SIZE * 0.5, spawn_y=WORLD_SIZE * 0.5)
|
|
112
|
+
self._player = self._world.players[0] if self._world.players else None
|
|
113
|
+
self._weapon_index = 0
|
|
114
|
+
self._apply_weapon()
|
|
115
|
+
self._reset_targets()
|
|
116
|
+
self._world.update_camera(0.0)
|
|
117
|
+
|
|
118
|
+
def _world_scale(self) -> float:
|
|
119
|
+
_cam_x, _cam_y, scale_x, scale_y = self._world._world_params()
|
|
120
|
+
return (scale_x + scale_y) * 0.5
|
|
121
|
+
|
|
122
|
+
def _draw_grid(self) -> None:
|
|
123
|
+
step = 64.0
|
|
124
|
+
out_w = float(rl.get_screen_width())
|
|
125
|
+
out_h = float(rl.get_screen_height())
|
|
126
|
+
screen_w, screen_h = self._world._camera_screen_size()
|
|
127
|
+
cam_x, cam_y, scale_x, scale_y = self._world._world_params()
|
|
128
|
+
|
|
129
|
+
start_x = math.floor((-cam_x) / step) * step
|
|
130
|
+
end_x = (-cam_x) + screen_w
|
|
131
|
+
x = start_x
|
|
132
|
+
while x <= end_x:
|
|
133
|
+
sx = int((x + cam_x) * scale_x)
|
|
134
|
+
rl.draw_line(sx, 0, sx, int(out_h), GRID_COLOR)
|
|
135
|
+
x += step
|
|
136
|
+
|
|
137
|
+
start_y = math.floor((-cam_y) / step) * step
|
|
138
|
+
end_y = (-cam_y) + screen_h
|
|
139
|
+
y = start_y
|
|
140
|
+
while y <= end_y:
|
|
141
|
+
sy = int((y + cam_y) * scale_y)
|
|
142
|
+
rl.draw_line(0, sy, int(out_w), sy, GRID_COLOR)
|
|
143
|
+
y += step
|
|
144
|
+
|
|
145
|
+
def _handle_debug_input(self) -> None:
|
|
146
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
147
|
+
self.close_requested = True
|
|
148
|
+
|
|
149
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
|
|
150
|
+
self._paused = not self._paused
|
|
151
|
+
|
|
152
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
|
|
153
|
+
self._weapon_index = (self._weapon_index - 1) % max(1, len(self._weapon_ids))
|
|
154
|
+
self._apply_weapon()
|
|
155
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
|
|
156
|
+
self._weapon_index = (self._weapon_index + 1) % max(1, len(self._weapon_ids))
|
|
157
|
+
self._apply_weapon()
|
|
158
|
+
|
|
159
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_T):
|
|
160
|
+
self._reset_targets()
|
|
161
|
+
|
|
162
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
|
|
163
|
+
self._reset_scene()
|
|
164
|
+
|
|
165
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_P):
|
|
166
|
+
self._screenshot_requested = True
|
|
167
|
+
|
|
168
|
+
def _build_input(self) -> PlayerInput:
|
|
169
|
+
move_x = 0.0
|
|
170
|
+
move_y = 0.0
|
|
171
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_A):
|
|
172
|
+
move_x -= 1.0
|
|
173
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_D):
|
|
174
|
+
move_x += 1.0
|
|
175
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_W):
|
|
176
|
+
move_y -= 1.0
|
|
177
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_S):
|
|
178
|
+
move_y += 1.0
|
|
179
|
+
|
|
180
|
+
mouse = rl.get_mouse_position()
|
|
181
|
+
aim_x, aim_y = self._world.screen_to_world(float(mouse.x), float(mouse.y))
|
|
182
|
+
|
|
183
|
+
fire_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
184
|
+
fire_pressed = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
185
|
+
reload_pressed = rl.is_key_pressed(rl.KeyboardKey.KEY_R)
|
|
186
|
+
|
|
187
|
+
return PlayerInput(
|
|
188
|
+
move_x=move_x,
|
|
189
|
+
move_y=move_y,
|
|
190
|
+
aim_x=float(aim_x),
|
|
191
|
+
aim_y=float(aim_y),
|
|
192
|
+
fire_down=fire_down,
|
|
193
|
+
fire_pressed=fire_pressed,
|
|
194
|
+
reload_pressed=reload_pressed,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def open(self) -> None:
|
|
198
|
+
self._missing_assets.clear()
|
|
199
|
+
try:
|
|
200
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
201
|
+
except Exception:
|
|
202
|
+
self._small = None
|
|
203
|
+
|
|
204
|
+
bootstrap = init_view_audio(self._assets_root)
|
|
205
|
+
self._world.config = bootstrap.config
|
|
206
|
+
self._console = bootstrap.console
|
|
207
|
+
self._audio = bootstrap.audio
|
|
208
|
+
self._audio_rng = bootstrap.audio_rng
|
|
209
|
+
self._world.audio = self._audio
|
|
210
|
+
self._world.audio_rng = self._audio_rng
|
|
211
|
+
|
|
212
|
+
self._world.open()
|
|
213
|
+
self._aim_texture = self._world._load_texture(
|
|
214
|
+
"ui_aim",
|
|
215
|
+
cache_path="ui/ui_aim.jaz",
|
|
216
|
+
file_path="ui/ui_aim.png",
|
|
217
|
+
)
|
|
218
|
+
self._reset_scene()
|
|
219
|
+
rl.hide_cursor()
|
|
220
|
+
|
|
221
|
+
def close(self) -> None:
|
|
222
|
+
rl.show_cursor()
|
|
223
|
+
if self._small is not None:
|
|
224
|
+
rl.unload_texture(self._small.texture)
|
|
225
|
+
self._small = None
|
|
226
|
+
if self._audio is not None:
|
|
227
|
+
shutdown_audio(self._audio)
|
|
228
|
+
self._audio = None
|
|
229
|
+
self._audio_rng = None
|
|
230
|
+
self._console = None
|
|
231
|
+
self._world.audio = None
|
|
232
|
+
self._world.audio_rng = None
|
|
233
|
+
self._world.close()
|
|
234
|
+
self._aim_texture = None
|
|
235
|
+
|
|
236
|
+
def consume_screenshot_request(self) -> bool:
|
|
237
|
+
requested = self._screenshot_requested
|
|
238
|
+
self._screenshot_requested = False
|
|
239
|
+
return requested
|
|
240
|
+
|
|
241
|
+
def update(self, dt: float) -> None:
|
|
242
|
+
self._handle_debug_input()
|
|
243
|
+
|
|
244
|
+
if self._paused:
|
|
245
|
+
dt = 0.0
|
|
246
|
+
|
|
247
|
+
if self._world.ground is not None:
|
|
248
|
+
self._world._sync_ground_settings()
|
|
249
|
+
self._world.ground.process_pending()
|
|
250
|
+
|
|
251
|
+
if self._player is None:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
prev_audio = None
|
|
255
|
+
if self._world.audio is not None:
|
|
256
|
+
prev_audio = (int(self._player.shot_seq), bool(self._player.reload_active), float(self._player.reload_timer))
|
|
257
|
+
|
|
258
|
+
detail_preset = 5
|
|
259
|
+
if self._world.config is not None:
|
|
260
|
+
detail_preset = int(self._world.config.data.get("detail_preset", 5) or 5)
|
|
261
|
+
|
|
262
|
+
# Keep the scene stable: targets are static, only projectiles + player advance.
|
|
263
|
+
hits = self._world.state.projectiles.update(
|
|
264
|
+
float(dt),
|
|
265
|
+
self._targets,
|
|
266
|
+
world_size=WORLD_SIZE,
|
|
267
|
+
damage_scale_by_type=self._world._damage_scale_by_type,
|
|
268
|
+
detail_preset=int(detail_preset),
|
|
269
|
+
rng=self._world.state.rng.rand,
|
|
270
|
+
runtime_state=self._world.state,
|
|
271
|
+
)
|
|
272
|
+
self._world.state.secondary_projectiles.update_pulse_gun(float(dt), self._targets)
|
|
273
|
+
if hits:
|
|
274
|
+
self._world._queue_projectile_decals(hits)
|
|
275
|
+
self._world._play_hit_sfx(hits, game_mode=1)
|
|
276
|
+
self._targets = [target for target in self._targets if target.hp > 0.0]
|
|
277
|
+
|
|
278
|
+
input_state = self._build_input()
|
|
279
|
+
player_update(self._player, input_state, float(dt), self._world.state, world_size=WORLD_SIZE)
|
|
280
|
+
|
|
281
|
+
if prev_audio is not None:
|
|
282
|
+
prev_shot_seq, prev_reload_active, prev_reload_timer = prev_audio
|
|
283
|
+
self._world._handle_player_audio(
|
|
284
|
+
self._player,
|
|
285
|
+
prev_shot_seq=prev_shot_seq,
|
|
286
|
+
prev_reload_active=prev_reload_active,
|
|
287
|
+
prev_reload_timer=prev_reload_timer,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
self._world._bake_fx_queues()
|
|
291
|
+
self._world.update_camera(float(dt))
|
|
292
|
+
|
|
293
|
+
def draw(self) -> None:
|
|
294
|
+
rl.clear_background(BG)
|
|
295
|
+
|
|
296
|
+
cam_x, cam_y, scale_x, scale_y = self._world._world_params()
|
|
297
|
+
screen_w, screen_h = self._world._camera_screen_size()
|
|
298
|
+
|
|
299
|
+
if self._world.ground is not None:
|
|
300
|
+
self._world.ground.draw(cam_x, cam_y, screen_w=screen_w, screen_h=screen_h)
|
|
301
|
+
|
|
302
|
+
warn_x = 24.0
|
|
303
|
+
warn_y = 24.0
|
|
304
|
+
warn_line = float(self._ui_line_height())
|
|
305
|
+
if self._missing_assets:
|
|
306
|
+
self._draw_ui_text("Missing assets (ui): " + ", ".join(self._missing_assets), warn_x, warn_y, UI_ERROR)
|
|
307
|
+
warn_y += warn_line
|
|
308
|
+
if self._world.missing_assets:
|
|
309
|
+
self._draw_ui_text(
|
|
310
|
+
"Missing assets (world): " + ", ".join(self._world.missing_assets),
|
|
311
|
+
warn_x,
|
|
312
|
+
warn_y,
|
|
313
|
+
UI_ERROR,
|
|
314
|
+
)
|
|
315
|
+
warn_y += warn_line
|
|
316
|
+
|
|
317
|
+
scale = self._world_scale()
|
|
318
|
+
|
|
319
|
+
self._draw_grid()
|
|
320
|
+
|
|
321
|
+
# Targets.
|
|
322
|
+
for target in self._targets:
|
|
323
|
+
sx, sy = self._world.world_to_screen(float(target.x), float(target.y))
|
|
324
|
+
radius = max(2.0, float(target.size) * 0.5 * scale)
|
|
325
|
+
rl.draw_circle(int(sx), int(sy), radius, TARGET_FILL)
|
|
326
|
+
rl.draw_circle_lines(int(sx), int(sy), int(max(1.0, radius)), TARGET_OUTLINE)
|
|
327
|
+
|
|
328
|
+
# Projectiles.
|
|
329
|
+
for proj_index, proj in enumerate(self._world.state.projectiles.entries):
|
|
330
|
+
if not proj.active:
|
|
331
|
+
continue
|
|
332
|
+
self._world._draw_projectile(proj, proj_index=proj_index, scale=scale)
|
|
333
|
+
for proj in self._world.state.secondary_projectiles.iter_active():
|
|
334
|
+
self._world._draw_secondary_projectile(proj, scale=scale)
|
|
335
|
+
|
|
336
|
+
# Player.
|
|
337
|
+
player = self._player
|
|
338
|
+
if player is not None:
|
|
339
|
+
texture = self._world.creature_textures.get("trooper")
|
|
340
|
+
if texture is not None:
|
|
341
|
+
self._world._draw_player_trooper_sprite(
|
|
342
|
+
texture,
|
|
343
|
+
player,
|
|
344
|
+
cam_x=cam_x,
|
|
345
|
+
cam_y=cam_y,
|
|
346
|
+
scale_x=scale_x,
|
|
347
|
+
scale_y=scale_y,
|
|
348
|
+
scale=scale,
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
px, py = self._world.world_to_screen(float(player.pos_x), float(player.pos_y))
|
|
352
|
+
rl.draw_circle(int(px), int(py), max(1.0, 14.0 * scale), rl.Color(90, 190, 120, 255))
|
|
353
|
+
|
|
354
|
+
if player is not None and player.health > 0.0:
|
|
355
|
+
aim_x = float(getattr(player, "aim_x", player.pos_x))
|
|
356
|
+
aim_y = float(getattr(player, "aim_y", player.pos_y))
|
|
357
|
+
dist = math.hypot(aim_x - float(player.pos_x), aim_y - float(player.pos_y))
|
|
358
|
+
radius = max(6.0, dist * float(getattr(player, "spread_heat", 0.0)) * 0.5)
|
|
359
|
+
screen_radius = max(1.0, radius * scale)
|
|
360
|
+
aim_screen_x, aim_screen_y = self._world.world_to_screen(aim_x, aim_y)
|
|
361
|
+
self._world._draw_aim_circle(x=aim_screen_x, y=aim_screen_y, radius=screen_radius)
|
|
362
|
+
|
|
363
|
+
# UI.
|
|
364
|
+
x = 16.0
|
|
365
|
+
y = 12.0
|
|
366
|
+
line = float(self._ui_line_height())
|
|
367
|
+
|
|
368
|
+
weapon_id = int(player.weapon_id) if player is not None else 0
|
|
369
|
+
weapon_name = next((w.name for w in WEAPON_TABLE if w.weapon_id == weapon_id), None) or f"weapon_{weapon_id}"
|
|
370
|
+
self._draw_ui_text("Projectile render debug", x, y, UI_TEXT)
|
|
371
|
+
y += line
|
|
372
|
+
self._draw_ui_text(f"{weapon_name} (weapon_id={weapon_id})", x, y, UI_TEXT)
|
|
373
|
+
y += line
|
|
374
|
+
if player is not None:
|
|
375
|
+
self._draw_ui_text(
|
|
376
|
+
f"ammo {player.ammo}/{player.clip_size} reload {player.reload_timer:.2f}/{player.reload_timer_max:.2f}",
|
|
377
|
+
x,
|
|
378
|
+
y,
|
|
379
|
+
UI_TEXT,
|
|
380
|
+
)
|
|
381
|
+
y += line
|
|
382
|
+
y += 6.0
|
|
383
|
+
self._draw_ui_text("WASD move LMB fire R reload [/] cycle weapons Space pause P screenshot", x, y, UI_HINT)
|
|
384
|
+
y += line
|
|
385
|
+
self._draw_ui_text("T reset targets Backspace reset scene Esc quit", x, y, UI_HINT)
|
|
386
|
+
|
|
387
|
+
mouse = rl.get_mouse_position()
|
|
388
|
+
draw_aim_cursor(self._world.particles_texture, self._aim_texture, x=float(mouse.x), y=float(mouse.y))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@register_view("projectile-render-debug", "Projectile render debug")
|
|
392
|
+
def build_projectile_render_debug_view(ctx: ViewContext) -> View:
|
|
393
|
+
return ProjectileRenderDebugView(ctx)
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
UI_KNOWN_COLOR = rl.Color(80, 160, 240, 255)
|
|
16
|
+
UI_HOVER_COLOR = rl.Color(240, 200, 80, 255)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class KnownProjectile:
|
|
21
|
+
type_id: int
|
|
22
|
+
grid: int
|
|
23
|
+
frame: int
|
|
24
|
+
label: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
KNOWN_PROJECTILES = [
|
|
28
|
+
KnownProjectile(type_id=0x13, grid=2, frame=0, label="Pulse Gun"),
|
|
29
|
+
KnownProjectile(type_id=0x1D, grid=4, frame=3, label="Splitter Gun"),
|
|
30
|
+
KnownProjectile(type_id=0x19, grid=4, frame=6, label="Blade Gun"),
|
|
31
|
+
KnownProjectile(type_id=0x15, grid=4, frame=2, label="Ion Rifle"),
|
|
32
|
+
KnownProjectile(type_id=0x16, grid=4, frame=2, label="Ion Minigun"),
|
|
33
|
+
KnownProjectile(type_id=0x17, grid=4, frame=2, label="Ion Cannon"),
|
|
34
|
+
KnownProjectile(type_id=0x18, grid=4, frame=2, label="Shrinkifier 5k"),
|
|
35
|
+
KnownProjectile(type_id=0x2D, grid=4, frame=2, label="Fire Bullets"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_known_map() -> dict[int, dict[int, list[KnownProjectile]]]:
|
|
40
|
+
known: dict[int, dict[int, list[KnownProjectile]]] = {}
|
|
41
|
+
for entry in KNOWN_PROJECTILES:
|
|
42
|
+
grid_map = known.setdefault(entry.grid, {})
|
|
43
|
+
grid_map.setdefault(entry.frame, []).append(entry)
|
|
44
|
+
return known
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
KNOWN_BY_GRID = _build_known_map()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProjectileView:
|
|
51
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
52
|
+
self._assets_root = ctx.assets_dir
|
|
53
|
+
self._missing_assets: list[str] = []
|
|
54
|
+
self._texture: rl.Texture | None = None
|
|
55
|
+
self._small: SmallFontData | None = None
|
|
56
|
+
self._grid = 4
|
|
57
|
+
|
|
58
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
59
|
+
if self._small is not None:
|
|
60
|
+
return int(self._small.cell_size * scale)
|
|
61
|
+
return int(20 * scale)
|
|
62
|
+
|
|
63
|
+
def _draw_ui_text(
|
|
64
|
+
self,
|
|
65
|
+
text: str,
|
|
66
|
+
x: float,
|
|
67
|
+
y: float,
|
|
68
|
+
color: rl.Color,
|
|
69
|
+
scale: float = UI_TEXT_SCALE,
|
|
70
|
+
) -> None:
|
|
71
|
+
if self._small is not None:
|
|
72
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
73
|
+
else:
|
|
74
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
75
|
+
|
|
76
|
+
def open(self) -> None:
|
|
77
|
+
self._missing_assets.clear()
|
|
78
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
79
|
+
path = self._assets_root / "crimson" / "game" / "projs.png"
|
|
80
|
+
if not path.is_file():
|
|
81
|
+
self._missing_assets.append("game/projs.png")
|
|
82
|
+
raise FileNotFoundError(f"Missing asset: {path}")
|
|
83
|
+
self._texture = rl.load_texture(str(path))
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
if self._texture is not None:
|
|
87
|
+
rl.unload_texture(self._texture)
|
|
88
|
+
self._texture = None
|
|
89
|
+
if self._small is not None:
|
|
90
|
+
rl.unload_texture(self._small.texture)
|
|
91
|
+
self._small = None
|
|
92
|
+
|
|
93
|
+
def update(self, dt: float) -> None:
|
|
94
|
+
del dt
|
|
95
|
+
|
|
96
|
+
def _handle_input(self) -> None:
|
|
97
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_TWO):
|
|
98
|
+
self._grid = 2
|
|
99
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_FOUR):
|
|
100
|
+
self._grid = 4
|
|
101
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_G):
|
|
102
|
+
self._grid = 2 if self._grid == 4 else 4
|
|
103
|
+
|
|
104
|
+
def draw(self) -> None:
|
|
105
|
+
rl.clear_background(rl.Color(12, 12, 14, 255))
|
|
106
|
+
if self._missing_assets:
|
|
107
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
108
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
109
|
+
return
|
|
110
|
+
if self._texture is None:
|
|
111
|
+
self._draw_ui_text("No projectile texture loaded.", 24, 24, UI_TEXT_COLOR)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
self._handle_input()
|
|
115
|
+
|
|
116
|
+
margin = 24
|
|
117
|
+
panel_gap = 32
|
|
118
|
+
panel_width = min(360, int(rl.get_screen_width() * 0.35))
|
|
119
|
+
available_width = rl.get_screen_width() - margin * 2 - panel_gap - panel_width
|
|
120
|
+
available_height = rl.get_screen_height() - margin * 2 - 60
|
|
121
|
+
scale = min(
|
|
122
|
+
2.0,
|
|
123
|
+
available_width / self._texture.width,
|
|
124
|
+
available_height / self._texture.height,
|
|
125
|
+
)
|
|
126
|
+
draw_w = self._texture.width * scale
|
|
127
|
+
draw_h = self._texture.height * scale
|
|
128
|
+
x = margin
|
|
129
|
+
y = margin + 60
|
|
130
|
+
|
|
131
|
+
src = rl.Rectangle(0.0, 0.0, float(self._texture.width), float(self._texture.height))
|
|
132
|
+
dst = rl.Rectangle(float(x), float(y), float(draw_w), float(draw_h))
|
|
133
|
+
rl.draw_texture_pro(self._texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
134
|
+
|
|
135
|
+
cell_w = draw_w / self._grid
|
|
136
|
+
cell_h = draw_h / self._grid
|
|
137
|
+
for i in range(1, self._grid):
|
|
138
|
+
rl.draw_line(
|
|
139
|
+
int(x + i * cell_w),
|
|
140
|
+
int(y),
|
|
141
|
+
int(x + i * cell_w),
|
|
142
|
+
int(y + draw_h),
|
|
143
|
+
rl.Color(60, 60, 70, 255),
|
|
144
|
+
)
|
|
145
|
+
rl.draw_line(
|
|
146
|
+
int(x),
|
|
147
|
+
int(y + i * cell_h),
|
|
148
|
+
int(x + draw_w),
|
|
149
|
+
int(y + i * cell_h),
|
|
150
|
+
rl.Color(60, 60, 70, 255),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
known_frames = KNOWN_BY_GRID.get(self._grid, {})
|
|
154
|
+
for frame_index in known_frames:
|
|
155
|
+
row = frame_index // self._grid
|
|
156
|
+
col = frame_index % self._grid
|
|
157
|
+
hl = rl.Rectangle(
|
|
158
|
+
float(x + col * cell_w),
|
|
159
|
+
float(y + row * cell_h),
|
|
160
|
+
float(cell_w),
|
|
161
|
+
float(cell_h),
|
|
162
|
+
)
|
|
163
|
+
rl.draw_rectangle_lines_ex(hl, 2, UI_KNOWN_COLOR)
|
|
164
|
+
|
|
165
|
+
hovered_index = None
|
|
166
|
+
mouse = rl.get_mouse_position()
|
|
167
|
+
if x <= mouse.x <= x + draw_w and y <= mouse.y <= y + draw_h:
|
|
168
|
+
col = int((mouse.x - x) // cell_w)
|
|
169
|
+
row = int((mouse.y - y) // cell_h)
|
|
170
|
+
if 0 <= col < self._grid and 0 <= row < self._grid:
|
|
171
|
+
hovered_index = row * self._grid + col
|
|
172
|
+
hl = rl.Rectangle(
|
|
173
|
+
float(x + col * cell_w),
|
|
174
|
+
float(y + row * cell_h),
|
|
175
|
+
float(cell_w),
|
|
176
|
+
float(cell_h),
|
|
177
|
+
)
|
|
178
|
+
rl.draw_rectangle_lines_ex(hl, 3, UI_HOVER_COLOR)
|
|
179
|
+
|
|
180
|
+
info_x = x + draw_w + panel_gap
|
|
181
|
+
info_y = margin
|
|
182
|
+
self._draw_ui_text(
|
|
183
|
+
f"projs.png (grid {self._grid}x{self._grid})",
|
|
184
|
+
info_x,
|
|
185
|
+
info_y,
|
|
186
|
+
UI_TEXT_COLOR,
|
|
187
|
+
)
|
|
188
|
+
info_y += self._ui_line_height() + 6
|
|
189
|
+
self._draw_ui_text("2/4: grid G: toggle", info_x, info_y, UI_HINT_COLOR)
|
|
190
|
+
info_y += self._ui_line_height() + 12
|
|
191
|
+
|
|
192
|
+
if hovered_index is not None:
|
|
193
|
+
self._draw_ui_text(f"frame {hovered_index:02d}", info_x, info_y, UI_TEXT_COLOR)
|
|
194
|
+
info_y += self._ui_line_height() + 6
|
|
195
|
+
entries = known_frames.get(hovered_index, [])
|
|
196
|
+
if entries:
|
|
197
|
+
for entry in entries:
|
|
198
|
+
self._draw_ui_text(
|
|
199
|
+
f"0x{entry.type_id:02x} {entry.label}",
|
|
200
|
+
info_x,
|
|
201
|
+
info_y,
|
|
202
|
+
UI_TEXT_COLOR,
|
|
203
|
+
)
|
|
204
|
+
info_y += self._ui_line_height() + 4
|
|
205
|
+
else:
|
|
206
|
+
self._draw_ui_text("no known mapping", info_x, info_y, UI_HINT_COLOR)
|
|
207
|
+
info_y += self._ui_line_height() + 4
|
|
208
|
+
info_y += 8
|
|
209
|
+
|
|
210
|
+
self._draw_ui_text("Known frames", info_x, info_y, UI_TEXT_COLOR)
|
|
211
|
+
info_y += self._ui_line_height() + 6
|
|
212
|
+
for frame_index in sorted(known_frames.keys()):
|
|
213
|
+
entries = known_frames[frame_index]
|
|
214
|
+
labels = ", ".join(f"0x{entry.type_id:02x} {entry.label}" for entry in entries)
|
|
215
|
+
self._draw_ui_text(f"{frame_index:02d}: {labels}", info_x, info_y, UI_HINT_COLOR)
|
|
216
|
+
info_y += self._ui_line_height() + 4
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@register_view("projectiles", "Projectile atlas preview")
|
|
220
|
+
def build_projectile_view(ctx: ViewContext) -> View:
|
|
221
|
+
return ProjectileView(ctx)
|