sci2-ts 0.0.0-stage → 0.2.1
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.
- package/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +113 -2
- package/apps/viewer/editor.html +43 -0
- package/apps/viewer/explore.html +27 -0
- package/apps/viewer/index.html +26 -0
- package/apps/viewer/package.json +27 -0
- package/apps/viewer/play.html +26 -0
- package/apps/viewer/plugin.d.ts +18 -0
- package/apps/viewer/plugin.js +75 -0
- package/apps/viewer/plugin.ts +84 -0
- package/apps/viewer/src/adlib.d.ts +20 -0
- package/apps/viewer/src/adlib.js +55 -0
- package/apps/viewer/src/adlib.ts +62 -0
- package/apps/viewer/src/app.d.ts +5 -0
- package/apps/viewer/src/app.js +5 -0
- package/apps/viewer/src/app.ts +5 -0
- package/apps/viewer/src/asset-index.d.ts +22 -0
- package/apps/viewer/src/asset-index.js +74 -0
- package/apps/viewer/src/asset-index.ts +90 -0
- package/apps/viewer/src/audio.d.ts +28 -0
- package/apps/viewer/src/audio.js +102 -0
- package/apps/viewer/src/audio.ts +110 -0
- package/apps/viewer/src/editor-handles.d.ts +63 -0
- package/apps/viewer/src/editor-handles.js +332 -0
- package/apps/viewer/src/editor-handles.ts +368 -0
- package/apps/viewer/src/editor-languages.d.ts +23 -0
- package/apps/viewer/src/editor-languages.js +157 -0
- package/apps/viewer/src/editor-languages.ts +135 -0
- package/apps/viewer/src/editor.css +213 -0
- package/apps/viewer/src/editor.d.ts +1 -0
- package/apps/viewer/src/editor.js +393 -0
- package/apps/viewer/src/editor.ts +412 -0
- package/apps/viewer/src/explore/classes.d.ts +7 -0
- package/apps/viewer/src/explore/classes.js +63 -0
- package/apps/viewer/src/explore/classes.ts +61 -0
- package/apps/viewer/src/explore/context.d.ts +33 -0
- package/apps/viewer/src/explore/context.js +96 -0
- package/apps/viewer/src/explore/context.ts +124 -0
- package/apps/viewer/src/explore/explore.css +93 -0
- package/apps/viewer/src/explore/explore.d.ts +1 -0
- package/apps/viewer/src/explore/explore.js +102 -0
- package/apps/viewer/src/explore/explore.ts +104 -0
- package/apps/viewer/src/explore/inspect.d.ts +5 -0
- package/apps/viewer/src/explore/inspect.js +208 -0
- package/apps/viewer/src/explore/inspect.ts +279 -0
- package/apps/viewer/src/explore/live.d.ts +22 -0
- package/apps/viewer/src/explore/live.js +318 -0
- package/apps/viewer/src/explore/live.ts +365 -0
- package/apps/viewer/src/explore/map.d.ts +11 -0
- package/apps/viewer/src/explore/map.js +182 -0
- package/apps/viewer/src/explore/map.ts +193 -0
- package/apps/viewer/src/files.d.ts +21 -0
- package/apps/viewer/src/files.js +69 -0
- package/apps/viewer/src/files.ts +72 -0
- package/apps/viewer/src/game-profile.d.ts +19 -0
- package/apps/viewer/src/game-profile.js +4 -0
- package/apps/viewer/src/game-profile.ts +26 -0
- package/apps/viewer/src/main.d.ts +1 -0
- package/apps/viewer/src/main.js +473 -0
- package/apps/viewer/src/main.ts +527 -0
- package/apps/viewer/src/music.d.ts +14 -0
- package/apps/viewer/src/music.js +32 -0
- package/apps/viewer/src/music.ts +34 -0
- package/apps/viewer/src/opl-worklet.d.ts +1 -0
- package/apps/viewer/src/opl-worklet.js +47 -0
- package/apps/viewer/src/opl-worklet.ts +55 -0
- package/apps/viewer/src/overlay.d.ts +31 -0
- package/apps/viewer/src/overlay.js +103 -0
- package/apps/viewer/src/overlay.ts +123 -0
- package/apps/viewer/src/play.css +14 -0
- package/apps/viewer/src/play.d.ts +1 -0
- package/apps/viewer/src/play.js +179 -0
- package/apps/viewer/src/play.ts +177 -0
- package/apps/viewer/src/saves.d.ts +10 -0
- package/apps/viewer/src/saves.js +50 -0
- package/apps/viewer/src/saves.ts +51 -0
- package/apps/viewer/src/session.d.ts +91 -0
- package/apps/viewer/src/session.js +301 -0
- package/apps/viewer/src/session.ts +321 -0
- package/apps/viewer/src/sound-preview.d.ts +7 -0
- package/apps/viewer/src/sound-preview.js +214 -0
- package/apps/viewer/src/sound-preview.ts +228 -0
- package/apps/viewer/src/style.css +64 -0
- package/apps/viewer/vite.config.ts +25 -0
- package/bin/sci-ts.mjs +9 -0
- package/lib/994.sc +49 -0
- package/lib/995.sc +53 -0
- package/lib/996.sc +299 -0
- package/lib/997.sc +198 -0
- package/lib/998.sc +338 -0
- package/lib/999.sc +189 -0
- package/lib/resources.d.ts +2 -0
- package/lib/resources.js +84 -0
- package/lib/resources.ts +88 -0
- package/lib/system.sh +62 -0
- package/package.json +75 -4
- package/packages/content/room.schema.json +267 -0
- package/packages/content/src/index.d.ts +3 -0
- package/packages/content/src/index.js +3 -0
- package/packages/content/src/index.ts +3 -0
- package/packages/content/src/room.d.ts +111 -0
- package/packages/content/src/room.js +886 -0
- package/packages/content/src/room.ts +1011 -0
- package/packages/content/src/target.d.ts +80 -0
- package/packages/content/src/target.js +1 -0
- package/packages/content/src/target.ts +60 -0
- package/packages/content/src/yarn.d.ts +90 -0
- package/packages/content/src/yarn.js +214 -0
- package/packages/content/src/yarn.ts +261 -0
- package/packages/sci/src/analysis/game.d.ts +90 -0
- package/packages/sci/src/analysis/game.js +221 -0
- package/packages/sci/src/analysis/game.ts +305 -0
- package/packages/sci/src/analysis/runtime.d.ts +58 -0
- package/packages/sci/src/analysis/runtime.js +133 -0
- package/packages/sci/src/analysis/runtime.ts +163 -0
- package/packages/sci/src/audio/adlib.d.ts +73 -0
- package/packages/sci/src/audio/adlib.js +280 -0
- package/packages/sci/src/audio/adlib.ts +329 -0
- package/packages/sci/src/audio/index.d.ts +43 -0
- package/packages/sci/src/audio/index.js +99 -0
- package/packages/sci/src/audio/index.ts +122 -0
- package/packages/sci/src/audio/midi.d.ts +92 -0
- package/packages/sci/src/audio/midi.js +242 -0
- package/packages/sci/src/audio/midi.ts +262 -0
- package/packages/sci/src/audio/opl2.d.ts +74 -0
- package/packages/sci/src/audio/opl2.js +283 -0
- package/packages/sci/src/audio/opl2.ts +283 -0
- package/packages/sci/src/audio/sol.d.ts +32 -0
- package/packages/sci/src/audio/sol.js +81 -0
- package/packages/sci/src/audio/sol.ts +100 -0
- package/packages/sci/src/audio/write.d.ts +35 -0
- package/packages/sci/src/audio/write.js +91 -0
- package/packages/sci/src/audio/write.ts +108 -0
- package/packages/sci/src/gfx/cel.d.ts +36 -0
- package/packages/sci/src/gfx/cel.js +82 -0
- package/packages/sci/src/gfx/cel.ts +110 -0
- package/packages/sci/src/gfx/palette.d.ts +24 -0
- package/packages/sci/src/gfx/palette.js +49 -0
- package/packages/sci/src/gfx/palette.ts +56 -0
- package/packages/sci/src/gfx/pic-file.d.ts +46 -0
- package/packages/sci/src/gfx/pic-file.js +114 -0
- package/packages/sci/src/gfx/pic-file.ts +155 -0
- package/packages/sci/src/gfx/pic.d.ts +27 -0
- package/packages/sci/src/gfx/pic.js +34 -0
- package/packages/sci/src/gfx/pic.ts +53 -0
- package/packages/sci/src/gfx/quantize.d.ts +42 -0
- package/packages/sci/src/gfx/quantize.js +199 -0
- package/packages/sci/src/gfx/quantize.ts +218 -0
- package/packages/sci/src/gfx/view-file.d.ts +62 -0
- package/packages/sci/src/gfx/view-file.js +182 -0
- package/packages/sci/src/gfx/view-file.ts +233 -0
- package/packages/sci/src/gfx/view.d.ts +28 -0
- package/packages/sci/src/gfx/view.js +45 -0
- package/packages/sci/src/gfx/view.ts +62 -0
- package/packages/sci/src/index.d.ts +38 -0
- package/packages/sci/src/index.js +38 -0
- package/packages/sci/src/index.ts +38 -0
- package/packages/sci/src/motion/pathfinding.d.ts +40 -0
- package/packages/sci/src/motion/pathfinding.js +140 -0
- package/packages/sci/src/motion/pathfinding.ts +147 -0
- package/packages/sci/src/resource/archive.d.ts +16 -0
- package/packages/sci/src/resource/archive.js +51 -0
- package/packages/sci/src/resource/archive.ts +56 -0
- package/packages/sci/src/resource/lzs.d.ts +12 -0
- package/packages/sci/src/resource/lzs.js +64 -0
- package/packages/sci/src/resource/lzs.ts +66 -0
- package/packages/sci/src/resource/manager.d.ts +45 -0
- package/packages/sci/src/resource/manager.js +158 -0
- package/packages/sci/src/resource/manager.ts +184 -0
- package/packages/sci/src/resource/map.d.ts +18 -0
- package/packages/sci/src/resource/map.js +34 -0
- package/packages/sci/src/resource/map.ts +44 -0
- package/packages/sci/src/resource/types.d.ts +37 -0
- package/packages/sci/src/resource/types.js +45 -0
- package/packages/sci/src/resource/types.ts +57 -0
- package/packages/sci/src/script/assembly.d.ts +67 -0
- package/packages/sci/src/script/assembly.js +748 -0
- package/packages/sci/src/script/assembly.ts +714 -0
- package/packages/sci/src/script/compiler.d.ts +75 -0
- package/packages/sci/src/script/compiler.js +993 -0
- package/packages/sci/src/script/compiler.ts +1040 -0
- package/packages/sci/src/script/disasm.d.ts +81 -0
- package/packages/sci/src/script/disasm.js +318 -0
- package/packages/sci/src/script/disasm.ts +323 -0
- package/packages/sci/src/script/kernel-names.d.ts +6 -0
- package/packages/sci/src/script/kernel-names.js +165 -0
- package/packages/sci/src/script/kernel-names.ts +165 -0
- package/packages/sci/src/script/opcodes.d.ts +22 -0
- package/packages/sci/src/script/opcodes.js +45 -0
- package/packages/sci/src/script/opcodes.ts +72 -0
- package/packages/sci/src/script/script.d.ts +56 -0
- package/packages/sci/src/script/script.js +63 -0
- package/packages/sci/src/script/script.ts +111 -0
- package/packages/sci/src/script/sexpr.d.ts +45 -0
- package/packages/sci/src/script/sexpr.js +151 -0
- package/packages/sci/src/script/sexpr.ts +139 -0
- package/packages/sci/src/script/vocab.d.ts +15 -0
- package/packages/sci/src/script/vocab.js +44 -0
- package/packages/sci/src/script/vocab.ts +48 -0
- package/packages/sci/src/script/world.d.ts +25 -0
- package/packages/sci/src/script/world.js +65 -0
- package/packages/sci/src/script/world.ts +70 -0
- package/packages/sci/src/text/font.d.ts +37 -0
- package/packages/sci/src/text/font.js +103 -0
- package/packages/sci/src/text/font.ts +125 -0
- package/packages/sci/src/text/message.d.ts +69 -0
- package/packages/sci/src/text/message.js +171 -0
- package/packages/sci/src/text/message.ts +205 -0
- package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
- package/packages/sci/src/vm/kernels/arrays.js +182 -0
- package/packages/sci/src/vm/kernels/arrays.ts +164 -0
- package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
- package/packages/sci/src/vm/kernels/audio.js +347 -0
- package/packages/sci/src/vm/kernels/audio.ts +349 -0
- package/packages/sci/src/vm/kernels/core.d.ts +5 -0
- package/packages/sci/src/vm/kernels/core.js +87 -0
- package/packages/sci/src/vm/kernels/core.ts +91 -0
- package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
- package/packages/sci/src/vm/kernels/graphics.js +467 -0
- package/packages/sci/src/vm/kernels/graphics.ts +502 -0
- package/packages/sci/src/vm/kernels/index.d.ts +8 -0
- package/packages/sci/src/vm/kernels/index.js +29 -0
- package/packages/sci/src/vm/kernels/index.ts +32 -0
- package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
- package/packages/sci/src/vm/kernels/lists.js +127 -0
- package/packages/sci/src/vm/kernels/lists.ts +111 -0
- package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
- package/packages/sci/src/vm/kernels/motion.js +204 -0
- package/packages/sci/src/vm/kernels/motion.ts +194 -0
- package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
- package/packages/sci/src/vm/kernels/palette.js +292 -0
- package/packages/sci/src/vm/kernels/palette.ts +303 -0
- package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
- package/packages/sci/src/vm/kernels/saves.js +123 -0
- package/packages/sci/src/vm/kernels/saves.ts +149 -0
- package/packages/sci/src/vm/kernels/system.d.ts +62 -0
- package/packages/sci/src/vm/kernels/system.js +110 -0
- package/packages/sci/src/vm/kernels/system.ts +128 -0
- package/packages/sci/src/vm/kernels/text.d.ts +13 -0
- package/packages/sci/src/vm/kernels/text.js +338 -0
- package/packages/sci/src/vm/kernels/text.ts +328 -0
- package/packages/sci/src/vm/memory.d.ts +138 -0
- package/packages/sci/src/vm/memory.js +168 -0
- package/packages/sci/src/vm/memory.ts +266 -0
- package/packages/sci/src/vm/replace.d.ts +11 -0
- package/packages/sci/src/vm/replace.js +14 -0
- package/packages/sci/src/vm/replace.ts +16 -0
- package/packages/sci/src/vm/savegame.d.ts +89 -0
- package/packages/sci/src/vm/savegame.js +161 -0
- package/packages/sci/src/vm/savegame.ts +188 -0
- package/packages/sci/src/vm/value.d.ts +17 -0
- package/packages/sci/src/vm/value.js +12 -0
- package/packages/sci/src/vm/value.ts +25 -0
- package/packages/sci/src/vm/vm.d.ts +155 -0
- package/packages/sci/src/vm/vm.js +1053 -0
- package/packages/sci/src/vm/vm.ts +914 -0
- package/tools/adlib-render.d.ts +1 -0
- package/tools/adlib-render.js +49 -0
- package/tools/adlib-render.ts +42 -0
- package/tools/analyze.d.ts +1 -0
- package/tools/analyze.js +25 -0
- package/tools/analyze.ts +25 -0
- package/tools/art/build.d.ts +34 -0
- package/tools/art/build.js +175 -0
- package/tools/art/build.ts +153 -0
- package/tools/art.d.ts +1 -0
- package/tools/art.js +31 -0
- package/tools/art.ts +29 -0
- package/tools/asm.d.ts +1 -0
- package/tools/asm.js +61 -0
- package/tools/asm.ts +55 -0
- package/tools/cli.d.ts +1 -0
- package/tools/cli.js +99 -0
- package/tools/cli.ts +100 -0
- package/tools/editor-server.d.ts +29 -0
- package/tools/editor-server.js +162 -0
- package/tools/editor-server.ts +176 -0
- package/tools/explore.d.ts +1 -0
- package/tools/explore.js +305 -0
- package/tools/explore.ts +250 -0
- package/tools/game/audio.d.ts +19 -0
- package/tools/game/audio.js +143 -0
- package/tools/game/audio.ts +130 -0
- package/tools/game/build.d.ts +46 -0
- package/tools/game/build.js +259 -0
- package/tools/game/build.ts +283 -0
- package/tools/game/defaults.d.ts +26 -0
- package/tools/game/defaults.js +82 -0
- package/tools/game/defaults.ts +83 -0
- package/tools/game/font.d.ts +3 -0
- package/tools/game/font.js +124 -0
- package/tools/game/font.ts +126 -0
- package/tools/game/kit.d.ts +7 -0
- package/tools/game/kit.js +7 -0
- package/tools/game/kit.ts +10 -0
- package/tools/game/new.d.ts +8 -0
- package/tools/game/new.js +39 -0
- package/tools/game/new.ts +38 -0
- package/tools/game/target.d.ts +7 -0
- package/tools/game/target.js +41 -0
- package/tools/game/target.ts +42 -0
- package/tools/game/template/resources.ts.tmpl +43 -0
- package/tools/game/template/rooms/1.room.yaml +9 -0
- package/tools/game/template/rooms/1.yarn +17 -0
- package/tools/game/template/scripts/0.sc +24 -0
- package/tools/game.d.ts +1 -0
- package/tools/game.js +77 -0
- package/tools/game.ts +70 -0
- package/tools/mod-build.d.ts +28 -0
- package/tools/mod-build.js +173 -0
- package/tools/mod-build.ts +179 -0
- package/tools/mod-target.d.ts +16 -0
- package/tools/mod-target.js +26 -0
- package/tools/mod-target.ts +32 -0
- package/tools/mod.d.ts +1 -0
- package/tools/mod.js +47 -0
- package/tools/mod.ts +45 -0
- package/tools/msg.d.ts +1 -0
- package/tools/msg.js +40 -0
- package/tools/msg.ts +36 -0
- package/tools/node-files.d.ts +3 -0
- package/tools/node-files.js +34 -0
- package/tools/node-files.ts +33 -0
- package/tools/patches.d.ts +3 -0
- package/tools/patches.js +13 -0
- package/tools/patches.ts +13 -0
- package/tools/pic-build.d.ts +9 -0
- package/tools/pic-build.js +26 -0
- package/tools/pic-build.ts +27 -0
- package/tools/pic.d.ts +1 -0
- package/tools/pic.js +64 -0
- package/tools/pic.ts +61 -0
- package/tools/play.d.ts +1 -0
- package/tools/play.js +54 -0
- package/tools/play.ts +54 -0
- package/tools/png.d.ts +16 -0
- package/tools/png.js +124 -0
- package/tools/png.ts +123 -0
- package/tools/res.d.ts +1 -0
- package/tools/res.js +48 -0
- package/tools/res.ts +50 -0
- package/tools/scenario.d.ts +1 -0
- package/tools/scenario.js +74 -0
- package/tools/scenario.ts +80 -0
- package/tools/site/build.d.ts +8 -0
- package/tools/site/build.js +110 -0
- package/tools/site/build.ts +116 -0
- package/tools/site/markdown.d.ts +11 -0
- package/tools/site/markdown.js +88 -0
- package/tools/site/markdown.ts +84 -0
- package/tools/view-build.d.ts +34 -0
- package/tools/view-build.js +47 -0
- package/tools/view-build.ts +71 -0
- package/tools/view.d.ts +1 -0
- package/tools/view.js +54 -0
- package/tools/view.ts +52 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { indentWithTab } from "@codemirror/commands";
|
|
2
|
+
import { yaml as yamlLanguage } from "@codemirror/lang-yaml";
|
|
3
|
+
import { lintGutter, setDiagnostics, type Diagnostic } from "@codemirror/lint";
|
|
4
|
+
import { EditorState, type Extension } from "@codemirror/state";
|
|
5
|
+
import { oneDark } from "@codemirror/theme-one-dark";
|
|
6
|
+
import { EditorView, keymap } from "@codemirror/view";
|
|
7
|
+
import { ResourceManager, gameGlobal, drawnItems, graphics, heroInPlay, replaceResources, requestRoom, roomPlane, type ResourceType, type VmSnapshot } from "../../../packages/sci/src/index.ts";
|
|
8
|
+
import { basicSetup } from "codemirror";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import { EditLayer, type EditMode } from "./editor-handles.ts";
|
|
11
|
+
import { sciAssembly, sciMessages, sciScript, yarn } from "./editor-languages.ts";
|
|
12
|
+
import { httpFiles } from "./files.ts";
|
|
13
|
+
import { OVERLAY_COLORS, createOverlay, drawRoomOverlay } from "./overlay.ts";
|
|
14
|
+
import { gameProfile } from "./game-profile.ts";
|
|
15
|
+
import { GameSession } from "./session.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The live room editor: a room's .room.yaml and .yarn on the left, the game on the right.
|
|
19
|
+
* Saving (⌘S, or a pause in typing with "live" on) writes the files into the mod and builds
|
|
20
|
+
* it on the dev server (tools/editor-server.ts); the built resources are swapped into the
|
|
21
|
+
* running game, which goes back to a snapshot taken just before entering the room and
|
|
22
|
+
* walks in again. The snapshot comes from a quick new hero (the game's profile in games/), so it
|
|
23
|
+
* never holds the room being edited. Outlines show what the room is made of, by name.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface BuildResult {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
resources?: { type: ResourceType; number: number; data: string }[];
|
|
29
|
+
generated?: Record<string, string>;
|
|
30
|
+
flags?: string;
|
|
31
|
+
reload?: boolean;
|
|
32
|
+
ms?: number;
|
|
33
|
+
error?: { message: string; file?: string; line?: number };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
|
|
37
|
+
const params = new URLSearchParams(location.search);
|
|
38
|
+
const statusEl = $("status");
|
|
39
|
+
const status = (text: string, kind: "" | "ok" | "error" = "") => {
|
|
40
|
+
statusEl.textContent = text;
|
|
41
|
+
statusEl.className = kind;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// --- Which mod and room ---------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
// A mod of Sierra's game, or the game of ours the dev server is serving (its rooms/).
|
|
47
|
+
const mods = (await (await fetch("/__editor/mods")).json()) as { name: string; kind: "mod" | "game"; rooms: number[] }[];
|
|
48
|
+
const editable = mods.filter((m) => m.rooms.length);
|
|
49
|
+
if (!editable.length) {
|
|
50
|
+
status(mods[0]?.kind === "game" ? `${mods[0].name} has no rooms/<n>.room.yaml yet.` : "No mod has a <n>.room.yaml yet (see mods/README.md).", "error");
|
|
51
|
+
throw new Error("nothing to edit");
|
|
52
|
+
}
|
|
53
|
+
const mod = editable.find((m) => m.name === params.get("mod")) ?? editable[0]!;
|
|
54
|
+
const room = mod.rooms.includes(Number(params.get("room"))) ? Number(params.get("room")) : mod.rooms[0]!;
|
|
55
|
+
const modSelect = $<HTMLSelectElement>("mod"), roomSelect = $<HTMLSelectElement>("room");
|
|
56
|
+
modSelect.append(...editable.map((m) => new Option(m.name, m.name, false, m === mod)));
|
|
57
|
+
roomSelect.append(...mod.rooms.map((r) => new Option(String(r), String(r), false, r === room)));
|
|
58
|
+
const go = (m: string, r?: number) => {
|
|
59
|
+
const q = new URLSearchParams({ mod: m, ...(r !== undefined ? { room: String(r) } : {}) });
|
|
60
|
+
location.search = q.toString();
|
|
61
|
+
};
|
|
62
|
+
modSelect.onchange = () => go(modSelect.value);
|
|
63
|
+
roomSelect.onchange = () => go(mod.name, Number(roomSelect.value));
|
|
64
|
+
document.title = `${mod.name} ${room} · Room Editor`;
|
|
65
|
+
|
|
66
|
+
// --- Files, tabs and the code editor ----------------------------------------------------
|
|
67
|
+
|
|
68
|
+
const files = (await (await fetch(`/__editor/files?mod=${mod.name}`)).json()) as Record<string, string>;
|
|
69
|
+
const yamlName = `${room}.room.yaml`, yarnName = `${room}.yarn`;
|
|
70
|
+
const scaName = `${room}.sca (compiled)`, msgName = `${room}.msg (compiled)`;
|
|
71
|
+
// A room written as a script (a game of ours): its .sc and message file, not YAML.
|
|
72
|
+
const scriptName = `${room}.sc`, messagesName = `${room}.msg`;
|
|
73
|
+
const scriptMode = files[yamlName] === undefined && files[scriptName] !== undefined;
|
|
74
|
+
const editableNames = scriptMode ? [scriptName, ...(files[messagesName] !== undefined ? [messagesName] : [])] : [yamlName, yarnName];
|
|
75
|
+
const tabNames = scriptMode ? [...editableNames, scaName] : [...editableNames, "flags.yaml", scaName, msgName];
|
|
76
|
+
/** What was last sent to the server, per editable file. */
|
|
77
|
+
const saved = new Map(editableNames.map((n) => [n, files[n] ?? ""]));
|
|
78
|
+
|
|
79
|
+
const languageOf = (name: string): Extension =>
|
|
80
|
+
name.endsWith(".yaml") ? yamlLanguage() : name.endsWith(".yarn") ? yarn : name.endsWith(".sc") ? sciScript
|
|
81
|
+
: name === scaName ? sciAssembly : name === msgName || name === messagesName ? sciMessages : [];
|
|
82
|
+
|
|
83
|
+
const theme = EditorView.theme({
|
|
84
|
+
"&": { height: "100%" },
|
|
85
|
+
".cm-content": { fontFamily: "inherit" },
|
|
86
|
+
".cm-gutters": { fontFamily: "inherit" },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function makeState(name: string, text: string): EditorState {
|
|
90
|
+
const isEditable = editableNames.includes(name);
|
|
91
|
+
return EditorState.create({
|
|
92
|
+
doc: text,
|
|
93
|
+
extensions: [
|
|
94
|
+
basicSetup,
|
|
95
|
+
oneDark,
|
|
96
|
+
theme,
|
|
97
|
+
languageOf(name),
|
|
98
|
+
lintGutter(),
|
|
99
|
+
keymap.of([indentWithTab, { key: "Mod-s", run: () => (void build(), true) }]),
|
|
100
|
+
EditorState.readOnly.of(!isEditable),
|
|
101
|
+
EditorView.editable.of(isEditable),
|
|
102
|
+
EditorView.updateListener.of((u) => {
|
|
103
|
+
if (u.docChanged && isEditable) {
|
|
104
|
+
renderTabs();
|
|
105
|
+
scheduleBuild();
|
|
106
|
+
}
|
|
107
|
+
}),
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const states = new Map<string, EditorState>(tabNames.map((n) => [
|
|
113
|
+
n,
|
|
114
|
+
makeState(n, n === "flags.yaml" ? files[n] ?? "# No variables yet: <<set $name to true>> in Yarn adds them here.\n" : editableNames.includes(n) ? files[n] ?? "" : "Builds on save."),
|
|
115
|
+
]));
|
|
116
|
+
let current = editableNames[0]!;
|
|
117
|
+
const view = new EditorView({ parent: $("code"), state: states.get(current)! });
|
|
118
|
+
let error: BuildResult["error"];
|
|
119
|
+
|
|
120
|
+
const textOf = (name: string) => (name === current ? view.state : states.get(name)!).doc.toString();
|
|
121
|
+
|
|
122
|
+
const tabs = $("tabs"), problems = $("problems");
|
|
123
|
+
function renderTabs(): void {
|
|
124
|
+
tabs.replaceChildren(
|
|
125
|
+
...tabNames.map((name) => {
|
|
126
|
+
const b = document.createElement("button");
|
|
127
|
+
b.type = "button";
|
|
128
|
+
b.textContent = name;
|
|
129
|
+
b.classList.toggle("active", name === current);
|
|
130
|
+
b.classList.toggle("dirty", editableNames.includes(name) && textOf(name) !== saved.get(name));
|
|
131
|
+
b.classList.toggle("has-error", error?.file === name);
|
|
132
|
+
b.onclick = () => show(name);
|
|
133
|
+
return b;
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The error as a diagnostic on its line, in the tab it belongs to. */
|
|
139
|
+
function diagnostics(name: string, state: EditorState): Diagnostic[] {
|
|
140
|
+
if (!error || error.file !== name || !error.line || error.line > state.doc.lines) return [];
|
|
141
|
+
const line = state.doc.line(error.line);
|
|
142
|
+
return [{ from: line.from, to: Math.max(line.to, line.from), severity: "error", message: error.message.replace(/^[^:]+:\d+:\s*/, "") }];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function show(name: string, line?: number): void {
|
|
146
|
+
states.set(current, view.state);
|
|
147
|
+
current = name;
|
|
148
|
+
view.setState(states.get(name)!);
|
|
149
|
+
view.dispatch(setDiagnostics(view.state, diagnostics(name, view.state)));
|
|
150
|
+
if (line && line <= view.state.doc.lines) {
|
|
151
|
+
const pos = view.state.doc.line(line).from;
|
|
152
|
+
view.dispatch({ selection: { anchor: pos }, effects: EditorView.scrollIntoView(pos, { y: "center" }) });
|
|
153
|
+
view.focus();
|
|
154
|
+
}
|
|
155
|
+
renderTabs();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Replaces an editable file's text with the smallest change (so the cursor, scrolling and
|
|
160
|
+
* undo behave), shown in its tab: what dragging and drawing on the game do to the YAML.
|
|
161
|
+
*/
|
|
162
|
+
function editText(name: string, text: string): void {
|
|
163
|
+
if (current !== name) show(name);
|
|
164
|
+
const old = view.state.doc.toString();
|
|
165
|
+
if (old === text) return;
|
|
166
|
+
let from = 0;
|
|
167
|
+
while (from < old.length && from < text.length && old[from] === text[from]) from++;
|
|
168
|
+
let oldEnd = old.length, newEnd = text.length;
|
|
169
|
+
while (oldEnd > from && newEnd > from && old[oldEnd - 1] === text[newEnd - 1]) (oldEnd--, newEnd--);
|
|
170
|
+
view.dispatch({
|
|
171
|
+
changes: { from, to: oldEnd, insert: text.slice(from, newEnd) },
|
|
172
|
+
effects: EditorView.scrollIntoView(from, { y: "nearest", yMargin: 40 }),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Replaces a read-only tab's text (flags, compiled output). */
|
|
177
|
+
function setView(name: string, text: string): void {
|
|
178
|
+
const state = makeState(name, text);
|
|
179
|
+
if (name === current) view.setState(state);
|
|
180
|
+
else states.set(name, state);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
window.addEventListener("keydown", (e) => {
|
|
184
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s") {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
void build();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
problems.onclick = () => error?.file && editableNames.includes(error.file) && show(error.file, error.line);
|
|
190
|
+
$("save").onclick = () => void build();
|
|
191
|
+
renderTabs();
|
|
192
|
+
|
|
193
|
+
// --- Building -------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
const auto = $<HTMLInputElement>("auto");
|
|
196
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
197
|
+
/** While a drag or brush stroke is in progress, builds wait for it to end. */
|
|
198
|
+
let held = false;
|
|
199
|
+
function scheduleBuild(): void {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
if (auto.checked && !held) timer = setTimeout(() => void build(), 700);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let building: Promise<void> | undefined;
|
|
205
|
+
let again = false;
|
|
206
|
+
async function build(): Promise<void> {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
if (building) {
|
|
209
|
+
again = true;
|
|
210
|
+
return building;
|
|
211
|
+
}
|
|
212
|
+
building = (async () => {
|
|
213
|
+
const sent = new Map(editableNames.map((n) => [n, textOf(n)]));
|
|
214
|
+
status("Building…");
|
|
215
|
+
const body = { files: Object.fromEntries([...sent].filter(([name, t]) => t !== "" || files[name] !== undefined)) };
|
|
216
|
+
let result: BuildResult;
|
|
217
|
+
try {
|
|
218
|
+
result = await (await fetch(`/__editor/save?mod=${mod.name}`, { method: "POST", body: JSON.stringify(body) })).json();
|
|
219
|
+
} catch (e) {
|
|
220
|
+
result = { ok: false, error: { message: `The dev server didn't answer: ${(e as Error).message}` } };
|
|
221
|
+
}
|
|
222
|
+
for (const [name, t] of sent) saved.set(name, t);
|
|
223
|
+
applyResult(result);
|
|
224
|
+
})();
|
|
225
|
+
await building;
|
|
226
|
+
building = undefined;
|
|
227
|
+
if (again) {
|
|
228
|
+
again = false;
|
|
229
|
+
await build();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function applyResult(result: BuildResult): void {
|
|
234
|
+
error = result.ok ? undefined : result.error;
|
|
235
|
+
problems.hidden = result.ok;
|
|
236
|
+
problems.textContent = error ? `${error.message}${error.file && editableNames.includes(error.file) ? "\n(click to go there)" : ""}` : "";
|
|
237
|
+
view.dispatch(setDiagnostics(view.state, diagnostics(current, view.state)));
|
|
238
|
+
if (result.ok) {
|
|
239
|
+
if (result.reload) {
|
|
240
|
+
// The game's selector or class tables changed: the running game can't take that.
|
|
241
|
+
status("Rebuilt: reloading…");
|
|
242
|
+
location.reload();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (result.flags !== undefined && tabNames.includes("flags.yaml")) setView("flags.yaml", result.flags);
|
|
246
|
+
setView(scaName, result.generated?.[`${room}.sca`] ?? "");
|
|
247
|
+
if (tabNames.includes(msgName)) setView(msgName, result.generated?.[`${room}.msg`] ?? "");
|
|
248
|
+
readExits();
|
|
249
|
+
const resources = (result.resources ?? []).map((r) => ({ type: r.type, number: r.number, data: Uint8Array.from(atob(r.data), (c) => c.charCodeAt(0)) }));
|
|
250
|
+
if (session) {
|
|
251
|
+
replaceResources(session.vm, resources);
|
|
252
|
+
restartScene();
|
|
253
|
+
} else for (const r of resources) rm.override(r, r.data);
|
|
254
|
+
status(`Built in ${result.ms} ms`, "ok");
|
|
255
|
+
} else status("Build failed: see below", "error");
|
|
256
|
+
renderTabs();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The room's exits, by name, from the YAML that built (they're outlined in their own colour). */
|
|
260
|
+
let exits = new Set<string>();
|
|
261
|
+
function readExits(): void {
|
|
262
|
+
try {
|
|
263
|
+
exits = new Set(Object.keys((parseYaml(saved.get(yamlName) ?? "") as { exits?: object } | null)?.exits ?? {}));
|
|
264
|
+
} catch {
|
|
265
|
+
/* keep the last good set */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- The game -------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
// A mod's resources come from the builds (swapped in memory), not from out/mods; a game's
|
|
272
|
+
// from out/games, with the builds' changes swapped in.
|
|
273
|
+
const rm = await ResourceManager.open(httpFiles, ["", "PATCHES"]);
|
|
274
|
+
let session: GameSession | undefined;
|
|
275
|
+
await build();
|
|
276
|
+
while (error) {
|
|
277
|
+
// Nothing to run until the mod builds: wait for the next successful save.
|
|
278
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
279
|
+
}
|
|
280
|
+
session = await GameSession.create({
|
|
281
|
+
canvas: $<HTMLCanvasElement>("screen"), rm, mods: mod.kind === "mod" ? [mod.name] : [], saveNamespace: `editor:${mod.name}`, muted: true,
|
|
282
|
+
});
|
|
283
|
+
const vm = session.vm;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The game just before entering the room: a hero, outside it (a new one, made by the quick
|
|
287
|
+
* route, awake in the first cave; or one made by hand if that didn't work out).
|
|
288
|
+
*/
|
|
289
|
+
let base: VmSnapshot | undefined;
|
|
290
|
+
let entering = false;
|
|
291
|
+
let readyAt = 0;
|
|
292
|
+
const quickStart = gameProfile(vm)?.quickStart(vm);
|
|
293
|
+
if (quickStart) {
|
|
294
|
+
status("Creating a hero…");
|
|
295
|
+
session.start({
|
|
296
|
+
fastForward: {
|
|
297
|
+
...quickStart,
|
|
298
|
+
done: () => {
|
|
299
|
+
entering = true;
|
|
300
|
+
readyAt = performance.now();
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
// No quick route for this game: the player makes a character, then it goes to the room.
|
|
306
|
+
session.start();
|
|
307
|
+
entering = true;
|
|
308
|
+
readyAt = performance.now();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function restartScene(): void {
|
|
312
|
+
if (!base || !session) return;
|
|
313
|
+
session.restore(base);
|
|
314
|
+
entering = true;
|
|
315
|
+
}
|
|
316
|
+
$("restart").onclick = restartScene;
|
|
317
|
+
|
|
318
|
+
const roomNow = () => gameGlobal(vm, "curRoomNum");
|
|
319
|
+
session.onDraw.push(() => {
|
|
320
|
+
if (!entering || session!.fastForwarding) return;
|
|
321
|
+
if (roomNow() === room) {
|
|
322
|
+
entering = false;
|
|
323
|
+
if (!error) status(`In room ${room}`, "ok");
|
|
324
|
+
} else if (heroInPlay(vm)) {
|
|
325
|
+
base ??= session!.snapshot();
|
|
326
|
+
requestRoom(vm, room);
|
|
327
|
+
} else if (performance.now() - readyAt > 4000) {
|
|
328
|
+
status("No hero yet: finish creating one in the game (Play), then it goes to the room", "error");
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Outlines: features, exits, sprites, walk areas, by name (on by default).
|
|
333
|
+
const overlay = createOverlay();
|
|
334
|
+
$("screen-box").append(overlay);
|
|
335
|
+
const outlines = $<HTMLInputElement>("outlines");
|
|
336
|
+
$("legend").replaceChildren(
|
|
337
|
+
...([["features", OVERLAY_COLORS.feature], ["exits", OVERLAY_COLORS.exit], ["props", OVERLAY_COLORS.sprite], ["hero", OVERLAY_COLORS.hero], ["walkable", OVERLAY_COLORS.polygons[3]!], ["obstacles", OVERLAY_COLORS.polygons[2]!]] as const).map(([name, color]) => {
|
|
338
|
+
const s = document.createElement("span");
|
|
339
|
+
s.textContent = name;
|
|
340
|
+
s.style.setProperty("--swatch", color);
|
|
341
|
+
return s;
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
const legend = $("legend");
|
|
345
|
+
const drawOutlines = () => {
|
|
346
|
+
legend.style.display = outlines.checked ? "" : "none";
|
|
347
|
+
if (!outlines.checked || roomNow() !== room) overlay.replaceChildren();
|
|
348
|
+
else {
|
|
349
|
+
drawRoomOverlay(overlay, vm, { sprites: true, features: true, polygons: true, paths: true, isExit: (n) => exits.has(n) });
|
|
350
|
+
// A dialog (a plane above the room's and the status bar's) is what matters then.
|
|
351
|
+
overlay.style.opacity = dialogOpen() ? "0.12" : "1";
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
function dialogOpen(): boolean {
|
|
355
|
+
const g = graphics(vm);
|
|
356
|
+
const roomPriority = roomPlane(vm) ? g.prop(roomPlane(vm)!.address, "priority") : 0;
|
|
357
|
+
return [...g.planes].some((p) => vm.memory.object(p) && g.prop(p, "priority") > roomPriority + 1);
|
|
358
|
+
}
|
|
359
|
+
outlines.onchange = drawOutlines;
|
|
360
|
+
session.onDraw.push(drawOutlines);
|
|
361
|
+
readExits();
|
|
362
|
+
|
|
363
|
+
// Moving things by dragging them, and painting walk areas: edits to the YAML.
|
|
364
|
+
const roomOffset = (): [number, number] => {
|
|
365
|
+
const plane = roomPlane(vm);
|
|
366
|
+
return plane ? [plane.left, plane.top] : [0, 10];
|
|
367
|
+
};
|
|
368
|
+
const editLayer = new EditLayer({
|
|
369
|
+
yaml: () => textOf(yamlName),
|
|
370
|
+
setYaml: (text) => editText(yamlName, text),
|
|
371
|
+
roomOffset,
|
|
372
|
+
spriteBox: (name) => {
|
|
373
|
+
const [ox, oy] = roomOffset();
|
|
374
|
+
const d = drawnItems(vm).find((item) => item.item.name === name);
|
|
375
|
+
return d && { x: d.x - ox, y: d.y - oy, w: d.w, h: d.h };
|
|
376
|
+
},
|
|
377
|
+
previewProp: (name, [x, y]) => {
|
|
378
|
+
const prop = [...vm.memory.objects.values()].find((o) => o.name === name && !o.isClone);
|
|
379
|
+
if (!prop) return;
|
|
380
|
+
vm.setProp(prop, "x", x);
|
|
381
|
+
vm.setProp(prop, "y", y);
|
|
382
|
+
},
|
|
383
|
+
holdBuild: (hold) => {
|
|
384
|
+
held = hold;
|
|
385
|
+
if (hold) clearTimeout(timer);
|
|
386
|
+
else if (auto.checked) void build();
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
$("screen-box").append(editLayer.svg);
|
|
390
|
+
const modeButtons = [...document.querySelectorAll<HTMLButtonElement>("#modes button")];
|
|
391
|
+
const setMode = (mode: EditMode) => {
|
|
392
|
+
editLayer.setMode(mode);
|
|
393
|
+
for (const b of modeButtons) b.classList.toggle("active", b.dataset.mode === mode);
|
|
394
|
+
// The game's own outlines step back while the handles are up.
|
|
395
|
+
overlay.style.filter = mode === "play" ? "" : "opacity(0.35)";
|
|
396
|
+
};
|
|
397
|
+
for (const b of modeButtons) b.onclick = () => setMode(b.dataset.mode as EditMode);
|
|
398
|
+
// Dragging and drawing change YAML: a room written as a script is edited as text.
|
|
399
|
+
if (scriptMode) for (const b of modeButtons) b.hidden = b.dataset.mode !== "play";
|
|
400
|
+
setMode("play");
|
|
401
|
+
// Handles follow what's drawn (props move, the YAML changes); not mid-drag, which redraws itself.
|
|
402
|
+
session.onDraw.push(() => {
|
|
403
|
+
if (editLayer.mode === "play") return;
|
|
404
|
+
if (!held) editLayer.render();
|
|
405
|
+
editLayer.svg.style.opacity = !held && dialogOpen() ? "0.3" : "1";
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const sound = $("sound");
|
|
409
|
+
sound.onclick = () => {
|
|
410
|
+
session!.setMuted(!session!.muted);
|
|
411
|
+
sound.textContent = session!.muted ? "Sound off" : "Sound on";
|
|
412
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Explorer } from "./context.ts";
|
|
2
|
+
export interface ClassTree {
|
|
3
|
+
element: HTMLElement;
|
|
4
|
+
select(species: number | undefined): void;
|
|
5
|
+
}
|
|
6
|
+
/** The class hierarchy as an indented tree, with a filter that keeps matches' ancestors. */
|
|
7
|
+
export declare function createClassTree(ex: Explorer): ClassTree;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { el } from "./context.js";
|
|
2
|
+
/** The class hierarchy as an indented tree, with a filter that keeps matches' ancestors. */
|
|
3
|
+
export function createClassTree(ex) {
|
|
4
|
+
const { classes } = ex.index;
|
|
5
|
+
const children = new Map();
|
|
6
|
+
for (const c of classes)
|
|
7
|
+
children.set(c.superclass, [...(children.get(c.superclass) ?? []), c]);
|
|
8
|
+
for (const list of children.values())
|
|
9
|
+
list.sort((a, b) => a.name.localeCompare(b.name));
|
|
10
|
+
const instanceCount = new Map();
|
|
11
|
+
for (const s of ex.index.scripts)
|
|
12
|
+
for (const o of s.objects)
|
|
13
|
+
instanceCount.set(o.superclass, (instanceCount.get(o.superclass) ?? 0) + 1);
|
|
14
|
+
const links = new Map();
|
|
15
|
+
const items = new Map();
|
|
16
|
+
const build = (parent) => {
|
|
17
|
+
const kids = children.get(parent);
|
|
18
|
+
if (!kids?.length)
|
|
19
|
+
return null;
|
|
20
|
+
const ul = el("ul");
|
|
21
|
+
for (const c of kids) {
|
|
22
|
+
const a = el("a", { href: ex.href(`class/${c.species}`) }, c.name);
|
|
23
|
+
links.set(c.species, a);
|
|
24
|
+
const n = instanceCount.get(c.species) ?? 0;
|
|
25
|
+
const li = el("li", {}, a, " ", el("span", { className: "count" }, n ? `${n} instance${n > 1 ? "s" : ""}` : ""), build(c.species));
|
|
26
|
+
items.set(c.species, li);
|
|
27
|
+
ul.append(li);
|
|
28
|
+
}
|
|
29
|
+
return ul;
|
|
30
|
+
};
|
|
31
|
+
// Roots: no superclass, or a superclass that isn't a known class.
|
|
32
|
+
const known = new Set(classes.map((c) => c.species));
|
|
33
|
+
for (const c of classes)
|
|
34
|
+
if (c.superclass >= 0 && !known.has(c.superclass))
|
|
35
|
+
children.set(-1, [...(children.get(-1) ?? []), c]);
|
|
36
|
+
const filter = el("input", { type: "search", placeholder: "Filter classes…" });
|
|
37
|
+
const element = el("div", { className: "tree" }, filter, build(-1));
|
|
38
|
+
filter.addEventListener("input", () => {
|
|
39
|
+
const q = filter.value.trim().toLowerCase();
|
|
40
|
+
const keep = new Set();
|
|
41
|
+
for (const c of classes) {
|
|
42
|
+
if (q && !c.name.toLowerCase().includes(q))
|
|
43
|
+
continue;
|
|
44
|
+
// Keep the match and its ancestors, so it stays in context.
|
|
45
|
+
for (let s = c.species; s !== undefined && s >= 0 && !keep.has(s); s = classes.find((x) => x.species === s)?.superclass)
|
|
46
|
+
keep.add(s);
|
|
47
|
+
}
|
|
48
|
+
for (const [species, li] of items)
|
|
49
|
+
li.style.display = keep.has(species) ? "" : "none";
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
element,
|
|
53
|
+
select(species) {
|
|
54
|
+
for (const a of links.values())
|
|
55
|
+
a.classList.remove("active");
|
|
56
|
+
if (species === undefined)
|
|
57
|
+
return;
|
|
58
|
+
const a = links.get(species);
|
|
59
|
+
a?.classList.add("active");
|
|
60
|
+
a?.scrollIntoView({ block: "nearest" });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ClassInfo } from "../../../../packages/sci/src/index.ts";
|
|
2
|
+
import { el, type Explorer } from "./context.ts";
|
|
3
|
+
|
|
4
|
+
export interface ClassTree {
|
|
5
|
+
element: HTMLElement;
|
|
6
|
+
select(species: number | undefined): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The class hierarchy as an indented tree, with a filter that keeps matches' ancestors. */
|
|
10
|
+
export function createClassTree(ex: Explorer): ClassTree {
|
|
11
|
+
const { classes } = ex.index;
|
|
12
|
+
const children = new Map<number, ClassInfo[]>();
|
|
13
|
+
for (const c of classes) children.set(c.superclass, [...(children.get(c.superclass) ?? []), c]);
|
|
14
|
+
for (const list of children.values()) list.sort((a, b) => a.name.localeCompare(b.name));
|
|
15
|
+
const instanceCount = new Map<number, number>();
|
|
16
|
+
for (const s of ex.index.scripts) for (const o of s.objects) instanceCount.set(o.superclass, (instanceCount.get(o.superclass) ?? 0) + 1);
|
|
17
|
+
|
|
18
|
+
const links = new Map<number, HTMLAnchorElement>();
|
|
19
|
+
const items = new Map<number, HTMLLIElement>();
|
|
20
|
+
const build = (parent: number): HTMLUListElement | null => {
|
|
21
|
+
const kids = children.get(parent);
|
|
22
|
+
if (!kids?.length) return null;
|
|
23
|
+
const ul = el("ul");
|
|
24
|
+
for (const c of kids) {
|
|
25
|
+
const a = el("a", { href: ex.href(`class/${c.species}`) }, c.name);
|
|
26
|
+
links.set(c.species, a);
|
|
27
|
+
const n = instanceCount.get(c.species) ?? 0;
|
|
28
|
+
const li = el("li", {}, a, " ", el("span", { className: "count" }, n ? `${n} instance${n > 1 ? "s" : ""}` : ""), build(c.species));
|
|
29
|
+
items.set(c.species, li);
|
|
30
|
+
ul.append(li);
|
|
31
|
+
}
|
|
32
|
+
return ul;
|
|
33
|
+
};
|
|
34
|
+
// Roots: no superclass, or a superclass that isn't a known class.
|
|
35
|
+
const known = new Set(classes.map((c) => c.species));
|
|
36
|
+
for (const c of classes) if (c.superclass >= 0 && !known.has(c.superclass)) children.set(-1, [...(children.get(-1) ?? []), c]);
|
|
37
|
+
const filter = el("input", { type: "search", placeholder: "Filter classes…" });
|
|
38
|
+
const element = el("div", { className: "tree" }, filter, build(-1));
|
|
39
|
+
|
|
40
|
+
filter.addEventListener("input", () => {
|
|
41
|
+
const q = filter.value.trim().toLowerCase();
|
|
42
|
+
const keep = new Set<number>();
|
|
43
|
+
for (const c of classes) {
|
|
44
|
+
if (q && !c.name.toLowerCase().includes(q)) continue;
|
|
45
|
+
// Keep the match and its ancestors, so it stays in context.
|
|
46
|
+
for (let s: number | undefined = c.species; s !== undefined && s >= 0 && !keep.has(s); s = classes.find((x) => x.species === s)?.superclass) keep.add(s);
|
|
47
|
+
}
|
|
48
|
+
for (const [species, li] of items) li.style.display = keep.has(species) ? "" : "none";
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
element,
|
|
53
|
+
select(species) {
|
|
54
|
+
for (const a of links.values()) a.classList.remove("active");
|
|
55
|
+
if (species === undefined) return;
|
|
56
|
+
const a = links.get(species);
|
|
57
|
+
a?.classList.add("active");
|
|
58
|
+
a?.scrollIntoView({ block: "nearest" });
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type GameIndex, type MessageFile, type ObjectInfo, type Palette, type ResourceManager, type ScriptWorld } from "../../../../packages/sci/src/index.ts";
|
|
2
|
+
/** Everything the explorer's views share. */
|
|
3
|
+
export interface Explorer {
|
|
4
|
+
rm: ResourceManager;
|
|
5
|
+
/** Mods loaded over the game (their saves are kept apart). */
|
|
6
|
+
mods: string[];
|
|
7
|
+
world: ScriptWorld;
|
|
8
|
+
index: GameIndex;
|
|
9
|
+
basePalette: Palette;
|
|
10
|
+
/** Navigates to a hash route, e.g. "room/710" (the current tab is kept). */
|
|
11
|
+
href(route: string): string;
|
|
12
|
+
messages(n: number): Promise<MessageFile | undefined>;
|
|
13
|
+
pic(n: number): Promise<HTMLCanvasElement | undefined>;
|
|
14
|
+
object(script: number, offset: number): ObjectInfo | undefined;
|
|
15
|
+
objectsNamed(name: string): ObjectInfo[];
|
|
16
|
+
}
|
|
17
|
+
type Child = Node | string | null | undefined | false;
|
|
18
|
+
/** `el("a", { href }, "text", child)`: a tiny DOM builder. */
|
|
19
|
+
export declare function el<K extends keyof HTMLElementTagNameMap>(tag: K, props?: Partial<HTMLElementTagNameMap[K]> & Record<string, unknown>, ...children: Child[]): HTMLElementTagNameMap[K];
|
|
20
|
+
export declare const hex: (n: number) => string;
|
|
21
|
+
/** Property values: small numbers as decimal, the rest (addresses, flags) as hex too. */
|
|
22
|
+
export declare const showValue: (v: number) => string;
|
|
23
|
+
export declare function createExplorer(rm: ResourceManager, mods: string[], world: ScriptWorld, index: GameIndex, basePalette: Palette, currentTab: () => string): Explorer;
|
|
24
|
+
/** A small copy of a canvas as a data URL, for map thumbnails. */
|
|
25
|
+
export declare function thumbnail(canvas: HTMLCanvasElement, width?: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* The room editor's address for room `n`, if a mod describes it in a .room.yaml (preferring
|
|
28
|
+
* the mods loaded here). The dev server's /__editor/mods lists them; elsewhere, none.
|
|
29
|
+
*/
|
|
30
|
+
export declare function editorUrl(n: number, preferred?: string[]): Promise<string | undefined>;
|
|
31
|
+
/** An "Edit room n" link to the editor, filled in once we know the room is editable. */
|
|
32
|
+
export declare function editorLink(n: number, preferred?: string[]): HTMLAnchorElement;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ResourceType, celToRgba, mergePalette, parseMessages, parsePic, } from "../../../../packages/sci/src/index.js";
|
|
2
|
+
/** `el("a", { href }, "text", child)`: a tiny DOM builder. */
|
|
3
|
+
export function el(tag, props = {}, ...children) {
|
|
4
|
+
const node = Object.assign(document.createElement(tag), props);
|
|
5
|
+
for (const c of children)
|
|
6
|
+
if (c !== null && c !== undefined && c !== false)
|
|
7
|
+
node.append(c);
|
|
8
|
+
return node;
|
|
9
|
+
}
|
|
10
|
+
export const hex = (n) => `$${n.toString(16).padStart(4, "0")}`;
|
|
11
|
+
/** Property values: small numbers as decimal, the rest (addresses, flags) as hex too. */
|
|
12
|
+
export const showValue = (v) => (v >= 0x8000 ? `${v - 0x10000}` : v > 255 ? `${v} (${hex(v)})` : `${v}`);
|
|
13
|
+
export function createExplorer(rm, mods, world, index, basePalette, currentTab) {
|
|
14
|
+
const messageCache = new Map();
|
|
15
|
+
const picCache = new Map();
|
|
16
|
+
const objects = new Map();
|
|
17
|
+
const byName = new Map();
|
|
18
|
+
for (const s of index.scripts) {
|
|
19
|
+
for (const o of s.objects) {
|
|
20
|
+
objects.set(`${o.script}:${o.offset}`, o);
|
|
21
|
+
byName.set(o.name, [...(byName.get(o.name) ?? []), o]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
rm, mods, world, index, basePalette,
|
|
26
|
+
href: (route) => `#/${currentTab()}/${route}`,
|
|
27
|
+
object: (script, offset) => objects.get(`${script}:${offset}`),
|
|
28
|
+
objectsNamed: (name) => byName.get(name) ?? [],
|
|
29
|
+
messages(n) {
|
|
30
|
+
let p = messageCache.get(n);
|
|
31
|
+
if (!p) {
|
|
32
|
+
p = rm.has({ type: ResourceType.Message, number: n })
|
|
33
|
+
? rm.load({ type: ResourceType.Message, number: n }).then((r) => parseMessages(r.data))
|
|
34
|
+
: Promise.resolve(undefined);
|
|
35
|
+
messageCache.set(n, p);
|
|
36
|
+
}
|
|
37
|
+
return p;
|
|
38
|
+
},
|
|
39
|
+
pic(n) {
|
|
40
|
+
let p = picCache.get(n);
|
|
41
|
+
if (!p) {
|
|
42
|
+
p = rm.has({ type: ResourceType.Pic, number: n }) ? rm.load({ type: ResourceType.Pic, number: n }).then((r) => renderPic(r.data, basePalette)) : Promise.resolve(undefined);
|
|
43
|
+
picCache.set(n, p);
|
|
44
|
+
}
|
|
45
|
+
return p;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Remap colours (shadows, torch light) only mean something at run time: approximate.
|
|
50
|
+
const REMAP = new Map([[253, 64], [254, 110]]);
|
|
51
|
+
function renderPic(data, base) {
|
|
52
|
+
const pic = parsePic(data);
|
|
53
|
+
const palette = mergePalette(base, pic.palette);
|
|
54
|
+
const canvas = el("canvas", { width: pic.width, height: pic.height });
|
|
55
|
+
const ctx = canvas.getContext("2d");
|
|
56
|
+
for (const cel of [...pic.cels].sort((a, b) => a.priority - b.priority)) {
|
|
57
|
+
if (!cel.width || !cel.height)
|
|
58
|
+
continue;
|
|
59
|
+
const layer = el("canvas", { width: cel.width, height: cel.height });
|
|
60
|
+
layer.getContext("2d").putImageData(new ImageData(celToRgba(cel, palette, { remap: REMAP }), cel.width, cel.height), 0, 0);
|
|
61
|
+
ctx.drawImage(layer, cel.x, cel.y);
|
|
62
|
+
}
|
|
63
|
+
return canvas;
|
|
64
|
+
}
|
|
65
|
+
/** A small copy of a canvas as a data URL, for map thumbnails. */
|
|
66
|
+
export function thumbnail(canvas, width = 96) {
|
|
67
|
+
const height = Math.round((canvas.height / canvas.width) * width * 1.2);
|
|
68
|
+
const small = el("canvas", { width, height });
|
|
69
|
+
const ctx = small.getContext("2d");
|
|
70
|
+
ctx.imageSmoothingQuality = "high";
|
|
71
|
+
ctx.drawImage(canvas, 0, 0, width, height);
|
|
72
|
+
return small.toDataURL();
|
|
73
|
+
}
|
|
74
|
+
// --- Rooms the editor can open ------------------------------------------------------------
|
|
75
|
+
let editable;
|
|
76
|
+
/**
|
|
77
|
+
* The room editor's address for room `n`, if a mod describes it in a .room.yaml (preferring
|
|
78
|
+
* the mods loaded here). The dev server's /__editor/mods lists them; elsewhere, none.
|
|
79
|
+
*/
|
|
80
|
+
export async function editorUrl(n, preferred = []) {
|
|
81
|
+
editable ??= fetch("/__editor/mods")
|
|
82
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
83
|
+
.catch(() => []);
|
|
84
|
+
const mods = (await editable).filter((m) => m.rooms.includes(n));
|
|
85
|
+
const mod = mods.find((m) => preferred.includes(m.name)) ?? mods[0];
|
|
86
|
+
return mod && `/editor.html?mod=${encodeURIComponent(mod.name)}&room=${n}`;
|
|
87
|
+
}
|
|
88
|
+
/** An "Edit room n" link to the editor, filled in once we know the room is editable. */
|
|
89
|
+
export function editorLink(n, preferred = []) {
|
|
90
|
+
const a = el("a", { className: "edit-link", target: "_blank", hidden: true, title: "Open this room in the room editor" }, `Edit room ${n}`);
|
|
91
|
+
void editorUrl(n, preferred).then((url) => {
|
|
92
|
+
if (url)
|
|
93
|
+
(a.href = url), (a.hidden = false);
|
|
94
|
+
});
|
|
95
|
+
return a;
|
|
96
|
+
}
|