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.
Files changed (357) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -2
  4. package/apps/viewer/editor.html +43 -0
  5. package/apps/viewer/explore.html +27 -0
  6. package/apps/viewer/index.html +26 -0
  7. package/apps/viewer/package.json +27 -0
  8. package/apps/viewer/play.html +26 -0
  9. package/apps/viewer/plugin.d.ts +18 -0
  10. package/apps/viewer/plugin.js +75 -0
  11. package/apps/viewer/plugin.ts +84 -0
  12. package/apps/viewer/src/adlib.d.ts +20 -0
  13. package/apps/viewer/src/adlib.js +55 -0
  14. package/apps/viewer/src/adlib.ts +62 -0
  15. package/apps/viewer/src/app.d.ts +5 -0
  16. package/apps/viewer/src/app.js +5 -0
  17. package/apps/viewer/src/app.ts +5 -0
  18. package/apps/viewer/src/asset-index.d.ts +22 -0
  19. package/apps/viewer/src/asset-index.js +74 -0
  20. package/apps/viewer/src/asset-index.ts +90 -0
  21. package/apps/viewer/src/audio.d.ts +28 -0
  22. package/apps/viewer/src/audio.js +102 -0
  23. package/apps/viewer/src/audio.ts +110 -0
  24. package/apps/viewer/src/editor-handles.d.ts +63 -0
  25. package/apps/viewer/src/editor-handles.js +332 -0
  26. package/apps/viewer/src/editor-handles.ts +368 -0
  27. package/apps/viewer/src/editor-languages.d.ts +23 -0
  28. package/apps/viewer/src/editor-languages.js +157 -0
  29. package/apps/viewer/src/editor-languages.ts +135 -0
  30. package/apps/viewer/src/editor.css +213 -0
  31. package/apps/viewer/src/editor.d.ts +1 -0
  32. package/apps/viewer/src/editor.js +393 -0
  33. package/apps/viewer/src/editor.ts +412 -0
  34. package/apps/viewer/src/explore/classes.d.ts +7 -0
  35. package/apps/viewer/src/explore/classes.js +63 -0
  36. package/apps/viewer/src/explore/classes.ts +61 -0
  37. package/apps/viewer/src/explore/context.d.ts +33 -0
  38. package/apps/viewer/src/explore/context.js +96 -0
  39. package/apps/viewer/src/explore/context.ts +124 -0
  40. package/apps/viewer/src/explore/explore.css +93 -0
  41. package/apps/viewer/src/explore/explore.d.ts +1 -0
  42. package/apps/viewer/src/explore/explore.js +102 -0
  43. package/apps/viewer/src/explore/explore.ts +104 -0
  44. package/apps/viewer/src/explore/inspect.d.ts +5 -0
  45. package/apps/viewer/src/explore/inspect.js +208 -0
  46. package/apps/viewer/src/explore/inspect.ts +279 -0
  47. package/apps/viewer/src/explore/live.d.ts +22 -0
  48. package/apps/viewer/src/explore/live.js +318 -0
  49. package/apps/viewer/src/explore/live.ts +365 -0
  50. package/apps/viewer/src/explore/map.d.ts +11 -0
  51. package/apps/viewer/src/explore/map.js +182 -0
  52. package/apps/viewer/src/explore/map.ts +193 -0
  53. package/apps/viewer/src/files.d.ts +21 -0
  54. package/apps/viewer/src/files.js +69 -0
  55. package/apps/viewer/src/files.ts +72 -0
  56. package/apps/viewer/src/game-profile.d.ts +19 -0
  57. package/apps/viewer/src/game-profile.js +4 -0
  58. package/apps/viewer/src/game-profile.ts +26 -0
  59. package/apps/viewer/src/main.d.ts +1 -0
  60. package/apps/viewer/src/main.js +473 -0
  61. package/apps/viewer/src/main.ts +527 -0
  62. package/apps/viewer/src/music.d.ts +14 -0
  63. package/apps/viewer/src/music.js +32 -0
  64. package/apps/viewer/src/music.ts +34 -0
  65. package/apps/viewer/src/opl-worklet.d.ts +1 -0
  66. package/apps/viewer/src/opl-worklet.js +47 -0
  67. package/apps/viewer/src/opl-worklet.ts +55 -0
  68. package/apps/viewer/src/overlay.d.ts +31 -0
  69. package/apps/viewer/src/overlay.js +103 -0
  70. package/apps/viewer/src/overlay.ts +123 -0
  71. package/apps/viewer/src/play.css +14 -0
  72. package/apps/viewer/src/play.d.ts +1 -0
  73. package/apps/viewer/src/play.js +179 -0
  74. package/apps/viewer/src/play.ts +177 -0
  75. package/apps/viewer/src/saves.d.ts +10 -0
  76. package/apps/viewer/src/saves.js +50 -0
  77. package/apps/viewer/src/saves.ts +51 -0
  78. package/apps/viewer/src/session.d.ts +91 -0
  79. package/apps/viewer/src/session.js +301 -0
  80. package/apps/viewer/src/session.ts +321 -0
  81. package/apps/viewer/src/sound-preview.d.ts +7 -0
  82. package/apps/viewer/src/sound-preview.js +214 -0
  83. package/apps/viewer/src/sound-preview.ts +228 -0
  84. package/apps/viewer/src/style.css +64 -0
  85. package/apps/viewer/vite.config.ts +25 -0
  86. package/bin/sci-ts.mjs +9 -0
  87. package/lib/994.sc +49 -0
  88. package/lib/995.sc +53 -0
  89. package/lib/996.sc +299 -0
  90. package/lib/997.sc +198 -0
  91. package/lib/998.sc +338 -0
  92. package/lib/999.sc +189 -0
  93. package/lib/resources.d.ts +2 -0
  94. package/lib/resources.js +84 -0
  95. package/lib/resources.ts +88 -0
  96. package/lib/system.sh +62 -0
  97. package/package.json +75 -4
  98. package/packages/content/room.schema.json +267 -0
  99. package/packages/content/src/index.d.ts +3 -0
  100. package/packages/content/src/index.js +3 -0
  101. package/packages/content/src/index.ts +3 -0
  102. package/packages/content/src/room.d.ts +111 -0
  103. package/packages/content/src/room.js +886 -0
  104. package/packages/content/src/room.ts +1011 -0
  105. package/packages/content/src/target.d.ts +80 -0
  106. package/packages/content/src/target.js +1 -0
  107. package/packages/content/src/target.ts +60 -0
  108. package/packages/content/src/yarn.d.ts +90 -0
  109. package/packages/content/src/yarn.js +214 -0
  110. package/packages/content/src/yarn.ts +261 -0
  111. package/packages/sci/src/analysis/game.d.ts +90 -0
  112. package/packages/sci/src/analysis/game.js +221 -0
  113. package/packages/sci/src/analysis/game.ts +305 -0
  114. package/packages/sci/src/analysis/runtime.d.ts +58 -0
  115. package/packages/sci/src/analysis/runtime.js +133 -0
  116. package/packages/sci/src/analysis/runtime.ts +163 -0
  117. package/packages/sci/src/audio/adlib.d.ts +73 -0
  118. package/packages/sci/src/audio/adlib.js +280 -0
  119. package/packages/sci/src/audio/adlib.ts +329 -0
  120. package/packages/sci/src/audio/index.d.ts +43 -0
  121. package/packages/sci/src/audio/index.js +99 -0
  122. package/packages/sci/src/audio/index.ts +122 -0
  123. package/packages/sci/src/audio/midi.d.ts +92 -0
  124. package/packages/sci/src/audio/midi.js +242 -0
  125. package/packages/sci/src/audio/midi.ts +262 -0
  126. package/packages/sci/src/audio/opl2.d.ts +74 -0
  127. package/packages/sci/src/audio/opl2.js +283 -0
  128. package/packages/sci/src/audio/opl2.ts +283 -0
  129. package/packages/sci/src/audio/sol.d.ts +32 -0
  130. package/packages/sci/src/audio/sol.js +81 -0
  131. package/packages/sci/src/audio/sol.ts +100 -0
  132. package/packages/sci/src/audio/write.d.ts +35 -0
  133. package/packages/sci/src/audio/write.js +91 -0
  134. package/packages/sci/src/audio/write.ts +108 -0
  135. package/packages/sci/src/gfx/cel.d.ts +36 -0
  136. package/packages/sci/src/gfx/cel.js +82 -0
  137. package/packages/sci/src/gfx/cel.ts +110 -0
  138. package/packages/sci/src/gfx/palette.d.ts +24 -0
  139. package/packages/sci/src/gfx/palette.js +49 -0
  140. package/packages/sci/src/gfx/palette.ts +56 -0
  141. package/packages/sci/src/gfx/pic-file.d.ts +46 -0
  142. package/packages/sci/src/gfx/pic-file.js +114 -0
  143. package/packages/sci/src/gfx/pic-file.ts +155 -0
  144. package/packages/sci/src/gfx/pic.d.ts +27 -0
  145. package/packages/sci/src/gfx/pic.js +34 -0
  146. package/packages/sci/src/gfx/pic.ts +53 -0
  147. package/packages/sci/src/gfx/quantize.d.ts +42 -0
  148. package/packages/sci/src/gfx/quantize.js +199 -0
  149. package/packages/sci/src/gfx/quantize.ts +218 -0
  150. package/packages/sci/src/gfx/view-file.d.ts +62 -0
  151. package/packages/sci/src/gfx/view-file.js +182 -0
  152. package/packages/sci/src/gfx/view-file.ts +233 -0
  153. package/packages/sci/src/gfx/view.d.ts +28 -0
  154. package/packages/sci/src/gfx/view.js +45 -0
  155. package/packages/sci/src/gfx/view.ts +62 -0
  156. package/packages/sci/src/index.d.ts +38 -0
  157. package/packages/sci/src/index.js +38 -0
  158. package/packages/sci/src/index.ts +38 -0
  159. package/packages/sci/src/motion/pathfinding.d.ts +40 -0
  160. package/packages/sci/src/motion/pathfinding.js +140 -0
  161. package/packages/sci/src/motion/pathfinding.ts +147 -0
  162. package/packages/sci/src/resource/archive.d.ts +16 -0
  163. package/packages/sci/src/resource/archive.js +51 -0
  164. package/packages/sci/src/resource/archive.ts +56 -0
  165. package/packages/sci/src/resource/lzs.d.ts +12 -0
  166. package/packages/sci/src/resource/lzs.js +64 -0
  167. package/packages/sci/src/resource/lzs.ts +66 -0
  168. package/packages/sci/src/resource/manager.d.ts +45 -0
  169. package/packages/sci/src/resource/manager.js +158 -0
  170. package/packages/sci/src/resource/manager.ts +184 -0
  171. package/packages/sci/src/resource/map.d.ts +18 -0
  172. package/packages/sci/src/resource/map.js +34 -0
  173. package/packages/sci/src/resource/map.ts +44 -0
  174. package/packages/sci/src/resource/types.d.ts +37 -0
  175. package/packages/sci/src/resource/types.js +45 -0
  176. package/packages/sci/src/resource/types.ts +57 -0
  177. package/packages/sci/src/script/assembly.d.ts +67 -0
  178. package/packages/sci/src/script/assembly.js +748 -0
  179. package/packages/sci/src/script/assembly.ts +714 -0
  180. package/packages/sci/src/script/compiler.d.ts +75 -0
  181. package/packages/sci/src/script/compiler.js +993 -0
  182. package/packages/sci/src/script/compiler.ts +1040 -0
  183. package/packages/sci/src/script/disasm.d.ts +81 -0
  184. package/packages/sci/src/script/disasm.js +318 -0
  185. package/packages/sci/src/script/disasm.ts +323 -0
  186. package/packages/sci/src/script/kernel-names.d.ts +6 -0
  187. package/packages/sci/src/script/kernel-names.js +165 -0
  188. package/packages/sci/src/script/kernel-names.ts +165 -0
  189. package/packages/sci/src/script/opcodes.d.ts +22 -0
  190. package/packages/sci/src/script/opcodes.js +45 -0
  191. package/packages/sci/src/script/opcodes.ts +72 -0
  192. package/packages/sci/src/script/script.d.ts +56 -0
  193. package/packages/sci/src/script/script.js +63 -0
  194. package/packages/sci/src/script/script.ts +111 -0
  195. package/packages/sci/src/script/sexpr.d.ts +45 -0
  196. package/packages/sci/src/script/sexpr.js +151 -0
  197. package/packages/sci/src/script/sexpr.ts +139 -0
  198. package/packages/sci/src/script/vocab.d.ts +15 -0
  199. package/packages/sci/src/script/vocab.js +44 -0
  200. package/packages/sci/src/script/vocab.ts +48 -0
  201. package/packages/sci/src/script/world.d.ts +25 -0
  202. package/packages/sci/src/script/world.js +65 -0
  203. package/packages/sci/src/script/world.ts +70 -0
  204. package/packages/sci/src/text/font.d.ts +37 -0
  205. package/packages/sci/src/text/font.js +103 -0
  206. package/packages/sci/src/text/font.ts +125 -0
  207. package/packages/sci/src/text/message.d.ts +69 -0
  208. package/packages/sci/src/text/message.js +171 -0
  209. package/packages/sci/src/text/message.ts +205 -0
  210. package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
  211. package/packages/sci/src/vm/kernels/arrays.js +182 -0
  212. package/packages/sci/src/vm/kernels/arrays.ts +164 -0
  213. package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
  214. package/packages/sci/src/vm/kernels/audio.js +347 -0
  215. package/packages/sci/src/vm/kernels/audio.ts +349 -0
  216. package/packages/sci/src/vm/kernels/core.d.ts +5 -0
  217. package/packages/sci/src/vm/kernels/core.js +87 -0
  218. package/packages/sci/src/vm/kernels/core.ts +91 -0
  219. package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
  220. package/packages/sci/src/vm/kernels/graphics.js +467 -0
  221. package/packages/sci/src/vm/kernels/graphics.ts +502 -0
  222. package/packages/sci/src/vm/kernels/index.d.ts +8 -0
  223. package/packages/sci/src/vm/kernels/index.js +29 -0
  224. package/packages/sci/src/vm/kernels/index.ts +32 -0
  225. package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
  226. package/packages/sci/src/vm/kernels/lists.js +127 -0
  227. package/packages/sci/src/vm/kernels/lists.ts +111 -0
  228. package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
  229. package/packages/sci/src/vm/kernels/motion.js +204 -0
  230. package/packages/sci/src/vm/kernels/motion.ts +194 -0
  231. package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
  232. package/packages/sci/src/vm/kernels/palette.js +292 -0
  233. package/packages/sci/src/vm/kernels/palette.ts +303 -0
  234. package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
  235. package/packages/sci/src/vm/kernels/saves.js +123 -0
  236. package/packages/sci/src/vm/kernels/saves.ts +149 -0
  237. package/packages/sci/src/vm/kernels/system.d.ts +62 -0
  238. package/packages/sci/src/vm/kernels/system.js +110 -0
  239. package/packages/sci/src/vm/kernels/system.ts +128 -0
  240. package/packages/sci/src/vm/kernels/text.d.ts +13 -0
  241. package/packages/sci/src/vm/kernels/text.js +338 -0
  242. package/packages/sci/src/vm/kernels/text.ts +328 -0
  243. package/packages/sci/src/vm/memory.d.ts +138 -0
  244. package/packages/sci/src/vm/memory.js +168 -0
  245. package/packages/sci/src/vm/memory.ts +266 -0
  246. package/packages/sci/src/vm/replace.d.ts +11 -0
  247. package/packages/sci/src/vm/replace.js +14 -0
  248. package/packages/sci/src/vm/replace.ts +16 -0
  249. package/packages/sci/src/vm/savegame.d.ts +89 -0
  250. package/packages/sci/src/vm/savegame.js +161 -0
  251. package/packages/sci/src/vm/savegame.ts +188 -0
  252. package/packages/sci/src/vm/value.d.ts +17 -0
  253. package/packages/sci/src/vm/value.js +12 -0
  254. package/packages/sci/src/vm/value.ts +25 -0
  255. package/packages/sci/src/vm/vm.d.ts +155 -0
  256. package/packages/sci/src/vm/vm.js +1053 -0
  257. package/packages/sci/src/vm/vm.ts +914 -0
  258. package/tools/adlib-render.d.ts +1 -0
  259. package/tools/adlib-render.js +49 -0
  260. package/tools/adlib-render.ts +42 -0
  261. package/tools/analyze.d.ts +1 -0
  262. package/tools/analyze.js +25 -0
  263. package/tools/analyze.ts +25 -0
  264. package/tools/art/build.d.ts +34 -0
  265. package/tools/art/build.js +175 -0
  266. package/tools/art/build.ts +153 -0
  267. package/tools/art.d.ts +1 -0
  268. package/tools/art.js +31 -0
  269. package/tools/art.ts +29 -0
  270. package/tools/asm.d.ts +1 -0
  271. package/tools/asm.js +61 -0
  272. package/tools/asm.ts +55 -0
  273. package/tools/cli.d.ts +1 -0
  274. package/tools/cli.js +99 -0
  275. package/tools/cli.ts +100 -0
  276. package/tools/editor-server.d.ts +29 -0
  277. package/tools/editor-server.js +162 -0
  278. package/tools/editor-server.ts +176 -0
  279. package/tools/explore.d.ts +1 -0
  280. package/tools/explore.js +305 -0
  281. package/tools/explore.ts +250 -0
  282. package/tools/game/audio.d.ts +19 -0
  283. package/tools/game/audio.js +143 -0
  284. package/tools/game/audio.ts +130 -0
  285. package/tools/game/build.d.ts +46 -0
  286. package/tools/game/build.js +259 -0
  287. package/tools/game/build.ts +283 -0
  288. package/tools/game/defaults.d.ts +26 -0
  289. package/tools/game/defaults.js +82 -0
  290. package/tools/game/defaults.ts +83 -0
  291. package/tools/game/font.d.ts +3 -0
  292. package/tools/game/font.js +124 -0
  293. package/tools/game/font.ts +126 -0
  294. package/tools/game/kit.d.ts +7 -0
  295. package/tools/game/kit.js +7 -0
  296. package/tools/game/kit.ts +10 -0
  297. package/tools/game/new.d.ts +8 -0
  298. package/tools/game/new.js +39 -0
  299. package/tools/game/new.ts +38 -0
  300. package/tools/game/target.d.ts +7 -0
  301. package/tools/game/target.js +41 -0
  302. package/tools/game/target.ts +42 -0
  303. package/tools/game/template/resources.ts.tmpl +43 -0
  304. package/tools/game/template/rooms/1.room.yaml +9 -0
  305. package/tools/game/template/rooms/1.yarn +17 -0
  306. package/tools/game/template/scripts/0.sc +24 -0
  307. package/tools/game.d.ts +1 -0
  308. package/tools/game.js +77 -0
  309. package/tools/game.ts +70 -0
  310. package/tools/mod-build.d.ts +28 -0
  311. package/tools/mod-build.js +173 -0
  312. package/tools/mod-build.ts +179 -0
  313. package/tools/mod-target.d.ts +16 -0
  314. package/tools/mod-target.js +26 -0
  315. package/tools/mod-target.ts +32 -0
  316. package/tools/mod.d.ts +1 -0
  317. package/tools/mod.js +47 -0
  318. package/tools/mod.ts +45 -0
  319. package/tools/msg.d.ts +1 -0
  320. package/tools/msg.js +40 -0
  321. package/tools/msg.ts +36 -0
  322. package/tools/node-files.d.ts +3 -0
  323. package/tools/node-files.js +34 -0
  324. package/tools/node-files.ts +33 -0
  325. package/tools/patches.d.ts +3 -0
  326. package/tools/patches.js +13 -0
  327. package/tools/patches.ts +13 -0
  328. package/tools/pic-build.d.ts +9 -0
  329. package/tools/pic-build.js +26 -0
  330. package/tools/pic-build.ts +27 -0
  331. package/tools/pic.d.ts +1 -0
  332. package/tools/pic.js +64 -0
  333. package/tools/pic.ts +61 -0
  334. package/tools/play.d.ts +1 -0
  335. package/tools/play.js +54 -0
  336. package/tools/play.ts +54 -0
  337. package/tools/png.d.ts +16 -0
  338. package/tools/png.js +124 -0
  339. package/tools/png.ts +123 -0
  340. package/tools/res.d.ts +1 -0
  341. package/tools/res.js +48 -0
  342. package/tools/res.ts +50 -0
  343. package/tools/scenario.d.ts +1 -0
  344. package/tools/scenario.js +74 -0
  345. package/tools/scenario.ts +80 -0
  346. package/tools/site/build.d.ts +8 -0
  347. package/tools/site/build.js +110 -0
  348. package/tools/site/build.ts +116 -0
  349. package/tools/site/markdown.d.ts +11 -0
  350. package/tools/site/markdown.js +88 -0
  351. package/tools/site/markdown.ts +84 -0
  352. package/tools/view-build.d.ts +34 -0
  353. package/tools/view-build.js +47 -0
  354. package/tools/view-build.ts +71 -0
  355. package/tools/view.d.ts +1 -0
  356. package/tools/view.js +54 -0
  357. package/tools/view.ts +52 -0
