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,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from grim.audio import AudioState, init_audio_state
|
|
8
|
+
from grim.config import CrimsonConfig, ensure_crimson_cfg
|
|
9
|
+
from grim.console import ConsoleLog, ConsoleState
|
|
10
|
+
|
|
11
|
+
from ..assets_fetch import download_missing_paqs
|
|
12
|
+
from ..paths import default_runtime_dir
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class ViewAudioBootstrap:
|
|
17
|
+
config: CrimsonConfig | None
|
|
18
|
+
console: ConsoleState | None
|
|
19
|
+
audio: AudioState | None
|
|
20
|
+
audio_rng: random.Random | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def init_view_audio(assets_dir: Path, *, seed: int = 0xBEEF) -> ViewAudioBootstrap:
|
|
24
|
+
runtime_dir = default_runtime_dir()
|
|
25
|
+
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
try:
|
|
27
|
+
config = ensure_crimson_cfg(runtime_dir)
|
|
28
|
+
except Exception:
|
|
29
|
+
return ViewAudioBootstrap(None, None, None, None)
|
|
30
|
+
|
|
31
|
+
console = ConsoleState(
|
|
32
|
+
base_dir=runtime_dir,
|
|
33
|
+
log=ConsoleLog(base_dir=runtime_dir),
|
|
34
|
+
assets_dir=assets_dir,
|
|
35
|
+
)
|
|
36
|
+
try:
|
|
37
|
+
download_missing_paqs(assets_dir, console)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
console.log.log(f"assets: download failed: {exc}")
|
|
40
|
+
console.log.flush()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
audio = init_audio_state(config, assets_dir, console)
|
|
44
|
+
except Exception:
|
|
45
|
+
return ViewAudioBootstrap(config, console, None, None)
|
|
46
|
+
|
|
47
|
+
return ViewAudioBootstrap(config, console, audio, random.Random(seed))
|
crimson/views/bonuses.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pyray as rl
|
|
6
|
+
|
|
7
|
+
from ..bonuses import BONUS_TABLE, BonusMeta
|
|
8
|
+
from ..weapons import WEAPON_TABLE
|
|
9
|
+
from .registry import register_view
|
|
10
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
11
|
+
from grim.view import View, ViewContext
|
|
12
|
+
|
|
13
|
+
UI_TEXT_SCALE = 1.0
|
|
14
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
15
|
+
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
|
|
16
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
17
|
+
UI_HOVER_COLOR = rl.Color(240, 200, 80, 255)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class BonusIconGroup:
|
|
22
|
+
icon_id: int
|
|
23
|
+
bonuses: tuple[BonusMeta, ...]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_icon_groups() -> dict[int, BonusIconGroup]:
|
|
27
|
+
grouped: dict[int, list[BonusMeta]] = {}
|
|
28
|
+
for entry in BONUS_TABLE:
|
|
29
|
+
if entry.icon_id is None or entry.icon_id < 0:
|
|
30
|
+
continue
|
|
31
|
+
grouped.setdefault(entry.icon_id, []).append(entry)
|
|
32
|
+
return {icon_id: BonusIconGroup(icon_id=icon_id, bonuses=tuple(entries)) for icon_id, entries in grouped.items()}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
BONUS_ICON_GROUPS = _build_icon_groups()
|
|
36
|
+
WEAPON_BONUS = next(
|
|
37
|
+
(entry for entry in BONUS_TABLE if entry.icon_id is not None and entry.icon_id < 0),
|
|
38
|
+
None,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BonusIconView:
|
|
43
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
44
|
+
self._assets_root = ctx.assets_dir
|
|
45
|
+
self._missing_assets: list[str] = []
|
|
46
|
+
self._texture: rl.Texture | None = None
|
|
47
|
+
self._small: SmallFontData | None = None
|
|
48
|
+
|
|
49
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
50
|
+
if self._small is not None:
|
|
51
|
+
return int(self._small.cell_size * scale)
|
|
52
|
+
return int(20 * scale)
|
|
53
|
+
|
|
54
|
+
def _draw_ui_text(
|
|
55
|
+
self,
|
|
56
|
+
text: str,
|
|
57
|
+
x: float,
|
|
58
|
+
y: float,
|
|
59
|
+
color: rl.Color,
|
|
60
|
+
scale: float = UI_TEXT_SCALE,
|
|
61
|
+
) -> None:
|
|
62
|
+
if self._small is not None:
|
|
63
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
64
|
+
else:
|
|
65
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
66
|
+
|
|
67
|
+
def open(self) -> None:
|
|
68
|
+
self._missing_assets.clear()
|
|
69
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
70
|
+
path = self._assets_root / "crimson" / "game" / "bonuses.png"
|
|
71
|
+
if not path.is_file():
|
|
72
|
+
self._missing_assets.append("game/bonuses.png")
|
|
73
|
+
raise FileNotFoundError(f"Missing asset: {path}")
|
|
74
|
+
self._texture = rl.load_texture(str(path))
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
if self._texture is not None:
|
|
78
|
+
rl.unload_texture(self._texture)
|
|
79
|
+
self._texture = None
|
|
80
|
+
if self._small is not None:
|
|
81
|
+
rl.unload_texture(self._small.texture)
|
|
82
|
+
self._small = None
|
|
83
|
+
|
|
84
|
+
def update(self, dt: float) -> None:
|
|
85
|
+
del dt
|
|
86
|
+
|
|
87
|
+
def draw(self) -> None:
|
|
88
|
+
rl.clear_background(rl.Color(12, 12, 14, 255))
|
|
89
|
+
if self._missing_assets:
|
|
90
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
91
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
92
|
+
return
|
|
93
|
+
if self._texture is None:
|
|
94
|
+
self._draw_ui_text("No bonuses texture loaded.", 24, 24, UI_TEXT_COLOR)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
margin = 24
|
|
98
|
+
panel_gap = 32
|
|
99
|
+
panel_width = min(420, int(rl.get_screen_width() * 0.4))
|
|
100
|
+
available_width = rl.get_screen_width() - margin * 2 - panel_gap - panel_width
|
|
101
|
+
available_height = rl.get_screen_height() - margin * 2 - 60
|
|
102
|
+
scale = min(
|
|
103
|
+
3.0,
|
|
104
|
+
available_width / self._texture.width,
|
|
105
|
+
available_height / self._texture.height,
|
|
106
|
+
)
|
|
107
|
+
draw_w = self._texture.width * scale
|
|
108
|
+
draw_h = self._texture.height * scale
|
|
109
|
+
x = margin
|
|
110
|
+
y = margin + 60
|
|
111
|
+
|
|
112
|
+
src = rl.Rectangle(0.0, 0.0, float(self._texture.width), float(self._texture.height))
|
|
113
|
+
dst = rl.Rectangle(float(x), float(y), float(draw_w), float(draw_h))
|
|
114
|
+
rl.draw_texture_pro(self._texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
115
|
+
|
|
116
|
+
grid = 4
|
|
117
|
+
cell_w = draw_w / grid
|
|
118
|
+
cell_h = draw_h / grid
|
|
119
|
+
for i in range(1, grid):
|
|
120
|
+
rl.draw_line(
|
|
121
|
+
int(x + i * cell_w),
|
|
122
|
+
int(y),
|
|
123
|
+
int(x + i * cell_w),
|
|
124
|
+
int(y + draw_h),
|
|
125
|
+
rl.Color(60, 60, 70, 255),
|
|
126
|
+
)
|
|
127
|
+
rl.draw_line(
|
|
128
|
+
int(x),
|
|
129
|
+
int(y + i * cell_h),
|
|
130
|
+
int(x + draw_w),
|
|
131
|
+
int(y + i * cell_h),
|
|
132
|
+
rl.Color(60, 60, 70, 255),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
hovered_index = None
|
|
136
|
+
mouse = rl.get_mouse_position()
|
|
137
|
+
if x <= mouse.x <= x + draw_w and y <= mouse.y <= y + draw_h:
|
|
138
|
+
col = int((mouse.x - x) // cell_w)
|
|
139
|
+
row = int((mouse.y - y) // cell_h)
|
|
140
|
+
if 0 <= col < grid and 0 <= row < grid:
|
|
141
|
+
hovered_index = row * grid + col
|
|
142
|
+
hl = rl.Rectangle(
|
|
143
|
+
float(x + col * cell_w),
|
|
144
|
+
float(y + row * cell_h),
|
|
145
|
+
float(cell_w),
|
|
146
|
+
float(cell_h),
|
|
147
|
+
)
|
|
148
|
+
rl.draw_rectangle_lines_ex(hl, 3, UI_HOVER_COLOR)
|
|
149
|
+
|
|
150
|
+
info_x = x + draw_w + panel_gap
|
|
151
|
+
info_y = margin
|
|
152
|
+
self._draw_ui_text("bonuses.png (grid 4x4)", info_x, info_y, UI_TEXT_COLOR)
|
|
153
|
+
info_y += self._ui_line_height() + 12
|
|
154
|
+
|
|
155
|
+
if hovered_index is not None:
|
|
156
|
+
group = BONUS_ICON_GROUPS.get(hovered_index)
|
|
157
|
+
self._draw_ui_text(f"icon_id {hovered_index}", info_x, info_y, UI_TEXT_COLOR)
|
|
158
|
+
info_y += self._ui_line_height() + 6
|
|
159
|
+
if group is None:
|
|
160
|
+
self._draw_ui_text("no bonus mapping", info_x, info_y, UI_HINT_COLOR)
|
|
161
|
+
info_y += self._ui_line_height() + 6
|
|
162
|
+
else:
|
|
163
|
+
for entry in group.bonuses:
|
|
164
|
+
bonus_id = int(entry.bonus_id)
|
|
165
|
+
amount = entry.default_amount
|
|
166
|
+
amount_label = f" default={amount}" if amount is not None else ""
|
|
167
|
+
self._draw_ui_text(
|
|
168
|
+
f"id {bonus_id:02d} {entry.name}{amount_label}",
|
|
169
|
+
info_x,
|
|
170
|
+
info_y,
|
|
171
|
+
UI_TEXT_COLOR,
|
|
172
|
+
)
|
|
173
|
+
info_y += self._ui_line_height() + 4
|
|
174
|
+
if entry.description:
|
|
175
|
+
self._draw_ui_text(
|
|
176
|
+
entry.description,
|
|
177
|
+
info_x,
|
|
178
|
+
info_y,
|
|
179
|
+
UI_HINT_COLOR,
|
|
180
|
+
)
|
|
181
|
+
info_y += self._ui_line_height() + 4
|
|
182
|
+
info_y += 8
|
|
183
|
+
|
|
184
|
+
if WEAPON_BONUS is not None:
|
|
185
|
+
self._draw_ui_text("Weapon bonus icon", info_x, info_y, UI_TEXT_COLOR)
|
|
186
|
+
info_y += self._ui_line_height() + 4
|
|
187
|
+
weapon_id = WEAPON_BONUS.default_amount
|
|
188
|
+
weapon_name = None
|
|
189
|
+
if weapon_id is not None:
|
|
190
|
+
for weapon in WEAPON_TABLE:
|
|
191
|
+
if weapon.weapon_id == weapon_id:
|
|
192
|
+
weapon_name = weapon.name
|
|
193
|
+
break
|
|
194
|
+
name_label = f" ({weapon_name})" if weapon_name else ""
|
|
195
|
+
weapon_label = f"icon_id = -1 → ui_wicons (default weapon {weapon_id}{name_label})"
|
|
196
|
+
self._draw_ui_text(weapon_label, info_x, info_y, UI_HINT_COLOR)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@register_view("bonuses", "Bonus icon preview")
|
|
200
|
+
def build_bonus_view(ctx: ViewContext) -> View:
|
|
201
|
+
return BonusIconView(ctx)
|
|
@@ -0,0 +1,359 @@
|
|
|
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 grim.assets import resolve_asset_path
|
|
12
|
+
from grim.config import ensure_crimson_cfg
|
|
13
|
+
from grim.terrain_render import GroundRenderer
|
|
14
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
15
|
+
from grim.view import View, ViewContext
|
|
16
|
+
|
|
17
|
+
from ..paths import default_runtime_dir
|
|
18
|
+
from .registry import register_view
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
WORLD_SIZE = 1024.0
|
|
22
|
+
WINDOW_W = 640
|
|
23
|
+
WINDOW_H = 480
|
|
24
|
+
GRID_STEP = 64.0
|
|
25
|
+
LOG_INTERVAL_S = 0.1
|
|
26
|
+
|
|
27
|
+
UI_TEXT_SCALE = 1.0
|
|
28
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
29
|
+
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
|
|
30
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class CameraDebugAssets:
|
|
35
|
+
base: rl.Texture
|
|
36
|
+
overlay: rl.Texture | None
|
|
37
|
+
detail: rl.Texture | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CameraDebugView:
|
|
41
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
42
|
+
self._assets_root = ctx.assets_dir
|
|
43
|
+
self._missing_assets: list[str] = []
|
|
44
|
+
self._small: SmallFontData | None = None
|
|
45
|
+
self._assets: CameraDebugAssets | None = None
|
|
46
|
+
self._renderer: GroundRenderer | None = None
|
|
47
|
+
self._config_screen_w = float(WINDOW_W)
|
|
48
|
+
self._config_screen_h = float(WINDOW_H)
|
|
49
|
+
self._texture_scale = 1.0
|
|
50
|
+
self._use_config_screen = False
|
|
51
|
+
self._player_x = WORLD_SIZE * 0.5
|
|
52
|
+
self._player_y = WORLD_SIZE * 0.5
|
|
53
|
+
self._camera_x = -1.0
|
|
54
|
+
self._camera_y = -1.0
|
|
55
|
+
self._camera_target_x = -1.0
|
|
56
|
+
self._camera_target_y = -1.0
|
|
57
|
+
self._log_timer = 0.0
|
|
58
|
+
self._log_path: Path | None = None
|
|
59
|
+
self._log_file = None
|
|
60
|
+
|
|
61
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
62
|
+
if self._small is not None:
|
|
63
|
+
return int(self._small.cell_size * scale)
|
|
64
|
+
return int(20 * scale)
|
|
65
|
+
|
|
66
|
+
def _draw_ui_text(
|
|
67
|
+
self,
|
|
68
|
+
text: str,
|
|
69
|
+
x: float,
|
|
70
|
+
y: float,
|
|
71
|
+
color: rl.Color,
|
|
72
|
+
scale: float = UI_TEXT_SCALE,
|
|
73
|
+
) -> None:
|
|
74
|
+
if self._small is not None:
|
|
75
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
76
|
+
else:
|
|
77
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
78
|
+
|
|
79
|
+
def _load_runtime_config(self) -> None:
|
|
80
|
+
runtime_dir = default_runtime_dir()
|
|
81
|
+
if not runtime_dir.is_dir():
|
|
82
|
+
return
|
|
83
|
+
try:
|
|
84
|
+
cfg = ensure_crimson_cfg(runtime_dir)
|
|
85
|
+
except Exception:
|
|
86
|
+
return
|
|
87
|
+
self._config_screen_w = float(cfg.screen_width)
|
|
88
|
+
self._config_screen_h = float(cfg.screen_height)
|
|
89
|
+
self._texture_scale = float(cfg.texture_scale)
|
|
90
|
+
|
|
91
|
+
def _camera_screen_size(self) -> tuple[float, float]:
|
|
92
|
+
if self._use_config_screen:
|
|
93
|
+
screen_w = float(self._config_screen_w)
|
|
94
|
+
screen_h = float(self._config_screen_h)
|
|
95
|
+
else:
|
|
96
|
+
screen_w = float(rl.get_screen_width())
|
|
97
|
+
screen_h = float(rl.get_screen_height())
|
|
98
|
+
if screen_w > WORLD_SIZE:
|
|
99
|
+
screen_w = WORLD_SIZE
|
|
100
|
+
if screen_h > WORLD_SIZE:
|
|
101
|
+
screen_h = WORLD_SIZE
|
|
102
|
+
return screen_w, screen_h
|
|
103
|
+
|
|
104
|
+
def _clamp_camera(self, cam_x: float, cam_y: float, screen_w: float, screen_h: float) -> tuple[float, float]:
|
|
105
|
+
min_x = screen_w - WORLD_SIZE
|
|
106
|
+
min_y = screen_h - WORLD_SIZE
|
|
107
|
+
if cam_x > -1.0:
|
|
108
|
+
cam_x = -1.0
|
|
109
|
+
if cam_y > -1.0:
|
|
110
|
+
cam_y = -1.0
|
|
111
|
+
if cam_x < min_x:
|
|
112
|
+
cam_x = min_x
|
|
113
|
+
if cam_y < min_y:
|
|
114
|
+
cam_y = min_y
|
|
115
|
+
return cam_x, cam_y
|
|
116
|
+
|
|
117
|
+
def _world_params(self) -> tuple[float, float, float, float, float, float]:
|
|
118
|
+
out_w = float(rl.get_screen_width())
|
|
119
|
+
out_h = float(rl.get_screen_height())
|
|
120
|
+
screen_w, screen_h = self._camera_screen_size()
|
|
121
|
+
cam_x, cam_y = self._clamp_camera(self._camera_x, self._camera_y, screen_w, screen_h)
|
|
122
|
+
scale_x = out_w / screen_w if screen_w > 0 else 1.0
|
|
123
|
+
scale_y = out_h / screen_h if screen_h > 0 else 1.0
|
|
124
|
+
return cam_x, cam_y, scale_x, scale_y, screen_w, screen_h
|
|
125
|
+
|
|
126
|
+
def _write_log(self, payload: dict) -> None:
|
|
127
|
+
if self._log_file is None:
|
|
128
|
+
return
|
|
129
|
+
try:
|
|
130
|
+
self._log_file.write(json.dumps(payload, sort_keys=True) + "\n")
|
|
131
|
+
self._log_file.flush()
|
|
132
|
+
except Exception:
|
|
133
|
+
self._log_file = None
|
|
134
|
+
|
|
135
|
+
def open(self) -> None:
|
|
136
|
+
rl.set_window_size(WINDOW_W, WINDOW_H)
|
|
137
|
+
self._missing_assets.clear()
|
|
138
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
139
|
+
base_path = resolve_asset_path(self._assets_root, "ter/ter_q1_base.png")
|
|
140
|
+
overlay_path = resolve_asset_path(self._assets_root, "ter/ter_q1_tex1.png")
|
|
141
|
+
if base_path is None:
|
|
142
|
+
self._missing_assets.append("ter/ter_q1_base.png")
|
|
143
|
+
if overlay_path is None:
|
|
144
|
+
self._missing_assets.append("ter/ter_q1_tex1.png")
|
|
145
|
+
if self._missing_assets:
|
|
146
|
+
raise FileNotFoundError("Missing assets: " + ", ".join(self._missing_assets))
|
|
147
|
+
base = rl.load_texture(str(base_path))
|
|
148
|
+
overlay = rl.load_texture(str(overlay_path)) if overlay_path is not None else None
|
|
149
|
+
detail = overlay or base
|
|
150
|
+
self._assets = CameraDebugAssets(base=base, overlay=overlay, detail=detail)
|
|
151
|
+
|
|
152
|
+
self._load_runtime_config()
|
|
153
|
+
self._renderer = GroundRenderer(
|
|
154
|
+
texture=base,
|
|
155
|
+
overlay=overlay,
|
|
156
|
+
overlay_detail=detail,
|
|
157
|
+
width=int(WORLD_SIZE),
|
|
158
|
+
height=int(WORLD_SIZE),
|
|
159
|
+
texture_scale=self._texture_scale,
|
|
160
|
+
screen_width=self._config_screen_w if self._use_config_screen else None,
|
|
161
|
+
screen_height=self._config_screen_h if self._use_config_screen else None,
|
|
162
|
+
)
|
|
163
|
+
self._renderer.schedule_generate(seed=0, layers=3)
|
|
164
|
+
|
|
165
|
+
log_dir = Path("artifacts") / "debug"
|
|
166
|
+
try:
|
|
167
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
except Exception:
|
|
169
|
+
log_dir = Path("artifacts")
|
|
170
|
+
self._log_path = log_dir / "camera_debug.jsonl"
|
|
171
|
+
try:
|
|
172
|
+
self._log_file = self._log_path.open("w", encoding="utf-8")
|
|
173
|
+
except Exception:
|
|
174
|
+
self._log_file = None
|
|
175
|
+
|
|
176
|
+
def close(self) -> None:
|
|
177
|
+
if self._assets is not None:
|
|
178
|
+
rl.unload_texture(self._assets.base)
|
|
179
|
+
if self._assets.overlay is not None:
|
|
180
|
+
rl.unload_texture(self._assets.overlay)
|
|
181
|
+
self._assets = None
|
|
182
|
+
if self._renderer is not None:
|
|
183
|
+
if self._renderer.render_target is not None:
|
|
184
|
+
rl.unload_render_texture(self._renderer.render_target)
|
|
185
|
+
self._renderer = None
|
|
186
|
+
if self._small is not None:
|
|
187
|
+
rl.unload_texture(self._small.texture)
|
|
188
|
+
self._small = None
|
|
189
|
+
if self._log_file is not None:
|
|
190
|
+
try:
|
|
191
|
+
self._log_file.close()
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
self._log_file = None
|
|
195
|
+
|
|
196
|
+
def update(self, dt: float) -> None:
|
|
197
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_F1):
|
|
198
|
+
self._use_config_screen = not self._use_config_screen
|
|
199
|
+
speed = 120.0
|
|
200
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_LEFT_SHIFT) or rl.is_key_down(rl.KeyboardKey.KEY_RIGHT_SHIFT):
|
|
201
|
+
speed *= 2.0
|
|
202
|
+
move_x = 0.0
|
|
203
|
+
move_y = 0.0
|
|
204
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_A):
|
|
205
|
+
move_x -= 1.0
|
|
206
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_D):
|
|
207
|
+
move_x += 1.0
|
|
208
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_W):
|
|
209
|
+
move_y -= 1.0
|
|
210
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_S):
|
|
211
|
+
move_y += 1.0
|
|
212
|
+
if move_x != 0.0 or move_y != 0.0:
|
|
213
|
+
length = math.hypot(move_x, move_y)
|
|
214
|
+
if length > 0.0:
|
|
215
|
+
move_x /= length
|
|
216
|
+
move_y /= length
|
|
217
|
+
self._player_x += move_x * speed * dt
|
|
218
|
+
self._player_y += move_y * speed * dt
|
|
219
|
+
self._player_x = max(0.0, min(WORLD_SIZE, self._player_x))
|
|
220
|
+
self._player_y = max(0.0, min(WORLD_SIZE, self._player_y))
|
|
221
|
+
|
|
222
|
+
screen_w, screen_h = self._camera_screen_size()
|
|
223
|
+
desired_x = (screen_w * 0.5) - self._player_x
|
|
224
|
+
desired_y = (screen_h * 0.5) - self._player_y
|
|
225
|
+
desired_x, desired_y = self._clamp_camera(desired_x, desired_y, screen_w, screen_h)
|
|
226
|
+
self._camera_target_x = desired_x
|
|
227
|
+
self._camera_target_y = desired_y
|
|
228
|
+
|
|
229
|
+
t = max(0.0, min(dt * 6.0, 1.0))
|
|
230
|
+
self._camera_x = self._camera_x + (desired_x - self._camera_x) * t
|
|
231
|
+
self._camera_y = self._camera_y + (desired_y - self._camera_y) * t
|
|
232
|
+
|
|
233
|
+
if self._renderer is not None:
|
|
234
|
+
self._renderer.texture_scale = self._texture_scale
|
|
235
|
+
if self._use_config_screen:
|
|
236
|
+
self._renderer.screen_width = self._config_screen_w
|
|
237
|
+
self._renderer.screen_height = self._config_screen_h
|
|
238
|
+
else:
|
|
239
|
+
self._renderer.screen_width = None
|
|
240
|
+
self._renderer.screen_height = None
|
|
241
|
+
self._renderer.process_pending()
|
|
242
|
+
|
|
243
|
+
self._log_timer += dt
|
|
244
|
+
if self._log_timer >= LOG_INTERVAL_S:
|
|
245
|
+
self._log_timer -= LOG_INTERVAL_S
|
|
246
|
+
cam_x, cam_y, scale_x, scale_y, screen_w, screen_h = self._world_params()
|
|
247
|
+
payload = {
|
|
248
|
+
"ts": time.time(),
|
|
249
|
+
"dt": dt,
|
|
250
|
+
"player": {"x": self._player_x, "y": self._player_y},
|
|
251
|
+
"camera": {"x": cam_x, "y": cam_y},
|
|
252
|
+
"camera_raw": {"x": self._camera_x, "y": self._camera_y},
|
|
253
|
+
"camera_target": {"x": self._camera_target_x, "y": self._camera_target_y},
|
|
254
|
+
"world": {"size": WORLD_SIZE},
|
|
255
|
+
"screen": {
|
|
256
|
+
"window": {"w": rl.get_screen_width(), "h": rl.get_screen_height()},
|
|
257
|
+
"camera": {"w": screen_w, "h": screen_h},
|
|
258
|
+
"config": {"w": self._config_screen_w, "h": self._config_screen_h},
|
|
259
|
+
"use_config": self._use_config_screen,
|
|
260
|
+
},
|
|
261
|
+
"texture_scale": self._texture_scale,
|
|
262
|
+
"scale": {"x": scale_x, "y": scale_y},
|
|
263
|
+
"uv": {
|
|
264
|
+
"u0": -cam_x / WORLD_SIZE,
|
|
265
|
+
"v0": -cam_y / WORLD_SIZE,
|
|
266
|
+
"u1": (-cam_x + screen_w) / WORLD_SIZE,
|
|
267
|
+
"v1": (-cam_y + screen_h) / WORLD_SIZE,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
if self._log_path is not None:
|
|
271
|
+
payload["log_path"] = str(self._log_path)
|
|
272
|
+
self._write_log(payload)
|
|
273
|
+
|
|
274
|
+
def draw(self) -> None:
|
|
275
|
+
clear_color = rl.Color(10, 10, 12, 255)
|
|
276
|
+
rl.clear_background(clear_color)
|
|
277
|
+
|
|
278
|
+
if self._renderer is None:
|
|
279
|
+
self._draw_ui_text("Ground renderer not initialized.", 16, 16, UI_ERROR_COLOR)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
cam_x, cam_y, scale_x, scale_y, screen_w, screen_h = self._world_params()
|
|
283
|
+
self._renderer.draw(cam_x, cam_y, screen_w=screen_w, screen_h=screen_h)
|
|
284
|
+
|
|
285
|
+
# Grid in world space
|
|
286
|
+
grid_major = rl.Color(70, 80, 95, 180)
|
|
287
|
+
grid_minor = rl.Color(40, 50, 65, 140)
|
|
288
|
+
for i in range(0, int(WORLD_SIZE) + 1, int(GRID_STEP)):
|
|
289
|
+
color = grid_major if i % 256 == 0 else grid_minor
|
|
290
|
+
sx = (float(i) + cam_x) * scale_x
|
|
291
|
+
sy0 = (0.0 + cam_y) * scale_y
|
|
292
|
+
sy1 = (WORLD_SIZE + cam_y) * scale_y
|
|
293
|
+
rl.draw_line(int(sx), int(sy0), int(sx), int(sy1), color)
|
|
294
|
+
sy = (float(i) + cam_y) * scale_y
|
|
295
|
+
sx0 = (0.0 + cam_x) * scale_x
|
|
296
|
+
sx1 = (WORLD_SIZE + cam_x) * scale_x
|
|
297
|
+
rl.draw_line(int(sx0), int(sy), int(sx1), int(sy), color)
|
|
298
|
+
|
|
299
|
+
# Player
|
|
300
|
+
px = (self._player_x + cam_x) * scale_x
|
|
301
|
+
py = (self._player_y + cam_y) * scale_y
|
|
302
|
+
rl.draw_circle(int(px), int(py), max(2, int(6 * (scale_x + scale_y) * 0.5)), rl.Color(255, 200, 120, 255))
|
|
303
|
+
|
|
304
|
+
# Minimap
|
|
305
|
+
out_w = float(rl.get_screen_width())
|
|
306
|
+
map_size = 160.0
|
|
307
|
+
margin = 12.0
|
|
308
|
+
map_x = out_w - map_size - margin
|
|
309
|
+
map_y = margin
|
|
310
|
+
rl.draw_rectangle(int(map_x), int(map_y), int(map_size), int(map_size), rl.Color(12, 12, 18, 220))
|
|
311
|
+
rl.draw_rectangle_lines(int(map_x), int(map_y), int(map_size), int(map_size), rl.Color(180, 180, 200, 220))
|
|
312
|
+
|
|
313
|
+
map_scale = map_size / WORLD_SIZE
|
|
314
|
+
view_left = -cam_x
|
|
315
|
+
view_top = -cam_y
|
|
316
|
+
view_w = screen_w
|
|
317
|
+
view_h = screen_h
|
|
318
|
+
vx = map_x + view_left * map_scale
|
|
319
|
+
vy = map_y + view_top * map_scale
|
|
320
|
+
vw = view_w * map_scale
|
|
321
|
+
vh = view_h * map_scale
|
|
322
|
+
rl.draw_rectangle_lines(int(vx), int(vy), int(vw), int(vh), rl.Color(120, 200, 255, 220))
|
|
323
|
+
mx = map_x + self._player_x * map_scale
|
|
324
|
+
my = map_y + self._player_y * map_scale
|
|
325
|
+
rl.draw_circle(int(mx), int(my), 3, rl.Color(255, 200, 120, 255))
|
|
326
|
+
|
|
327
|
+
# HUD
|
|
328
|
+
x = 16.0
|
|
329
|
+
y = 16.0
|
|
330
|
+
line = self._ui_line_height()
|
|
331
|
+
mode = "config" if self._use_config_screen else "window"
|
|
332
|
+
self._draw_ui_text(
|
|
333
|
+
f"window={int(out_w)}x{int(rl.get_screen_height())} camera={int(screen_w)}x{int(screen_h)} ({mode})",
|
|
334
|
+
x,
|
|
335
|
+
y,
|
|
336
|
+
UI_TEXT_COLOR,
|
|
337
|
+
)
|
|
338
|
+
y += line
|
|
339
|
+
self._draw_ui_text(
|
|
340
|
+
f"config={int(self._config_screen_w)}x{int(self._config_screen_h)} "
|
|
341
|
+
f"scale={scale_x:.3f},{scale_y:.3f} tex={self._texture_scale:.2f}",
|
|
342
|
+
x,
|
|
343
|
+
y,
|
|
344
|
+
UI_TEXT_COLOR,
|
|
345
|
+
)
|
|
346
|
+
y += line
|
|
347
|
+
self._draw_ui_text(f"player={self._player_x:.1f},{self._player_y:.1f}", x, y, UI_TEXT_COLOR)
|
|
348
|
+
y += line
|
|
349
|
+
self._draw_ui_text(f"camera={cam_x:.1f},{cam_y:.1f}", x, y, UI_TEXT_COLOR)
|
|
350
|
+
y += line
|
|
351
|
+
if self._log_path is not None:
|
|
352
|
+
self._draw_ui_text(f"log: {self._log_path}", x, y, UI_HINT_COLOR, scale=0.9)
|
|
353
|
+
y += line
|
|
354
|
+
self._draw_ui_text("F1: toggle camera size (config/window)", x, y, UI_HINT_COLOR)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@register_view("camera-debug", "Camera debug")
|
|
358
|
+
def build_camera_debug_view(*, ctx: ViewContext) -> View:
|
|
359
|
+
return CameraDebugView(ctx)
|