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,739 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import pyray as rl
|
|
10
|
+
|
|
11
|
+
from crimson.creatures.anim import creature_anim_advance_phase, creature_anim_select_frame, creature_corpse_frame_for_type
|
|
12
|
+
from crimson.creatures.runtime import CreaturePool
|
|
13
|
+
from crimson.creatures.spawn import CreatureFlags, CreatureInit, CreatureTypeId, SpawnEnv
|
|
14
|
+
from crimson.effects import FxQueue, FxQueueRotated
|
|
15
|
+
from crimson.gameplay import GameplayState, PlayerState
|
|
16
|
+
from crimson.render.terrain_fx import FxQueueTextures, bake_fx_queues
|
|
17
|
+
from grim.assets import resolve_asset_path
|
|
18
|
+
from grim.config import ensure_crimson_cfg
|
|
19
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
20
|
+
from grim.terrain_render import GroundRenderer
|
|
21
|
+
from grim.view import View, ViewContext
|
|
22
|
+
|
|
23
|
+
from ..paths import default_runtime_dir
|
|
24
|
+
from .registry import register_view
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
UI_TEXT_SCALE = 1.0
|
|
28
|
+
UI_TEXT_DARK = rl.Color(30, 30, 30, 255)
|
|
29
|
+
UI_HINT_DARK = rl.Color(70, 70, 70, 255)
|
|
30
|
+
UI_TEXT_LIGHT = rl.Color(220, 220, 220, 255)
|
|
31
|
+
UI_HINT_LIGHT = rl.Color(140, 140, 140, 255)
|
|
32
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
33
|
+
|
|
34
|
+
BG_DARK = rl.Color(12, 12, 14, 255)
|
|
35
|
+
BG_LIGHT = rl.Color(235, 235, 235, 255)
|
|
36
|
+
GRID_COLOR = rl.Color(0, 0, 0, 20)
|
|
37
|
+
|
|
38
|
+
WORLD_SIZE = 1024.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class _CreatureAnimInfo:
|
|
43
|
+
base: int
|
|
44
|
+
anim_rate: float
|
|
45
|
+
mirror: bool
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_CREATURE_ANIM: dict[CreatureTypeId, _CreatureAnimInfo] = {
|
|
49
|
+
CreatureTypeId.ZOMBIE: _CreatureAnimInfo(base=0x20, anim_rate=1.2, mirror=False),
|
|
50
|
+
CreatureTypeId.LIZARD: _CreatureAnimInfo(base=0x10, anim_rate=1.6, mirror=True),
|
|
51
|
+
CreatureTypeId.ALIEN: _CreatureAnimInfo(base=0x20, anim_rate=1.35, mirror=False),
|
|
52
|
+
CreatureTypeId.SPIDER_SP1: _CreatureAnimInfo(base=0x10, anim_rate=1.5, mirror=True),
|
|
53
|
+
CreatureTypeId.SPIDER_SP2: _CreatureAnimInfo(base=0x10, anim_rate=1.5, mirror=True),
|
|
54
|
+
CreatureTypeId.TROOPER: _CreatureAnimInfo(base=0x00, anim_rate=1.0, mirror=False),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_CREATURE_ASSET: dict[CreatureTypeId, str] = {
|
|
58
|
+
CreatureTypeId.ZOMBIE: "zombie",
|
|
59
|
+
CreatureTypeId.LIZARD: "lizard",
|
|
60
|
+
CreatureTypeId.ALIEN: "alien",
|
|
61
|
+
CreatureTypeId.SPIDER_SP1: "spider_sp1",
|
|
62
|
+
CreatureTypeId.SPIDER_SP2: "spider_sp2",
|
|
63
|
+
CreatureTypeId.TROOPER: "trooper",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
TERRAIN_TEXTURES: list[tuple[int, str]] = [
|
|
68
|
+
(0, "ter/ter_q1_base.png"),
|
|
69
|
+
(1, "ter/ter_q1_tex1.png"),
|
|
70
|
+
(2, "ter/ter_q2_base.png"),
|
|
71
|
+
(3, "ter/ter_q2_tex1.png"),
|
|
72
|
+
(4, "ter/ter_q3_base.png"),
|
|
73
|
+
(5, "ter/ter_q3_tex1.png"),
|
|
74
|
+
(6, "ter/ter_q4_base.png"),
|
|
75
|
+
(7, "ter/ter_q4_tex1.png"),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DecalsDebugView:
|
|
80
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
81
|
+
self._assets_root = ctx.assets_dir
|
|
82
|
+
self._missing_assets: list[str] = []
|
|
83
|
+
self._small: SmallFontData | None = None
|
|
84
|
+
|
|
85
|
+
self._terrain_textures: dict[int, rl.Texture] = {}
|
|
86
|
+
self._creature_textures: dict[str, rl.Texture] = {}
|
|
87
|
+
self._owned_textures: list[rl.Texture] = []
|
|
88
|
+
|
|
89
|
+
self._fx_textures: FxQueueTextures | None = None
|
|
90
|
+
self._ground: GroundRenderer | None = None
|
|
91
|
+
self._camera_x = 0.0
|
|
92
|
+
self._camera_y = 0.0
|
|
93
|
+
self._light_mode = False
|
|
94
|
+
|
|
95
|
+
self._terrain_seed = 0xBEEF
|
|
96
|
+
self._terrain_pair = 0 # 0..3, maps to (0,1),(2,3),(4,5),(6,7)
|
|
97
|
+
self._show_stamp_log = True
|
|
98
|
+
self._frame = 0
|
|
99
|
+
self._stamp_log_path: Path | None = None
|
|
100
|
+
self._stamp_log_file = None
|
|
101
|
+
|
|
102
|
+
self._state = GameplayState()
|
|
103
|
+
self._player = PlayerState(index=0, pos_x=WORLD_SIZE * 0.5, pos_y=WORLD_SIZE * 0.5)
|
|
104
|
+
self._creatures = CreaturePool()
|
|
105
|
+
self._env = SpawnEnv(
|
|
106
|
+
terrain_width=WORLD_SIZE,
|
|
107
|
+
terrain_height=WORLD_SIZE,
|
|
108
|
+
demo_mode_active=True,
|
|
109
|
+
hardcore=False,
|
|
110
|
+
difficulty_level=0,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._fx_queue = FxQueue()
|
|
114
|
+
self._fx_queue_rotated = FxQueueRotated()
|
|
115
|
+
|
|
116
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
117
|
+
if self._small is not None:
|
|
118
|
+
return int(self._small.cell_size * scale)
|
|
119
|
+
return int(20 * scale)
|
|
120
|
+
|
|
121
|
+
def _draw_ui_text(self, text: str, x: float, y: float, color: rl.Color, scale: float = UI_TEXT_SCALE) -> None:
|
|
122
|
+
if self._small is not None:
|
|
123
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
124
|
+
else:
|
|
125
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
126
|
+
|
|
127
|
+
def _write_stamp_log(self, payload: dict) -> None:
|
|
128
|
+
if self._stamp_log_file is None:
|
|
129
|
+
return
|
|
130
|
+
try:
|
|
131
|
+
self._stamp_log_file.write(json.dumps(payload, sort_keys=True) + "\n")
|
|
132
|
+
self._stamp_log_file.flush()
|
|
133
|
+
except Exception:
|
|
134
|
+
self._stamp_log_file = None
|
|
135
|
+
|
|
136
|
+
def _load_runtime_config(self) -> tuple[float, float | None, float | None]:
|
|
137
|
+
runtime_dir = default_runtime_dir()
|
|
138
|
+
if runtime_dir.is_dir():
|
|
139
|
+
try:
|
|
140
|
+
cfg = ensure_crimson_cfg(runtime_dir)
|
|
141
|
+
return (
|
|
142
|
+
float(cfg.texture_scale),
|
|
143
|
+
float(cfg.screen_width),
|
|
144
|
+
float(cfg.screen_height),
|
|
145
|
+
)
|
|
146
|
+
except Exception:
|
|
147
|
+
return 1.0, None, None
|
|
148
|
+
return 1.0, None, None
|
|
149
|
+
|
|
150
|
+
def _apply_terrain_pair(self) -> None:
|
|
151
|
+
if self._ground is None:
|
|
152
|
+
return
|
|
153
|
+
base_id = int(self._terrain_pair) * 2
|
|
154
|
+
overlay_id = base_id + 1
|
|
155
|
+
base = self._terrain_textures.get(base_id)
|
|
156
|
+
overlay = self._terrain_textures.get(overlay_id)
|
|
157
|
+
if base is None:
|
|
158
|
+
return
|
|
159
|
+
self._ground.texture = base
|
|
160
|
+
self._ground.overlay = overlay
|
|
161
|
+
self._ground.overlay_detail = overlay or base
|
|
162
|
+
self._ground.schedule_generate(seed=int(self._terrain_seed), layers=3)
|
|
163
|
+
|
|
164
|
+
def _clear_ground_light(self) -> None:
|
|
165
|
+
ground = self._ground
|
|
166
|
+
if ground is None:
|
|
167
|
+
return
|
|
168
|
+
ground.create_render_target()
|
|
169
|
+
if ground.render_target is None:
|
|
170
|
+
return
|
|
171
|
+
rl.begin_texture_mode(ground.render_target)
|
|
172
|
+
rl.clear_background(BG_LIGHT)
|
|
173
|
+
rl.end_texture_mode()
|
|
174
|
+
# GroundRenderer treats this as an internal invariant; set it for debug fills.
|
|
175
|
+
ground._render_target_ready = True # type: ignore[attr-defined]
|
|
176
|
+
|
|
177
|
+
def _reset_ground(self) -> None:
|
|
178
|
+
if self._ground is None:
|
|
179
|
+
return
|
|
180
|
+
if self._light_mode:
|
|
181
|
+
self._clear_ground_light()
|
|
182
|
+
else:
|
|
183
|
+
self._apply_terrain_pair()
|
|
184
|
+
|
|
185
|
+
def _world_to_screen(self, x: float, y: float) -> tuple[float, float]:
|
|
186
|
+
ground = self._ground
|
|
187
|
+
screen_w = float(rl.get_screen_width())
|
|
188
|
+
screen_h = float(rl.get_screen_height())
|
|
189
|
+
if ground is None:
|
|
190
|
+
return x, y
|
|
191
|
+
|
|
192
|
+
# Mirror GameWorld camera behavior (ground.draw uses the same clamp rules).
|
|
193
|
+
cfg_w = float(ground.screen_width or screen_w)
|
|
194
|
+
cfg_h = float(ground.screen_height or screen_h)
|
|
195
|
+
if cfg_w > WORLD_SIZE:
|
|
196
|
+
cfg_w = WORLD_SIZE
|
|
197
|
+
if cfg_h > WORLD_SIZE:
|
|
198
|
+
cfg_h = WORLD_SIZE
|
|
199
|
+
|
|
200
|
+
min_x = cfg_w - WORLD_SIZE
|
|
201
|
+
min_y = cfg_h - WORLD_SIZE
|
|
202
|
+
cam_x = self._camera_x
|
|
203
|
+
cam_y = self._camera_y
|
|
204
|
+
if cam_x > -1.0:
|
|
205
|
+
cam_x = -1.0
|
|
206
|
+
if cam_x < min_x:
|
|
207
|
+
cam_x = min_x
|
|
208
|
+
if cam_y > -1.0:
|
|
209
|
+
cam_y = -1.0
|
|
210
|
+
if cam_y < min_y:
|
|
211
|
+
cam_y = min_y
|
|
212
|
+
|
|
213
|
+
scale_x = screen_w / cfg_w if cfg_w > 0 else 1.0
|
|
214
|
+
scale_y = screen_h / cfg_h if cfg_h > 0 else 1.0
|
|
215
|
+
return (x + cam_x) * scale_x, (y + cam_y) * scale_y
|
|
216
|
+
|
|
217
|
+
def _screen_to_world(self, x: float, y: float) -> tuple[float, float]:
|
|
218
|
+
ground = self._ground
|
|
219
|
+
screen_w = float(rl.get_screen_width())
|
|
220
|
+
screen_h = float(rl.get_screen_height())
|
|
221
|
+
if ground is None:
|
|
222
|
+
return x, y
|
|
223
|
+
|
|
224
|
+
cfg_w = float(ground.screen_width or screen_w)
|
|
225
|
+
cfg_h = float(ground.screen_height or screen_h)
|
|
226
|
+
if cfg_w > WORLD_SIZE:
|
|
227
|
+
cfg_w = WORLD_SIZE
|
|
228
|
+
if cfg_h > WORLD_SIZE:
|
|
229
|
+
cfg_h = WORLD_SIZE
|
|
230
|
+
|
|
231
|
+
min_x = cfg_w - WORLD_SIZE
|
|
232
|
+
min_y = cfg_h - WORLD_SIZE
|
|
233
|
+
cam_x = self._camera_x
|
|
234
|
+
cam_y = self._camera_y
|
|
235
|
+
if cam_x > -1.0:
|
|
236
|
+
cam_x = -1.0
|
|
237
|
+
if cam_x < min_x:
|
|
238
|
+
cam_x = min_x
|
|
239
|
+
if cam_y > -1.0:
|
|
240
|
+
cam_y = -1.0
|
|
241
|
+
if cam_y < min_y:
|
|
242
|
+
cam_y = min_y
|
|
243
|
+
|
|
244
|
+
scale_x = screen_w / cfg_w if cfg_w > 0 else 1.0
|
|
245
|
+
scale_y = screen_h / cfg_h if cfg_h > 0 else 1.0
|
|
246
|
+
world_x = (x / scale_x) - cam_x
|
|
247
|
+
world_y = (y / scale_y) - cam_y
|
|
248
|
+
return world_x, world_y
|
|
249
|
+
|
|
250
|
+
def _world_scale(self) -> float:
|
|
251
|
+
ground = self._ground
|
|
252
|
+
if ground is None:
|
|
253
|
+
return 1.0
|
|
254
|
+
out_w = float(rl.get_screen_width())
|
|
255
|
+
out_h = float(rl.get_screen_height())
|
|
256
|
+
cfg_w = float(ground.screen_width or out_w)
|
|
257
|
+
cfg_h = float(ground.screen_height or out_h)
|
|
258
|
+
if cfg_w > WORLD_SIZE:
|
|
259
|
+
cfg_w = WORLD_SIZE
|
|
260
|
+
if cfg_h > WORLD_SIZE:
|
|
261
|
+
cfg_h = WORLD_SIZE
|
|
262
|
+
if cfg_w <= 0.0 or cfg_h <= 0.0:
|
|
263
|
+
return 1.0
|
|
264
|
+
scale_x = out_w / cfg_w
|
|
265
|
+
scale_y = out_h / cfg_h
|
|
266
|
+
return (scale_x + scale_y) * 0.5
|
|
267
|
+
|
|
268
|
+
def _draw_grid(self) -> None:
|
|
269
|
+
ground = self._ground
|
|
270
|
+
if ground is None:
|
|
271
|
+
return
|
|
272
|
+
step = 64.0
|
|
273
|
+
screen_w = float(rl.get_screen_width())
|
|
274
|
+
screen_h = float(rl.get_screen_height())
|
|
275
|
+
cfg_w = float(ground.screen_width or screen_w)
|
|
276
|
+
cfg_h = float(ground.screen_height or screen_h)
|
|
277
|
+
if cfg_w > WORLD_SIZE:
|
|
278
|
+
cfg_w = WORLD_SIZE
|
|
279
|
+
if cfg_h > WORLD_SIZE:
|
|
280
|
+
cfg_h = WORLD_SIZE
|
|
281
|
+
|
|
282
|
+
min_x = cfg_w - WORLD_SIZE
|
|
283
|
+
min_y = cfg_h - WORLD_SIZE
|
|
284
|
+
cam_x = self._camera_x
|
|
285
|
+
cam_y = self._camera_y
|
|
286
|
+
if cam_x > -1.0:
|
|
287
|
+
cam_x = -1.0
|
|
288
|
+
if cam_x < min_x:
|
|
289
|
+
cam_x = min_x
|
|
290
|
+
if cam_y > -1.0:
|
|
291
|
+
cam_y = -1.0
|
|
292
|
+
if cam_y < min_y:
|
|
293
|
+
cam_y = min_y
|
|
294
|
+
|
|
295
|
+
scale_x = screen_w / cfg_w if cfg_w > 0 else 1.0
|
|
296
|
+
scale_y = screen_h / cfg_h if cfg_h > 0 else 1.0
|
|
297
|
+
|
|
298
|
+
start_x = math.floor((-cam_x) / step) * step
|
|
299
|
+
end_x = (-cam_x) + cfg_w
|
|
300
|
+
x = start_x
|
|
301
|
+
while x <= end_x:
|
|
302
|
+
sx = int((x + cam_x) * scale_x)
|
|
303
|
+
rl.draw_line(sx, 0, sx, int(screen_h), GRID_COLOR)
|
|
304
|
+
x += step
|
|
305
|
+
|
|
306
|
+
start_y = math.floor((-cam_y) / step) * step
|
|
307
|
+
end_y = (-cam_y) + cfg_h
|
|
308
|
+
y = start_y
|
|
309
|
+
while y <= end_y:
|
|
310
|
+
sy = int((y + cam_y) * scale_y)
|
|
311
|
+
rl.draw_line(0, sy, int(screen_w), sy, GRID_COLOR)
|
|
312
|
+
y += step
|
|
313
|
+
|
|
314
|
+
def _draw_creature_sprite(
|
|
315
|
+
self,
|
|
316
|
+
texture: rl.Texture,
|
|
317
|
+
*,
|
|
318
|
+
info: _CreatureAnimInfo,
|
|
319
|
+
flags: CreatureFlags,
|
|
320
|
+
phase: float,
|
|
321
|
+
mirror_long: bool,
|
|
322
|
+
world_x: float,
|
|
323
|
+
world_y: float,
|
|
324
|
+
rotation_rad: float,
|
|
325
|
+
scale: float,
|
|
326
|
+
size_scale: float,
|
|
327
|
+
tint: rl.Color,
|
|
328
|
+
) -> None:
|
|
329
|
+
frame, _, _ = creature_anim_select_frame(
|
|
330
|
+
phase,
|
|
331
|
+
base_frame=info.base,
|
|
332
|
+
mirror_long=mirror_long,
|
|
333
|
+
flags=flags,
|
|
334
|
+
)
|
|
335
|
+
grid = 8
|
|
336
|
+
cell = float(texture.width) / grid if grid > 0 else float(texture.width)
|
|
337
|
+
row = frame // grid
|
|
338
|
+
col = frame % grid
|
|
339
|
+
src = rl.Rectangle(float(col * cell), float(row * cell), float(cell), float(cell))
|
|
340
|
+
sx, sy = self._world_to_screen(world_x, world_y)
|
|
341
|
+
width = cell * float(scale) * float(size_scale)
|
|
342
|
+
height = cell * float(scale) * float(size_scale)
|
|
343
|
+
dst = rl.Rectangle(float(sx), float(sy), float(width), float(height))
|
|
344
|
+
origin = rl.Vector2(float(width) * 0.5, float(height) * 0.5)
|
|
345
|
+
rl.draw_texture_pro(texture, src, dst, origin, math.degrees(float(rotation_rad)), tint)
|
|
346
|
+
|
|
347
|
+
def _spawn_enemy(self, x: float, y: float) -> None:
|
|
348
|
+
type_id = CreatureTypeId(int(self._state.rng.rand()) % 5)
|
|
349
|
+
size = float(int(self._state.rng.rand()) % 30 + 40)
|
|
350
|
+
move_speed = float(int(self._state.rng.rand()) % 30) * 0.05 + 1.0
|
|
351
|
+
hp = float(int(self._state.rng.rand()) % 4 + 2)
|
|
352
|
+
heading = float(int(self._state.rng.rand()) % 628) * 0.01
|
|
353
|
+
init = CreatureInit(
|
|
354
|
+
origin_template_id=-1,
|
|
355
|
+
pos_x=float(x),
|
|
356
|
+
pos_y=float(y),
|
|
357
|
+
heading=heading,
|
|
358
|
+
phase_seed=float(int(self._state.rng.rand()) & 0xFF),
|
|
359
|
+
type_id=type_id,
|
|
360
|
+
flags=CreatureFlags(0),
|
|
361
|
+
ai_mode=0,
|
|
362
|
+
health=hp,
|
|
363
|
+
max_health=hp,
|
|
364
|
+
move_speed=move_speed,
|
|
365
|
+
reward_value=0.0,
|
|
366
|
+
size=size,
|
|
367
|
+
contact_damage=0.0,
|
|
368
|
+
tint=(1.0, 1.0, 1.0, 1.0),
|
|
369
|
+
)
|
|
370
|
+
self._creatures.spawn_init(init, rand=self._state.rng.rand)
|
|
371
|
+
|
|
372
|
+
def open(self) -> None:
|
|
373
|
+
self._missing_assets.clear()
|
|
374
|
+
self._owned_textures.clear()
|
|
375
|
+
self._terrain_textures.clear()
|
|
376
|
+
self._creature_textures.clear()
|
|
377
|
+
self._fx_textures = None
|
|
378
|
+
self._fx_queue.clear()
|
|
379
|
+
self._fx_queue_rotated.clear()
|
|
380
|
+
self._creatures.reset()
|
|
381
|
+
|
|
382
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
383
|
+
|
|
384
|
+
for terrain_id, rel_path in TERRAIN_TEXTURES:
|
|
385
|
+
path = resolve_asset_path(self._assets_root, rel_path)
|
|
386
|
+
if path is None:
|
|
387
|
+
self._missing_assets.append(rel_path)
|
|
388
|
+
continue
|
|
389
|
+
texture = rl.load_texture(str(path))
|
|
390
|
+
self._owned_textures.append(texture)
|
|
391
|
+
self._terrain_textures[int(terrain_id)] = texture
|
|
392
|
+
|
|
393
|
+
for asset in sorted(set(_CREATURE_ASSET.values())):
|
|
394
|
+
rel_path = f"game/{asset}.png"
|
|
395
|
+
path = resolve_asset_path(self._assets_root, rel_path)
|
|
396
|
+
if path is None:
|
|
397
|
+
self._missing_assets.append(rel_path)
|
|
398
|
+
continue
|
|
399
|
+
texture = rl.load_texture(str(path))
|
|
400
|
+
self._owned_textures.append(texture)
|
|
401
|
+
self._creature_textures[asset] = texture
|
|
402
|
+
|
|
403
|
+
particles_path = resolve_asset_path(self._assets_root, "game/particles.png")
|
|
404
|
+
if particles_path is None:
|
|
405
|
+
self._missing_assets.append("game/particles.png")
|
|
406
|
+
bodyset_path = resolve_asset_path(self._assets_root, "game/bodyset.png")
|
|
407
|
+
if bodyset_path is None:
|
|
408
|
+
self._missing_assets.append("game/bodyset.png")
|
|
409
|
+
|
|
410
|
+
if self._missing_assets:
|
|
411
|
+
raise FileNotFoundError(f"Missing assets: {', '.join(self._missing_assets)}")
|
|
412
|
+
|
|
413
|
+
particles_tex = rl.load_texture(str(particles_path))
|
|
414
|
+
bodyset_tex = rl.load_texture(str(bodyset_path))
|
|
415
|
+
self._owned_textures.append(particles_tex)
|
|
416
|
+
self._owned_textures.append(bodyset_tex)
|
|
417
|
+
self._fx_textures = FxQueueTextures(particles=particles_tex, bodyset=bodyset_tex)
|
|
418
|
+
|
|
419
|
+
texture_scale, screen_w, screen_h = self._load_runtime_config()
|
|
420
|
+
base_id = self._terrain_pair * 2
|
|
421
|
+
base = self._terrain_textures.get(base_id)
|
|
422
|
+
overlay = self._terrain_textures.get(base_id + 1)
|
|
423
|
+
if base is None:
|
|
424
|
+
raise FileNotFoundError("Missing base terrain texture")
|
|
425
|
+
|
|
426
|
+
self._ground = GroundRenderer(
|
|
427
|
+
texture=base,
|
|
428
|
+
overlay=overlay,
|
|
429
|
+
overlay_detail=overlay or base,
|
|
430
|
+
width=int(WORLD_SIZE),
|
|
431
|
+
height=int(WORLD_SIZE),
|
|
432
|
+
texture_scale=texture_scale,
|
|
433
|
+
screen_width=screen_w,
|
|
434
|
+
screen_height=screen_h,
|
|
435
|
+
)
|
|
436
|
+
self._reset_ground()
|
|
437
|
+
self._camera_x = 0.0
|
|
438
|
+
self._camera_y = 0.0
|
|
439
|
+
self._frame = 0
|
|
440
|
+
|
|
441
|
+
log_dir = Path("artifacts") / "debug"
|
|
442
|
+
try:
|
|
443
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
444
|
+
except Exception:
|
|
445
|
+
log_dir = Path("artifacts")
|
|
446
|
+
self._stamp_log_path = log_dir / "decals_stamp_trace.jsonl"
|
|
447
|
+
try:
|
|
448
|
+
self._stamp_log_file = self._stamp_log_path.open("w", encoding="utf-8")
|
|
449
|
+
except Exception:
|
|
450
|
+
self._stamp_log_file = None
|
|
451
|
+
|
|
452
|
+
# Spawn a few enemies near the center for immediate testing.
|
|
453
|
+
for _ in range(6):
|
|
454
|
+
ox = float(int(self._state.rng.rand()) % 200 - 100)
|
|
455
|
+
oy = float(int(self._state.rng.rand()) % 200 - 100)
|
|
456
|
+
self._spawn_enemy(WORLD_SIZE * 0.5 + ox, WORLD_SIZE * 0.5 + oy)
|
|
457
|
+
|
|
458
|
+
def close(self) -> None:
|
|
459
|
+
if self._ground is not None and self._ground.render_target is not None:
|
|
460
|
+
rl.unload_render_texture(self._ground.render_target)
|
|
461
|
+
self._ground.render_target = None
|
|
462
|
+
self._ground = None
|
|
463
|
+
|
|
464
|
+
for texture in self._owned_textures:
|
|
465
|
+
rl.unload_texture(texture)
|
|
466
|
+
self._owned_textures.clear()
|
|
467
|
+
self._terrain_textures.clear()
|
|
468
|
+
self._creature_textures.clear()
|
|
469
|
+
self._fx_textures = None
|
|
470
|
+
|
|
471
|
+
if self._small is not None:
|
|
472
|
+
rl.unload_texture(self._small.texture)
|
|
473
|
+
self._small = None
|
|
474
|
+
|
|
475
|
+
self._fx_queue.clear()
|
|
476
|
+
self._fx_queue_rotated.clear()
|
|
477
|
+
self._creatures.reset()
|
|
478
|
+
if self._stamp_log_file is not None:
|
|
479
|
+
try:
|
|
480
|
+
self._stamp_log_file.close()
|
|
481
|
+
except Exception:
|
|
482
|
+
pass
|
|
483
|
+
self._stamp_log_file = None
|
|
484
|
+
|
|
485
|
+
def update(self, dt: float) -> None:
|
|
486
|
+
self._frame += 1
|
|
487
|
+
|
|
488
|
+
speed = 240.0
|
|
489
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_A):
|
|
490
|
+
self._camera_x += speed * dt
|
|
491
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_D):
|
|
492
|
+
self._camera_x -= speed * dt
|
|
493
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_W):
|
|
494
|
+
self._camera_y += speed * dt
|
|
495
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_S):
|
|
496
|
+
self._camera_y -= speed * dt
|
|
497
|
+
|
|
498
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_G):
|
|
499
|
+
self._light_mode = not self._light_mode
|
|
500
|
+
self._reset_ground()
|
|
501
|
+
|
|
502
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_C):
|
|
503
|
+
self._reset_ground()
|
|
504
|
+
|
|
505
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
|
|
506
|
+
self._terrain_seed = int(rl.get_random_value(0, 0x7FFFFFFF))
|
|
507
|
+
if not self._light_mode:
|
|
508
|
+
self._apply_terrain_pair()
|
|
509
|
+
else:
|
|
510
|
+
self._clear_ground_light()
|
|
511
|
+
|
|
512
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_T):
|
|
513
|
+
self._terrain_pair = int(rl.get_random_value(0, 3))
|
|
514
|
+
if not self._light_mode:
|
|
515
|
+
self._apply_terrain_pair()
|
|
516
|
+
|
|
517
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_L):
|
|
518
|
+
self._show_stamp_log = not self._show_stamp_log
|
|
519
|
+
|
|
520
|
+
if self._ground is not None:
|
|
521
|
+
texture_scale, screen_w, screen_h = self._load_runtime_config()
|
|
522
|
+
self._ground.texture_scale = texture_scale
|
|
523
|
+
self._ground.screen_width = screen_w
|
|
524
|
+
self._ground.screen_height = screen_h
|
|
525
|
+
self._ground.process_pending()
|
|
526
|
+
self._ground.debug_log_stamps = self._show_stamp_log
|
|
527
|
+
if self._show_stamp_log:
|
|
528
|
+
self._ground.debug_clear_stamp_log()
|
|
529
|
+
|
|
530
|
+
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_RIGHT):
|
|
531
|
+
mouse = rl.get_mouse_position()
|
|
532
|
+
x, y = self._screen_to_world(float(mouse.x), float(mouse.y))
|
|
533
|
+
self._spawn_enemy(x, y)
|
|
534
|
+
|
|
535
|
+
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
536
|
+
mouse = rl.get_mouse_position()
|
|
537
|
+
x, y = self._screen_to_world(float(mouse.x), float(mouse.y))
|
|
538
|
+
hit = None
|
|
539
|
+
for creature in self._creatures.entries:
|
|
540
|
+
if not (creature.active and creature.hp > 0.0):
|
|
541
|
+
continue
|
|
542
|
+
dx = float(creature.x) - float(x)
|
|
543
|
+
dy = float(creature.y) - float(y)
|
|
544
|
+
r = float(creature.size) * 0.35 + 12.0
|
|
545
|
+
if dx * dx + dy * dy <= r * r:
|
|
546
|
+
hit = creature
|
|
547
|
+
break
|
|
548
|
+
if hit is not None:
|
|
549
|
+
hit.hp -= 1.0
|
|
550
|
+
self._fx_queue.add_random(pos_x=float(hit.x), pos_y=float(hit.y), rand=self._state.rng.rand)
|
|
551
|
+
else:
|
|
552
|
+
# Paint blood directly for ground decal checks.
|
|
553
|
+
self._fx_queue.add(
|
|
554
|
+
effect_id=0x07,
|
|
555
|
+
pos_x=float(x),
|
|
556
|
+
pos_y=float(y),
|
|
557
|
+
width=30.0,
|
|
558
|
+
height=30.0,
|
|
559
|
+
rotation=0.0,
|
|
560
|
+
rgba=(1.0, 1.0, 1.0, 1.0),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Keep the player fixed; creatures use it as a target for heading/movement.
|
|
564
|
+
self._player.pos_x = WORLD_SIZE * 0.5
|
|
565
|
+
self._player.pos_y = WORLD_SIZE * 0.5
|
|
566
|
+
self._player.health = 1e9
|
|
567
|
+
|
|
568
|
+
creature_result = self._creatures.update(
|
|
569
|
+
dt,
|
|
570
|
+
state=self._state,
|
|
571
|
+
players=[self._player],
|
|
572
|
+
env=self._env,
|
|
573
|
+
world_width=WORLD_SIZE,
|
|
574
|
+
world_height=WORLD_SIZE,
|
|
575
|
+
fx_queue=self._fx_queue,
|
|
576
|
+
fx_queue_rotated=self._fx_queue_rotated,
|
|
577
|
+
)
|
|
578
|
+
del creature_result
|
|
579
|
+
|
|
580
|
+
# Advance alive animation phase (CreaturePool intentionally does not).
|
|
581
|
+
for creature in self._creatures.entries:
|
|
582
|
+
if not (creature.active and creature.hp > 0.0):
|
|
583
|
+
continue
|
|
584
|
+
try:
|
|
585
|
+
type_id = CreatureTypeId(int(creature.type_id))
|
|
586
|
+
except ValueError:
|
|
587
|
+
continue
|
|
588
|
+
info = _CREATURE_ANIM.get(type_id)
|
|
589
|
+
if info is None:
|
|
590
|
+
continue
|
|
591
|
+
creature.anim_phase, _ = creature_anim_advance_phase(
|
|
592
|
+
float(creature.anim_phase),
|
|
593
|
+
anim_rate=info.anim_rate,
|
|
594
|
+
move_speed=float(creature.move_speed),
|
|
595
|
+
dt=dt,
|
|
596
|
+
size=float(creature.size),
|
|
597
|
+
local_scale=float(getattr(creature, "move_scale", 1.0)),
|
|
598
|
+
flags=creature.flags,
|
|
599
|
+
ai_mode=int(creature.ai_mode),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
if self._ground is not None and self._fx_textures is not None:
|
|
603
|
+
if self._fx_queue.count or self._fx_queue_rotated.count:
|
|
604
|
+
fx_count = int(self._fx_queue.count)
|
|
605
|
+
corpse_count = int(self._fx_queue_rotated.count)
|
|
606
|
+
bake_fx_queues(
|
|
607
|
+
self._ground,
|
|
608
|
+
fx_queue=self._fx_queue,
|
|
609
|
+
fx_queue_rotated=self._fx_queue_rotated,
|
|
610
|
+
textures=self._fx_textures,
|
|
611
|
+
corpse_frame_for_type=creature_corpse_frame_for_type,
|
|
612
|
+
corpse_shadow=not self._light_mode,
|
|
613
|
+
)
|
|
614
|
+
if self._show_stamp_log:
|
|
615
|
+
stamp_log = self._ground.debug_stamp_log()
|
|
616
|
+
if stamp_log:
|
|
617
|
+
ts = time.time()
|
|
618
|
+
for idx, event in enumerate(stamp_log):
|
|
619
|
+
self._write_stamp_log(
|
|
620
|
+
{
|
|
621
|
+
"ts": ts,
|
|
622
|
+
"dt": dt,
|
|
623
|
+
"frame": self._frame,
|
|
624
|
+
"event_idx": idx,
|
|
625
|
+
"queue": {"fx": fx_count, "corpse": corpse_count},
|
|
626
|
+
**event,
|
|
627
|
+
}
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
def draw(self) -> None:
|
|
631
|
+
rl.clear_background(BG_LIGHT if self._light_mode else BG_DARK)
|
|
632
|
+
|
|
633
|
+
if self._missing_assets:
|
|
634
|
+
self._draw_ui_text("Missing assets: " + ", ".join(self._missing_assets), 24, 24, UI_ERROR_COLOR)
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
if self._ground is None:
|
|
638
|
+
self._draw_ui_text("Ground renderer not initialized.", 24, 24, UI_ERROR_COLOR)
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
self._ground.draw(self._camera_x, self._camera_y)
|
|
642
|
+
if self._light_mode:
|
|
643
|
+
self._draw_grid()
|
|
644
|
+
|
|
645
|
+
# Creatures (including death slide/fade stage).
|
|
646
|
+
scale = self._world_scale()
|
|
647
|
+
for creature in self._creatures.entries:
|
|
648
|
+
if not creature.active:
|
|
649
|
+
continue
|
|
650
|
+
try:
|
|
651
|
+
type_id = CreatureTypeId(int(creature.type_id))
|
|
652
|
+
except ValueError:
|
|
653
|
+
continue
|
|
654
|
+
asset = _CREATURE_ASSET.get(type_id)
|
|
655
|
+
texture = self._creature_textures.get(asset) if asset is not None else None
|
|
656
|
+
info = _CREATURE_ANIM.get(type_id)
|
|
657
|
+
if texture is None or info is None:
|
|
658
|
+
continue
|
|
659
|
+
|
|
660
|
+
alpha = float(creature.tint_a)
|
|
661
|
+
if float(creature.hitbox_size) < 0.0:
|
|
662
|
+
alpha = max(0.0, alpha + float(creature.hitbox_size) * 0.1)
|
|
663
|
+
r = int(max(0.0, min(float(creature.tint_r), 1.0)) * 255.0 + 0.5)
|
|
664
|
+
g = int(max(0.0, min(float(creature.tint_g), 1.0)) * 255.0 + 0.5)
|
|
665
|
+
b = int(max(0.0, min(float(creature.tint_b), 1.0)) * 255.0 + 0.5)
|
|
666
|
+
a = int(max(0.0, min(alpha, 1.0)) * 255.0 + 0.5)
|
|
667
|
+
tint = rl.Color(r, g, b, a)
|
|
668
|
+
|
|
669
|
+
flags = creature.flags
|
|
670
|
+
long_strip = (flags & CreatureFlags.ANIM_PING_PONG) == 0 or (flags & CreatureFlags.ANIM_LONG_STRIP) != 0
|
|
671
|
+
phase = float(creature.anim_phase)
|
|
672
|
+
hitbox_size = float(creature.hitbox_size)
|
|
673
|
+
if long_strip:
|
|
674
|
+
if hitbox_size < 0.0:
|
|
675
|
+
phase = -1.0
|
|
676
|
+
elif hitbox_size < 16.0:
|
|
677
|
+
phase = float(info.base + 0x0F) - hitbox_size - 0.5
|
|
678
|
+
mirror_long = bool(info.mirror) and hitbox_size >= 16.0
|
|
679
|
+
|
|
680
|
+
size_scale = max(0.25, min(float(creature.size) / 64.0, 2.0))
|
|
681
|
+
self._draw_creature_sprite(
|
|
682
|
+
texture,
|
|
683
|
+
info=info,
|
|
684
|
+
flags=flags,
|
|
685
|
+
phase=phase,
|
|
686
|
+
mirror_long=mirror_long,
|
|
687
|
+
world_x=float(creature.x),
|
|
688
|
+
world_y=float(creature.y),
|
|
689
|
+
rotation_rad=float(creature.heading) - math.pi / 2.0,
|
|
690
|
+
scale=scale,
|
|
691
|
+
size_scale=size_scale,
|
|
692
|
+
tint=tint,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# UI overlay.
|
|
696
|
+
text_color = UI_TEXT_DARK if self._light_mode else UI_TEXT_LIGHT
|
|
697
|
+
hint_color = UI_HINT_DARK if self._light_mode else UI_HINT_LIGHT
|
|
698
|
+
x = 16
|
|
699
|
+
y = 12
|
|
700
|
+
line = self._ui_line_height()
|
|
701
|
+
self._draw_ui_text("Decals debug", x, y, text_color)
|
|
702
|
+
y += line
|
|
703
|
+
self._draw_ui_text("LMB: blood / damage enemy RMB: spawn enemy", x, y, hint_color)
|
|
704
|
+
y += line
|
|
705
|
+
self._draw_ui_text(
|
|
706
|
+
"WASD: pan R: random seed T: random terrain G: toggle light grid C: clear L: stamp log",
|
|
707
|
+
x,
|
|
708
|
+
y,
|
|
709
|
+
hint_color,
|
|
710
|
+
)
|
|
711
|
+
y += line
|
|
712
|
+
self._draw_ui_text(f"enemies={len([c for c in self._creatures.entries if c.active])}", x, y, hint_color)
|
|
713
|
+
y += line
|
|
714
|
+
if self._stamp_log_path is not None:
|
|
715
|
+
status = "on" if self._show_stamp_log else "off"
|
|
716
|
+
self._draw_ui_text(f"stamp log ({status}): {self._stamp_log_path}", x, y, hint_color)
|
|
717
|
+
y += line
|
|
718
|
+
if self._ground is not None and self._show_stamp_log:
|
|
719
|
+
stamp_log = self._ground.debug_stamp_log()
|
|
720
|
+
if stamp_log:
|
|
721
|
+
self._draw_ui_text("stamp order:", x, y, hint_color)
|
|
722
|
+
y += line
|
|
723
|
+
for event in stamp_log[-6:]:
|
|
724
|
+
kind = str(event.get("kind", "?"))
|
|
725
|
+
if kind == "bake_corpse_decals":
|
|
726
|
+
msg = f"{kind} shadow={event.get('shadow')} count={event.get('count')}"
|
|
727
|
+
elif kind == "bake_decals":
|
|
728
|
+
msg = f"{kind} count={event.get('count')}"
|
|
729
|
+
elif kind.endswith("_pass"):
|
|
730
|
+
msg = f"{kind} draws={event.get('draws')}"
|
|
731
|
+
else:
|
|
732
|
+
msg = kind
|
|
733
|
+
self._draw_ui_text(msg, x, y, hint_color)
|
|
734
|
+
y += line
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
@register_view("decals", "Decals debug")
|
|
738
|
+
def build_decals_debug_view(ctx: ViewContext) -> View:
|
|
739
|
+
return DecalsDebugView(ctx)
|