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,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
import pyray as rl
|
|
7
|
+
|
|
8
|
+
from grim.config import ensure_crimson_cfg
|
|
9
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
10
|
+
from grim.view import View, ViewContext
|
|
11
|
+
|
|
12
|
+
from ..bonuses import BonusId
|
|
13
|
+
from ..creatures.spawn import CreatureInit, CreatureTypeId
|
|
14
|
+
from ..game_world import GameWorld
|
|
15
|
+
from ..gameplay import PlayerInput, bonus_apply
|
|
16
|
+
from ..paths import default_runtime_dir
|
|
17
|
+
from .registry import register_view
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
WORLD_SIZE = 1024.0
|
|
21
|
+
|
|
22
|
+
UI_TEXT_SCALE = 1.0
|
|
23
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
24
|
+
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
|
|
25
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
29
|
+
if value < lo:
|
|
30
|
+
return lo
|
|
31
|
+
if value > hi:
|
|
32
|
+
return hi
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class _SpawnSpec:
|
|
38
|
+
r: float
|
|
39
|
+
angle_rad: float
|
|
40
|
+
type_id: CreatureTypeId
|
|
41
|
+
hp: float
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CameraShakeView:
|
|
45
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
46
|
+
self._assets_root = ctx.assets_dir
|
|
47
|
+
self._missing_assets: list[str] = []
|
|
48
|
+
self._small: SmallFontData | None = None
|
|
49
|
+
|
|
50
|
+
runtime_dir = default_runtime_dir()
|
|
51
|
+
config = None
|
|
52
|
+
if runtime_dir.is_dir():
|
|
53
|
+
try:
|
|
54
|
+
config = ensure_crimson_cfg(runtime_dir)
|
|
55
|
+
except Exception:
|
|
56
|
+
config = None
|
|
57
|
+
|
|
58
|
+
self.close_requested = False
|
|
59
|
+
self._world = GameWorld(
|
|
60
|
+
assets_dir=self._assets_root,
|
|
61
|
+
world_size=WORLD_SIZE,
|
|
62
|
+
demo_mode_active=False,
|
|
63
|
+
difficulty_level=0,
|
|
64
|
+
hardcore=False,
|
|
65
|
+
texture_cache=None,
|
|
66
|
+
config=config,
|
|
67
|
+
audio=None,
|
|
68
|
+
audio_rng=None,
|
|
69
|
+
)
|
|
70
|
+
self._reflex_boost_locked = False
|
|
71
|
+
self._reset_scene()
|
|
72
|
+
|
|
73
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
74
|
+
if self._small is not None:
|
|
75
|
+
return int(self._small.cell_size * scale)
|
|
76
|
+
return int(20 * scale)
|
|
77
|
+
|
|
78
|
+
def _draw_ui_text(
|
|
79
|
+
self,
|
|
80
|
+
text: str,
|
|
81
|
+
x: float,
|
|
82
|
+
y: float,
|
|
83
|
+
color: rl.Color,
|
|
84
|
+
scale: float = UI_TEXT_SCALE,
|
|
85
|
+
) -> None:
|
|
86
|
+
if self._small is not None:
|
|
87
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
88
|
+
else:
|
|
89
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
90
|
+
|
|
91
|
+
def _spawn_creature(self, *, world_x: float, world_y: float, type_id: CreatureTypeId, hp: float) -> None:
|
|
92
|
+
init = CreatureInit(
|
|
93
|
+
origin_template_id=0,
|
|
94
|
+
pos_x=_clamp(world_x, 64.0, WORLD_SIZE - 64.0),
|
|
95
|
+
pos_y=_clamp(world_y, 64.0, WORLD_SIZE - 64.0),
|
|
96
|
+
heading=math.pi,
|
|
97
|
+
phase_seed=0.0,
|
|
98
|
+
type_id=type_id,
|
|
99
|
+
health=float(hp),
|
|
100
|
+
max_health=float(hp),
|
|
101
|
+
move_speed=0.0,
|
|
102
|
+
reward_value=0.0,
|
|
103
|
+
size=50.0,
|
|
104
|
+
contact_damage=0.0,
|
|
105
|
+
)
|
|
106
|
+
self._world.creatures.spawn_init(init, rand=self._world.state.rng.rand)
|
|
107
|
+
|
|
108
|
+
def _reset_scene(self) -> None:
|
|
109
|
+
self._world.reset(seed=0xBEEF, player_count=1)
|
|
110
|
+
self._world.state.camera_shake_offset_x = 0.0
|
|
111
|
+
self._world.state.camera_shake_offset_y = 0.0
|
|
112
|
+
self._world.state.camera_shake_timer = 0.0
|
|
113
|
+
self._world.state.camera_shake_pulses = 0
|
|
114
|
+
|
|
115
|
+
player = self._world.players[0]
|
|
116
|
+
player.pos_x = WORLD_SIZE * 0.5
|
|
117
|
+
player.pos_y = WORLD_SIZE * 0.5
|
|
118
|
+
|
|
119
|
+
spawn = [
|
|
120
|
+
_SpawnSpec(r=140.0, angle_rad=0.0, type_id=CreatureTypeId.ZOMBIE, hp=50.0),
|
|
121
|
+
_SpawnSpec(r=160.0, angle_rad=math.pi * 0.5, type_id=CreatureTypeId.LIZARD, hp=60.0),
|
|
122
|
+
_SpawnSpec(r=180.0, angle_rad=math.pi, type_id=CreatureTypeId.ALIEN, hp=70.0),
|
|
123
|
+
_SpawnSpec(r=200.0, angle_rad=math.pi * 1.5, type_id=CreatureTypeId.SPIDER_SP1, hp=80.0),
|
|
124
|
+
_SpawnSpec(r=320.0, angle_rad=math.pi * 0.25, type_id=CreatureTypeId.SPIDER_SP2, hp=90.0),
|
|
125
|
+
_SpawnSpec(r=460.0, angle_rad=math.pi * 1.25, type_id=CreatureTypeId.ZOMBIE, hp=100.0),
|
|
126
|
+
]
|
|
127
|
+
for entry in spawn:
|
|
128
|
+
x = player.pos_x + math.cos(entry.angle_rad) * entry.r
|
|
129
|
+
y = player.pos_y + math.sin(entry.angle_rad) * entry.r
|
|
130
|
+
self._spawn_creature(world_x=x, world_y=y, type_id=entry.type_id, hp=entry.hp)
|
|
131
|
+
|
|
132
|
+
def open(self) -> None:
|
|
133
|
+
self._missing_assets.clear()
|
|
134
|
+
try:
|
|
135
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
136
|
+
except Exception:
|
|
137
|
+
self._small = None
|
|
138
|
+
self._world.open()
|
|
139
|
+
|
|
140
|
+
def close(self) -> None:
|
|
141
|
+
self._world.close()
|
|
142
|
+
if self._small is not None:
|
|
143
|
+
rl.unload_texture(self._small.texture)
|
|
144
|
+
self._small = None
|
|
145
|
+
|
|
146
|
+
def _handle_input(self) -> None:
|
|
147
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
148
|
+
self.close_requested = True
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
|
|
152
|
+
self._reset_scene()
|
|
153
|
+
|
|
154
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_N):
|
|
155
|
+
player = self._world.players[0]
|
|
156
|
+
bonus_apply(
|
|
157
|
+
self._world.state,
|
|
158
|
+
player,
|
|
159
|
+
BonusId.NUKE,
|
|
160
|
+
origin=player,
|
|
161
|
+
creatures=self._world.creatures.entries,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_T):
|
|
165
|
+
self._reflex_boost_locked = not self._reflex_boost_locked
|
|
166
|
+
self._world.state.bonuses.reflex_boost = 9999.0 if self._reflex_boost_locked else 0.0
|
|
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=bool(fire_down),
|
|
193
|
+
fire_pressed=bool(fire_pressed),
|
|
194
|
+
reload_pressed=bool(reload_pressed),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def update(self, dt: float) -> None:
|
|
198
|
+
self._handle_input()
|
|
199
|
+
if self._reflex_boost_locked:
|
|
200
|
+
self._world.state.bonuses.reflex_boost = 9999.0
|
|
201
|
+
self._world.update(dt, inputs=[self._build_input()], auto_pick_perks=True, perk_progression_enabled=False)
|
|
202
|
+
|
|
203
|
+
def draw(self) -> None:
|
|
204
|
+
self._world.draw()
|
|
205
|
+
|
|
206
|
+
if self._missing_assets:
|
|
207
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
208
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
209
|
+
|
|
210
|
+
state = self._world.state
|
|
211
|
+
cam_x, cam_y, _sx, _sy = self._world._world_params()
|
|
212
|
+
lines = [
|
|
213
|
+
"WASD move N: nuke shake T: toggle reflex-boost shake-rate R: reset Esc: exit",
|
|
214
|
+
f"camera_offset=({cam_x:.1f},{cam_y:.1f}) camera_raw=({self._world.camera_x:.1f},{self._world.camera_y:.1f})",
|
|
215
|
+
f"shake_offset=({state.camera_shake_offset_x:.1f},{state.camera_shake_offset_y:.1f}) "
|
|
216
|
+
f"shake_timer={state.camera_shake_timer:.3f} pulses={state.camera_shake_pulses}",
|
|
217
|
+
f"reflex_boost={state.bonuses.reflex_boost:.2f} creatures_alive={len(self._world.creatures.iter_active())}",
|
|
218
|
+
]
|
|
219
|
+
x = 24.0
|
|
220
|
+
y = 24.0 + float(self._ui_line_height()) + 12.0
|
|
221
|
+
for idx, line in enumerate(lines):
|
|
222
|
+
color = UI_HINT_COLOR if idx == 0 else UI_TEXT_COLOR
|
|
223
|
+
self._draw_ui_text(line, x, y, color)
|
|
224
|
+
y += float(self._ui_line_height())
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@register_view("camera-shake", "Camera shake")
|
|
228
|
+
def build_camera_shake_view(*, ctx: ViewContext) -> View:
|
|
229
|
+
return CameraShakeView(ctx)
|
|
@@ -0,0 +1,324 @@
|
|
|
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 crimson.creatures.anim import creature_corpse_frame_for_type
|
|
9
|
+
from crimson.creatures.spawn import CreatureTypeId
|
|
10
|
+
from grim.assets import resolve_asset_path
|
|
11
|
+
from grim.config import ensure_crimson_cfg
|
|
12
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
13
|
+
from grim.terrain_render import GroundCorpseDecal, GroundRenderer, _maybe_alpha_test
|
|
14
|
+
from grim.view import View, ViewContext
|
|
15
|
+
|
|
16
|
+
from ..paths import default_runtime_dir
|
|
17
|
+
from .registry import register_view
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
WORLD_SIZE = 1024.0
|
|
21
|
+
WINDOW_W = 1024
|
|
22
|
+
WINDOW_H = 768
|
|
23
|
+
|
|
24
|
+
BG = rl.Color(235, 235, 235, 255)
|
|
25
|
+
UI_TEXT = rl.Color(20, 20, 20, 255)
|
|
26
|
+
UI_HINT = rl.Color(70, 70, 70, 255)
|
|
27
|
+
UI_ERROR = rl.Color(240, 80, 80, 255)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class _Step:
|
|
32
|
+
name: str
|
|
33
|
+
description: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_STEPS: tuple[_Step, ...] = (
|
|
37
|
+
_Step(name="clear", description="Clear the ground render target"),
|
|
38
|
+
_Step(name="shadow", description="Bake shadow pass only (correct order)"),
|
|
39
|
+
_Step(name="color", description="Bake color pass only (correct order)"),
|
|
40
|
+
_Step(name="clear", description="Clear the ground render target"),
|
|
41
|
+
_Step(name="color", description="Bake color pass only (wrong order)"),
|
|
42
|
+
_Step(name="shadow", description="Bake shadow pass only (wrong order)"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CorpseStampDebugView:
|
|
47
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
48
|
+
self._assets_root = ctx.assets_dir
|
|
49
|
+
self._missing_assets: list[str] = []
|
|
50
|
+
self._small: SmallFontData | None = None
|
|
51
|
+
|
|
52
|
+
self._owned_textures: list[rl.Texture] = []
|
|
53
|
+
self._ground: GroundRenderer | None = None
|
|
54
|
+
self._bodyset: rl.Texture | None = None
|
|
55
|
+
|
|
56
|
+
self.close_requested = False
|
|
57
|
+
self._step_index = 0
|
|
58
|
+
self._corpse_size = 256.0
|
|
59
|
+
self._corpse_rotation = 0.0
|
|
60
|
+
self._screenshot_requested = False
|
|
61
|
+
self._dump_requested = False
|
|
62
|
+
self._dump_index = 0
|
|
63
|
+
|
|
64
|
+
def _ui_line_height(self) -> int:
|
|
65
|
+
if self._small is not None:
|
|
66
|
+
return int(self._small.cell_size)
|
|
67
|
+
return 20
|
|
68
|
+
|
|
69
|
+
def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color) -> None:
|
|
70
|
+
if self._small is not None:
|
|
71
|
+
draw_small_text(self._small, text, x, y, 1.0, color)
|
|
72
|
+
else:
|
|
73
|
+
rl.draw_text(text, int(x), int(y), 20, color)
|
|
74
|
+
|
|
75
|
+
def _load_runtime_config(self) -> tuple[float, float | None, float | None]:
|
|
76
|
+
runtime_dir = default_runtime_dir()
|
|
77
|
+
if runtime_dir.is_dir():
|
|
78
|
+
try:
|
|
79
|
+
cfg = ensure_crimson_cfg(runtime_dir)
|
|
80
|
+
return float(cfg.texture_scale), float(cfg.screen_width), float(cfg.screen_height)
|
|
81
|
+
except Exception:
|
|
82
|
+
return 1.0, None, None
|
|
83
|
+
return 1.0, None, None
|
|
84
|
+
|
|
85
|
+
def _dump_render_target(self) -> Path | None:
|
|
86
|
+
ground = self._ground
|
|
87
|
+
if ground is None or ground.render_target is None:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
log_dir = Path("artifacts") / "debug"
|
|
91
|
+
try:
|
|
92
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
except Exception:
|
|
94
|
+
log_dir = Path("artifacts")
|
|
95
|
+
|
|
96
|
+
step = _STEPS[self._step_index]
|
|
97
|
+
alpha_test = "a1" if bool(getattr(ground, "alpha_test", True)) else "a0"
|
|
98
|
+
filename = f"corpse_stamp_rt_{self._dump_index:03d}_step{self._step_index + 1:02d}_{step.name}_{alpha_test}.png"
|
|
99
|
+
out_path = log_dir / filename
|
|
100
|
+
|
|
101
|
+
image = rl.load_image_from_texture(ground.render_target.texture)
|
|
102
|
+
# Render textures are vertically flipped in raylib.
|
|
103
|
+
rl.image_flip_vertical(image)
|
|
104
|
+
try:
|
|
105
|
+
rl.export_image(image, str(out_path))
|
|
106
|
+
finally:
|
|
107
|
+
rl.unload_image(image)
|
|
108
|
+
self._dump_index += 1
|
|
109
|
+
return out_path
|
|
110
|
+
|
|
111
|
+
def _corpse_decal(self) -> GroundCorpseDecal:
|
|
112
|
+
size = float(self._corpse_size)
|
|
113
|
+
frame = creature_corpse_frame_for_type(int(CreatureTypeId.SPIDER_SP1))
|
|
114
|
+
cx = WORLD_SIZE * 0.5
|
|
115
|
+
cy = WORLD_SIZE * 0.5
|
|
116
|
+
return GroundCorpseDecal(
|
|
117
|
+
bodyset_frame=int(frame),
|
|
118
|
+
top_left_x=cx - size * 0.5,
|
|
119
|
+
top_left_y=cy - size * 0.5,
|
|
120
|
+
size=size,
|
|
121
|
+
rotation_rad=float(self._corpse_rotation),
|
|
122
|
+
tint=rl.Color(255, 255, 255, 255),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _clear_ground(self) -> None:
|
|
126
|
+
ground = self._ground
|
|
127
|
+
if ground is None:
|
|
128
|
+
return
|
|
129
|
+
ground.create_render_target()
|
|
130
|
+
if ground.render_target is None:
|
|
131
|
+
return
|
|
132
|
+
rl.begin_texture_mode(ground.render_target)
|
|
133
|
+
rl.clear_background(BG)
|
|
134
|
+
rl.end_texture_mode()
|
|
135
|
+
# GroundRenderer treats this as an internal invariant; set it for debug fills.
|
|
136
|
+
ground._render_target_ready = True # type: ignore[attr-defined]
|
|
137
|
+
|
|
138
|
+
def _bake_shadow_only(self) -> None:
|
|
139
|
+
ground = self._ground
|
|
140
|
+
bodyset = self._bodyset
|
|
141
|
+
if ground is None or bodyset is None:
|
|
142
|
+
return
|
|
143
|
+
ground.create_render_target()
|
|
144
|
+
if ground.render_target is None:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
scale = ground._normalized_texture_scale()
|
|
148
|
+
inv_scale = 1.0 / scale
|
|
149
|
+
offset = 2.0 * scale / float(ground.width)
|
|
150
|
+
ground._set_texture_filters((bodyset,), point=True)
|
|
151
|
+
|
|
152
|
+
rl.begin_texture_mode(ground.render_target)
|
|
153
|
+
with _maybe_alpha_test(ground.alpha_test):
|
|
154
|
+
ground._draw_corpse_shadow_pass(bodyset, [self._corpse_decal()], inv_scale, offset)
|
|
155
|
+
rl.end_texture_mode()
|
|
156
|
+
|
|
157
|
+
ground._set_texture_filters((bodyset,), point=False)
|
|
158
|
+
ground._render_target_ready = True # type: ignore[attr-defined]
|
|
159
|
+
|
|
160
|
+
def _bake_color_only(self) -> None:
|
|
161
|
+
ground = self._ground
|
|
162
|
+
bodyset = self._bodyset
|
|
163
|
+
if ground is None or bodyset is None:
|
|
164
|
+
return
|
|
165
|
+
ground.bake_corpse_decals(bodyset, [self._corpse_decal()], shadow=False)
|
|
166
|
+
|
|
167
|
+
def _apply_step(self) -> None:
|
|
168
|
+
step = _STEPS[self._step_index]
|
|
169
|
+
if step.name == "clear":
|
|
170
|
+
self._clear_ground()
|
|
171
|
+
elif step.name == "shadow":
|
|
172
|
+
self._bake_shadow_only()
|
|
173
|
+
elif step.name == "color":
|
|
174
|
+
self._bake_color_only()
|
|
175
|
+
|
|
176
|
+
def open(self) -> None:
|
|
177
|
+
rl.set_window_size(WINDOW_W, WINDOW_H)
|
|
178
|
+
self._missing_assets.clear()
|
|
179
|
+
self._owned_textures.clear()
|
|
180
|
+
self._ground = None
|
|
181
|
+
self._bodyset = None
|
|
182
|
+
self.close_requested = False
|
|
183
|
+
self._step_index = 0
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
187
|
+
except Exception:
|
|
188
|
+
self._small = None
|
|
189
|
+
|
|
190
|
+
base_path = resolve_asset_path(self._assets_root, "ter/ter_q1_base.png")
|
|
191
|
+
bodyset_path = resolve_asset_path(self._assets_root, "game/bodyset.png")
|
|
192
|
+
if base_path is None:
|
|
193
|
+
self._missing_assets.append("ter/ter_q1_base.png")
|
|
194
|
+
if bodyset_path is None:
|
|
195
|
+
self._missing_assets.append("game/bodyset.png")
|
|
196
|
+
if self._missing_assets:
|
|
197
|
+
raise FileNotFoundError("Missing assets: " + ", ".join(self._missing_assets))
|
|
198
|
+
|
|
199
|
+
base = rl.load_texture(str(base_path))
|
|
200
|
+
bodyset = rl.load_texture(str(bodyset_path))
|
|
201
|
+
self._owned_textures.extend([base, bodyset])
|
|
202
|
+
self._bodyset = bodyset
|
|
203
|
+
|
|
204
|
+
texture_scale, screen_w, screen_h = self._load_runtime_config()
|
|
205
|
+
self._ground = GroundRenderer(
|
|
206
|
+
texture=base,
|
|
207
|
+
width=int(WORLD_SIZE),
|
|
208
|
+
height=int(WORLD_SIZE),
|
|
209
|
+
texture_scale=float(texture_scale),
|
|
210
|
+
screen_width=screen_w,
|
|
211
|
+
screen_height=screen_h,
|
|
212
|
+
)
|
|
213
|
+
self._ground.alpha_test = True
|
|
214
|
+
self._clear_ground()
|
|
215
|
+
|
|
216
|
+
def close(self) -> None:
|
|
217
|
+
if self._ground is not None and self._ground.render_target is not None:
|
|
218
|
+
rl.unload_render_texture(self._ground.render_target)
|
|
219
|
+
self._ground.render_target = None
|
|
220
|
+
self._ground = None
|
|
221
|
+
self._bodyset = None
|
|
222
|
+
|
|
223
|
+
for texture in self._owned_textures:
|
|
224
|
+
rl.unload_texture(texture)
|
|
225
|
+
self._owned_textures.clear()
|
|
226
|
+
|
|
227
|
+
if self._small is not None:
|
|
228
|
+
rl.unload_texture(self._small.texture)
|
|
229
|
+
self._small = None
|
|
230
|
+
|
|
231
|
+
def update(self, dt: float) -> None:
|
|
232
|
+
del dt
|
|
233
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
234
|
+
self.close_requested = True
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
|
|
238
|
+
self._step_index = 0
|
|
239
|
+
self._clear_ground()
|
|
240
|
+
|
|
241
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_A):
|
|
242
|
+
if self._ground is not None:
|
|
243
|
+
self._ground.alpha_test = not bool(self._ground.alpha_test)
|
|
244
|
+
|
|
245
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_N) or rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
|
|
246
|
+
self._step_index = (self._step_index + 1) % len(_STEPS)
|
|
247
|
+
self._apply_step()
|
|
248
|
+
|
|
249
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_P):
|
|
250
|
+
self._screenshot_requested = True
|
|
251
|
+
|
|
252
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_D):
|
|
253
|
+
self._dump_requested = True
|
|
254
|
+
|
|
255
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_Q):
|
|
256
|
+
self._corpse_rotation -= 0.04
|
|
257
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_E):
|
|
258
|
+
self._corpse_rotation += 0.04
|
|
259
|
+
|
|
260
|
+
def consume_screenshot_request(self) -> bool:
|
|
261
|
+
requested = self._screenshot_requested
|
|
262
|
+
self._screenshot_requested = False
|
|
263
|
+
return requested
|
|
264
|
+
|
|
265
|
+
def draw(self) -> None:
|
|
266
|
+
rl.clear_background(BG)
|
|
267
|
+
|
|
268
|
+
if self._missing_assets:
|
|
269
|
+
self._draw_ui_text("Missing assets: " + ", ".join(self._missing_assets), 24, 24, UI_ERROR)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
ground = self._ground
|
|
273
|
+
if ground is None:
|
|
274
|
+
self._draw_ui_text("Ground renderer not initialized.", 24, 24, UI_ERROR)
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
if self._dump_requested:
|
|
278
|
+
self._dump_requested = False
|
|
279
|
+
self._dump_render_target()
|
|
280
|
+
|
|
281
|
+
screen_w = float(rl.get_screen_width())
|
|
282
|
+
screen_h = float(rl.get_screen_height())
|
|
283
|
+
cam_x = screen_w * 0.5 - WORLD_SIZE * 0.5
|
|
284
|
+
cam_y = screen_h * 0.5 - WORLD_SIZE * 0.5
|
|
285
|
+
ground.draw(cam_x, cam_y, screen_w=screen_w, screen_h=screen_h)
|
|
286
|
+
|
|
287
|
+
# UI
|
|
288
|
+
x = 24.0
|
|
289
|
+
y = 20.0
|
|
290
|
+
line = float(self._ui_line_height())
|
|
291
|
+
step = _STEPS[self._step_index]
|
|
292
|
+
alpha_test = bool(getattr(ground, "alpha_test", True))
|
|
293
|
+
self._draw_ui_text("Corpse stamp debug (SPIDER)", x, y, UI_TEXT)
|
|
294
|
+
y += line
|
|
295
|
+
self._draw_ui_text(
|
|
296
|
+
"N/Space: next step R: reset A: toggle alpha test Q/E: rotate P: screenshot D: dump RT",
|
|
297
|
+
x,
|
|
298
|
+
y,
|
|
299
|
+
UI_HINT,
|
|
300
|
+
)
|
|
301
|
+
y += line
|
|
302
|
+
self._draw_ui_text(f"step {self._step_index + 1}/{len(_STEPS)}: {step.description}", x, y, UI_HINT)
|
|
303
|
+
y += line
|
|
304
|
+
self._draw_ui_text(
|
|
305
|
+
f"alpha_test={'on' if alpha_test else 'off'} size={self._corpse_size:.1f} dump_index={self._dump_index}",
|
|
306
|
+
x,
|
|
307
|
+
y,
|
|
308
|
+
UI_HINT,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Source preview (bodyset frame) in the corner for inspection.
|
|
312
|
+
if self._bodyset is not None:
|
|
313
|
+
frame = creature_corpse_frame_for_type(int(CreatureTypeId.SPIDER_SP1))
|
|
314
|
+
src = ground._corpse_src(self._bodyset, int(frame))
|
|
315
|
+
preview = 256.0
|
|
316
|
+
pad = 18.0
|
|
317
|
+
dst = rl.Rectangle(screen_w - preview - pad, pad, preview, preview)
|
|
318
|
+
rl.draw_rectangle(int(dst.x) - 2, int(dst.y) - 2, int(preview) + 4, int(preview) + 4, rl.Color(0, 0, 0, 30))
|
|
319
|
+
rl.draw_texture_pro(self._bodyset, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@register_view("corpse-stamp-debug", "Corpse stamp debug")
|
|
323
|
+
def build_corpse_stamp_debug_view(ctx: ViewContext) -> View:
|
|
324
|
+
return CorpseStampDebugView(ctx)
|