sci2-ts 0.0.0-stage → 0.2.2

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 +78 -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,365 @@
1
+ import {
2
+ collectionElements, drawnItems, findMessage, gameGlobal, isKindOf, namedObject, objectAt,
3
+ ResourceType, heroInPlay, polygonOf, requestRoom, roomPlane, signedProp, type SciObject, type Value, type Vm, type VmSnapshot,
4
+ } from "../../../../packages/sci/src/index.ts";
5
+ import { createOverlay, drawRoomOverlay } from "../overlay.ts";
6
+ import { GameSession } from "../session.ts";
7
+ import { editorUrl, el, showValue, type Explorer } from "./context.ts";
8
+
9
+ /**
10
+ * The live view: the game running in the explorer, with overlays of what the interpreter
11
+ * knows (sprites, clickable features, walk polygons, paths) and inspectors that follow
12
+ * objects' properties as they change.
13
+ */
14
+ export interface LiveView {
15
+ element: HTMLElement;
16
+ /** Starts the game on first use; resumes if the view paused it on leaving. */
17
+ activate(): Promise<void>;
18
+ /** Pauses the game (and its audio) while another tab is shown. */
19
+ deactivate(): void;
20
+ /** Inspector panel for the running room, refreshed while shown. */
21
+ roomPanel(): LivePanel;
22
+ /** Inspector panel for one live object by address. */
23
+ objectPanel(address: number): LivePanel;
24
+ }
25
+
26
+ export interface LivePanel {
27
+ element: HTMLElement;
28
+ refresh(): void;
29
+ }
30
+
31
+ // --- The view -------------------------------------------------------------------------
32
+
33
+ export function createLiveView(ex: Explorer, inspect: (address: number) => void): LiveView {
34
+ const canvas = el("canvas", { width: 320, height: 200 });
35
+ const overlay = createOverlay();
36
+ const screen = el("div", { className: "live-screen" }, canvas);
37
+ screen.append(overlay);
38
+
39
+ const toggles = {
40
+ sprites: toggle("Sprites", true),
41
+ features: toggle("Features", false),
42
+ polygons: toggle("Polygons", true, "Walk polygons: green total access, yellow near point, red barred, blue contained"),
43
+ paths: toggle("Paths", true),
44
+ };
45
+ const inspectMode = toggle("Inspect clicks", false);
46
+ const pauseButton = el("button", { type: "button" }, "Pause");
47
+ const stepButton = el("button", { type: "button", disabled: true }, "Step");
48
+ const soundButton = el("button", { type: "button" }, "Sound: off");
49
+ const snapButton = el("button", { type: "button" }, "Snapshot");
50
+ const restoreButton = el("button", { type: "button", disabled: true }, "Restore");
51
+ const info = el("span", { className: "meta" }, "Starting…");
52
+ // Jump to any room once a hero is in play (the game changes rooms itself: global 13).
53
+ const roomInput = el("input", { type: "number", placeholder: "room", min: "1", max: "999" });
54
+ roomInput.style.width = "5.5em";
55
+ const goButton = el("button", { type: "button", title: "Rooms may expect story state (flags, items); ?debug adds Sierra's debug keys" }, "Teleport");
56
+ goButton.addEventListener("click", () => {
57
+ const room = Number(roomInput.value);
58
+ if (!session || !room) return;
59
+ if (!heroInPlay(session.vm)) info.textContent = "Create a hero (or restore a save) first";
60
+ else if (!ex.rm.has({ type: ResourceType.Script, number: room })) info.textContent = `No room ${room}`;
61
+ else requestRoom(session.vm, room);
62
+ });
63
+ // The room the game is in, in the room editor (when a mod describes it in YAML).
64
+ const editLink = el("a", { className: "edit-link", target: "_blank", hidden: true, title: "Open this room in the room editor" });
65
+ let editRoom: number | undefined;
66
+ const errorBox = el("pre", { className: "error", hidden: true });
67
+ const element = el("div", { className: "live" },
68
+ el("div", { className: "live-bar" }, pauseButton, stepButton, soundButton, snapButton, restoreButton, roomInput, goButton, info, editLink),
69
+ el("div", { className: "live-bar" }, ...Object.values(toggles).map((t) => t.label), inspectMode.label,
70
+ el("span", { className: "meta" }, "Alt+click also inspects")),
71
+ screen,
72
+ errorBox,
73
+ );
74
+
75
+ let session: GameSession | undefined;
76
+ let starting: Promise<void> | undefined;
77
+ let pausedByTab = false;
78
+ /** The object shown in the inspector, outlined on the overlay. */
79
+ let selected: number | undefined;
80
+ let quickSave: VmSnapshot | undefined;
81
+ const log: { frame: number; text: string }[] = [];
82
+ const rooms: { frame: number; room: number }[] = [];
83
+
84
+ function toggle(name: string, on: boolean, title = "") {
85
+ const input = el("input", { type: "checkbox", checked: on });
86
+ return { input, label: el("label", { title }, input, ` ${name}`) };
87
+ }
88
+
89
+ const setPaused = (paused: boolean) => {
90
+ if (!session) return;
91
+ session.paused = paused;
92
+ pauseButton.textContent = paused ? "Resume" : "Pause";
93
+ stepButton.disabled = !paused;
94
+ };
95
+ pauseButton.addEventListener("click", () => setPaused(!session?.paused));
96
+ stepButton.addEventListener("click", () => session?.step(1));
97
+ soundButton.addEventListener("click", () => {
98
+ if (!session) return;
99
+ session.setMuted(!session.muted);
100
+ soundButton.textContent = session.muted ? "Sound: off" : "Sound: on";
101
+ });
102
+ snapButton.addEventListener("click", () => {
103
+ if (!session) return;
104
+ quickSave = session.snapshot();
105
+ restoreButton.disabled = false;
106
+ restoreButton.title = `Snapshot from room ${gameGlobal(session.vm, "curRoomNum")}, frame ${session.frames}`;
107
+ });
108
+ restoreButton.addEventListener("click", () => quickSave && session?.restore(quickSave));
109
+
110
+ /** What's under a screen point: the frontmost sprite, else a feature of the room. */
111
+ function hitTest(x: number, y: number): SciObject | undefined {
112
+ const vm = session!.vm;
113
+ const hit = drawnItems(vm).find((d) => x >= d.x && x < d.x + d.w && y >= d.y && y < d.y + d.h);
114
+ if (hit) return hit.item;
115
+ const plane = roomPlane(vm);
116
+ if (!plane) return undefined;
117
+ const px = x - plane.left, py = y - plane.top;
118
+ return collectionElements(vm, namedObject(vm, "features")).find((f) => {
119
+ const check = objectAt(vm, vm.getProp(f, "onMeCheck"));
120
+ if (check && isKindOf(vm, check, "Polygon")) return pointInPolygon(px, py, polygonOf(vm, check).points);
121
+ const [l, t, r, b] = ["nsLeft", "nsTop", "nsRight", "nsBottom"].map((n) => signedProp(vm, f, n)) as [number, number, number, number];
122
+ return r > l && px >= l && px <= r && py >= t && py <= b;
123
+ });
124
+ }
125
+
126
+ function drawOverlay() {
127
+ if (!session) return;
128
+ drawRoomOverlay(overlay, session.vm, {
129
+ sprites: toggles.sprites.input.checked,
130
+ features: toggles.features.input.checked,
131
+ polygons: toggles.polygons.input.checked,
132
+ paths: toggles.paths.input.checked,
133
+ selected,
134
+ });
135
+ }
136
+
137
+ let lastInfo = 0;
138
+ async function start() {
139
+ session = await GameSession.create({
140
+ canvas,
141
+ rm: ex.rm,
142
+ mods: ex.mods,
143
+ muted: true,
144
+ runHidden: new URLSearchParams(location.search).has("background"),
145
+ debug: new URLSearchParams(location.search).has("debug"),
146
+ interceptClick: (x, y, e) => {
147
+ if (!inspectMode.input.checked && !e.altKey) return false;
148
+ const hit = hitTest(x, y);
149
+ if (hit) inspect(hit.address);
150
+ return true;
151
+ },
152
+ });
153
+ const vm = session.vm;
154
+ // Log what the game says: messages fetched by a Messager (`nextMsg`), not every UI label.
155
+ let saying = false;
156
+ vm.onSend = (_object, selector) => {
157
+ if (selector === "nextMsg") saying = true;
158
+ };
159
+ vm.onKernelCall = (name, args) => {
160
+ if (name === "Message" && args[0] === 0 && args.length === 7 && saying) {
161
+ saying = false;
162
+ const [, module = 0, noun = 0, verb = 0, cond = 0, seq = 0] = args;
163
+ const frame = session!.frames;
164
+ void ex.messages(module).then((file) => {
165
+ const text = (file && findMessage(file, { noun, verb, cond, seq })?.text) || `${module}/${noun}/${verb}/${cond}/${seq}`;
166
+ log.push({ frame, text });
167
+ if (log.length > 30) log.shift();
168
+ });
169
+ }
170
+ };
171
+ session.onDraw.push(() => {
172
+ if (session!.error) {
173
+ errorBox.hidden = false;
174
+ errorBox.textContent = `${session!.error.message}\n${session!.error.backtrace.map((l) => ` at ${l}`).join("\n")}`;
175
+ }
176
+ const room = gameGlobal(vm, "curRoomNum") ?? 0;
177
+ if (room && rooms.at(-1)?.room !== room) rooms.push({ frame: session!.frames, room });
178
+ drawOverlay();
179
+ const now = performance.now();
180
+ if (now - lastInfo > 500) {
181
+ lastInfo = now;
182
+ info.textContent = `room ${room} · frame ${session!.frames} · ${session!.paused ? "paused" : `${session!.fps} fps`}`;
183
+ if (room !== editRoom) {
184
+ editRoom = room;
185
+ editLink.hidden = true;
186
+ if (room) {
187
+ void editorUrl(room, ex.mods).then((url) => {
188
+ if (!url || editRoom !== room) return;
189
+ editLink.href = url;
190
+ editLink.textContent = `Edit room ${room}`;
191
+ editLink.hidden = false;
192
+ });
193
+ }
194
+ }
195
+ if (activePanel?.element.isConnected) activePanel.refresh();
196
+ }
197
+ });
198
+ // Automated testing (?background) can reach the session from the console.
199
+ if (new URLSearchParams(location.search).has("background")) (globalThis as Record<string, unknown>).liveSession = session;
200
+ session.start();
201
+ }
202
+
203
+ let activePanel: LivePanel | undefined;
204
+ const track = (panel: LivePanel) => {
205
+ activePanel = panel;
206
+ panel.refresh();
207
+ return panel;
208
+ };
209
+
210
+ return {
211
+ element,
212
+ async activate() {
213
+ await (starting ??= start());
214
+ if (pausedByTab) setPaused(false);
215
+ pausedByTab = false;
216
+ },
217
+ deactivate() {
218
+ if (session && !session.paused) {
219
+ setPaused(true);
220
+ pausedByTab = true;
221
+ }
222
+ },
223
+ roomPanel: () => {
224
+ selected = undefined;
225
+ return track(roomPanel(ex, () => session, log, rooms));
226
+ },
227
+ objectPanel: (address) => {
228
+ selected = address;
229
+ return track(objectPanel(ex, () => session, address));
230
+ },
231
+ };
232
+ }
233
+
234
+ function pointInPolygon(x: number, y: number, points: [number, number][]): boolean {
235
+ let inside = false;
236
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
237
+ const [xi, yi] = points[i]!, [xj, yj] = points[j]!;
238
+ if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;
239
+ }
240
+ return inside;
241
+ }
242
+
243
+ // --- Inspector panels ---------------------------------------------------------------
244
+
245
+ const liveHref = (ex: Explorer, o: SciObject) => ex.href(`live/${o.address.toString(16)}`);
246
+ const liveLink = (ex: Explorer, o: SciObject | undefined) => (o ? el("a", { href: liveHref(ex, o) }, o.name) : el("span", { className: "meta" }, "—"));
247
+
248
+ /** Where an object is defined, for a link to the static inspector. */
249
+ function staticRoute(o: SciObject): string {
250
+ if (o.isClass) return `class/${o.species}`;
251
+ const def = o.isClone && o.template ? o.template : o;
252
+ return def.isClass ? `class/${def.species}` : `object/${def.script.number}/${def.address & 0xffff}`;
253
+ }
254
+
255
+ function roomPanel(ex: Explorer, getSession: () => GameSession | undefined, log: { frame: number; text: string }[], rooms: { frame: number; room: number }[]): LivePanel {
256
+ const element = el("div", {}, el("p", { className: "meta" }, "Starting the game…"));
257
+ return {
258
+ element,
259
+ refresh() {
260
+ const session = getSession();
261
+ if (!session) return;
262
+ const vm = session.vm;
263
+ const room = objectAt(vm, gameGlobal(vm, "curRoom"));
264
+ const roomNum = gameGlobal(vm, "curRoomNum") ?? 0;
265
+ const ego = objectAt(vm, gameGlobal(vm, "ego"));
266
+ const scriptState = (o: SciObject) => {
267
+ const s = objectAt(vm, vm.getProp(o, "script"));
268
+ return s ? el("span", {}, liveLink(ex, s), el("span", { className: "meta" }, ` state ${signedProp(vm, s, "state")}`)) : "";
269
+ };
270
+ const table = (list: SciObject[], extra: (o: SciObject) => (Node | string)[]) => {
271
+ const t = el("table");
272
+ for (const o of list) t.append(el("tr", {}, el("td", {}, liveLink(ex, o)), ...extra(o).map((c) => el("td", { className: "num" }, c))));
273
+ return list.length ? t : el("p", { className: "meta" }, "none");
274
+ };
275
+ const cast = collectionElements(vm, namedObject(vm, "cast"));
276
+ const features = collectionElements(vm, namedObject(vm, "features"));
277
+ const regions = collectionElements(vm, namedObject(vm, "regions"));
278
+ element.replaceChildren(
279
+ el("h2", {}, `Live: room ${roomNum}`),
280
+ el("p", { className: "meta" }, room ? liveLink(ex, room) : "", " · ", el("a", { href: ex.href(`room/${roomNum}`) }, "static view"),
281
+ room ? el("span", {}, " · script ", scriptState(room)) : ""),
282
+ ego ? el("p", {}, "ego ", liveLink(ex, ego), el("span", { className: "meta" }, ` at ${signedProp(vm, ego, "x")},${signedProp(vm, ego, "y")} · view ${signedProp(vm, ego, "view")} loop ${signedProp(vm, ego, "loop")} cel ${signedProp(vm, ego, "cel")} `), scriptState(ego)) : "",
283
+ el("h3", {}, `Cast (${cast.length})`),
284
+ table(cast, (o) => [`${signedProp(vm, o, "x")},${signedProp(vm, o, "y")}`, `view ${signedProp(vm, o, "view")}`, scriptState(o)]),
285
+ el("h3", {}, `Features (${features.length})`),
286
+ table(features, (o) => [`noun ${signedProp(vm, o, "noun")}`]),
287
+ el("h3", {}, `Regions (${regions.length})`),
288
+ table(regions, (o) => [scriptState(o)]),
289
+ el("h3", {}, "Messages"),
290
+ log.length ? el("div", {}, ...[...log].reverse().map((m) => el("p", {}, el("span", { className: "meta" }, `${m.frame} `), m.text))) : el("p", { className: "meta" }, "none yet"),
291
+ el("h3", {}, "Rooms visited"),
292
+ el("div", { className: "chips" }, ...rooms.map((r) => el("a", { href: ex.href(`room/${r.room}`), title: `frame ${r.frame}` }, `${r.room}`))),
293
+ );
294
+ },
295
+ };
296
+ }
297
+
298
+ /** Objects' header slots come first; the name pointer is the last of them. */
299
+ const NAME_SLOT = 8;
300
+
301
+ function objectPanel(ex: Explorer, getSession: () => GameSession | undefined, address: number): LivePanel {
302
+ const element = el("div", {}, el("p", { className: "meta" }, "Starting the game…"));
303
+ const cells = new Map<number, HTMLTableCellElement>();
304
+ const values = new Map<number, number>();
305
+ let built: SciObject | undefined;
306
+
307
+ const render = (vm: Vm, v: Value): Node | string => {
308
+ const o = v && v < 0x10000 ? undefined : objectAt(vm, v);
309
+ if (o) return liveLink(ex, o);
310
+ return showValue(v & 0xffff);
311
+ };
312
+
313
+ return {
314
+ element,
315
+ refresh() {
316
+ const session = getSession();
317
+ if (!session) return;
318
+ const vm = session.vm;
319
+ const o = objectAt(vm, address);
320
+ if (!o) {
321
+ element.replaceChildren(el("h2", {}, "Gone"), el("p", { className: "meta" }, `Object ${address.toString(16)} no longer exists (disposed, or the room changed).`));
322
+ built = undefined;
323
+ return;
324
+ }
325
+ if (built !== o) {
326
+ built = o;
327
+ cells.clear();
328
+ values.clear();
329
+ // By slot: some properties have no selector name in the vocabulary.
330
+ const props = [...o.propIndex].sort((a, b) => a[1] - b[1]).map(([sel, slot]) => ({ name: vm.selectors[sel] ?? `sel_${sel}`, slot }));
331
+ const table = el("table");
332
+ for (const { name, slot } of props) {
333
+ // Header slots (-objID-, -size-, dictionaries...) aren't interesting; `name` is shown as text.
334
+ if (slot < NAME_SLOT || name.startsWith("-")) continue;
335
+ const cell = el("td");
336
+ cells.set(slot, cell);
337
+ table.append(el("tr", {}, el("td", { className: "num" }, name), cell));
338
+ }
339
+ const cls = vm.classObject(o.isClass ? o.species : o.superclass);
340
+ element.replaceChildren(
341
+ el("h2", {}, o.name),
342
+ el("p", { className: "meta" },
343
+ o.isClone ? "clone" : o.isClass ? "class" : "instance",
344
+ " of ", cls ? el("a", { href: ex.href(`class/${cls.species}`) }, cls.name) : "?",
345
+ " · ", el("a", { href: ex.href(staticRoute(o)) }, "definition"),
346
+ ` · address ${o.address.toString(16)}`),
347
+ el("p", { className: "meta" }, "Values update live; changes flash."),
348
+ table,
349
+ );
350
+ }
351
+ for (const [slot, cell] of cells) {
352
+ const v = o.vars[slot] ?? 0;
353
+ if (values.get(slot) === v) continue;
354
+ const first = !values.has(slot);
355
+ values.set(slot, v);
356
+ cell.replaceChildren(slot === NAME_SLOT ? o.name : render(vm, v));
357
+ if (!first) {
358
+ cell.classList.remove("flash");
359
+ void cell.offsetWidth; // restart the animation
360
+ cell.classList.add("flash");
361
+ }
362
+ }
363
+ },
364
+ };
365
+ }
@@ -0,0 +1,11 @@
1
+ import { type Explorer } from "./context.ts";
2
+ export interface RoomMap {
3
+ element: HTMLElement;
4
+ select(room: number | undefined): void;
5
+ }
6
+ /**
7
+ * The room graph: one node per room (thumbnail of its pic), grouped by hundreds, which is
8
+ * how Sierra numbered areas. One edge per connected pair; arrows show direction, and a
9
+ * dashed edge means the only evidence is the target checking where the hero came from.
10
+ */
11
+ export declare function createRoomMap(ex: Explorer, onSelect: (room: number) => void): RoomMap;
@@ -0,0 +1,182 @@
1
+ import cytoscape from "cytoscape";
2
+ import { el, thumbnail } from "./context.js";
3
+ /** Rooms linked to this many others are hubs (combat): their links are hidden by default. */
4
+ const HUB_DEGREE = 20;
5
+ /**
6
+ * The room graph: one node per room (thumbnail of its pic), grouped by hundreds, which is
7
+ * how Sierra numbered areas. One edge per connected pair; arrows show direction, and a
8
+ * dashed edge means the only evidence is the target checking where the hero came from.
9
+ */
10
+ export function createRoomMap(ex, onSelect) {
11
+ const { rooms, exits } = ex.index;
12
+ const container = el("div", { id: "map" });
13
+ const pairs = new Map();
14
+ for (const e of exits) {
15
+ const [a, b] = e.from < e.to ? [e.from, e.to] : [e.to, e.from];
16
+ const key = `${a}-${b}`;
17
+ const p = pairs.get(key) ?? { a, b, ab: new Set(), ba: new Set() };
18
+ (e.from === a ? p.ab : p.ba).add(e.kind);
19
+ pairs.set(key, p);
20
+ }
21
+ const degree = new Map();
22
+ for (const p of pairs.values())
23
+ for (const r of [p.a, p.b])
24
+ degree.set(r, (degree.get(r) ?? 0) + 1);
25
+ const hubs = new Set([...degree].filter(([, d]) => d >= HUB_DEGREE).map(([r]) => r));
26
+ const areas = new Set(rooms.map((r) => Math.floor(r.number / 100)));
27
+ const elements = [
28
+ // Area boxes are plain nodes drawn behind the rooms; they get sized after layout.
29
+ ...[...areas].map((a) => ({ data: { id: `area${a}`, label: `${a}00s`, w: 1, h: 1 }, classes: "area", selectable: false, grabbable: false })),
30
+ ...rooms.map((r) => ({
31
+ data: { id: `r${r.number}`, label: `${r.number} ${r.name.replace(/^rm\d+$/, "")}`.trim(), area: Math.floor(r.number / 100), room: r.number },
32
+ classes: hubs.has(r.number) ? "hub" : "",
33
+ })),
34
+ ...[...pairs.values()].map((p) => {
35
+ const kinds = new Set([...p.ab, ...p.ba]);
36
+ const entryOnly = [...kinds].every((k) => k === "entry");
37
+ const direction = kinds.has("direction") && ![...kinds].some((k) => k !== "direction" && k !== "entry");
38
+ // Edge runs a → b; arrows at whichever ends are entered.
39
+ const classes = [entryOnly ? "entry" : direction ? "direction" : "code", p.ab.size ? "to-b" : "", p.ba.size ? "to-a" : ""];
40
+ if (hubs.has(p.a) || hubs.has(p.b))
41
+ classes.push("hub-link");
42
+ return { data: { id: `e${p.a}-${p.b}`, source: `r${p.a}`, target: `r${p.b}`, kinds: [...kinds].join(", ") }, classes: classes.join(" ") };
43
+ }),
44
+ ];
45
+ const cy = cytoscape({
46
+ container,
47
+ elements,
48
+ minZoom: 0.15,
49
+ maxZoom: 3,
50
+ style: [
51
+ { selector: "node", style: {
52
+ "z-index-compare": "manual", "z-index": 2,
53
+ shape: "round-rectangle", width: 64, height: 46, "background-color": "#262626",
54
+ "border-width": 1, "border-color": "#3a3a3a",
55
+ label: "data(label)", color: "#ccc", "font-size": 10, "font-family": "ui-monospace, Menlo, monospace",
56
+ "text-valign": "bottom", "text-margin-y": 3, "text-background-color": "#111", "text-background-opacity": 0.7, "text-background-padding": "1px",
57
+ } },
58
+ { selector: "node[thumb]", style: { "background-image": "data(thumb)", "background-fit": "cover" } },
59
+ { selector: ".area", style: {
60
+ shape: "round-rectangle", width: "data(w)", height: "data(h)", "z-index": 0, events: "no",
61
+ "background-color": "#181818", "border-color": "#2e2e2e", "border-width": 1,
62
+ label: "data(label)", "text-valign": "top", "text-halign": "center", "text-margin-y": -4, color: "#777", "font-size": 16,
63
+ "text-background-opacity": 0,
64
+ } },
65
+ { selector: "node.hub", style: { "border-color": "#a05050", "border-width": 2 } },
66
+ { selector: "node:selected", style: { "border-color": "#e8b04a", "border-width": 3 } },
67
+ { selector: "edge", style: {
68
+ "z-index-compare": "manual", "z-index": 1,
69
+ width: 1.6, "curve-style": "bezier", "line-color": "#7fb3e8", opacity: 0.8,
70
+ "target-arrow-color": "#7fb3e8", "source-arrow-color": "#7fb3e8", "arrow-scale": 0.8,
71
+ } },
72
+ { selector: "edge.to-b", style: { "target-arrow-shape": "triangle" } },
73
+ { selector: "edge.to-a", style: { "source-arrow-shape": "triangle" } },
74
+ { selector: "edge.direction", style: { "line-color": "#6fb07a", "target-arrow-color": "#6fb07a", "source-arrow-color": "#6fb07a" } },
75
+ { selector: "edge.entry", style: { "line-style": "dashed", "line-color": "#666", "target-arrow-color": "#666", "source-arrow-color": "#666" } },
76
+ { selector: "edge.hub-link", style: { display: "none" } },
77
+ { selector: ".faded", style: { opacity: 0.18 } },
78
+ { selector: "edge.lit", style: { width: 3, opacity: 1, "z-index": 10 } },
79
+ ],
80
+ layout: { name: "preset" },
81
+ });
82
+ // Lay out once the map is in the page: cytoscape measures its container.
83
+ let laidOut = false;
84
+ const layout = () => {
85
+ if (laidOut || !container.isConnected)
86
+ return;
87
+ laidOut = true;
88
+ cy.resize();
89
+ layoutAreas(cy);
90
+ cy.fit(undefined, 20);
91
+ if (pending !== undefined)
92
+ select(pending);
93
+ };
94
+ let pending;
95
+ // Cytoscape caches its container's size; keep it current as the window changes.
96
+ new ResizeObserver(() => cy.resize()).observe(container);
97
+ cy.on("tap", "node", (evt) => {
98
+ const room = evt.target.data("room");
99
+ if (room !== undefined)
100
+ onSelect(room);
101
+ });
102
+ // Thumbnails load in the background, a few at a time.
103
+ void (async () => {
104
+ for (const r of rooms) {
105
+ if (r.picture < 0)
106
+ continue;
107
+ const canvas = await ex.pic(r.picture).catch(() => undefined);
108
+ if (canvas)
109
+ cy.getElementById(`r${r.number}`).data("thumb", thumbnail(canvas));
110
+ await new Promise((resolve) => setTimeout(resolve, 0));
111
+ }
112
+ })();
113
+ const swatch = (css) => {
114
+ const i = el("i");
115
+ i.style.cssText = css;
116
+ return i;
117
+ };
118
+ const hubToggle = el("input", { type: "checkbox" });
119
+ hubToggle.addEventListener("change", () => cy.edges(".hub-link").style("display", hubToggle.checked ? "element" : "none"));
120
+ const legend = el("div", { className: "legend" }, el("span", {}, swatch("border-color: #7fb3e8"), "script exit"), el("span", {}, swatch("border-color: #6fb07a"), "north/south/east/west"), el("span", {}, swatch("border-color: #666; border-top-style: dashed"), "entry check only"), el("label", {}, hubToggle, ` combat links (${[...hubs].join(", ")})`));
121
+ const element = el("div", {}, container, legend);
122
+ element.style.cssText = "position: absolute; inset: 0;";
123
+ function select(room) {
124
+ pending = room;
125
+ if (!laidOut)
126
+ return;
127
+ cy.elements().removeClass("faded lit").unselect();
128
+ if (room === undefined)
129
+ return;
130
+ const node = cy.getElementById(`r${room}`);
131
+ if (node.empty())
132
+ return;
133
+ node.select();
134
+ const hood = node.closedNeighborhood();
135
+ cy.elements().not(hood).not(".area").addClass("faded");
136
+ node.connectedEdges().addClass("lit");
137
+ if (!node.visible())
138
+ return;
139
+ const bb = cy.extent();
140
+ const p = node.position();
141
+ if (p.x < bb.x1 || p.x > bb.x2 || p.y < bb.y1 || p.y > bb.y2)
142
+ cy.animate({ center: { eles: node } }, { duration: 300 });
143
+ }
144
+ return {
145
+ element,
146
+ select(room) {
147
+ select(room);
148
+ layout();
149
+ },
150
+ };
151
+ }
152
+ /**
153
+ * Each area (a hundreds series of room numbers) gets its own force layout, over its rooms
154
+ * and the links inside it, in a box sized for its room count; boxes are packed into rows.
155
+ * One global force layout of compound nodes blows apart, and cross-area links (and combat)
156
+ * would pull everything into a knot anyway.
157
+ */
158
+ function layoutAreas(cy) {
159
+ const GAP = 110;
160
+ const MAX_ROW = 2800;
161
+ let x = 0, y = 0, rowHeight = 0;
162
+ for (const area of cy.nodes(".area").sort((a, b) => Number(a.id().slice(4)) - Number(b.id().slice(4)))) {
163
+ const rooms = cy.nodes(`[area = ${area.id().slice(4)}]`);
164
+ const inner = rooms.connectedEdges().filter((e) => rooms.contains(e.source()) && rooms.contains(e.target()) && !e.hasClass("hub-link"));
165
+ rooms.union(inner).layout({
166
+ name: "cose", animate: false, randomize: true, fit: false, nodeDimensionsIncludeLabels: true,
167
+ idealEdgeLength: () => 60, nodeRepulsion: () => 6000, gravity: 1.2, numIter: 1500, componentSpacing: 40,
168
+ }).run();
169
+ // Move the area, at its natural size, to the next slot in the current row.
170
+ const bb = rooms.boundingBox({ includeLabels: true });
171
+ if (x > 0 && x + bb.w > MAX_ROW) {
172
+ x = 0;
173
+ y += rowHeight + GAP;
174
+ rowHeight = 0;
175
+ }
176
+ const dx = x - bb.x1, dy = y - bb.y1;
177
+ rooms.positions((n) => ({ x: n.position().x + dx, y: n.position().y + dy }));
178
+ area.data({ w: bb.w + 30, h: bb.h + 30 }).position({ x: x + bb.w / 2, y: y + bb.h / 2 });
179
+ x += bb.w + GAP;
180
+ rowHeight = Math.max(rowHeight, bb.h);
181
+ }
182
+ }