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,349 @@
1
+ import { AudioIndex, speechKey, type AudioClip, type AudioOutput } from "../../audio/index.ts";
2
+ import { SongPlayer, SoundDevice, parseSound, type MidiOutput, type Song } from "../../audio/midi.ts";
3
+ import { ResourceType } from "../../resource/types.ts";
4
+ import { findMessage, parseMessages, type MessageFile } from "../../text/message.ts";
5
+ import type { KernelFn, Vm } from "../vm.ts";
6
+ import { toSigned, type Value } from "../value.ts";
7
+
8
+ const NOT_PLAYING = 0xffff;
9
+ const MAX_VOLUME = 127;
10
+
11
+ interface Channel {
12
+ clip: AudioClip;
13
+ /** VM clock (ms) when playback started; undefined while waiting to start. */
14
+ start: number | undefined;
15
+ pausedAt: number | undefined;
16
+ ticks: number;
17
+ loop: boolean;
18
+ volume: number;
19
+ }
20
+
21
+ /**
22
+ * Digital audio state. Durations are known synchronously from the index, so the scripts'
23
+ * position polling works off the VM clock; the host's AudioOutput does the actual playback
24
+ * (fetching and decoding clips asynchronously).
25
+ */
26
+ export class AudioState {
27
+ readonly index: AudioIndex;
28
+ readonly channels = new Map<string, Channel>();
29
+ output: AudioOutput | undefined;
30
+ /** MIDI music goes here (a SoundFont synth or the AdLib driver). */
31
+ midi: MidiOutput | undefined;
32
+ /** Which track of each sound resource to play: General MIDI or AdLib. */
33
+ device: number = SoundDevice.GeneralMidi;
34
+ readonly songs = new Map<Value, SongPlayer>();
35
+ private readonly songNumbers = new Map<Value, number>();
36
+ private readonly songCache = new Map<string, Song | null>();
37
+ masterVolume = 15;
38
+ /**
39
+ * Ticks to report when a speech clip doesn't exist (new dialogue in a mod). The game times
40
+ * a spoken line by its clip, so a missing clip would flash the text for a second; hosts
41
+ * that load mods set this (see `readingTime`). Unset, missing clips report 0 like SSCI.
42
+ */
43
+ missingSpeechTicks?: (module: number, noun: number, verb: number, cond: number, seq: number) => number;
44
+
45
+ constructor(private readonly vm: Vm) {
46
+ this.index = AudioIndex.build(vm.resources);
47
+ vm.frameHooks.push(() => {
48
+ const now = this.ticks();
49
+ for (const player of this.songs.values()) player.update(now);
50
+ });
51
+ }
52
+
53
+ ticks(): number {
54
+ return Math.floor((this.vm.clock() * 60) / 1000);
55
+ }
56
+
57
+ song(number: number, device = this.device): Song | undefined {
58
+ const key = `${number}:${device}`;
59
+ if (!this.songCache.has(key)) {
60
+ const id = { type: ResourceType.Sound, number };
61
+ this.songCache.set(key, this.vm.resources.has(id) ? (parseSound(this.vm.resources.loadSync(id).data, device) ?? null) : null);
62
+ }
63
+ return this.songCache.get(key) ?? undefined;
64
+ }
65
+
66
+ startSong(obj: Value, number: number, loop: boolean, volume: number, position = 0): void {
67
+ const song = this.song(number);
68
+ if (!song) return;
69
+ const player = new SongPlayer(song, this.midi, loop, volume, this.ticks());
70
+ if (position) player.seek(position % Math.max(song.length, 1), this.ticks());
71
+ this.songs.set(obj, player);
72
+ this.songNumbers.set(obj, number);
73
+ }
74
+
75
+ playingSongs(): { object: Value; number: number; player: SongPlayer }[] {
76
+ return [...this.songs].map(([object, player]) => ({ object, number: this.songNumbers.get(object)!, player }));
77
+ }
78
+
79
+ /** Silences all music, effects and speech (restart / restore). */
80
+ stopEverything(): void {
81
+ for (const obj of [...this.songs.keys()]) this.stopSong(obj);
82
+ for (const key of [...this.channels.keys()]) this.stop(key);
83
+ }
84
+
85
+ stopSong(obj: Value): void {
86
+ this.songs.get(obj)?.stop();
87
+ this.songs.delete(obj);
88
+ this.songNumbers.delete(obj);
89
+ }
90
+
91
+ /**
92
+ * Switches music to another device (track) and output, carrying every playing song over
93
+ * at its current position.
94
+ */
95
+ setMusicDevice(device: number, output: MidiOutput | undefined): void {
96
+ const now = this.ticks();
97
+ const playing = [...this.songs].map(([obj, player]) => ({ obj, player, pos: player.position(now) }));
98
+ for (const { player } of playing) player.stop();
99
+ this.device = device;
100
+ this.midi = output;
101
+ output?.setMasterVolume?.(this.masterVolume);
102
+ for (const { obj, player, pos } of playing) {
103
+ const song = this.song(this.songNumbers.get(obj)!);
104
+ if (!song) {
105
+ this.songs.delete(obj);
106
+ continue;
107
+ }
108
+ const next = new SongPlayer(song, output, player.loop, player.volume, now);
109
+ next.seek(song.length ? pos % Math.max(song.length, 1) : 0, now);
110
+ this.songs.set(obj, next);
111
+ }
112
+ }
113
+
114
+ play(key: string, clip: AudioClip, loop: boolean, volume: number, autoStart = true): number {
115
+ this.stop(key);
116
+ const ticks = AudioIndex.ticks(clip);
117
+ this.channels.set(key, { clip, start: undefined, pausedAt: undefined, ticks, loop, volume });
118
+ if (autoStart) this.start(key);
119
+ return ticks;
120
+ }
121
+
122
+ start(key: string): void {
123
+ const ch = this.channels.get(key);
124
+ if (!ch || ch.start !== undefined) return;
125
+ ch.start = this.vm.clock();
126
+ this.output?.play(key, ch.clip, { loop: ch.loop, volume: ch.volume / MAX_VOLUME });
127
+ }
128
+
129
+ stop(key: string): void {
130
+ if (!this.channels.delete(key)) return;
131
+ this.output?.stop(key);
132
+ }
133
+
134
+ stopAll(prefix = ""): void {
135
+ for (const key of [...this.channels.keys()]) if (key.startsWith(prefix)) this.stop(key);
136
+ }
137
+
138
+ /** Elapsed ticks, or undefined once the clip has finished (non-looping). */
139
+ position(key: string): number | undefined {
140
+ const ch = this.channels.get(key);
141
+ if (!ch) return undefined;
142
+ if (ch.start === undefined) return 0;
143
+ const now = ch.pausedAt ?? this.vm.clock();
144
+ const elapsed = Math.floor(((now - ch.start) * 60) / 1000);
145
+ if (!ch.loop && elapsed >= ch.ticks) {
146
+ this.stop(key);
147
+ return undefined;
148
+ }
149
+ return ch.loop ? elapsed % ch.ticks : elapsed;
150
+ }
151
+
152
+ pause(key: string, paused: boolean): void {
153
+ const ch = this.channels.get(key);
154
+ if (!ch || ch.start === undefined) return;
155
+ if (paused && ch.pausedAt === undefined) {
156
+ ch.pausedAt = this.vm.clock();
157
+ this.output?.pause(key);
158
+ } else if (!paused && ch.pausedAt !== undefined) {
159
+ ch.start += this.vm.clock() - ch.pausedAt;
160
+ ch.pausedAt = undefined;
161
+ this.output?.resume(key);
162
+ }
163
+ }
164
+ }
165
+
166
+ const stateOf = new WeakMap<Vm, AudioState>();
167
+ export const audio = (vm: Vm): AudioState => {
168
+ let a = stateOf.get(vm);
169
+ if (!a) {
170
+ const created = (a = new AudioState(vm));
171
+ stateOf.set(vm, created);
172
+ vm.hostRoots.push(() => created.songs.keys());
173
+ }
174
+ return a;
175
+ };
176
+
177
+ /**
178
+ * DoAudio arguments name a clip either as an audio36 (speech) tuple
179
+ * `module, noun, verb, cond, seq, [loop], [volume]` or as a plain audio number
180
+ * `number, [loop], [volume]`.
181
+ */
182
+ function resolveArgs(a: AudioState, args: Value[]): { key: string; clip?: AudioClip; loop: boolean; volume: number } | undefined {
183
+ if (args.length >= 5) {
184
+ const [module = 0, noun = 0, verb = 0, cond = 0, seq = 0, loop, volume] = args;
185
+ return {
186
+ key: `aud:${speechKey(module, noun, verb, cond, seq)}`,
187
+ clip: a.index.speech.get(speechKey(module, noun, verb, cond, seq)),
188
+ loop: loop !== undefined && toSigned(loop) === -1,
189
+ volume: volume !== undefined && toSigned(volume) >= 0 ? Math.min(toSigned(volume), MAX_VOLUME) : MAX_VOLUME,
190
+ };
191
+ }
192
+ if (args.length >= 1) {
193
+ const [number = 0, loop, volume] = args;
194
+ return {
195
+ key: `sfx:${number}`,
196
+ clip: a.index.effects.get(number),
197
+ loop: loop !== undefined && toSigned(loop) === -1,
198
+ volume: volume !== undefined && toSigned(volume) >= 0 ? Math.min(toSigned(volume), MAX_VOLUME) : MAX_VOLUME,
199
+ };
200
+ }
201
+ return undefined;
202
+ }
203
+
204
+ const soundKey = (obj: Value) => `snd:${obj.toString(16)}`;
205
+ const prop = (vm: Vm, obj: Value, name: string) => toSigned(vm.getProp(obj, name) ?? 0);
206
+
207
+ export const audioKernels: Record<string, KernelFn> = {
208
+ DoAudio: (vm, [subop = 0, ...args]) => {
209
+ const a = audio(vm);
210
+ const target = resolveArgs(a, args);
211
+ switch (subop) {
212
+ case 0: return 0; // init
213
+ case 1: // wait-for-play: prepare without starting, return length
214
+ case 2: { // play
215
+ if (!target) return a.channels.size;
216
+ if (!target.clip) {
217
+ const [module = 0, noun = 0, verb = 0, cond = 0, seq = 0] = args;
218
+ return args.length >= 5 && a.missingSpeechTicks ? a.missingSpeechTicks(module, noun, verb, cond, seq) & 0xffff : 0;
219
+ }
220
+ return a.play(target.key, target.clip, target.loop, target.volume, subop === 2);
221
+ }
222
+ case 3: // stop
223
+ if (target) a.stop(target.key);
224
+ else a.stopAll("aud:"), a.stopAll("sfx:");
225
+ return 0;
226
+ case 4: case 5: // pause / resume
227
+ for (const key of target ? [target.key] : [...a.channels.keys()]) a.pause(key, subop === 4);
228
+ return 0;
229
+ case 6: { // position in ticks, -1 when not playing
230
+ const key = target?.key ?? [...a.channels.keys()].find((k) => k.startsWith("aud:"));
231
+ const pos = key === undefined ? undefined : a.position(key);
232
+ return pos === undefined ? NOT_PLAYING : pos & 0xffff;
233
+ }
234
+ case 8: return 0; // volume
235
+ case 9: return 1; // capability: audio present
236
+ case 10: return 16; // bit depth
237
+ case 12: return 1; // mixing
238
+ case 13: return 2; // channels
239
+ default: return 0;
240
+ }
241
+ },
242
+
243
+ // Sound objects. Numbers with a digital effect in RESOURCE.SFX play as samples;
244
+ // everything else is MIDI, sequenced here and sent to `midi` (a SoundFont synth).
245
+ DoSound: (vm, [subop = 0, obj = 0, arg = 0, , , stopAfter = 0]) => {
246
+ const a = audio(vm);
247
+ switch (subop) {
248
+ case 0: // master volume (get, or set to arg)
249
+ if (obj !== 0 || arg) {
250
+ a.masterVolume = obj & 0xf;
251
+ a.midi?.setMasterVolume?.(a.masterVolume);
252
+ }
253
+ return a.masterVolume;
254
+ case 3: return 32; // polyphony
255
+ case 4: return 1; // digital audio available
256
+ case 6: // init
257
+ vm.setProp(obj, "nodePtr", obj);
258
+ return 0;
259
+ case 7: // dispose
260
+ a.stop(soundKey(obj));
261
+ a.stopSong(obj);
262
+ vm.setProp(obj, "nodePtr", 0);
263
+ return 0;
264
+ case 8: { // play: a digital effect if RESOURCE.SFX has this number, else MIDI
265
+ const number = prop(vm, obj, "number");
266
+ const loop = prop(vm, obj, "loop") === -1;
267
+ const volume = vm.getProp(obj, "vol") === undefined ? MAX_VOLUME : prop(vm, obj, "vol");
268
+ a.stop(soundKey(obj));
269
+ a.stopSong(obj);
270
+ vm.setProp(obj, "signal", 0);
271
+ // SCI2 Sounds ask for updates (Sound::check -> updateCues, which reports the end and
272
+ // cues the client) only while `handle` is set; playing sets it, stopping clears it.
273
+ vm.setProp(obj, "handle", obj);
274
+ const clip = a.index.effects.get(number);
275
+ if (clip) a.play(soundKey(obj), clip, loop, volume);
276
+ else a.startSong(obj, number, loop, volume);
277
+ return 0;
278
+ }
279
+ case 9: // stop
280
+ a.stop(soundKey(obj));
281
+ a.stopSong(obj);
282
+ vm.setProp(obj, "signal", NOT_PLAYING);
283
+ vm.setProp(obj, "handle", 0);
284
+ return 0;
285
+ case 10: { // pause (obj or 0 = all, arg = pause?)
286
+ for (const key of obj ? [soundKey(obj)] : [...a.channels.keys()].filter((k) => k.startsWith("snd:"))) a.pause(key, arg !== 0);
287
+ for (const player of obj ? [a.songs.get(obj)] : [...a.songs.values()]) {
288
+ if (arg !== 0) player?.pause(a.ticks());
289
+ else player?.resume(a.ticks());
290
+ }
291
+ return 0;
292
+ }
293
+ case 11: { // fade(obj, targetVolume, ticks, step, stopAfter): fading out → stop
294
+ if (arg === 0 || stopAfter) {
295
+ a.stop(soundKey(obj));
296
+ a.stopSong(obj);
297
+ vm.setProp(obj, "signal", NOT_PLAYING);
298
+ } else a.songs.get(obj)?.setVolume(arg);
299
+ return 0;
300
+ }
301
+ case 14: // set volume
302
+ a.output?.setVolume(soundKey(obj), arg / MAX_VOLUME);
303
+ a.songs.get(obj)?.setVolume(arg);
304
+ vm.setProp(obj, "vol", arg);
305
+ return 0;
306
+ case 16: { // set loop
307
+ vm.setProp(obj, "loop", arg);
308
+ const player = a.songs.get(obj);
309
+ if (player) player.loop = toSigned(arg) === -1;
310
+ return 0;
311
+ }
312
+ case 17: { // update cues: report song signals/cues and finished sounds to the script
313
+ if (a.channels.has(soundKey(obj)) && a.position(soundKey(obj)) === undefined) vm.setProp(obj, "signal", NOT_PLAYING);
314
+ const player = a.songs.get(obj);
315
+ if (player) {
316
+ player.update(a.ticks());
317
+ if (player.signal) {
318
+ vm.setProp(obj, "signal", player.signal);
319
+ vm.setProp(obj, "dataInc", player.dataInc);
320
+ player.signal = 0;
321
+ }
322
+ if (player.finished) a.stopSong(obj);
323
+ }
324
+ return 0;
325
+ }
326
+ default:
327
+ return 0;
328
+ }
329
+ },
330
+
331
+ DoSync: () => 0,
332
+ };
333
+
334
+ /**
335
+ * A `missingSpeechTicks` that gives a line without a recording as long as it takes to read:
336
+ * ~15 characters a second, at least two seconds. Reads the text from the message resources.
337
+ */
338
+ export function readingTime(vm: Vm): (module: number, noun: number, verb: number, cond: number, seq: number) => number {
339
+ const files = new Map<number, MessageFile | undefined>();
340
+ return (module, noun, verb, cond, seq) => {
341
+ if (!files.has(module)) {
342
+ const id = { type: ResourceType.Message, number: module };
343
+ files.set(module, vm.resources.has(id) ? parseMessages(vm.resources.loadSync(id).data) : undefined);
344
+ }
345
+ const file = files.get(module);
346
+ const text = file ? findMessage(file, { noun, verb, cond, seq })?.text ?? "" : "";
347
+ return Math.max(120, text.length * 4);
348
+ };
349
+ }
@@ -0,0 +1,5 @@
1
+ import type { KernelFn } from "../vm.ts";
2
+ /** Script, object and resource management. */
3
+ export declare const coreKernels: Record<string, KernelFn>;
4
+ /** Maths, time and the host platform. */
5
+ export declare const mathKernels: Record<string, KernelFn>;
@@ -0,0 +1,87 @@
1
+ import { ResourceType } from "../../resource/types.js";
2
+ import { NULL, bool, fromInt, toSigned } from "../value.js";
3
+ /** Script, object and resource management. */
4
+ export const coreKernels = {
5
+ // Resource loading is eager in this engine, so these only need to answer sensibly.
6
+ Load: (_vm, [, number = 0]) => number,
7
+ Unload: () => 0,
8
+ Lock: () => 0,
9
+ Purge: () => 0,
10
+ ResCheck: (vm, [type = 0, number = 0]) => bool(vm.resources.has({ type: (type & 0x7f), number })),
11
+ ScriptID: (vm, [script = 0, index = 0]) => {
12
+ if (!vm.resources.has({ type: ResourceType.Script, number: script }))
13
+ return NULL;
14
+ return vm.exportAddress(script, index);
15
+ },
16
+ // Unloads the script (see Vm.disposeScript) and returns its optional second argument:
17
+ // scripts use it to hand a value back from code that's about to be unloaded (SRDialog
18
+ // returns the chosen save id this way).
19
+ DisposeScript: (vm, args) => {
20
+ vm.disposeScript(args[0] & 0xffff);
21
+ return args.length >= 2 ? args[1] : undefined;
22
+ },
23
+ Clone: (vm, [obj = 0]) => vm.memory.clone(vm.object(obj)).address,
24
+ DisposeClone: (vm, [obj = 0]) => {
25
+ const o = vm.memory.object(obj);
26
+ if (o)
27
+ vm.memory.disposeClone(o);
28
+ },
29
+ RespondsTo: (vm, [obj = 0, selector = 0]) => {
30
+ const o = vm.memory.object(obj);
31
+ return bool(!!o && vm.respondsTo(o, selector));
32
+ },
33
+ };
34
+ let seed = 0x1234;
35
+ const bootDate = Date.now();
36
+ /** Maths, time and the host platform. */
37
+ export const mathKernels = {
38
+ Random: (_vm, args) => {
39
+ if (args.length === 1) {
40
+ seed = args[0]; // seed the generator
41
+ return 0;
42
+ }
43
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
44
+ if (args.length < 2)
45
+ return (seed >> 8) & 0xffff;
46
+ const lo = toSigned(args[0]), hi = toSigned(args[1]);
47
+ return fromInt(lo + ((seed >> 8) % (hi - lo + 1)));
48
+ },
49
+ Abs: (_vm, [n = 0]) => fromInt(Math.abs(toSigned(n))),
50
+ Sqrt: (_vm, [n = 0]) => Math.floor(Math.sqrt(Math.abs(toSigned(n)))),
51
+ GetAngle: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0]) => sciAngle(toSigned(x2) - toSigned(x1), toSigned(y2) - toSigned(y1)),
52
+ GetDistance: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0, perspective = 0]) => {
53
+ const dx = toSigned(x2) - toSigned(x1);
54
+ let dy = toSigned(y2) - toSigned(y1);
55
+ if (perspective)
56
+ dy = Math.trunc((dy * 3) / 2);
57
+ return Math.round(Math.sqrt(dx * dx + dy * dy)) & 0xffff;
58
+ },
59
+ ATan: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0]) => sciAngle(toSigned(x2) - toSigned(x1), toSigned(y2) - toSigned(y1)),
60
+ SinMult: (_vm, [angle = 0, n = 0]) => fromInt(Math.round(Math.sin(rad(angle)) * toSigned(n))),
61
+ CosMult: (_vm, [angle = 0, n = 0]) => fromInt(Math.round(Math.cos(rad(angle)) * toSigned(n))),
62
+ SinDiv: (_vm, [angle = 0, n = 0]) => {
63
+ const s = Math.sin(rad(angle));
64
+ return s === 0 ? 0 : fromInt(Math.round(toSigned(n) / s));
65
+ },
66
+ CosDiv: (_vm, [angle = 0, n = 0]) => {
67
+ const c = Math.cos(rad(angle));
68
+ return c === 0 ? 0 : fromInt(Math.round(toSigned(n) / c));
69
+ },
70
+ GetTime: (vm, [mode = 0]) => {
71
+ // All modes follow vm.clock so headless runs see time pass consistently.
72
+ const now = new Date(bootDate + vm.clock());
73
+ switch (mode) {
74
+ case 0: return Math.floor((vm.clock() * 60) / 1000) & 0xffff; // ticks (1/60 s)
75
+ case 1: return ((now.getHours() % 12) << 12) | (now.getMinutes() << 6) | now.getSeconds();
76
+ case 2: return (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1);
77
+ default: return ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
78
+ }
79
+ },
80
+ Platform: (_vm, [op = 0]) => (op === 0 ? 1 /* DOS */ : op === 2 ? 2 /* 256 colours */ : 0),
81
+ };
82
+ const rad = (deg) => (toSigned(deg) * Math.PI) / 180;
83
+ /** SCI angles: 0 = north (up), clockwise, 0..359. */
84
+ function sciAngle(dx, dy) {
85
+ const deg = (Math.atan2(dx, -dy) * 180) / Math.PI;
86
+ return Math.round((deg + 360) % 360);
87
+ }
@@ -0,0 +1,91 @@
1
+ import { ResourceType } from "../../resource/types.ts";
2
+ import type { KernelFn } from "../vm.ts";
3
+ import { NULL, bool, fromInt, toSigned } from "../value.ts";
4
+
5
+ /** Script, object and resource management. */
6
+ export const coreKernels: Record<string, KernelFn> = {
7
+ // Resource loading is eager in this engine, so these only need to answer sensibly.
8
+ Load: (_vm, [, number = 0]) => number,
9
+ Unload: () => 0,
10
+ Lock: () => 0,
11
+ Purge: () => 0,
12
+ ResCheck: (vm, [type = 0, number = 0]) => bool(vm.resources.has({ type: (type & 0x7f) as ResourceType, number })),
13
+
14
+ ScriptID: (vm, [script = 0, index = 0]) => {
15
+ if (!vm.resources.has({ type: ResourceType.Script, number: script })) return NULL;
16
+ return vm.exportAddress(script, index);
17
+ },
18
+ // Unloads the script (see Vm.disposeScript) and returns its optional second argument:
19
+ // scripts use it to hand a value back from code that's about to be unloaded (SRDialog
20
+ // returns the chosen save id this way).
21
+ DisposeScript: (vm, args) => {
22
+ vm.disposeScript(args[0]! & 0xffff);
23
+ return args.length >= 2 ? args[1] : undefined;
24
+ },
25
+
26
+ Clone: (vm, [obj = 0]) => vm.memory.clone(vm.object(obj)).address,
27
+ DisposeClone: (vm, [obj = 0]) => {
28
+ const o = vm.memory.object(obj);
29
+ if (o) vm.memory.disposeClone(o);
30
+ },
31
+ RespondsTo: (vm, [obj = 0, selector = 0]) => {
32
+ const o = vm.memory.object(obj);
33
+ return bool(!!o && vm.respondsTo(o, selector));
34
+ },
35
+ };
36
+
37
+ let seed = 0x1234;
38
+ const bootDate = Date.now();
39
+
40
+ /** Maths, time and the host platform. */
41
+ export const mathKernels: Record<string, KernelFn> = {
42
+ Random: (_vm, args) => {
43
+ if (args.length === 1) {
44
+ seed = args[0]!; // seed the generator
45
+ return 0;
46
+ }
47
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
48
+ if (args.length < 2) return (seed >> 8) & 0xffff;
49
+ const lo = toSigned(args[0]!), hi = toSigned(args[1]!);
50
+ return fromInt(lo + ((seed >> 8) % (hi - lo + 1)));
51
+ },
52
+ Abs: (_vm, [n = 0]) => fromInt(Math.abs(toSigned(n))),
53
+ Sqrt: (_vm, [n = 0]) => Math.floor(Math.sqrt(Math.abs(toSigned(n)))),
54
+ GetAngle: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0]) => sciAngle(toSigned(x2) - toSigned(x1), toSigned(y2) - toSigned(y1)),
55
+ GetDistance: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0, perspective = 0]) => {
56
+ const dx = toSigned(x2) - toSigned(x1);
57
+ let dy = toSigned(y2) - toSigned(y1);
58
+ if (perspective) dy = Math.trunc((dy * 3) / 2);
59
+ return Math.round(Math.sqrt(dx * dx + dy * dy)) & 0xffff;
60
+ },
61
+ ATan: (_vm, [x1 = 0, y1 = 0, x2 = 0, y2 = 0]) => sciAngle(toSigned(x2) - toSigned(x1), toSigned(y2) - toSigned(y1)),
62
+ SinMult: (_vm, [angle = 0, n = 0]) => fromInt(Math.round(Math.sin(rad(angle)) * toSigned(n))),
63
+ CosMult: (_vm, [angle = 0, n = 0]) => fromInt(Math.round(Math.cos(rad(angle)) * toSigned(n))),
64
+ SinDiv: (_vm, [angle = 0, n = 0]) => {
65
+ const s = Math.sin(rad(angle));
66
+ return s === 0 ? 0 : fromInt(Math.round(toSigned(n) / s));
67
+ },
68
+ CosDiv: (_vm, [angle = 0, n = 0]) => {
69
+ const c = Math.cos(rad(angle));
70
+ return c === 0 ? 0 : fromInt(Math.round(toSigned(n) / c));
71
+ },
72
+ GetTime: (vm, [mode = 0]) => {
73
+ // All modes follow vm.clock so headless runs see time pass consistently.
74
+ const now = new Date(bootDate + vm.clock());
75
+ switch (mode) {
76
+ case 0: return Math.floor((vm.clock() * 60) / 1000) & 0xffff; // ticks (1/60 s)
77
+ case 1: return ((now.getHours() % 12) << 12) | (now.getMinutes() << 6) | now.getSeconds();
78
+ case 2: return (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1);
79
+ default: return ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
80
+ }
81
+ },
82
+ Platform: (_vm, [op = 0]) => (op === 0 ? 1 /* DOS */ : op === 2 ? 2 /* 256 colours */ : 0),
83
+ };
84
+
85
+ const rad = (deg: number) => (toSigned(deg) * Math.PI) / 180;
86
+
87
+ /** SCI angles: 0 = north (up), clockwise, 0..359. */
88
+ function sciAngle(dx: number, dy: number): number {
89
+ const deg = (Math.atan2(dx, -dy) * 180) / Math.PI;
90
+ return Math.round((deg + 360) % 360);
91
+ }
@@ -0,0 +1,141 @@
1
+ import type { Cel } from "../../gfx/cel.ts";
2
+ import { type Palette } from "../../gfx/palette.ts";
3
+ import { type Pic } from "../../gfx/pic.ts";
4
+ import { type View } from "../../gfx/view.ts";
5
+ import { type KernelFn, type Vm } from "../vm.ts";
6
+ import { type Value } from "../value.ts";
7
+ export declare const SCREEN_WIDTH = 320;
8
+ export declare const SCREEN_HEIGHT = 200;
9
+ /**
10
+ * SCI32 graphics model: the scripts create Plane objects (layers with a rect, a background
11
+ * pic or colour, and a priority) and register View-like objects as screen items in a plane.
12
+ * Each FrameOut composes planes by priority, then items within a plane by priority.
13
+ * The kernel only remembers *which* objects are registered; their current state (x, y,
14
+ * view, loop, cel...) is read from the objects' properties at compose time.
15
+ */
16
+ export declare class Graphics {
17
+ private readonly vm;
18
+ readonly planes: Set<number>;
19
+ readonly items: Set<number>;
20
+ frames: number;
21
+ private readonly views;
22
+ private readonly pics;
23
+ readonly basePalette: Palette;
24
+ /** Called on every FrameOut with the composed frame. */
25
+ onFrame?: (frame: Frame) => void;
26
+ /** Drops parsed views and pictures (their resources were replaced). */
27
+ forgetResources(): void;
28
+ constructor(vm: Vm);
29
+ /**
30
+ * View numbers are unsigned (SCI32's system views live at 64xxx), but scripts store them
31
+ * in signed properties; only 0xFFFF (-1) means "no view".
32
+ */
33
+ view(n: number): View | undefined;
34
+ pic(n: number): Pic | undefined;
35
+ cel(viewNo: number, loopNo: number, celNo: number): {
36
+ cel: Cel;
37
+ mirror: boolean;
38
+ } | undefined;
39
+ /** Reads a numeric property (signed), 0 if absent. */
40
+ prop(obj: Value, name: string): number;
41
+ /** Palette built from every palette submitted so far (base, scripts, pics, views). */
42
+ private source;
43
+ /** Picture each plane last submitted, and view each item last submitted. */
44
+ private readonly planePictures;
45
+ private readonly itemViews;
46
+ /** The palette after PalVary/PalCycle (before fades), as of the last frame. */
47
+ currentPalette: Palette;
48
+ /** Active show-style transitions, by plane. */
49
+ readonly transitions: Map<number, Transition>;
50
+ /** The mouse cursor: a view cel drawn on top at the mouse position (its origin = hotspot). */
51
+ cursor: {
52
+ view: number;
53
+ loop: number;
54
+ cel: number;
55
+ visible: boolean;
56
+ x: number;
57
+ y: number;
58
+ };
59
+ /** Palette range affected by fade show styles (SetPalStyleRange). */
60
+ styleRange: {
61
+ from: number;
62
+ to: number;
63
+ };
64
+ lastFrame: Frame | undefined;
65
+ get sourcePalette(): Palette | undefined;
66
+ set sourcePalette(pal: Palette | undefined);
67
+ submitPalette(pal: Palette | undefined): void;
68
+ /** Forgets submitted palettes (restart/restore); they're resubmitted as things are drawn. */
69
+ resetPalette(): void;
70
+ /**
71
+ * Where a view item's cel lands, in its plane's coordinates. Scripts scale cels for
72
+ * perspective (the hero shrinks with distance) and to stretch UI backgrounds (dialog boxes)
73
+ * to fit their contents.
74
+ */
75
+ viewRect(item: Value): {
76
+ cel: Cel;
77
+ mirror: boolean;
78
+ sx: number;
79
+ sy: number;
80
+ x: number;
81
+ y: number;
82
+ width: number;
83
+ height: number;
84
+ } | undefined;
85
+ /**
86
+ * Composes the current frame in three phases, because remap colours (shadows) depend on
87
+ * the final palette: collect what each plane draws (submitting palettes as pictures and
88
+ * views change, like the original), apply palette effects, then blit.
89
+ */
90
+ compose(): Frame;
91
+ }
92
+ type Clip = {
93
+ left: number;
94
+ top: number;
95
+ right: number;
96
+ bottom: number;
97
+ };
98
+ export interface Frame {
99
+ width: number;
100
+ height: number;
101
+ pixels: Uint8Array;
102
+ palette: Palette;
103
+ /** Set during wipes/dissolves, which mix two differently-coloured images: RGB per pixel. */
104
+ rgb?: Uint8Array;
105
+ }
106
+ /** SCI32 show styles (SetShowStyle): how a plane's new content appears. */
107
+ export declare const ShowStyle: {
108
+ readonly None: 0;
109
+ readonly HShutterOut: 1;
110
+ readonly HShutterIn: 2;
111
+ readonly VShutterOut: 3;
112
+ readonly VShutterIn: 4;
113
+ readonly WipeLeft: 5;
114
+ readonly WipeRight: 6;
115
+ readonly WipeUp: 7;
116
+ readonly WipeDown: 8;
117
+ readonly IrisOut: 9;
118
+ readonly IrisIn: 10;
119
+ readonly DissolveNoMorph: 11;
120
+ readonly Dissolve: 12;
121
+ readonly FadeOut: 13;
122
+ readonly FadeIn: 14;
123
+ };
124
+ interface Transition {
125
+ style: number;
126
+ start: number;
127
+ duration: number;
128
+ rect: Clip;
129
+ /** The screen as it was when the transition started (RGB), for wipes and dissolves. */
130
+ from: Uint8Array | undefined;
131
+ }
132
+ /** Converts an indexed frame to RGB. */
133
+ export declare function frameRgb(f: Frame): Uint8Array;
134
+ /** View cel anchor point, relative to the cel's top-left. */
135
+ export declare function celOrigin(cel: Cel, mirror?: boolean): {
136
+ x: number;
137
+ y: number;
138
+ };
139
+ export declare const graphics: (vm: Vm) => Graphics;
140
+ export declare const graphicsKernels: Record<string, KernelFn>;
141
+ export {};