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,55 @@
1
+ /**
2
+ * AudioWorklet running the OPL2 emulator. The main thread's AdLib driver posts register
3
+ * writes as flat [reg, value, reg, value, ...] arrays; this renders the chip at its native
4
+ * 49,716 Hz and linearly resamples to the AudioContext's rate.
5
+ */
6
+ import { OPL_RATE, Opl2 } from "../../../packages/sci/src/audio/opl2.ts";
7
+
8
+ declare const sampleRate: number;
9
+ declare function registerProcessor(name: string, ctor: new () => AudioWorkletProcessorBase): void;
10
+ declare class AudioWorkletProcessorBase {
11
+ readonly port: MessagePort;
12
+ process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean;
13
+ }
14
+ const Base = (globalThis as unknown as { AudioWorkletProcessor: typeof AudioWorkletProcessorBase }).AudioWorkletProcessor;
15
+
16
+ class OplProcessor extends Base {
17
+ private chip = new Opl2();
18
+ private readonly one = new Float32Array(1);
19
+ private prev = 0;
20
+ private next = 0;
21
+ private frac = 0;
22
+
23
+ constructor() {
24
+ super();
25
+ this.port.onmessage = (e: MessageEvent<number[] | "reset">) => {
26
+ if (e.data === "reset") {
27
+ this.chip = new Opl2();
28
+ return;
29
+ }
30
+ const w = e.data;
31
+ for (let i = 0; i + 1 < w.length; i += 2) this.chip.write(w[i]!, w[i + 1]!);
32
+ };
33
+ }
34
+
35
+ override process(_inputs: Float32Array[][], outputs: Float32Array[][]): boolean {
36
+ const out = outputs[0];
37
+ if (!out?.[0]) return true;
38
+ const step = OPL_RATE / sampleRate;
39
+ const left = out[0];
40
+ for (let i = 0; i < left.length; i++) {
41
+ this.frac += step;
42
+ while (this.frac >= 1) {
43
+ this.prev = this.next;
44
+ this.chip.generate(this.one, 0, 1);
45
+ this.next = this.one[0]!;
46
+ this.frac -= 1;
47
+ }
48
+ left[i] = this.prev + (this.next - this.prev) * this.frac;
49
+ }
50
+ for (let c = 1; c < out.length; c++) out[c]!.set(left);
51
+ return true;
52
+ }
53
+ }
54
+
55
+ registerProcessor("opl2", OplProcessor);
@@ -0,0 +1,31 @@
1
+ import { type Vm } from "../../../packages/sci/src/index.ts";
2
+ /**
3
+ * What the interpreter knows about the room, drawn over the game screen as SVG in game
4
+ * coordinates (320x200): clickable features, sprites, walk polygons and where actors are
5
+ * going. Used by the explorer's live view and the room editor.
6
+ */
7
+ export interface OverlayOptions {
8
+ /** Drawn sprites (cast members) with their names. */
9
+ sprites?: boolean;
10
+ /** The room's features (clickable areas) with their names, and approach points. */
11
+ features?: boolean;
12
+ /** Walk polygons: green total access, yellow near point, red barred, blue contained. */
13
+ polygons?: boolean;
14
+ /** Where moving actors are headed (PolyPath routes). */
15
+ paths?: boolean;
16
+ /** An object to outline in white. */
17
+ selected?: number;
18
+ /** Tells exits from other features (the editor knows from the room's YAML). */
19
+ isExit?: (name: string) => boolean;
20
+ }
21
+ export declare const OVERLAY_COLORS: {
22
+ polygons: string[];
23
+ feature: string;
24
+ exit: string;
25
+ sprite: string;
26
+ hero: string;
27
+ selected: string;
28
+ };
29
+ /** An SVG to lay over the game canvas (position it over the canvas, same size). */
30
+ export declare function createOverlay(): SVGSVGElement;
31
+ export declare function drawRoomOverlay(root: SVGSVGElement, vm: Vm, o: OverlayOptions): void;
@@ -0,0 +1,103 @@
1
+ import { collectionElements, drawnItems, gameGlobal, isKindOf, namedObject, objectAt, polygonOf, readInts, roomPlane, signedProp, } from "../../../packages/sci/src/index.js";
2
+ const SVG = "http://www.w3.org/2000/svg";
3
+ const svg = (tag, attrs = {}, text) => {
4
+ const node = document.createElementNS(SVG, tag);
5
+ for (const [k, v] of Object.entries(attrs))
6
+ node.setAttribute(k, String(v));
7
+ if (text)
8
+ node.textContent = text;
9
+ return node;
10
+ };
11
+ export const OVERLAY_COLORS = {
12
+ polygons: ["#6fb07a", "#d8c050", "#e05555", "#5fa0e8"],
13
+ feature: "#e8b04a",
14
+ exit: "#5fd08c",
15
+ sprite: "#7fb3e8",
16
+ hero: "#f08fd0",
17
+ selected: "#ffffff",
18
+ };
19
+ /** An SVG to lay over the game canvas (position it over the canvas, same size). */
20
+ export function createOverlay() {
21
+ const root = svg("svg", { viewBox: "0 0 320 200", preserveAspectRatio: "none" });
22
+ root.style.pointerEvents = "none";
23
+ return root;
24
+ }
25
+ const label = (x, y, text, color) => svg("text", {
26
+ x, y, fill: color, stroke: "#000", "stroke-width": 1.2, "paint-order": "stroke", "font-size": 5, "font-family": "ui-monospace, Menlo, monospace",
27
+ }, text);
28
+ export function drawRoomOverlay(root, vm, o) {
29
+ root.replaceChildren();
30
+ const plane = roomPlane(vm);
31
+ const room = objectAt(vm, gameGlobal(vm, "curRoom"));
32
+ // Room objects use the room plane's coordinates (below the status bar).
33
+ const local = svg("g", { transform: plane ? `translate(${plane.left} ${plane.top})` : "" });
34
+ root.append(local);
35
+ const colorOf = (address, normal) => (address === o.selected ? OVERLAY_COLORS.selected : normal);
36
+ if (o.polygons && room) {
37
+ for (const poly of collectionElements(vm, vm.getProp(room, "obstacles"))) {
38
+ const { type, points } = polygonOf(vm, poly);
39
+ if (points.length < 3)
40
+ continue;
41
+ const color = OVERLAY_COLORS.polygons[type] ?? "#aaa";
42
+ local.append(svg("polygon", { points: points.map((p) => p.join(",")).join(" "), fill: `${color}22`, stroke: color, "stroke-width": 0.6 }));
43
+ }
44
+ }
45
+ if (o.features) {
46
+ for (const f of collectionElements(vm, namedObject(vm, "features"))) {
47
+ const color = colorOf(f.address, o.isExit?.(f.name) ? OVERLAY_COLORS.exit : OVERLAY_COLORS.feature);
48
+ const style = { fill: `${color}18`, stroke: color, "stroke-width": f.address === o.selected ? 1.2 : 0.5 };
49
+ const check = objectAt(vm, vm.getProp(f, "onMeCheck"));
50
+ let at;
51
+ if (check && isKindOf(vm, check, "Polygon")) {
52
+ const { points } = polygonOf(vm, check);
53
+ if (points.length < 3)
54
+ continue;
55
+ local.append(svg("polygon", { points: points.map((p) => p.join(",")).join(" "), ...style }));
56
+ at = [points[0][0] + 2, points[0][1] + 6];
57
+ }
58
+ else {
59
+ const [l, t, r, b] = ["nsLeft", "nsTop", "nsRight", "nsBottom"].map((n) => signedProp(vm, f, n));
60
+ if (r <= l || b <= t)
61
+ continue;
62
+ local.append(svg("rect", { x: l, y: t, width: r - l, height: b - t, ...style }));
63
+ at = [l + 2, t + 6];
64
+ }
65
+ local.append(label(at[0], at[1], f.name, color));
66
+ // Where the hero walks to before using it.
67
+ const ax = signedProp(vm, f, "approachX"), ay = signedProp(vm, f, "approachY");
68
+ if (ax || ay) {
69
+ local.append(svg("path", { d: `M${ax - 2} ${ay - 2}L${ax + 2} ${ay + 2}M${ax - 2} ${ay + 2}L${ax + 2} ${ay - 2}`, stroke: color, "stroke-width": 0.6 }));
70
+ }
71
+ }
72
+ }
73
+ if (o.paths) {
74
+ for (const actor of collectionElements(vm, namedObject(vm, "cast"))) {
75
+ const mover = objectAt(vm, vm.getProp(actor, "mover"));
76
+ if (!mover)
77
+ continue;
78
+ const pts = [[signedProp(vm, actor, "x"), signedProp(vm, actor, "y")]];
79
+ // PolyPath keeps the whole route: an array ending in 0x7777, `value` = next index.
80
+ const route = vm.getProp(mover, "points");
81
+ if (route) {
82
+ const flat = readInts(vm, route, 200);
83
+ for (let i = signedProp(vm, mover, "value"); i + 1 < flat.length && flat[i] !== 0x7777; i += 2)
84
+ pts.push([flat[i], flat[i + 1]]);
85
+ }
86
+ const target = [signedProp(vm, mover, "x"), signedProp(vm, mover, "y")];
87
+ if (pts.length === 1)
88
+ pts.push(target);
89
+ local.append(svg("polyline", { points: pts.map((p) => p.join(",")).join(" "), fill: "none", stroke: "#fff", "stroke-width": 0.6, "stroke-dasharray": "2 1.5" }));
90
+ local.append(svg("circle", { cx: target[0], cy: target[1], r: 1.8, fill: "#fff" }));
91
+ }
92
+ }
93
+ if (o.sprites) {
94
+ const ego = gameGlobal(vm, "ego");
95
+ for (const d of drawnItems(vm)) {
96
+ if (plane && d.plane !== plane.address)
97
+ continue;
98
+ const color = colorOf(d.item.address, d.item.address === ego ? OVERLAY_COLORS.hero : OVERLAY_COLORS.sprite);
99
+ root.append(svg("rect", { x: d.x, y: d.y, width: d.w, height: d.h, fill: "none", stroke: color, "stroke-width": d.item.address === o.selected ? 1.2 : 0.5 }));
100
+ root.append(label(d.x, d.y - 1, d.item.name, color));
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,123 @@
1
+ import {
2
+ collectionElements, drawnItems, gameGlobal, isKindOf, namedObject, objectAt, polygonOf, readInts, roomPlane, signedProp,
3
+ type Vm,
4
+ } from "../../../packages/sci/src/index.ts";
5
+
6
+ /**
7
+ * What the interpreter knows about the room, drawn over the game screen as SVG in game
8
+ * coordinates (320x200): clickable features, sprites, walk polygons and where actors are
9
+ * going. Used by the explorer's live view and the room editor.
10
+ */
11
+ export interface OverlayOptions {
12
+ /** Drawn sprites (cast members) with their names. */
13
+ sprites?: boolean;
14
+ /** The room's features (clickable areas) with their names, and approach points. */
15
+ features?: boolean;
16
+ /** Walk polygons: green total access, yellow near point, red barred, blue contained. */
17
+ polygons?: boolean;
18
+ /** Where moving actors are headed (PolyPath routes). */
19
+ paths?: boolean;
20
+ /** An object to outline in white. */
21
+ selected?: number;
22
+ /** Tells exits from other features (the editor knows from the room's YAML). */
23
+ isExit?: (name: string) => boolean;
24
+ }
25
+
26
+ const SVG = "http://www.w3.org/2000/svg";
27
+ const svg = <K extends keyof SVGElementTagNameMap>(tag: K, attrs: Record<string, string | number> = {}, text?: string) => {
28
+ const node = document.createElementNS(SVG, tag);
29
+ for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
30
+ if (text) node.textContent = text;
31
+ return node;
32
+ };
33
+
34
+ export const OVERLAY_COLORS = {
35
+ polygons: ["#6fb07a", "#d8c050", "#e05555", "#5fa0e8"],
36
+ feature: "#e8b04a",
37
+ exit: "#5fd08c",
38
+ sprite: "#7fb3e8",
39
+ hero: "#f08fd0",
40
+ selected: "#ffffff",
41
+ };
42
+
43
+ /** An SVG to lay over the game canvas (position it over the canvas, same size). */
44
+ export function createOverlay(): SVGSVGElement {
45
+ const root = svg("svg", { viewBox: "0 0 320 200", preserveAspectRatio: "none" });
46
+ root.style.pointerEvents = "none";
47
+ return root;
48
+ }
49
+
50
+ const label = (x: number, y: number, text: string, color: string) =>
51
+ svg("text", {
52
+ x, y, fill: color, stroke: "#000", "stroke-width": 1.2, "paint-order": "stroke", "font-size": 5, "font-family": "ui-monospace, Menlo, monospace",
53
+ }, text);
54
+
55
+ export function drawRoomOverlay(root: SVGSVGElement, vm: Vm, o: OverlayOptions): void {
56
+ root.replaceChildren();
57
+ const plane = roomPlane(vm);
58
+ const room = objectAt(vm, gameGlobal(vm, "curRoom"));
59
+ // Room objects use the room plane's coordinates (below the status bar).
60
+ const local = svg("g", { transform: plane ? `translate(${plane.left} ${plane.top})` : "" });
61
+ root.append(local);
62
+ const colorOf = (address: number, normal: string) => (address === o.selected ? OVERLAY_COLORS.selected : normal);
63
+
64
+ if (o.polygons && room) {
65
+ for (const poly of collectionElements(vm, vm.getProp(room, "obstacles"))) {
66
+ const { type, points } = polygonOf(vm, poly);
67
+ if (points.length < 3) continue;
68
+ const color = OVERLAY_COLORS.polygons[type] ?? "#aaa";
69
+ local.append(svg("polygon", { points: points.map((p) => p.join(",")).join(" "), fill: `${color}22`, stroke: color, "stroke-width": 0.6 }));
70
+ }
71
+ }
72
+ if (o.features) {
73
+ for (const f of collectionElements(vm, namedObject(vm, "features"))) {
74
+ const color = colorOf(f.address, o.isExit?.(f.name) ? OVERLAY_COLORS.exit : OVERLAY_COLORS.feature);
75
+ const style = { fill: `${color}18`, stroke: color, "stroke-width": f.address === o.selected ? 1.2 : 0.5 };
76
+ const check = objectAt(vm, vm.getProp(f, "onMeCheck"));
77
+ let at: [number, number] | undefined;
78
+ if (check && isKindOf(vm, check, "Polygon")) {
79
+ const { points } = polygonOf(vm, check);
80
+ if (points.length < 3) continue;
81
+ local.append(svg("polygon", { points: points.map((p) => p.join(",")).join(" "), ...style }));
82
+ at = [points[0]![0] + 2, points[0]![1] + 6];
83
+ } else {
84
+ const [l, t, r, b] = ["nsLeft", "nsTop", "nsRight", "nsBottom"].map((n) => signedProp(vm, f, n)) as [number, number, number, number];
85
+ if (r <= l || b <= t) continue;
86
+ local.append(svg("rect", { x: l, y: t, width: r - l, height: b - t, ...style }));
87
+ at = [l + 2, t + 6];
88
+ }
89
+ local.append(label(at[0], at[1], f.name, color));
90
+ // Where the hero walks to before using it.
91
+ const ax = signedProp(vm, f, "approachX"), ay = signedProp(vm, f, "approachY");
92
+ if (ax || ay) {
93
+ local.append(svg("path", { d: `M${ax - 2} ${ay - 2}L${ax + 2} ${ay + 2}M${ax - 2} ${ay + 2}L${ax + 2} ${ay - 2}`, stroke: color, "stroke-width": 0.6 }));
94
+ }
95
+ }
96
+ }
97
+ if (o.paths) {
98
+ for (const actor of collectionElements(vm, namedObject(vm, "cast"))) {
99
+ const mover = objectAt(vm, vm.getProp(actor, "mover"));
100
+ if (!mover) continue;
101
+ const pts = [[signedProp(vm, actor, "x"), signedProp(vm, actor, "y")]];
102
+ // PolyPath keeps the whole route: an array ending in 0x7777, `value` = next index.
103
+ const route = vm.getProp(mover, "points");
104
+ if (route) {
105
+ const flat = readInts(vm, route, 200);
106
+ for (let i = signedProp(vm, mover, "value"); i + 1 < flat.length && flat[i] !== 0x7777; i += 2) pts.push([flat[i]!, flat[i + 1]!]);
107
+ }
108
+ const target = [signedProp(vm, mover, "x"), signedProp(vm, mover, "y")];
109
+ if (pts.length === 1) pts.push(target);
110
+ local.append(svg("polyline", { points: pts.map((p) => p.join(",")).join(" "), fill: "none", stroke: "#fff", "stroke-width": 0.6, "stroke-dasharray": "2 1.5" }));
111
+ local.append(svg("circle", { cx: target[0]!, cy: target[1]!, r: 1.8, fill: "#fff" }));
112
+ }
113
+ }
114
+ if (o.sprites) {
115
+ const ego = gameGlobal(vm, "ego");
116
+ for (const d of drawnItems(vm)) {
117
+ if (plane && d.plane !== plane.address) continue;
118
+ const color = colorOf(d.item.address, d.item.address === ego ? OVERLAY_COLORS.hero : OVERLAY_COLORS.sprite);
119
+ root.append(svg("rect", { x: d.x, y: d.y, width: d.w, height: d.h, fill: "none", stroke: color, "stroke-width": d.item.address === o.selected ? 1.2 : 0.5 }));
120
+ root.append(label(d.x, d.y - 1, d.item.name, color));
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,14 @@
1
+ :root { color-scheme: dark; }
2
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #0b0b0b; color: #999; font: 12px/1.4 ui-monospace, Menlo, monospace; }
3
+ main { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 16px; }
4
+ canvas { width: min(960px, 100vw - 32px); aspect-ratio: 4 / 3; image-rendering: pixelated; background: #000; outline: 1px solid #222; cursor: none; }
5
+ #error { color: #f77; max-width: 960px; white-space: pre-wrap; }
6
+ .bar { display: flex; gap: 16px; align-items: center; justify-content: space-between; width: min(960px, 100vw - 32px); }
7
+ #music { background: #1b1b1b; color: #ddd; border: 1px solid #333; border-radius: 4px; padding: 6px 10px; font: inherit; cursor: pointer; }
8
+ #music:hover { border-color: #e8b04a; color: #e8b04a; }
9
+ #mods { position: relative; }
10
+ #mods summary { cursor: pointer; color: #ddd; }
11
+ #mods[open] > div, #mods[open] > button { position: absolute; right: 0; z-index: 1; background: #161616; border: 1px solid #333; }
12
+ #mods > div { bottom: 2.4em; min-width: 220px; padding: 8px 10px; border-radius: 4px; display: flex; flex-direction: column; gap: 4px; }
13
+ #mods > button { bottom: -2.2em; background: #1b1b1b; color: #ddd; border-radius: 4px; padding: 4px 8px; font: inherit; cursor: pointer; white-space: nowrap; }
14
+ #mods .none { color: #777; }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,179 @@
1
+ import { activeMods, availableMods, openGame, rememberMods } from "./files.js";
2
+ import { GameSession } from "./session.js";
3
+ import { ResourceType, gameGlobal, heroInPlay, requestRoom, saves, savesToJson } from "../../../packages/sci/src/index.js";
4
+ import { APP_ID } from "./app.js";
5
+ import { gameProfile } from "./game-profile.js";
6
+ const canvas = document.getElementById("screen");
7
+ const status = document.getElementById("status");
8
+ const errorBox = document.getElementById("error");
9
+ const musicButton = document.getElementById("music");
10
+ // Mods (?mods=a,b or the last choice) load as patch directories over the game.
11
+ const { rm, mods } = await openGame();
12
+ // `?mute` keeps audio off (e.g. automated testing in a background tab).
13
+ const params = new URLSearchParams(location.search);
14
+ // `?debug` turns on Sierra's own debug room (Alt-T teleport, Alt-G flags, Alt-I items...).
15
+ const session = await GameSession.create({ canvas, rm, mods, muted: params.has("mute"), debug: params.has("debug") });
16
+ await setUpModPicker();
17
+ // For poking at the running game from the browser console (and automated checks).
18
+ window.sci = { session };
19
+ // Saved games as a JSON file, to replay them headlessly (tools/explore.ts loadsaves:<file>).
20
+ document.getElementById("export-saves").addEventListener("click", () => {
21
+ const store = saves(session.vm).store;
22
+ const a = document.createElement("a");
23
+ a.href = URL.createObjectURL(new Blob([savesToJson([...store.saves.values()])], { type: "application/json" }));
24
+ a.download = `${APP_ID}-saves${mods.length ? `-${mods.join("+")}` : ""}.json`;
25
+ a.click();
26
+ URL.revokeObjectURL(a.href);
27
+ });
28
+ const startRoom = /^#room\/(\d+)$/.exec(location.hash) ? Number(location.hash.slice(6)) : undefined;
29
+ /** What room navigation is doing, shown on the status line. */
30
+ let roomStatus;
31
+ setUpRoomNavigation();
32
+ /**
33
+ * Rooms in the address bar: `#room/250` goes there. Opened that way, the game doesn't make
34
+ * you sit through the intro: it loads your newest saved game (your hero, your progress), or
35
+ * with none (or `?fresh`), fast-forwards through character creation to a new Wizard called
36
+ * Hero. Then it jumps. Editing the hash jumps again, and Back returns to the last jump; the
37
+ * hash follows the game's own room changes without adding history entries.
38
+ */
39
+ function setUpRoomNavigation() {
40
+ /** The room asked for, kept until the game gets there (or gives up). */
41
+ let wanted;
42
+ let shown = 0;
43
+ const readHash = () => {
44
+ const m = /^#room\/(\d+)$/.exec(location.hash);
45
+ if (!m)
46
+ return;
47
+ const room = Number(m[1]);
48
+ if (room === shown && !wanted)
49
+ return; // the hash we wrote ourselves
50
+ if (!rm.has({ type: ResourceType.Script, number: room })) {
51
+ roomStatus = `There's no room ${room}`;
52
+ return;
53
+ }
54
+ wanted = { room, since: performance.now() };
55
+ };
56
+ readHash();
57
+ // Editing the address bar fires hashchange (and popstate, in some browsers and for Back).
58
+ addEventListener("hashchange", readHash);
59
+ addEventListener("popstate", readHash);
60
+ session.onDraw.push(() => {
61
+ const vm = session.vm;
62
+ // The 20 s allowance only counts while a hero is in play (making one takes a while).
63
+ if (session.fastForwarding || !heroInPlay(vm)) {
64
+ if (wanted)
65
+ wanted.since = performance.now();
66
+ if (session.fastForwarding)
67
+ return;
68
+ }
69
+ const room = gameGlobal(vm, "curRoomNum") ?? 0;
70
+ if (wanted) {
71
+ if (room === wanted.room) {
72
+ roomStatus = undefined;
73
+ wanted = undefined;
74
+ }
75
+ else if (!heroInPlay(vm)) {
76
+ roomStatus = `Room ${wanted.room}: waiting for a hero (create one, or restore a save)`;
77
+ }
78
+ else if (performance.now() - wanted.since > 20_000) {
79
+ roomStatus = `Couldn't get to room ${wanted.room} (the game didn't leave room ${room})`;
80
+ wanted = undefined;
81
+ }
82
+ else {
83
+ // Ask again every frame: while a message or a scripted scene holds the game, its
84
+ // main loop isn't checking, and a room may reset the request when it starts.
85
+ requestRoom(vm, wanted.room);
86
+ roomStatus = performance.now() - wanted.since > 1500
87
+ ? `Going to room ${wanted.room}… (close any message on screen)`
88
+ : `Going to room ${wanted.room}…`;
89
+ }
90
+ }
91
+ if (room && room !== shown && heroInPlay(vm)) {
92
+ shown = room;
93
+ if (location.hash !== `#room/${room}`)
94
+ history.replaceState(null, "", `#room/${room}`);
95
+ }
96
+ });
97
+ }
98
+ /** How a `#room/N` visit gets a hero: the newest save, or a quick new one. */
99
+ const quickStart = gameProfile(session.vm)?.quickStart(session.vm);
100
+ function startGame() {
101
+ const store = saves(session.vm).store;
102
+ const info = params.has("fresh") ? undefined : store.list()[0];
103
+ const newest = info && store.get(info.id);
104
+ if (startRoom === undefined)
105
+ return session.start();
106
+ if (newest) {
107
+ status.textContent = `Loading "${newest.info.description}" to go to room ${startRoom}…`;
108
+ session.start();
109
+ // Restore once the game is up (it needs its first frame to exist), like the game's own Restore.
110
+ let done = false;
111
+ session.onDraw.push(() => {
112
+ if (done)
113
+ return;
114
+ done = true;
115
+ session.restore(newest.snapshot);
116
+ });
117
+ return;
118
+ }
119
+ if (!quickStart) {
120
+ status.textContent = `Play until your character can walk around, then it goes to room ${startRoom}`;
121
+ return session.start();
122
+ }
123
+ status.textContent = `Creating a hero to go to room ${startRoom}…`;
124
+ session.start({ fastForward: quickStart });
125
+ }
126
+ /** Checkboxes for the built mods; applying reloads the page with the new set. */
127
+ async function setUpModPicker() {
128
+ const list = document.getElementById("mod-list");
129
+ const apply = document.getElementById("mods-apply");
130
+ const summary = document.querySelector("#mods summary");
131
+ const built = await availableMods();
132
+ const on = new Set(activeMods());
133
+ summary.textContent = on.size ? `Mods: ${[...on].join(", ")}` : "Mods";
134
+ if (!built.length) {
135
+ list.innerHTML = '<span class="none">No mods built. Try <code>pnpm mod build mods/altar-hello</code>.</span>';
136
+ apply.hidden = true;
137
+ return;
138
+ }
139
+ for (const name of built) {
140
+ const box = Object.assign(document.createElement("input"), { type: "checkbox", checked: on.has(name), value: name });
141
+ const label = document.createElement("label");
142
+ label.append(box, ` ${name}`);
143
+ list.append(label);
144
+ }
145
+ apply.addEventListener("click", () => {
146
+ const chosen = [...list.querySelectorAll("input:checked")].map((b) => b.value);
147
+ rememberMods(chosen);
148
+ const url = new URL(location.href);
149
+ url.searchParams.set("mods", chosen.join(","));
150
+ location.href = url.toString();
151
+ });
152
+ }
153
+ const showMusicMode = () => (musicButton.textContent = session.musicMode === "adlib" ? "Music: AdLib (OPL2 FM, 1994 Sound Blaster)" : "Music: General MIDI (SoundFont)");
154
+ musicButton.addEventListener("click", () => {
155
+ if (session.hasAdLib)
156
+ session.setMusicMode(session.musicMode === "adlib" ? "gm" : "adlib");
157
+ showMusicMode();
158
+ });
159
+ showMusicMode();
160
+ let lastStatus = 0;
161
+ session.onDraw.push(() => {
162
+ if (session.error) {
163
+ errorBox.hidden = false;
164
+ errorBox.textContent = `${session.error.message}\n${session.error.backtrace.map((l) => ` at ${l}`).join("\n")}`;
165
+ return;
166
+ }
167
+ const now = performance.now();
168
+ if (session.fastForwarding) {
169
+ if (now - lastStatus > 250)
170
+ (status.textContent = `Creating a hero… (${Math.min(100, Math.round((session.frames / (quickStart?.until ?? 1)) * 100))}%)`), (lastStatus = now);
171
+ return;
172
+ }
173
+ if (now - lastStatus < 1000)
174
+ return;
175
+ lastStatus = now;
176
+ status.textContent = !session.vm.running ? "Game exited." : roomStatus ?? `${session.fps} fps · ${session.vm.instructions.toLocaleString()} instructions${mods.length ? ` · mods: ${mods.join(", ")}` : ""}${params.has("debug") ? " · debug: Alt-T teleport" : ""}${session.musicMode === "gm" && !session.music.ready ? " · SoundFont loading" : ""}`;
177
+ });
178
+ status.textContent = "Running";
179
+ startGame();