@@ -0,0 +1,124 @@
1
+ import {
2
+ ResourceType, celToRgba, mergePalette, parseMessages, parsePic,
3
+ type GameIndex, type MessageFile, type ObjectInfo, type Palette, type ResourceManager, type ScriptWorld,
4
+ } from "../../../../packages/sci/src/index.ts";
5
+
6
+ /** Everything the explorer's views share. */
7
+ export interface Explorer {
8
+ rm: ResourceManager;
9
+ /** Mods loaded over the game (their saves are kept apart). */
10
+ mods: string[];
11
+ world: ScriptWorld;
12
+ index: GameIndex;
13
+ basePalette: Palette;
14
+ /** Navigates to a hash route, e.g. "room/710" (the current tab is kept). */
15
+ href(route: string): string;
16
+ messages(n: number): Promise<MessageFile | undefined>;
17
+ pic(n: number): Promise<HTMLCanvasElement | undefined>;
18
+ object(script: number, offset: number): ObjectInfo | undefined;
19
+ objectsNamed(name: string): ObjectInfo[];
20
+ }
21
+
22
+ type Child = Node | string | null | undefined | false;
23
+
24
+ /** `el("a", { href }, "text", child)`: a tiny DOM builder. */
25
+ export function el<K extends keyof HTMLElementTagNameMap>(tag: K, props: Partial<HTMLElementTagNameMap[K]> & Record<string, unknown> = {}, ...children: Child[]): HTMLElementTagNameMap[K] {
26
+ const node = Object.assign(document.createElement(tag), props);
27
+ for (const c of children) if (c !== null && c !== undefined && c !== false) node.append(c);
28
+ return node;
29
+ }
30
+
31
+ export const hex = (n: number) => `$${n.toString(16).padStart(4, "0")}`;
32
+ /** Property values: small numbers as decimal, the rest (addresses, flags) as hex too. */
33
+ export const showValue = (v: number) => (v >= 0x8000 ? `${v - 0x10000}` : v > 255 ? `${v} (${hex(v)})` : `${v}`);
34
+
35
+ export function createExplorer(rm: ResourceManager, mods: string[], world: ScriptWorld, index: GameIndex, basePalette: Palette, currentTab: () => string): Explorer {
36
+ const messageCache = new Map<number, Promise<MessageFile | undefined>>();
37
+ const picCache = new Map<number, Promise<HTMLCanvasElement | undefined>>();
38
+ const objects = new Map<string, ObjectInfo>();
39
+ const byName = new Map<string, ObjectInfo[]>();
40
+ for (const s of index.scripts) {
41
+ for (const o of s.objects) {
42
+ objects.set(`${o.script}:${o.offset}`, o);
43
+ byName.set(o.name, [...(byName.get(o.name) ?? []), o]);
44
+ }
45
+ }
46
+
47
+ return {
48
+ rm, mods, world, index, basePalette,
49
+ href: (route) => `#/${currentTab()}/${route}`,
50
+ object: (script, offset) => objects.get(`${script}:${offset}`),
51
+ objectsNamed: (name) => byName.get(name) ?? [],
52
+ messages(n) {
53
+ let p = messageCache.get(n);
54
+ if (!p) {
55
+ p = rm.has({ type: ResourceType.Message, number: n })
56
+ ? rm.load({ type: ResourceType.Message, number: n }).then((r) => parseMessages(r.data))
57
+ : Promise.resolve(undefined);
58
+ messageCache.set(n, p);
59
+ }
60
+ return p;
61
+ },
62
+ pic(n) {
63
+ let p = picCache.get(n);
64
+ if (!p) {
65
+ p = rm.has({ type: ResourceType.Pic, number: n }) ? rm.load({ type: ResourceType.Pic, number: n }).then((r) => renderPic(r.data, basePalette)) : Promise.resolve(undefined);
66
+ picCache.set(n, p);
67
+ }
68
+ return p;
69
+ },
70
+ };
71
+ }
72
+
73
+ // Remap colours (shadows, torch light) only mean something at run time: approximate.
74
+ const REMAP = new Map([[253, 64], [254, 110]]);
75
+
76
+ function renderPic(data: Uint8Array, base: Palette): HTMLCanvasElement {
77
+ const pic = parsePic(data);
78
+ const palette = mergePalette(base, pic.palette);
79
+ const canvas = el("canvas", { width: pic.width, height: pic.height });
80
+ const ctx = canvas.getContext("2d")!;
81
+ for (const cel of [...pic.cels].sort((a, b) => a.priority - b.priority)) {
82
+ if (!cel.width || !cel.height) continue;
83
+ const layer = el("canvas", { width: cel.width, height: cel.height });
84
+ layer.getContext("2d")!.putImageData(new ImageData(celToRgba(cel, palette, { remap: REMAP }), cel.width, cel.height), 0, 0);
85
+ ctx.drawImage(layer, cel.x, cel.y);
86
+ }
87
+ return canvas;
88
+ }
89
+
90
+ /** A small copy of a canvas as a data URL, for map thumbnails. */
91
+ export function thumbnail(canvas: HTMLCanvasElement, width = 96): string {
92
+ const height = Math.round((canvas.height / canvas.width) * width * 1.2);
93
+ const small = el("canvas", { width, height });
94
+ const ctx = small.getContext("2d")!;
95
+ ctx.imageSmoothingQuality = "high";
96
+ ctx.drawImage(canvas, 0, 0, width, height);
97
+ return small.toDataURL();
98
+ }
99
+
100
+ // --- Rooms the editor can open ------------------------------------------------------------
101
+
102
+ let editable: Promise<{ name: string; rooms: number[] }[]> | undefined;
103
+
104
+ /**
105
+ * The room editor's address for room `n`, if a mod describes it in a .room.yaml (preferring
106
+ * the mods loaded here). The dev server's /__editor/mods lists them; elsewhere, none.
107
+ */
108
+ export async function editorUrl(n: number, preferred: string[] = []): Promise<string | undefined> {
109
+ editable ??= fetch("/__editor/mods")
110
+ .then((r) => (r.ok ? r.json() : []))
111
+ .catch(() => []);
112
+ const mods = (await editable).filter((m) => m.rooms.includes(n));
113
+ const mod = mods.find((m) => preferred.includes(m.name)) ?? mods[0];
114
+ return mod && `/editor.html?mod=${encodeURIComponent(mod.name)}&room=${n}`;
115
+ }
116
+
117
+ /** An "Edit room n" link to the editor, filled in once we know the room is editable. */
118
+ export function editorLink(n: number, preferred: string[] = []): HTMLAnchorElement {
119
+ const a = el("a", { className: "edit-link", target: "_blank", hidden: true, title: "Open this room in the room editor" }, `Edit room ${n}`);
120
+ void editorUrl(n, preferred).then((url) => {
121
+ if (url) (a.href = url), (a.hidden = false);
122
+ });
123
+ return a;
124
+ }
@@ -0,0 +1,93 @@
1
+ :root {
2
+ --bg: #111; --panel: #161616; --fg: #ddd; --muted: #888; --accent: #e8b04a; --line: #2a2a2a;
3
+ --code: #7fb3e8; --dir: #6fb07a; --entry: #666;
4
+ color-scheme: dark;
5
+ }
6
+ * { box-sizing: border-box; }
7
+ body {
8
+ margin: 0; height: 100vh; display: grid; grid-template-columns: 1fr minmax(360px, 38%); grid-template-rows: auto 1fr;
9
+ background: var(--bg); color: var(--fg); font: 13px/1.45 ui-monospace, Menlo, monospace;
10
+ }
11
+ header { grid-column: 1 / -1; display: flex; gap: 16px; align-items: center; padding: 8px 14px; border-bottom: 1px solid var(--line); }
12
+ header strong { color: var(--accent); }
13
+ nav { display: flex; gap: 4px; }
14
+ nav a { color: var(--muted); text-decoration: none; padding: 3px 10px; border-radius: 4px; }
15
+ nav a.active { color: var(--fg); background: #232323; }
16
+ #search { flex: 0 1 320px; background: #1b1b1b; color: var(--fg); border: 1px solid var(--line); padding: 4px 8px; border-radius: 4px; }
17
+ #status { color: var(--muted); margin-left: auto; }
18
+ main { position: relative; min-height: 0; overflow: auto; }
19
+ aside { border-left: 1px solid var(--line); background: var(--panel); overflow: auto; padding: 12px 16px; min-height: 0; }
20
+ a { color: var(--accent); cursor: pointer; text-decoration: none; }
21
+ a:hover { text-decoration: underline; }
22
+ h2 { font-size: 15px; color: var(--accent); margin: 0 0 4px; }
23
+ h3 { font-size: 13px; color: var(--fg); margin: 18px 0 6px; border-bottom: 1px solid var(--line); padding-bottom: 3px; }
24
+ .meta { color: var(--muted); margin: 4px 0; }
25
+ .error { color: #f66; }
26
+ canvas { image-rendering: pixelated; display: block; }
27
+ table { border-collapse: collapse; width: 100%; }
28
+ th { text-align: left; color: var(--muted); font-weight: normal; }
29
+ td, th { padding: 2px 6px; border-bottom: 1px solid var(--line); vertical-align: top; }
30
+ td.num { color: var(--muted); white-space: nowrap; }
31
+ tr.hl td { background: #2a2414; }
32
+ .kind { font-size: 11px; padding: 0 5px; border-radius: 3px; border: 1px solid currentColor; white-space: nowrap; }
33
+ .kind.newRoom, .kind.script, .kind.property { color: var(--code); }
34
+ .kind.direction { color: var(--dir); }
35
+ .kind.entry { color: var(--muted); }
36
+ details { margin: 4px 0; }
37
+ summary { cursor: pointer; }
38
+ pre.asm { font-size: 12px; line-height: 1.45; color: #cfcfcf; white-space: pre; overflow: auto; max-height: 70vh; background: #0c0c0c; padding: 8px; }
39
+ .chips { display: flex; flex-wrap: wrap; gap: 4px; }
40
+ .chips a { background: #222; padding: 1px 6px; border-radius: 3px; }
41
+
42
+ /* Map */
43
+ #map { position: absolute; inset: 0; }
44
+ .legend { position: absolute; left: 10px; bottom: 10px; background: #000a; padding: 6px 10px; border-radius: 4px; display: flex; gap: 12px; align-items: center; }
45
+ .legend label { color: var(--muted); }
46
+ .legend i { display: inline-block; width: 18px; height: 0; border-top: 2px solid; vertical-align: middle; margin-right: 4px; }
47
+
48
+ /* Room picture with object overlay */
49
+ .pic { position: relative; width: 100%; aspect-ratio: 320 / 228; background: #000; margin: 8px 0; }
50
+ .pic canvas { width: 100%; height: 100%; }
51
+ .pic svg { position: absolute; inset: 0; width: 100%; height: 100%; }
52
+ .pic rect { fill: #e8b04a18; stroke: #e8b04a; stroke-width: 0.6; }
53
+ .pic circle { fill: #7fb3e8; }
54
+ .pic .hl rect { fill: #e8b04a55; stroke-width: 1.2; }
55
+ .pic .hl circle { fill: #fff; }
56
+ .pic text { font: 6px ui-monospace, monospace; fill: #fff; paint-order: stroke; stroke: #000; stroke-width: 1.5; }
57
+
58
+ /* Class tree */
59
+ .tree { padding: 10px 16px; }
60
+ .tree ul { list-style: none; margin: 0; padding-left: 16px; border-left: 1px solid var(--line); }
61
+ .tree > ul { padding-left: 0; border: 0; }
62
+ .tree li { margin: 1px 0; }
63
+ .tree .count { color: var(--muted); font-size: 11px; }
64
+ .tree a.active { background: #2a2414; }
65
+ .tree input { background: #1b1b1b; color: var(--fg); border: 1px solid var(--line); padding: 4px 8px; margin-bottom: 8px; width: 280px; }
66
+ .prop-own { color: var(--fg); }
67
+ .prop-inherited { color: var(--muted); }
68
+ .prop-changed { color: var(--accent); }
69
+
70
+ #loading { position: absolute; inset: 0; display: grid; place-content: center; color: var(--muted); }
71
+
72
+ /* Live view */
73
+ .live { padding: 10px 14px; display: flex; flex-direction: column; gap: 8px; }
74
+ .live-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
75
+ .live-bar button { background: #1b1b1b; color: var(--fg); border: 1px solid #333; border-radius: 4px; padding: 3px 10px; font: inherit; cursor: pointer; }
76
+ .live-bar button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
77
+ .live-bar button:disabled { opacity: 0.4; cursor: default; }
78
+ .live-bar label { color: var(--muted); }
79
+ .live-bar input[type=number] { background: #1b1b1b; color: var(--fg); border: 1px solid #333; border-radius: 4px; padding: 3px 6px; font: inherit; }
80
+ .live-screen { position: relative; width: min(100%, 960px); aspect-ratio: 4 / 3; background: #000; }
81
+ .live-screen canvas { width: 100%; height: 100%; cursor: none; }
82
+ .live-screen svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
83
+ .live-screen text { font: 5px ui-monospace, monospace; fill: #fff; paint-order: stroke; stroke: #000; stroke-width: 1.2; }
84
+ .live-screen .sprite rect { fill: none; stroke: #7fb3e8; stroke-width: 0.5; }
85
+ .live-screen .feature rect, .live-screen .feature polygon { fill: #e8b04a14; stroke: #e8b04a; stroke-width: 0.5; }
86
+ .live-screen .selected rect, .live-screen .selected polygon { stroke: #fff; stroke-width: 1.2; fill: #ffffff22; }
87
+ .live-screen .path { fill: none; stroke: #fff; stroke-width: 0.6; stroke-dasharray: 2 1.5; }
88
+ .live-screen .path-end { fill: #fff; }
89
+ td.flash { animation: flash 0.8s ease-out; }
90
+ @keyframes flash { from { background: #6b5320; } to { background: transparent; } }
91
+ .edit-link { margin-left: 8px; padding: 2px 8px; border: 1px solid #e8b04a; border-radius: 4px; color: #e8b04a; font: 12px ui-monospace, monospace; text-decoration: none; white-space: nowrap; }
92
+ .edit-link:hover { background: #e8b04a; color: #000; }
93
+ .edit-link[hidden] { display: none; }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,102 @@
1
+ import { ResourceType, ScriptWorld, analyzeGame, emptyPalette, parseHunkPalette } from "../../../../packages/sci/src/index.js";
2
+ import { openGame } from "../files.js";
3
+ import { createClassTree } from "./classes.js";
4
+ import { createExplorer, el } from "./context.js";
5
+ import { renderClass, renderObject, renderRoom, renderScript } from "./inspect.js";
6
+ import { createLiveView } from "./live.js";
7
+ import { createRoomMap } from "./map.js";
8
+ /**
9
+ * The game structure explorer: a static map of rooms, classes and objects built by
10
+ * `analyzeGame` from the scripts, with inspectors for each.
11
+ *
12
+ * Routes: #/<tab>/<kind>/<id...> where tab is map|classes|live and kind is room, script,
13
+ * class, object (static) or live (a running object by address). The tab and the inspected
14
+ * thing are independent.
15
+ */
16
+ const stage = document.getElementById("stage");
17
+ const inspector = document.getElementById("inspector");
18
+ const status = document.getElementById("status");
19
+ const search = document.getElementById("search");
20
+ const loading = el("div", { id: "loading" }, "Loading resources…");
21
+ stage.append(loading);
22
+ // The explorer shows the game with the same mods as the player (?mods=, or the last choice).
23
+ const { rm, mods } = await openGame();
24
+ const world = await ScriptWorld.open(rm);
25
+ const started = performance.now();
26
+ const index = await analyzeGame(rm, { world, onProgress: (done, total) => (loading.textContent = `Analyzing scripts… ${done}/${total}`) });
27
+ const basePalette = parseHunkPalette((await rm.load({ type: ResourceType.Palette, number: 999 })).data) ?? emptyPalette();
28
+ loading.remove();
29
+ let tab = "map";
30
+ const ex = createExplorer(rm, mods, world, index, basePalette, () => tab);
31
+ const pairs = new Set(index.exits.map((e) => [e.from, e.to].sort((a, b) => a - b).join("-")));
32
+ const objectCount = index.scripts.reduce((n, s) => n + s.objects.length, 0);
33
+ status.textContent = `${mods.length ? `mods: ${mods.join(", ")} · ` : ""}${index.rooms.length} rooms · ${pairs.size} connections · ${index.classes.length} classes · ${objectCount} objects · analyzed in ${Math.round(performance.now() - started)} ms`;
34
+ let map;
35
+ let tree;
36
+ let live;
37
+ let shown = {};
38
+ async function route() {
39
+ const [t = "map", kind, a, b] = location.hash.replace(/^#\/?/, "").split("/");
40
+ tab = t === "classes" || t === "live" ? t : "map";
41
+ for (const link of document.querySelectorAll("nav a[data-tab]")) {
42
+ link.classList.toggle("active", link.dataset.tab === tab);
43
+ link.href = `#/${link.dataset.tab}${kind ? `/${kind}/${[a, b].filter(Boolean).join("/")}` : ""}`;
44
+ }
45
+ if (shown.tab !== tab) {
46
+ if (shown.tab === "live")
47
+ live?.deactivate();
48
+ shown.tab = tab;
49
+ shown.key = undefined; // the live tab's default panel differs
50
+ if (tab === "map")
51
+ stage.replaceChildren((map ??= createRoomMap(ex, (room) => (location.hash = ex.href(`room/${room}`)))).element);
52
+ else if (tab === "classes")
53
+ stage.replaceChildren((tree ??= createClassTree(ex)).element);
54
+ else {
55
+ live ??= createLiveView(ex, (address) => (location.hash = ex.href(`live/${address.toString(16)}`)));
56
+ stage.replaceChildren(live.element);
57
+ await live.activate();
58
+ }
59
+ }
60
+ const key = `${kind}/${a}/${b}`;
61
+ map?.select(kind === "room" ? Number(a) : undefined);
62
+ tree?.select(kind === "class" ? Number(a) : undefined);
63
+ if (shown.key === key)
64
+ return;
65
+ shown.key = key;
66
+ const view = kind === "room" ? await renderRoom(ex, Number(a))
67
+ : kind === "script" ? await renderScript(ex, Number(a))
68
+ : kind === "class" ? renderClass(ex, Number(a))
69
+ : kind === "object" ? await renderObject(ex, Number(a), Number(b))
70
+ : kind === "live" && live ? live.objectPanel(parseInt(a ?? "0", 16)).element
71
+ : tab === "live" && live ? live.roomPanel().element
72
+ : el("p", { className: "meta" }, "Select a room or class.");
73
+ if (shown.key !== key)
74
+ return; // a newer route won the race
75
+ inspector.replaceChildren(view);
76
+ inspector.scrollTop = 0;
77
+ }
78
+ // Search: every room, class and object by name, through a datalist.
79
+ const targets = new Map();
80
+ for (const r of index.rooms)
81
+ targets.set(`room ${r.number} ${r.name}`, `room/${r.number}`);
82
+ for (const c of index.classes)
83
+ targets.set(`class ${c.name}`, `class/${c.species}`);
84
+ for (const s of index.scripts)
85
+ for (const o of s.objects)
86
+ targets.set(`${o.name} (${o.className}, script ${s.number})`, `object/${s.number}/${o.offset}`);
87
+ document.getElementById("search-options").append(...[...targets.keys()].map((value) => el("option", { value })));
88
+ search.addEventListener("input", () => {
89
+ const target = targets.get(search.value);
90
+ if (!target)
91
+ return;
92
+ location.hash = ex.href(target);
93
+ search.value = "";
94
+ search.blur();
95
+ });
96
+ /** A failed route shows its error in the inspector instead of failing silently. */
97
+ const navigate = () => route().catch((e) => {
98
+ console.error(e);
99
+ inspector.replaceChildren(el("pre", { className: "error" }, e.stack ?? e.message));
100
+ });
101
+ addEventListener("hashchange", () => void navigate());
102
+ await navigate();
@@ -0,0 +1,104 @@
1
+ import { ResourceType, ScriptWorld, analyzeGame, emptyPalette, parseHunkPalette } from "../../../../packages/sci/src/index.ts";
2
+ import { openGame } from "../files.ts";
3
+ import { createClassTree, type ClassTree } from "./classes.ts";
4
+ import { createExplorer, el } from "./context.ts";
5
+ import { renderClass, renderObject, renderRoom, renderScript } from "./inspect.ts";
6
+ import { createLiveView, type LiveView } from "./live.ts";
7
+ import { createRoomMap, type RoomMap } from "./map.ts";
8
+
9
+ /**
10
+ * The game structure explorer: a static map of rooms, classes and objects built by
11
+ * `analyzeGame` from the scripts, with inspectors for each.
12
+ *
13
+ * Routes: #/<tab>/<kind>/<id...> where tab is map|classes|live and kind is room, script,
14
+ * class, object (static) or live (a running object by address). The tab and the inspected
15
+ * thing are independent.
16
+ */
17
+ const stage = document.getElementById("stage")!;
18
+ const inspector = document.getElementById("inspector")!;
19
+ const status = document.getElementById("status")!;
20
+ const search = document.getElementById("search") as HTMLInputElement;
21
+
22
+ const loading = el("div", { id: "loading" }, "Loading resources…");
23
+ stage.append(loading);
24
+
25
+ // The explorer shows the game with the same mods as the player (?mods=, or the last choice).
26
+ const { rm, mods } = await openGame();
27
+ const world = await ScriptWorld.open(rm);
28
+ const started = performance.now();
29
+ const index = await analyzeGame(rm, { world, onProgress: (done, total) => (loading.textContent = `Analyzing scripts… ${done}/${total}`) });
30
+ const basePalette = parseHunkPalette((await rm.load({ type: ResourceType.Palette, number: 999 })).data) ?? emptyPalette();
31
+ loading.remove();
32
+
33
+ let tab = "map";
34
+ const ex = createExplorer(rm, mods, world, index, basePalette, () => tab);
35
+ const pairs = new Set(index.exits.map((e) => [e.from, e.to].sort((a, b) => a - b).join("-")));
36
+ const objectCount = index.scripts.reduce((n, s) => n + s.objects.length, 0);
37
+ status.textContent = `${mods.length ? `mods: ${mods.join(", ")} · ` : ""}${index.rooms.length} rooms · ${pairs.size} connections · ${index.classes.length} classes · ${objectCount} objects · analyzed in ${Math.round(performance.now() - started)} ms`;
38
+
39
+ let map: RoomMap | undefined;
40
+ let tree: ClassTree | undefined;
41
+ let live: LiveView | undefined;
42
+ let shown: { tab?: string; key?: string } = {};
43
+
44
+ async function route() {
45
+ const [t = "map", kind, a, b] = location.hash.replace(/^#\/?/, "").split("/");
46
+ tab = t === "classes" || t === "live" ? t : "map";
47
+ for (const link of document.querySelectorAll<HTMLAnchorElement>("nav a[data-tab]")) {
48
+ link.classList.toggle("active", link.dataset.tab === tab);
49
+ link.href = `#/${link.dataset.tab}${kind ? `/${kind}/${[a, b].filter(Boolean).join("/")}` : ""}`;
50
+ }
51
+
52
+ if (shown.tab !== tab) {
53
+ if (shown.tab === "live") live?.deactivate();
54
+ shown.tab = tab;
55
+ shown.key = undefined; // the live tab's default panel differs
56
+ if (tab === "map") stage.replaceChildren((map ??= createRoomMap(ex, (room) => (location.hash = ex.href(`room/${room}`)))).element);
57
+ else if (tab === "classes") stage.replaceChildren((tree ??= createClassTree(ex)).element);
58
+ else {
59
+ live ??= createLiveView(ex, (address) => (location.hash = ex.href(`live/${address.toString(16)}`)));
60
+ stage.replaceChildren(live.element);
61
+ await live.activate();
62
+ }
63
+ }
64
+
65
+ const key = `${kind}/${a}/${b}`;
66
+ map?.select(kind === "room" ? Number(a) : undefined);
67
+ tree?.select(kind === "class" ? Number(a) : undefined);
68
+ if (shown.key === key) return;
69
+ shown.key = key;
70
+ const view =
71
+ kind === "room" ? await renderRoom(ex, Number(a))
72
+ : kind === "script" ? await renderScript(ex, Number(a))
73
+ : kind === "class" ? renderClass(ex, Number(a))
74
+ : kind === "object" ? await renderObject(ex, Number(a), Number(b))
75
+ : kind === "live" && live ? live.objectPanel(parseInt(a ?? "0", 16)).element
76
+ : tab === "live" && live ? live.roomPanel().element
77
+ : el("p", { className: "meta" }, "Select a room or class.");
78
+ if (shown.key !== key) return; // a newer route won the race
79
+ inspector.replaceChildren(view);
80
+ inspector.scrollTop = 0;
81
+ }
82
+
83
+ // Search: every room, class and object by name, through a datalist.
84
+ const targets = new Map<string, string>();
85
+ for (const r of index.rooms) targets.set(`room ${r.number} ${r.name}`, `room/${r.number}`);
86
+ for (const c of index.classes) targets.set(`class ${c.name}`, `class/${c.species}`);
87
+ for (const s of index.scripts) for (const o of s.objects) targets.set(`${o.name} (${o.className}, script ${s.number})`, `object/${s.number}/${o.offset}`);
88
+ document.getElementById("search-options")!.append(...[...targets.keys()].map((value) => el("option", { value })));
89
+ search.addEventListener("input", () => {
90
+ const target = targets.get(search.value);
91
+ if (!target) return;
92
+ location.hash = ex.href(target);
93
+ search.value = "";
94
+ search.blur();
95
+ });
96
+
97
+ /** A failed route shows its error in the inspector instead of failing silently. */
98
+ const navigate = () =>
99
+ route().catch((e: Error) => {
100
+ console.error(e);
101
+ inspector.replaceChildren(el("pre", { className: "error" }, e.stack ?? e.message));
102
+ });
103
+ addEventListener("hashchange", () => void navigate());
104
+ await navigate();
@@ -0,0 +1,5 @@
1
+ import { type Explorer } from "./context.ts";
2
+ export declare function renderRoom(ex: Explorer, n: number): Promise<HTMLElement>;
3
+ export declare function renderScript(ex: Explorer, n: number): Promise<HTMLElement>;
4
+ export declare function renderObject(ex: Explorer, script: number, offset: number): Promise<HTMLElement>;
5
+ export declare function renderClass(ex: Explorer, species: number): HTMLElement;
@@ -0,0 +1,208 @@
1
+ import { disassemble, formatDisassembly } from "../../../../packages/sci/src/index.js";
2
+ import { editorLink, el, showValue } from "./context.js";
3
+ const link = (ex, route, text) => el("a", { href: ex.href(route) }, text);
4
+ const roomLink = (ex, n) => {
5
+ const room = ex.index.rooms.find((r) => r.number === n);
6
+ return link(ex, `room/${n}`, room ? `${n} ${room.name}` : `${n}`);
7
+ };
8
+ const scriptLink = (ex, n) => ex.index.rooms.some((r) => r.number === n) ? roomLink(ex, n) : link(ex, `script/${n}`, `script ${n}`);
9
+ const classBySpecies = (ex, species) => ex.index.classes.find((c) => c.species === species);
10
+ const classLink = (ex, species) => {
11
+ const c = classBySpecies(ex, species);
12
+ return c ? link(ex, `class/${c.species}`, c.name) : el("span", {}, `class ${species}`);
13
+ };
14
+ const objectLink = (ex, o) => link(ex, `object/${o.script}/${o.offset}`, o.name);
15
+ const prop = (o, name) => o.props.find((p) => p.name === name)?.value;
16
+ /** Root first. */
17
+ function ancestry(ex, species) {
18
+ const chain = [];
19
+ for (let c = classBySpecies(ex, species); c && chain.length < 64; c = c.superclass < 0 ? undefined : classBySpecies(ex, c.superclass))
20
+ chain.unshift(c);
21
+ return chain;
22
+ }
23
+ const breadcrumb = (ex, species) => {
24
+ const chain = ancestry(ex, species);
25
+ const out = el("p", { className: "meta" });
26
+ chain.forEach((c, i) => out.append(...(i ? [" → "] : []), link(ex, `class/${c.species}`, c.name)));
27
+ return out;
28
+ };
29
+ /** Lazily disassembled listing of a script, optionally only one object's methods. */
30
+ function disassembly(ex, script, title, owner) {
31
+ const pre = el("pre", { className: "asm" }, "…");
32
+ const details = el("details", {}, el("summary", {}, title), pre);
33
+ details.addEventListener("toggle", async () => {
34
+ if (!details.open || pre.dataset.done)
35
+ return;
36
+ pre.dataset.done = "1";
37
+ const s = await ex.world.script(script);
38
+ let fns = disassemble(s, ex.world.context());
39
+ if (owner !== undefined)
40
+ fns = fns.filter((f) => f.owner?.offset === owner);
41
+ pre.textContent = formatDisassembly(s, fns);
42
+ }, { once: false });
43
+ return details;
44
+ }
45
+ /** Messages for one module, optionally only for some nouns; grouped by noun. */
46
+ async function messageTable(ex, module, nouns, objects = []) {
47
+ const file = await ex.messages(module);
48
+ if (!file)
49
+ return undefined;
50
+ const records = file.records.filter((r) => !nouns || nouns.has(r.noun));
51
+ if (!records.length)
52
+ return undefined;
53
+ const namesByNoun = new Map();
54
+ for (const o of objects) {
55
+ const noun = prop(o, "noun");
56
+ if (noun)
57
+ namesByNoun.set(noun, [...(namesByNoun.get(noun) ?? []), o.name]);
58
+ }
59
+ const verb = (v) => (v ? ex.index.verbs[v] ?? `verb ${v}` : "—");
60
+ const table = el("table", {}, el("tr", {}, el("th", {}, "noun"), el("th", {}, "verb"), el("th", {}, "cond"), el("th", {}, "seq"), el("th", {}, "text")));
61
+ for (const r of [...records].sort((a, b) => a.noun - b.noun || a.verb - b.verb || a.cond - b.cond || a.seq - b.seq)) {
62
+ const who = namesByNoun.get(r.noun)?.join(", ");
63
+ table.append(el("tr", {}, el("td", { className: "num", title: who ?? "" }, `${r.noun}${who ? ` ${who}` : ""}`), el("td", { className: "num" }, verb(r.verb)), el("td", { className: "num" }, `${r.cond}`), el("td", { className: "num" }, `${r.seq}`), el("td", {}, r.text || el("span", { className: "meta" }, "(empty)"))));
64
+ }
65
+ return el("details", { open: records.length <= 40 }, el("summary", {}, `Messages (${records.length})`), table);
66
+ }
67
+ /** The room picture with static object positions drawn on top. */
68
+ async function roomPicture(ex, picture, objects) {
69
+ const canvas = await ex.pic(picture);
70
+ if (!canvas)
71
+ return undefined;
72
+ const copy = el("canvas", { width: canvas.width, height: canvas.height });
73
+ copy.getContext("2d").drawImage(canvas, 0, 0);
74
+ const box = el("div", { className: "pic" }, copy);
75
+ box.style.aspectRatio = `${canvas.width} / ${canvas.height * 1.2}`;
76
+ const NS = "http://www.w3.org/2000/svg";
77
+ const svg = document.createElementNS(NS, "svg");
78
+ svg.setAttribute("viewBox", `0 0 ${canvas.width} ${canvas.height}`);
79
+ svg.setAttribute("preserveAspectRatio", "none");
80
+ const marks = new Map();
81
+ for (const o of objects) {
82
+ const [l = 0, t = 0, r = 0, b = 0] = ["nsLeft", "nsTop", "nsRight", "nsBottom"].map((n) => prop(o, n) ?? 0);
83
+ const x = prop(o, "x") ?? 0, y = prop(o, "y") ?? 0;
84
+ const g = document.createElementNS(NS, "g");
85
+ const title = document.createElementNS(NS, "title");
86
+ title.textContent = `${o.name} (${o.className})`;
87
+ g.append(title);
88
+ if (r > l && b > t && r < 0x8000) {
89
+ const rect = document.createElementNS(NS, "rect");
90
+ Object.entries({ x: l, y: t, width: r - l, height: b - t }).forEach(([k, v]) => rect.setAttribute(k, String(v)));
91
+ g.append(rect);
92
+ }
93
+ else if ((x || y) && x < 0x8000 && y < 0x8000) {
94
+ const dot = document.createElementNS(NS, "circle");
95
+ Object.entries({ cx: x, cy: y, r: 2.5 }).forEach(([k, v]) => dot.setAttribute(k, String(v)));
96
+ g.append(dot);
97
+ }
98
+ else
99
+ continue;
100
+ const label = document.createElementNS(NS, "text");
101
+ label.setAttribute("x", String(r > l ? l + 2 : x + 4));
102
+ label.setAttribute("y", String(r > l ? t + 7 : y + 2));
103
+ label.textContent = o.name;
104
+ g.append(label);
105
+ svg.append(g);
106
+ marks.set(o, g);
107
+ }
108
+ box.append(svg);
109
+ return {
110
+ element: box,
111
+ highlight(o) {
112
+ for (const [obj, g] of marks)
113
+ g.classList.toggle("hl", obj === o);
114
+ },
115
+ };
116
+ }
117
+ function exitList(ex, room) {
118
+ const out = ex.index.exits.filter((e) => e.from === room);
119
+ const into = ex.index.exits.filter((e) => e.to === room);
120
+ const rows = (list, other) => {
121
+ const table = el("table");
122
+ for (const e of [...list].sort((a, b) => other(a) - other(b))) {
123
+ table.append(el("tr", {}, el("td", {}, roomLink(ex, other(e))), el("td", {}, el("span", { className: `kind ${e.kind}` }, e.kind)), el("td", { className: "num" }, e.script === room ? e.where : el("span", {}, scriptLink(ex, e.script), ` ${e.where}`))));
124
+ }
125
+ return list.length ? table : el("p", { className: "meta" }, "none found");
126
+ };
127
+ const unresolved = ex.index.unresolved.filter((e) => e.from === room);
128
+ return el("div", {}, el("h3", {}, `Leads to (${new Set(out.map((e) => e.to)).size})`), rows(out, (e) => e.to), el("h3", {}, `Entered from (${new Set(into.map((e) => e.from)).size})`), rows(into, (e) => e.from), unresolved.length ? el("p", { className: "meta" }, `Also ${unresolved.map((e) => `${e.where} = ${e.to}`).join(", ")}: not room numbers (maze codes or cut rooms).`) : null);
129
+ }
130
+ function objectTable(ex, objects, onHover) {
131
+ const table = el("table", {}, el("tr", {}, el("th", {}, "object"), el("th", {}, "class"), el("th", {}, "noun"), el("th", {}, "methods")));
132
+ for (const o of objects) {
133
+ const row = el("tr", {}, el("td", {}, objectLink(ex, o)), el("td", {}, classLink(ex, o.superclass)), el("td", { className: "num" }, `${prop(o, "noun") || ""}`), el("td", { className: "num" }, o.methods.join(" ")));
134
+ if (onHover) {
135
+ row.addEventListener("mouseenter", () => { row.classList.add("hl"); onHover(o); });
136
+ row.addEventListener("mouseleave", () => { row.classList.remove("hl"); onHover(undefined); });
137
+ }
138
+ table.append(row);
139
+ }
140
+ return table;
141
+ }
142
+ export async function renderRoom(ex, n) {
143
+ const room = ex.index.rooms.find((r) => r.number === n);
144
+ const info = ex.index.scripts.find((s) => s.number === n);
145
+ if (!room || !info)
146
+ return el("p", { className: "error" }, `No room ${n}`);
147
+ const helpers = room.uses.filter((u) => !ex.index.rooms.some((r) => r.number === u));
148
+ const helperObjects = ex.index.scripts.filter((s) => helpers.includes(s.number) && s.number >= 100).flatMap((s) => s.objects);
149
+ const pic = room.picture >= 0 ? await roomPicture(ex, room.picture, info.objects) : undefined;
150
+ const roomObject = info.objects.find((o) => o.name === room.name);
151
+ const messages = await messageTable(ex, n, undefined, [...info.objects, ...helperObjects]);
152
+ return el("div", {}, el("h2", {}, `Room ${n} · ${room.name} `, editorLink(n, ex.mods)), breadcrumb(ex, roomObject.superclass), el("p", { className: "meta" }, `picture ${room.picture} · noun ${room.noun} · ${info.objects.length} objects`), room.uses.length ? el("div", { className: "chips" }, el("span", { className: "meta" }, "uses"), ...room.uses.map((u) => scriptLink(ex, u))) : null, pic?.element, exitList(ex, n), el("h3", {}, `Objects (${info.objects.length})`), el("p", { className: "meta" }, "Boxes and dots are positions set in the object's declaration; objects placed by code at run time don't show."), objectTable(ex, info.objects, pic?.highlight), messages ?? el("p", { className: "meta" }, "No messages."), disassembly(ex, n, "Disassembly"));
153
+ }
154
+ export async function renderScript(ex, n) {
155
+ const info = ex.index.scripts.find((s) => s.number === n);
156
+ if (!info)
157
+ return el("p", { className: "error" }, `No script ${n}`);
158
+ const users = ex.index.scripts.filter((s) => s.references.includes(n)).map((s) => s.number);
159
+ const classes = ex.index.classes.filter((c) => c.script === n);
160
+ const messages = await messageTable(ex, n, undefined, info.objects);
161
+ return el("div", {}, el("h2", {}, `Script ${n}`), users.length ? el("div", { className: "chips" }, el("span", { className: "meta" }, "used by"), ...users.map((u) => scriptLink(ex, u))) : null, info.references.length ? el("div", { className: "chips" }, el("span", { className: "meta" }, "uses"), ...info.references.map((u) => scriptLink(ex, u))) : null, classes.length ? el("div", {}, el("h3", {}, `Classes (${classes.length})`), el("div", { className: "chips" }, ...classes.map((c) => classLink(ex, c.species)))) : null, el("h3", {}, `Objects (${info.objects.length})`), objectTable(ex, info.objects), messages, disassembly(ex, n, "Disassembly"));
162
+ }
163
+ /** Props with where each value comes from: the class's default or this object's own. */
164
+ function propTable(props, defaults) {
165
+ const table = el("table", {}, el("tr", {}, el("th", {}, "property"), el("th", {}, "value"), el("th", {}, defaults ? "class default" : "")));
166
+ for (const p of props) {
167
+ const d = defaults?.find((x) => x.name === p.name);
168
+ const changed = !!d && d.value !== p.value;
169
+ const isNew = defaults && !d;
170
+ table.append(el("tr", {}, el("td", { className: changed || isNew ? "prop-changed" : "prop-inherited" }, p.name), el("td", { className: changed || isNew ? "prop-changed" : "" }, showValue(p.value)), el("td", { className: "num" }, d ? showValue(d.value) : isNew ? "new" : "")));
171
+ }
172
+ return table;
173
+ }
174
+ /** Each method name with the nearest ancestor that also defines it (so this overrides it). */
175
+ function methodList(ex, methods, superclass) {
176
+ const chain = superclass < 0 ? [] : ancestry(ex, superclass).reverse();
177
+ const list = el("div", { className: "chips" });
178
+ for (const m of methods) {
179
+ const from = chain.find((c) => c.methods.includes(m));
180
+ list.append(el("span", { title: from ? `overrides ${from.name}::${m}` : "new" }, from ? `${m}` : el("strong", {}, m)));
181
+ }
182
+ return methods.length ? list : el("p", { className: "meta" }, "none");
183
+ }
184
+ export async function renderObject(ex, script, offset) {
185
+ const o = ex.object(script, offset);
186
+ if (!o)
187
+ return el("p", { className: "error" }, "No such object");
188
+ const cls = classBySpecies(ex, o.superclass);
189
+ const noun = prop(o, "noun");
190
+ const siblings = ex.index.scripts.find((s) => s.number === script)?.objects ?? [];
191
+ const messages = noun ? await messageTable(ex, script, new Set([noun]), siblings) : undefined;
192
+ return el("div", {}, el("h2", {}, o.name), el("p", { className: "meta" }, "instance in ", scriptLink(ex, script)), breadcrumb(ex, o.superclass), el("h3", {}, "Methods (bold = new, others override the class)"), methodList(ex, o.methods, o.superclass), messages ?? null, el("h3", {}, "Properties"), propTable(o.props, cls?.props), disassembly(ex, script, "Disassembly of its methods", offset));
193
+ }
194
+ export function renderClass(ex, species) {
195
+ const c = classBySpecies(ex, species);
196
+ if (!c)
197
+ return el("p", { className: "error" }, `No class ${species}`);
198
+ const sup = c.superclass < 0 ? undefined : classBySpecies(ex, c.superclass);
199
+ const subclasses = ex.index.classes.filter((x) => x.superclass === species);
200
+ const instances = ex.index.scripts.flatMap((s) => s.objects.filter((o) => o.superclass === species));
201
+ const inherited = ancestry(ex, species).slice(0, -1).reverse();
202
+ const shown = instances.slice(0, 300);
203
+ return el("div", {}, el("h2", {}, c.name), el("p", { className: "meta" }, `class #${c.species} in `, scriptLink(ex, c.script)), breadcrumb(ex, species), subclasses.length ? el("div", {}, el("h3", {}, `Subclasses (${subclasses.length})`), el("div", { className: "chips" }, ...subclasses.map((s) => classLink(ex, s.species)))) : null, el("h3", {}, "Methods (bold = new, others override an ancestor)"), methodList(ex, c.methods, c.superclass), inherited.length ? el("details", {}, el("summary", { className: "meta" }, "Inherited methods"), ...inherited.map((a) => el("p", {}, link(ex, `class/${a.species}`, a.name), el("span", { className: "meta" }, `: ${a.methods.filter((m) => !c.methods.includes(m)).join(" ")}`)))) : null, el("h3", {}, "Properties"), propTable(c.props, sup?.props), el("h3", {}, `Direct instances (${instances.length})`), instances.length ? el("div", { className: "chips" }, ...shown.map((o) => el("a", { href: ex.href(`object/${o.script}/${o.offset}`), title: `script ${o.script}` }, `${o.name}`))) : el("p", { className: "meta" }, "none"), instances.length > shown.length ? el("p", { className: "meta" }, `…and ${instances.length - shown.length} more`) : null, disassembly(ex, c.script, "Disassembly of its methods", ex.index.scripts.length ? classOffset(ex, c) : undefined));
204
+ }
205
+ /** Classes aren't in `scripts[].objects` (instances only): find the heap offset from the world. */
206
+ function classOffset(ex, c) {
207
+ return ex.world.classObject(c.species)?.offset;
208
+ }