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
grim/console.py
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable, Iterable
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
import pyray as rl
|
|
9
|
+
|
|
10
|
+
from . import paq
|
|
11
|
+
from grim.fonts.grim_mono import (
|
|
12
|
+
GrimMonoFont,
|
|
13
|
+
draw_grim_mono_text,
|
|
14
|
+
load_grim_mono_font,
|
|
15
|
+
)
|
|
16
|
+
from grim.fonts.small import (
|
|
17
|
+
SmallFontData,
|
|
18
|
+
draw_small_text,
|
|
19
|
+
load_small_font,
|
|
20
|
+
measure_small_text_width,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
CONSOLE_LOG_NAME = "console.log"
|
|
24
|
+
MAX_CONSOLE_LINES = 0x1000
|
|
25
|
+
MAX_CONSOLE_INPUT = 0x3FF
|
|
26
|
+
DEFAULT_CONSOLE_HEIGHT = 300
|
|
27
|
+
EXTENDED_CONSOLE_HEIGHT = 480
|
|
28
|
+
CONSOLE_VERSION_TEXT = "Crimsonland 1.9.93"
|
|
29
|
+
CONSOLE_ANIM_SPEED = 3.5
|
|
30
|
+
CONSOLE_BLINK_SPEED = 3.0
|
|
31
|
+
CONSOLE_LINE_HEIGHT = 16.0
|
|
32
|
+
CONSOLE_MONO_SCALE = 0.5
|
|
33
|
+
CONSOLE_SMALL_SCALE = 1.0
|
|
34
|
+
CONSOLE_TEXT_X = 10.0
|
|
35
|
+
CONSOLE_INPUT_X_MONO = 26.0
|
|
36
|
+
CONSOLE_VERSION_OFFSET_X = 210.0
|
|
37
|
+
CONSOLE_VERSION_OFFSET_Y = 18.0
|
|
38
|
+
CONSOLE_BORDER_HEIGHT = 4.0
|
|
39
|
+
CONSOLE_BG_COLOR = (0.140625, 0.1875, 0.2890625)
|
|
40
|
+
CONSOLE_BORDER_COLOR = (0.21875, 0.265625, 0.3671875)
|
|
41
|
+
CONSOLE_PROMPT_MONO = ">"
|
|
42
|
+
CONSOLE_PROMPT_SMALL_FMT = ">%s"
|
|
43
|
+
CONSOLE_CARET_TEXT = "_"
|
|
44
|
+
SCRIPT_PAQ_NAMES = ("music.paq", "crimson.paq", "sfx.paq")
|
|
45
|
+
|
|
46
|
+
CommandHandler = Callable[[list[str]], None]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def game_build_path(base_dir: Path, name: str) -> Path:
|
|
50
|
+
return base_dir / name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_float(value: str) -> float:
|
|
54
|
+
try:
|
|
55
|
+
return float(value)
|
|
56
|
+
except ValueError:
|
|
57
|
+
return 0.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize_script_path(name: str) -> Path:
|
|
61
|
+
raw = name.strip().strip("\"'")
|
|
62
|
+
normalized = raw.replace("\\", "/")
|
|
63
|
+
return Path(normalized)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _resolve_script_path(console: "ConsoleState", target: Path) -> Path | None:
|
|
67
|
+
if target.is_absolute():
|
|
68
|
+
return target if target.is_file() else None
|
|
69
|
+
for base in console.script_dirs:
|
|
70
|
+
candidate = base / target
|
|
71
|
+
if candidate.is_file():
|
|
72
|
+
return candidate
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _primary_script_dirs(console: "ConsoleState") -> tuple[Path, ...]:
|
|
77
|
+
dirs: list[Path] = [console.base_dir]
|
|
78
|
+
if console.assets_dir is not None and console.assets_dir not in dirs:
|
|
79
|
+
dirs.append(console.assets_dir)
|
|
80
|
+
return tuple(dirs)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _resolve_script_path_in(target: Path, roots: Iterable[Path]) -> Path | None:
|
|
84
|
+
if target.is_absolute():
|
|
85
|
+
return target if target.is_file() else None
|
|
86
|
+
for base in roots:
|
|
87
|
+
candidate = base / target
|
|
88
|
+
if candidate.is_file():
|
|
89
|
+
return candidate
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _iter_script_paq_paths(console: "ConsoleState") -> Iterable[Path]:
|
|
94
|
+
roots: list[Path] = []
|
|
95
|
+
if console.assets_dir is not None:
|
|
96
|
+
roots.append(console.assets_dir)
|
|
97
|
+
if console.base_dir not in roots:
|
|
98
|
+
roots.append(console.base_dir)
|
|
99
|
+
for root in roots:
|
|
100
|
+
for name in SCRIPT_PAQ_NAMES:
|
|
101
|
+
path = root / name
|
|
102
|
+
if path.is_file():
|
|
103
|
+
yield path
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _load_script_from_paq(console: "ConsoleState", target: Path) -> str | None:
|
|
107
|
+
if target.is_absolute():
|
|
108
|
+
return None
|
|
109
|
+
normalized = target.as_posix().replace("\\", "/")
|
|
110
|
+
normalized_lower = normalized.lower()
|
|
111
|
+
for paq_path in _iter_script_paq_paths(console):
|
|
112
|
+
try:
|
|
113
|
+
for name, data in paq.iter_entries(paq_path):
|
|
114
|
+
entry_name = name.replace("\\", "/")
|
|
115
|
+
if entry_name == normalized or entry_name.lower() == normalized_lower:
|
|
116
|
+
return data.decode("utf-8", errors="ignore")
|
|
117
|
+
except OSError:
|
|
118
|
+
continue
|
|
119
|
+
except Exception:
|
|
120
|
+
continue
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _clamp(value: float, low: float, high: float) -> float:
|
|
125
|
+
return max(low, min(high, value))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _rgba(r: float, g: float, b: float, a: float) -> rl.Color:
|
|
129
|
+
return rl.Color(
|
|
130
|
+
int(_clamp(r, 0.0, 1.0) * 255),
|
|
131
|
+
int(_clamp(g, 0.0, 1.0) * 255),
|
|
132
|
+
int(_clamp(b, 0.0, 1.0) * 255),
|
|
133
|
+
int(_clamp(a, 0.0, 1.0) * 255),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(slots=True)
|
|
138
|
+
class ConsoleCvar:
|
|
139
|
+
name: str
|
|
140
|
+
value: str
|
|
141
|
+
value_f: float
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_value(cls, name: str, value: str) -> "ConsoleCvar":
|
|
145
|
+
return cls(name=name, value=value, value_f=_parse_float(value))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(slots=True)
|
|
149
|
+
class ConsoleLog:
|
|
150
|
+
base_dir: Path
|
|
151
|
+
lines: list[str] = field(default_factory=list)
|
|
152
|
+
flushed_index: int = 0
|
|
153
|
+
|
|
154
|
+
def log(self, message: str) -> None:
|
|
155
|
+
self.lines.append(message)
|
|
156
|
+
if len(self.lines) > MAX_CONSOLE_LINES:
|
|
157
|
+
overflow = len(self.lines) - MAX_CONSOLE_LINES
|
|
158
|
+
del self.lines[:overflow]
|
|
159
|
+
self.flushed_index = max(0, self.flushed_index - overflow)
|
|
160
|
+
|
|
161
|
+
def clear(self) -> None:
|
|
162
|
+
self.lines.clear()
|
|
163
|
+
self.flushed_index = 0
|
|
164
|
+
|
|
165
|
+
def flush(self) -> None:
|
|
166
|
+
if self.flushed_index >= len(self.lines):
|
|
167
|
+
return
|
|
168
|
+
path = game_build_path(self.base_dir, CONSOLE_LOG_NAME)
|
|
169
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
171
|
+
for line in self.lines[self.flushed_index :]:
|
|
172
|
+
handle.write(line.rstrip() + "\n")
|
|
173
|
+
self.flushed_index = len(self.lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(slots=True)
|
|
177
|
+
class ConsoleState:
|
|
178
|
+
base_dir: Path
|
|
179
|
+
log: ConsoleLog
|
|
180
|
+
assets_dir: Path | None = None
|
|
181
|
+
script_dirs: tuple[Path, ...] = field(default_factory=tuple)
|
|
182
|
+
commands: dict[str, CommandHandler] = field(default_factory=dict)
|
|
183
|
+
cvars: dict[str, ConsoleCvar] = field(default_factory=dict)
|
|
184
|
+
open_flag: bool = False
|
|
185
|
+
input_enabled: bool = False
|
|
186
|
+
input_ready: bool = False
|
|
187
|
+
input_buffer: str = ""
|
|
188
|
+
input_caret: int = 0
|
|
189
|
+
history: list[str] = field(default_factory=list)
|
|
190
|
+
history_index: int | None = None
|
|
191
|
+
history_pending: str = ""
|
|
192
|
+
scroll_offset: int = 0
|
|
193
|
+
height_px: int = DEFAULT_CONSOLE_HEIGHT
|
|
194
|
+
echo_enabled: bool = True
|
|
195
|
+
quit_requested: bool = False
|
|
196
|
+
prompt_string: str = "> %s"
|
|
197
|
+
_mono_font: GrimMonoFont | None = field(default=None, init=False, repr=False)
|
|
198
|
+
_mono_font_error: str | None = field(default=None, init=False, repr=False)
|
|
199
|
+
_small_font: SmallFontData | None = field(default=None, init=False, repr=False)
|
|
200
|
+
_small_font_error: str | None = field(default=None, init=False, repr=False)
|
|
201
|
+
_slide_t: float = 1.0
|
|
202
|
+
_offset_y: float = field(default=0.0, init=False)
|
|
203
|
+
_blink_time: float = 0.0
|
|
204
|
+
|
|
205
|
+
def register_command(self, name: str, handler: CommandHandler) -> None:
|
|
206
|
+
self.commands[name] = handler
|
|
207
|
+
|
|
208
|
+
def register_cvar(self, name: str, value: str) -> None:
|
|
209
|
+
self.cvars[name] = ConsoleCvar.from_value(name, value)
|
|
210
|
+
|
|
211
|
+
def add_script_dir(self, path: Path | None) -> None:
|
|
212
|
+
if path is None:
|
|
213
|
+
return
|
|
214
|
+
if path in self.script_dirs:
|
|
215
|
+
return
|
|
216
|
+
self.script_dirs = (*self.script_dirs, path)
|
|
217
|
+
|
|
218
|
+
def set_open(self, open_flag: bool) -> None:
|
|
219
|
+
self.open_flag = open_flag
|
|
220
|
+
self.input_enabled = open_flag
|
|
221
|
+
self.input_ready = False
|
|
222
|
+
self.history_index = None
|
|
223
|
+
self._flush_input_queue()
|
|
224
|
+
|
|
225
|
+
def toggle_open(self) -> None:
|
|
226
|
+
self.set_open(not self.open_flag)
|
|
227
|
+
|
|
228
|
+
def handle_hotkey(self) -> None:
|
|
229
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_GRAVE):
|
|
230
|
+
self.toggle_open()
|
|
231
|
+
|
|
232
|
+
def exec_line(self, line: str) -> None:
|
|
233
|
+
tokens = self._tokenize_line(line)
|
|
234
|
+
if not tokens:
|
|
235
|
+
return
|
|
236
|
+
name, args = tokens[0], tokens[1:]
|
|
237
|
+
cvar = self.cvars.get(name)
|
|
238
|
+
if cvar is not None:
|
|
239
|
+
if args:
|
|
240
|
+
value = " ".join(args)
|
|
241
|
+
cvar.value = value
|
|
242
|
+
cvar.value_f = _parse_float(value)
|
|
243
|
+
self.log.log(f"\"{cvar.name}\" set to \"{cvar.value}\" ({cvar.value_f:.6f})")
|
|
244
|
+
else:
|
|
245
|
+
self.log.log(f"\"{cvar.name}\" is \"{cvar.value}\" ({cvar.value_f:.6f})")
|
|
246
|
+
return
|
|
247
|
+
handler = self.commands.get(name)
|
|
248
|
+
if handler is not None:
|
|
249
|
+
handler(args)
|
|
250
|
+
return
|
|
251
|
+
self.log.log(f"Unknown command \"{name}\"")
|
|
252
|
+
|
|
253
|
+
def update(self, dt: float) -> None:
|
|
254
|
+
frame_dt = min(dt, 0.1)
|
|
255
|
+
self._blink_time += frame_dt
|
|
256
|
+
self._update_slide(frame_dt)
|
|
257
|
+
if not self.open_flag or not self.input_enabled:
|
|
258
|
+
return
|
|
259
|
+
ctrl_down = rl.is_key_down(rl.KeyboardKey.KEY_LEFT_CONTROL) or rl.is_key_down(rl.KeyboardKey.KEY_RIGHT_CONTROL)
|
|
260
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
|
|
261
|
+
if ctrl_down:
|
|
262
|
+
self._scroll_lines(1)
|
|
263
|
+
else:
|
|
264
|
+
self._history_prev()
|
|
265
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
|
|
266
|
+
if ctrl_down:
|
|
267
|
+
self._scroll_lines(-1)
|
|
268
|
+
else:
|
|
269
|
+
self._history_next()
|
|
270
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_UP):
|
|
271
|
+
self._scroll_lines(2)
|
|
272
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_DOWN):
|
|
273
|
+
self._scroll_lines(-2)
|
|
274
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
|
|
275
|
+
self.input_caret = max(0, self.input_caret - 1)
|
|
276
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
|
|
277
|
+
self.input_caret = min(len(self.input_buffer), self.input_caret + 1)
|
|
278
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_HOME):
|
|
279
|
+
self._scroll_lines(0x14)
|
|
280
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_END):
|
|
281
|
+
self.scroll_offset = 0
|
|
282
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
|
|
283
|
+
self._autocomplete()
|
|
284
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
|
|
285
|
+
if self.input_caret > 0:
|
|
286
|
+
self._exit_history_edit()
|
|
287
|
+
self.input_buffer = (
|
|
288
|
+
self.input_buffer[: self.input_caret - 1] + self.input_buffer[self.input_caret :]
|
|
289
|
+
)
|
|
290
|
+
self.input_caret -= 1
|
|
291
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_DELETE):
|
|
292
|
+
if self.input_caret < len(self.input_buffer):
|
|
293
|
+
self._exit_history_edit()
|
|
294
|
+
self.input_buffer = (
|
|
295
|
+
self.input_buffer[: self.input_caret] + self.input_buffer[self.input_caret + 1 :]
|
|
296
|
+
)
|
|
297
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
|
|
298
|
+
self._submit_input()
|
|
299
|
+
self._poll_text_input()
|
|
300
|
+
|
|
301
|
+
def draw(self) -> None:
|
|
302
|
+
height = float(self.height_px)
|
|
303
|
+
if height <= 0.0:
|
|
304
|
+
return
|
|
305
|
+
ratio = self._open_ratio(height)
|
|
306
|
+
if ratio <= 0.0:
|
|
307
|
+
return
|
|
308
|
+
screen_w = float(rl.get_screen_width())
|
|
309
|
+
offset_y = self._offset_y
|
|
310
|
+
rl.draw_rectangle(
|
|
311
|
+
0,
|
|
312
|
+
int(offset_y),
|
|
313
|
+
int(screen_w),
|
|
314
|
+
int(height),
|
|
315
|
+
_rgba(*CONSOLE_BG_COLOR, ratio),
|
|
316
|
+
)
|
|
317
|
+
border_y = int(offset_y + height - CONSOLE_BORDER_HEIGHT)
|
|
318
|
+
rl.draw_rectangle(
|
|
319
|
+
0,
|
|
320
|
+
border_y,
|
|
321
|
+
int(screen_w),
|
|
322
|
+
int(CONSOLE_BORDER_HEIGHT),
|
|
323
|
+
_rgba(*CONSOLE_BORDER_COLOR, ratio),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
version_x = screen_w - CONSOLE_VERSION_OFFSET_X
|
|
327
|
+
version_y = offset_y + height - CONSOLE_VERSION_OFFSET_Y
|
|
328
|
+
self._draw_version_text(version_x, version_y, _rgba(1.0, 1.0, 1.0, ratio * 0.3))
|
|
329
|
+
|
|
330
|
+
visible, visible_count = self._visible_log_block(height)
|
|
331
|
+
input_y = offset_y + (visible_count + 1) * CONSOLE_LINE_HEIGHT
|
|
332
|
+
text_color = _rgba(1.0, 1.0, 1.0, ratio)
|
|
333
|
+
use_mono = self._use_mono_font()
|
|
334
|
+
if use_mono:
|
|
335
|
+
self._draw_mono_text(CONSOLE_PROMPT_MONO, CONSOLE_TEXT_X, input_y, text_color)
|
|
336
|
+
self._draw_mono_text(self.input_buffer, CONSOLE_INPUT_X_MONO, input_y, text_color)
|
|
337
|
+
else:
|
|
338
|
+
prompt = CONSOLE_PROMPT_SMALL_FMT.replace("%s", self.input_buffer)
|
|
339
|
+
self._draw_small_text(prompt, CONSOLE_TEXT_X, input_y, text_color)
|
|
340
|
+
|
|
341
|
+
log_color = _rgba(0.6, 0.6, 0.7, ratio)
|
|
342
|
+
y = offset_y + CONSOLE_LINE_HEIGHT
|
|
343
|
+
for line in visible:
|
|
344
|
+
if use_mono:
|
|
345
|
+
self._draw_mono_text(line, CONSOLE_TEXT_X, y, log_color)
|
|
346
|
+
else:
|
|
347
|
+
self._draw_small_text(line, CONSOLE_TEXT_X, y, log_color)
|
|
348
|
+
y += CONSOLE_LINE_HEIGHT
|
|
349
|
+
|
|
350
|
+
caret_alpha = ratio * self._caret_blink_alpha()
|
|
351
|
+
caret_color = _rgba(1.0, 1.0, 1.0, caret_alpha)
|
|
352
|
+
caret_y = input_y + 2.0
|
|
353
|
+
if use_mono:
|
|
354
|
+
caret_x = CONSOLE_INPUT_X_MONO + float(self.input_caret) * 8.0
|
|
355
|
+
self._draw_mono_text(CONSOLE_CARET_TEXT, caret_x, caret_y, caret_color)
|
|
356
|
+
else:
|
|
357
|
+
caret_x = self._small_caret_x()
|
|
358
|
+
self._draw_small_text(CONSOLE_CARET_TEXT, caret_x, caret_y, caret_color)
|
|
359
|
+
|
|
360
|
+
def close(self) -> None:
|
|
361
|
+
if self._mono_font is not None:
|
|
362
|
+
rl.unload_texture(self._mono_font.texture)
|
|
363
|
+
self._mono_font = None
|
|
364
|
+
if self._small_font is not None:
|
|
365
|
+
rl.unload_texture(self._small_font.texture)
|
|
366
|
+
self._small_font = None
|
|
367
|
+
|
|
368
|
+
def _tokenize_line(self, line: str) -> list[str]:
|
|
369
|
+
stripped = line.strip()
|
|
370
|
+
if not stripped:
|
|
371
|
+
return []
|
|
372
|
+
if stripped.startswith("//"):
|
|
373
|
+
return []
|
|
374
|
+
return stripped.split()
|
|
375
|
+
|
|
376
|
+
def _prompt_text(self) -> str:
|
|
377
|
+
if "%s" in self.prompt_string:
|
|
378
|
+
return self.prompt_string.replace("%s", self.input_buffer)
|
|
379
|
+
return f"{self.prompt_string}{self.input_buffer}"
|
|
380
|
+
|
|
381
|
+
def _history_prev(self) -> None:
|
|
382
|
+
if not self.history:
|
|
383
|
+
return
|
|
384
|
+
if self.history_index is None:
|
|
385
|
+
self.history_index = len(self.history) - 1
|
|
386
|
+
self.history_pending = self.input_buffer
|
|
387
|
+
elif self.history_index > 0:
|
|
388
|
+
self.history_index -= 1
|
|
389
|
+
self.input_buffer = self.history[self.history_index]
|
|
390
|
+
self.input_caret = len(self.input_buffer)
|
|
391
|
+
|
|
392
|
+
def _history_next(self) -> None:
|
|
393
|
+
if self.history_index is None:
|
|
394
|
+
return
|
|
395
|
+
if self.history_index < len(self.history) - 1:
|
|
396
|
+
self.history_index += 1
|
|
397
|
+
self.input_buffer = self.history[self.history_index]
|
|
398
|
+
else:
|
|
399
|
+
self.history_index = None
|
|
400
|
+
self.input_buffer = self.history_pending
|
|
401
|
+
self.input_caret = len(self.input_buffer)
|
|
402
|
+
|
|
403
|
+
def _exit_history_edit(self) -> None:
|
|
404
|
+
if self.history_index is not None:
|
|
405
|
+
self.history_index = None
|
|
406
|
+
self.history_pending = self.input_buffer
|
|
407
|
+
|
|
408
|
+
def _submit_input(self) -> None:
|
|
409
|
+
line = self.input_buffer.strip()
|
|
410
|
+
self.input_ready = True
|
|
411
|
+
self.input_buffer = ""
|
|
412
|
+
self.input_caret = 0
|
|
413
|
+
self.history_index = None
|
|
414
|
+
if not line:
|
|
415
|
+
return
|
|
416
|
+
if self.echo_enabled:
|
|
417
|
+
if "%s" in self.prompt_string:
|
|
418
|
+
self.log.log(self.prompt_string.replace("%s", line))
|
|
419
|
+
else:
|
|
420
|
+
self.log.log(f"{self.prompt_string}{line}")
|
|
421
|
+
if not self.history or self.history[-1] != line:
|
|
422
|
+
self.history.append(line)
|
|
423
|
+
self.exec_line(line)
|
|
424
|
+
self.input_ready = False
|
|
425
|
+
self.scroll_offset = 0
|
|
426
|
+
|
|
427
|
+
def _poll_text_input(self) -> None:
|
|
428
|
+
while True:
|
|
429
|
+
value = rl.get_char_pressed()
|
|
430
|
+
if value == 0:
|
|
431
|
+
break
|
|
432
|
+
if value < 0x20 or value > 0xFF:
|
|
433
|
+
continue
|
|
434
|
+
if len(self.input_buffer) >= MAX_CONSOLE_INPUT:
|
|
435
|
+
continue
|
|
436
|
+
char = chr(value)
|
|
437
|
+
self._exit_history_edit()
|
|
438
|
+
self.input_buffer = (
|
|
439
|
+
self.input_buffer[: self.input_caret] + char + self.input_buffer[self.input_caret :]
|
|
440
|
+
)
|
|
441
|
+
self.input_caret += 1
|
|
442
|
+
|
|
443
|
+
def _autocomplete(self) -> None:
|
|
444
|
+
if not self.input_buffer:
|
|
445
|
+
return
|
|
446
|
+
token_start = len(self.input_buffer) - len(self.input_buffer.lstrip())
|
|
447
|
+
if token_start >= len(self.input_buffer):
|
|
448
|
+
return
|
|
449
|
+
token_end = self.input_buffer.find(" ", token_start)
|
|
450
|
+
if token_end == -1:
|
|
451
|
+
token_end = len(self.input_buffer)
|
|
452
|
+
if self.input_caret > token_end:
|
|
453
|
+
return
|
|
454
|
+
prefix = self.input_buffer[token_start:self.input_caret]
|
|
455
|
+
if not prefix:
|
|
456
|
+
return
|
|
457
|
+
match = self._autocomplete_name(prefix, self.cvars.keys())
|
|
458
|
+
if match is None:
|
|
459
|
+
match = self._autocomplete_name(prefix, self.commands.keys())
|
|
460
|
+
if match is None:
|
|
461
|
+
return
|
|
462
|
+
self.input_buffer = self.input_buffer[:token_start] + match + self.input_buffer[token_end:]
|
|
463
|
+
self.input_caret = token_start + len(match)
|
|
464
|
+
|
|
465
|
+
def _autocomplete_name(self, prefix: str, names: Iterable[str]) -> str | None:
|
|
466
|
+
for name in names:
|
|
467
|
+
if name == prefix:
|
|
468
|
+
return name
|
|
469
|
+
for name in names:
|
|
470
|
+
if name.startswith(prefix):
|
|
471
|
+
return name
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
def _scroll_lines(self, delta: int) -> None:
|
|
475
|
+
max_offset = self._max_scroll_offset()
|
|
476
|
+
if max_offset <= 0:
|
|
477
|
+
self.scroll_offset = 0
|
|
478
|
+
return
|
|
479
|
+
self.scroll_offset = max(0, min(max_offset, self.scroll_offset + int(delta)))
|
|
480
|
+
|
|
481
|
+
def _max_visible_lines(self, height: float | None = None) -> int:
|
|
482
|
+
use_height = height if height is not None else float(self.height_px)
|
|
483
|
+
if use_height <= 0.0:
|
|
484
|
+
return 0
|
|
485
|
+
return max(int(use_height // CONSOLE_LINE_HEIGHT) - 2, 0)
|
|
486
|
+
|
|
487
|
+
def _max_scroll_offset(self) -> int:
|
|
488
|
+
max_lines = self._max_visible_lines()
|
|
489
|
+
log_count = len(self.log.lines)
|
|
490
|
+
visible = min(log_count, max_lines)
|
|
491
|
+
return max(0, log_count - visible)
|
|
492
|
+
|
|
493
|
+
def _visible_log_block(self, height: float) -> tuple[list[str], int]:
|
|
494
|
+
max_lines = self._max_visible_lines(height)
|
|
495
|
+
log_count = len(self.log.lines)
|
|
496
|
+
visible_count = min(log_count, max_lines)
|
|
497
|
+
if visible_count <= 0:
|
|
498
|
+
return [], 0
|
|
499
|
+
max_offset = max(0, log_count - visible_count)
|
|
500
|
+
if self.scroll_offset > max_offset:
|
|
501
|
+
self.scroll_offset = max_offset
|
|
502
|
+
start = max(0, log_count - visible_count - self.scroll_offset)
|
|
503
|
+
end = min(log_count, start + visible_count)
|
|
504
|
+
return self.log.lines[start:end], visible_count
|
|
505
|
+
|
|
506
|
+
def _update_slide(self, dt: float) -> None:
|
|
507
|
+
if self.open_flag:
|
|
508
|
+
self._slide_t = max(0.0, self._slide_t - dt * CONSOLE_ANIM_SPEED)
|
|
509
|
+
else:
|
|
510
|
+
self._slide_t = min(1.0, self._slide_t + dt * CONSOLE_ANIM_SPEED)
|
|
511
|
+
height = float(self.height_px)
|
|
512
|
+
if height <= 0.0:
|
|
513
|
+
self._offset_y = -height
|
|
514
|
+
return
|
|
515
|
+
eased = math.sin((1.0 - self._slide_t) * math.pi / 2.0)
|
|
516
|
+
self._offset_y = eased * height - height
|
|
517
|
+
|
|
518
|
+
def _open_ratio(self, height: float) -> float:
|
|
519
|
+
if height <= 0.0:
|
|
520
|
+
return 0.0
|
|
521
|
+
return _clamp((height + self._offset_y) / height, 0.0, 1.0)
|
|
522
|
+
|
|
523
|
+
def _caret_blink_alpha(self) -> float:
|
|
524
|
+
pulse = math.sin(self._blink_time * CONSOLE_BLINK_SPEED)
|
|
525
|
+
value = max(0.2, abs(pulse) ** 2)
|
|
526
|
+
return _clamp(value, 0.0, 1.0)
|
|
527
|
+
|
|
528
|
+
def _use_mono_font(self) -> bool:
|
|
529
|
+
cvar = self.cvars.get("con_monoFont")
|
|
530
|
+
if cvar is None:
|
|
531
|
+
return False
|
|
532
|
+
return bool(cvar.value_f)
|
|
533
|
+
|
|
534
|
+
def _ensure_mono_font(self) -> GrimMonoFont | None:
|
|
535
|
+
if self._mono_font is not None:
|
|
536
|
+
return self._mono_font
|
|
537
|
+
if self._mono_font_error is not None:
|
|
538
|
+
return None
|
|
539
|
+
if self.assets_dir is None:
|
|
540
|
+
self._mono_font_error = "missing assets dir"
|
|
541
|
+
return None
|
|
542
|
+
missing_assets: list[str] = []
|
|
543
|
+
try:
|
|
544
|
+
self._mono_font = load_grim_mono_font(self.assets_dir, missing_assets)
|
|
545
|
+
except FileNotFoundError as exc:
|
|
546
|
+
self._mono_font_error = str(exc)
|
|
547
|
+
self._mono_font = None
|
|
548
|
+
return self._mono_font
|
|
549
|
+
|
|
550
|
+
def _ensure_small_font(self) -> SmallFontData | None:
|
|
551
|
+
if self._small_font is not None:
|
|
552
|
+
return self._small_font
|
|
553
|
+
if self._small_font_error is not None:
|
|
554
|
+
return None
|
|
555
|
+
if self.assets_dir is None:
|
|
556
|
+
self._small_font_error = "missing assets dir"
|
|
557
|
+
return None
|
|
558
|
+
missing_assets: list[str] = []
|
|
559
|
+
try:
|
|
560
|
+
self._small_font = load_small_font(self.assets_dir, missing_assets)
|
|
561
|
+
except FileNotFoundError as exc:
|
|
562
|
+
self._small_font_error = str(exc)
|
|
563
|
+
self._small_font = None
|
|
564
|
+
return self._small_font
|
|
565
|
+
|
|
566
|
+
def _draw_mono_text(self, text: str, x: float, y: float, color: rl.Color) -> None:
|
|
567
|
+
font = self._ensure_mono_font()
|
|
568
|
+
if font is None:
|
|
569
|
+
rl.draw_text(text, int(x), int(y), int(16 * CONSOLE_MONO_SCALE), color)
|
|
570
|
+
return
|
|
571
|
+
advance = font.advance * CONSOLE_MONO_SCALE
|
|
572
|
+
draw_grim_mono_text(font, text, x - advance, y, CONSOLE_MONO_SCALE, color)
|
|
573
|
+
|
|
574
|
+
def _draw_small_text(self, text: str, x: float, y: float, color: rl.Color) -> None:
|
|
575
|
+
font = self._ensure_small_font()
|
|
576
|
+
if font is None:
|
|
577
|
+
rl.draw_text(text, int(x), int(y), int(16 * CONSOLE_SMALL_SCALE), color)
|
|
578
|
+
return
|
|
579
|
+
draw_small_text(font, text, x, y, CONSOLE_SMALL_SCALE, color)
|
|
580
|
+
|
|
581
|
+
def _draw_version_text(self, x: float, y: float, color: rl.Color) -> None:
|
|
582
|
+
font = self._ensure_small_font()
|
|
583
|
+
if font is None:
|
|
584
|
+
self._draw_mono_text(CONSOLE_VERSION_TEXT, x, y, color)
|
|
585
|
+
return
|
|
586
|
+
draw_small_text(font, CONSOLE_VERSION_TEXT, x, y, CONSOLE_SMALL_SCALE, color)
|
|
587
|
+
|
|
588
|
+
def _small_caret_x(self) -> float:
|
|
589
|
+
font = self._ensure_small_font()
|
|
590
|
+
if font is None:
|
|
591
|
+
return CONSOLE_TEXT_X + 16.0 + float(self.input_caret) * 8.0
|
|
592
|
+
prompt_w = measure_small_text_width(font, CONSOLE_PROMPT_SMALL_FMT.replace("%s", ""), CONSOLE_SMALL_SCALE)
|
|
593
|
+
input_w = measure_small_text_width(font, self.input_buffer[: self.input_caret], CONSOLE_SMALL_SCALE)
|
|
594
|
+
return CONSOLE_TEXT_X + prompt_w + input_w
|
|
595
|
+
|
|
596
|
+
def _flush_input_queue(self) -> None:
|
|
597
|
+
while rl.get_char_pressed():
|
|
598
|
+
pass
|
|
599
|
+
while rl.get_key_pressed():
|
|
600
|
+
pass
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def create_console(base_dir: Path, assets_dir: Path | None = None) -> ConsoleState:
|
|
604
|
+
script_dirs: tuple[Path, ...] = (base_dir,)
|
|
605
|
+
if assets_dir is not None and assets_dir != base_dir:
|
|
606
|
+
script_dirs = (*script_dirs, assets_dir)
|
|
607
|
+
console = ConsoleState(
|
|
608
|
+
base_dir=base_dir,
|
|
609
|
+
log=ConsoleLog(base_dir=base_dir),
|
|
610
|
+
assets_dir=assets_dir,
|
|
611
|
+
script_dirs=script_dirs,
|
|
612
|
+
)
|
|
613
|
+
console.register_cvar("version", CONSOLE_VERSION_TEXT)
|
|
614
|
+
console.register_cvar("con_monoFont", "1")
|
|
615
|
+
if console.open_flag:
|
|
616
|
+
console._slide_t = 0.0
|
|
617
|
+
console._offset_y = 0.0
|
|
618
|
+
else:
|
|
619
|
+
console._slide_t = 1.0
|
|
620
|
+
console._offset_y = -float(console.height_px)
|
|
621
|
+
register_core_commands(console)
|
|
622
|
+
return console
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _make_noop_command(console: ConsoleState, name: str) -> CommandHandler:
|
|
626
|
+
def _handler(args: list[str]) -> None:
|
|
627
|
+
console.log.log(f"command {name} called with {len(args)} args")
|
|
628
|
+
|
|
629
|
+
return _handler
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def register_boot_commands(
|
|
633
|
+
console: ConsoleState, handlers: dict[str, CommandHandler] | None = None
|
|
634
|
+
) -> None:
|
|
635
|
+
resolved = handlers or {}
|
|
636
|
+
commands = (
|
|
637
|
+
"setGammaRamp",
|
|
638
|
+
"snd_addGameTune",
|
|
639
|
+
"generateterrain",
|
|
640
|
+
"telltimesurvived",
|
|
641
|
+
"setresourcepaq",
|
|
642
|
+
"loadtexture",
|
|
643
|
+
"openurl",
|
|
644
|
+
"sndfreqadjustment",
|
|
645
|
+
)
|
|
646
|
+
for name in commands:
|
|
647
|
+
handler = resolved.get(name)
|
|
648
|
+
if handler is None:
|
|
649
|
+
handler = _make_noop_command(console, name)
|
|
650
|
+
console.register_command(name, handler)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def register_core_cvars(console: ConsoleState, width: int, height: int) -> None:
|
|
654
|
+
console.register_cvar("v_width", str(width))
|
|
655
|
+
console.register_cvar("v_height", str(height))
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def register_core_commands(console: ConsoleState) -> None:
|
|
659
|
+
def cmdlist(_args: list[str]) -> None:
|
|
660
|
+
for name in console.commands.keys():
|
|
661
|
+
console.log.log(name)
|
|
662
|
+
console.log.log(f"{len(console.commands)} commands")
|
|
663
|
+
|
|
664
|
+
def vars_cmd(_args: list[str]) -> None:
|
|
665
|
+
for name in console.cvars.keys():
|
|
666
|
+
console.log.log(name)
|
|
667
|
+
console.log.log(f"{len(console.cvars)} variables")
|
|
668
|
+
|
|
669
|
+
def cmd_set(args: list[str]) -> None:
|
|
670
|
+
if len(args) < 2:
|
|
671
|
+
console.log.log("Usage: set <var> <value>")
|
|
672
|
+
return
|
|
673
|
+
name = args[0]
|
|
674
|
+
value = " ".join(args[1:])
|
|
675
|
+
console.register_cvar(name, value)
|
|
676
|
+
console.log.log(f"'{name}' set to '{value}'")
|
|
677
|
+
|
|
678
|
+
def cmd_echo(args: list[str]) -> None:
|
|
679
|
+
if not args:
|
|
680
|
+
console.log.log(f"echo is {'on' if console.echo_enabled else 'off'}")
|
|
681
|
+
return
|
|
682
|
+
mode = args[0].lower()
|
|
683
|
+
if mode in {"on", "off"}:
|
|
684
|
+
console.echo_enabled = mode == "on"
|
|
685
|
+
console.log.log(f"echo {mode}")
|
|
686
|
+
return
|
|
687
|
+
console.log.log(" ".join(args))
|
|
688
|
+
|
|
689
|
+
def cmd_quit(_args: list[str]) -> None:
|
|
690
|
+
console.quit_requested = True
|
|
691
|
+
|
|
692
|
+
def cmd_clear(_args: list[str]) -> None:
|
|
693
|
+
console.log.clear()
|
|
694
|
+
console.scroll_offset = 0
|
|
695
|
+
|
|
696
|
+
def cmd_extend(_args: list[str]) -> None:
|
|
697
|
+
console.height_px = EXTENDED_CONSOLE_HEIGHT
|
|
698
|
+
|
|
699
|
+
def cmd_minimize(_args: list[str]) -> None:
|
|
700
|
+
console.height_px = DEFAULT_CONSOLE_HEIGHT
|
|
701
|
+
|
|
702
|
+
def cmd_exec(args: list[str]) -> None:
|
|
703
|
+
if not args:
|
|
704
|
+
console.log.log("exec <script>")
|
|
705
|
+
return
|
|
706
|
+
target = _normalize_script_path(args[0])
|
|
707
|
+
try:
|
|
708
|
+
script_text: str | None = None
|
|
709
|
+
primary_dirs = _primary_script_dirs(console)
|
|
710
|
+
path = _resolve_script_path_in(target, primary_dirs)
|
|
711
|
+
if path is None:
|
|
712
|
+
script_text = _load_script_from_paq(console, target)
|
|
713
|
+
if path is None and script_text is None:
|
|
714
|
+
fallback_dirs = [path for path in console.script_dirs if path not in primary_dirs]
|
|
715
|
+
path = _resolve_script_path_in(target, fallback_dirs)
|
|
716
|
+
if path is None and script_text is None:
|
|
717
|
+
console.log.log(f"Cannot open file '{args[0]}'")
|
|
718
|
+
return
|
|
719
|
+
console.log.log(f"Executing '{args[0]}'")
|
|
720
|
+
if script_text is None:
|
|
721
|
+
script_text = path.read_text(encoding="utf-8", errors="ignore")
|
|
722
|
+
for raw_line in script_text.splitlines():
|
|
723
|
+
line = raw_line.strip()
|
|
724
|
+
if line:
|
|
725
|
+
console.exec_line(line)
|
|
726
|
+
except OSError:
|
|
727
|
+
console.log.log(f"Cannot open file '{args[0]}'")
|
|
728
|
+
|
|
729
|
+
console.register_command("cmdlist", cmdlist)
|
|
730
|
+
console.register_command("vars", vars_cmd)
|
|
731
|
+
console.register_command("set", cmd_set)
|
|
732
|
+
console.register_command("echo", cmd_echo)
|
|
733
|
+
console.register_command("quit", cmd_quit)
|
|
734
|
+
console.register_command("clear", cmd_clear)
|
|
735
|
+
console.register_command("extendconsole", cmd_extend)
|
|
736
|
+
console.register_command("minimizeconsole", cmd_minimize)
|
|
737
|
+
console.register_command("exec", cmd_exec)
|