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,886 @@
1
+ import { LineCounter, parseDocument } from "yaml";
2
+ import { parseYarn } from "./yarn.js";
3
+ export class ContentError extends Error {
4
+ }
5
+ /** Compiles `<n>.room.yaml` (+ `<n>.yarn`) source text. File names only label errors. */
6
+ export function compileRoom(yamlSource, yarnSource, target, files = {}, options = {}) {
7
+ const spec = readSpec(yamlSource, files.yaml ?? "<room.yaml>");
8
+ const nodes = yarnSource === undefined ? [] : parseYarn(yarnSource, files.yarn ?? "<yarn>");
9
+ const flag = options.flag ?? (() => {
10
+ throw new Error("variables need flag numbers (CompileOptions.flag)");
11
+ });
12
+ return new RoomCompiler(spec, nodes, target, files.yarn ?? "<yarn>", flag).compile();
13
+ }
14
+ // --- Reading and checking the YAML ------------------------------------------------
15
+ function readSpec(source, file) {
16
+ const lines = new LineCounter();
17
+ const doc = parseDocument(source, { lineCounter: lines, prettyErrors: true, version: "1.2" });
18
+ if (doc.errors.length)
19
+ throw new ContentError(`${file}: ${doc.errors[0].message}`);
20
+ const at = (path) => {
21
+ const node = doc.getIn(path, true);
22
+ const pos = node?.range ? lines.linePos(node.range[0]) : undefined;
23
+ return `${file}${pos ? `:${pos.line}` : ""}: ${path.join(".") || "(top)"}`;
24
+ };
25
+ const spec = doc.toJS();
26
+ new SpecChecker(spec, at, doc).check();
27
+ return spec;
28
+ }
29
+ class SpecChecker {
30
+ spec;
31
+ at;
32
+ doc;
33
+ constructor(spec, at, doc) {
34
+ this.spec = spec;
35
+ this.at = at;
36
+ this.doc = doc;
37
+ }
38
+ fail(path, why) {
39
+ throw new ContentError(`${this.at(path)}: ${why}`);
40
+ }
41
+ check() {
42
+ const s = this.spec;
43
+ if (!s || typeof s !== "object")
44
+ this.fail([], "expected a mapping (room: ..., features: ...)");
45
+ this.known(s, [], ["room", "picture", "music", "hero", "walkable", "obstacles", "features", "exits", "props", "characters", "properties"]);
46
+ this.int(s.room, ["room"], 0, 65535);
47
+ if (s.picture !== undefined)
48
+ this.int(s.picture, ["picture"], 0, 65535);
49
+ if (s.music !== undefined)
50
+ this.int(s.music, ["music"], 0, 65535);
51
+ if (s.hero !== undefined) {
52
+ const h = this.map(s.hero, ["hero"]);
53
+ this.known(h, ["hero"], ["at", "enterTo", "scale"]);
54
+ if (h.at !== undefined)
55
+ this.point(h.at, ["hero", "at"]);
56
+ if (h.enterTo !== undefined)
57
+ this.point(h.enterTo, ["hero", "enterTo"]);
58
+ if (h.scale !== undefined) {
59
+ const sc = this.map(h.scale, ["hero", "scale"]);
60
+ this.known(sc, ["hero", "scale"], ["front", "back", "frontY", "backY"]);
61
+ for (const k of ["front", "back", "frontY", "backY"])
62
+ this.int(sc[k], ["hero", "scale", k], 0, 1000);
63
+ }
64
+ }
65
+ if (s.walkable !== undefined)
66
+ this.polygon(s.walkable, ["walkable"]);
67
+ if (s.obstacles !== undefined) {
68
+ if (!Array.isArray(s.obstacles))
69
+ this.fail(["obstacles"], "expected a list of polygons");
70
+ s.obstacles.forEach((p, i) => this.polygon(p, ["obstacles", i]));
71
+ }
72
+ const names = new Set(["room"]);
73
+ for (const group of ["features", "exits", "props"]) {
74
+ if (s[group] === undefined)
75
+ continue;
76
+ const things = this.map(s[group], [group]);
77
+ for (const [name, value] of Object.entries(things)) {
78
+ const path = [group, name];
79
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name))
80
+ this.fail(path, "names are letters and digits, starting with a letter");
81
+ if (names.has(name))
82
+ this.fail(path, `"${name}" is already used`);
83
+ names.add(name);
84
+ const t = this.map(value, path);
85
+ if (group === "props") {
86
+ this.known(t, path, ["view", "at", "loop", "cel", "cycle", "sightAngle", "moves"]);
87
+ if (t.moves !== undefined && typeof t.moves !== "boolean")
88
+ this.fail([...path, "moves"], "expected true or false");
89
+ this.int(t.view, [...path, "view"], 0, 65535);
90
+ this.point(t.at, [...path, "at"]);
91
+ if (t.loop !== undefined)
92
+ this.int(t.loop, [...path, "loop"], 0, 255);
93
+ if (t.cel !== undefined)
94
+ this.int(t.cel, [...path, "cel"], 0, 255);
95
+ if (t.cycle !== undefined && t.cycle !== "forward")
96
+ this.fail([...path, "cycle"], `unknown cycle "${String(t.cycle)}" (supported: forward)`);
97
+ }
98
+ else {
99
+ const keys = ["rect", "at", "approach", "sightAngle", ...(group === "exits" ? ["to", "walkTo"] : [])];
100
+ this.known(t, path, keys);
101
+ this.rect(t.rect, [...path, "rect"]);
102
+ if (t.at !== undefined)
103
+ this.point(t.at, [...path, "at"]);
104
+ if (t.approach !== undefined)
105
+ this.point(t.approach, [...path, "approach"]);
106
+ if (group === "exits") {
107
+ this.int(t.to, [...path, "to"], 0, 65535);
108
+ if (t.walkTo !== undefined)
109
+ this.point(t.walkTo, [...path, "walkTo"]);
110
+ }
111
+ }
112
+ if (t.sightAngle !== undefined)
113
+ this.int(t.sightAngle, [...path, "sightAngle"], 0, 360);
114
+ }
115
+ }
116
+ if (s.characters !== undefined) {
117
+ for (const [key, value] of Object.entries(this.map(s.characters, ["characters"]))) {
118
+ const path = ["characters", key];
119
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(key))
120
+ this.fail(path, "names are letters and digits, starting with a letter");
121
+ if (["narrator", "hero"].includes(key.toLowerCase()))
122
+ this.fail(path, `"${key}" is built in`);
123
+ const c = this.map(value ?? {}, path);
124
+ this.known(c, path, ["name", "portrait"]);
125
+ if (c.portrait !== undefined)
126
+ this.int(c.portrait, [...path, "portrait"], 0, 65535);
127
+ if (c.name !== undefined && (typeof c.name !== "string" || !/^[\x20-\x7e]{1,40}$/.test(c.name))) {
128
+ this.fail([...path, "name"], "expected plain text, up to 40 characters");
129
+ }
130
+ }
131
+ }
132
+ if (s.properties !== undefined) {
133
+ const props = this.map(s.properties, ["properties"]);
134
+ for (const [name, values] of Object.entries(props)) {
135
+ if (!names.has(name))
136
+ this.fail(["properties", name], `no feature, exit or prop called "${name}" (the room is "room")`);
137
+ for (const [k, v] of Object.entries(this.map(values, ["properties", name])))
138
+ this.int(v, ["properties", name, k], -32768, 65535);
139
+ }
140
+ }
141
+ }
142
+ known(o, path, keys) {
143
+ for (const k of Object.keys(o))
144
+ if (!keys.includes(k))
145
+ this.fail([...path, k], `unknown key (expected one of: ${keys.join(", ")})`);
146
+ }
147
+ map(v, path) {
148
+ if (!v || typeof v !== "object" || Array.isArray(v))
149
+ this.fail(path, "expected a mapping (key: value)");
150
+ return v;
151
+ }
152
+ int(v, path, min, max) {
153
+ if (v === undefined)
154
+ this.fail(path, "missing");
155
+ if (!Number.isInteger(v) || v < min || v > max)
156
+ this.fail(path, `expected a whole number from ${min} to ${max}, got ${JSON.stringify(v)}`);
157
+ }
158
+ point(v, path) {
159
+ if (!Array.isArray(v) || v.length !== 2)
160
+ this.fail(path, "expected [x, y]");
161
+ v.forEach((n, i) => this.int(n, [...path, i], -1000, 1000));
162
+ }
163
+ rect(v, path) {
164
+ if (!Array.isArray(v) || v.length !== 4)
165
+ this.fail(path, "expected [left, top, right, bottom]");
166
+ v.forEach((n, i) => this.int(n, [...path, i], -1000, 1000));
167
+ if (v[0] > v[2] || v[1] > v[3])
168
+ this.fail(path, "left must be <= right and top <= bottom");
169
+ }
170
+ polygon(v, path) {
171
+ if (!Array.isArray(v) || v.length < 3)
172
+ this.fail(path, "expected a list of at least 3 [x, y] points");
173
+ v.forEach((p, i) => this.point(p, [...path, i]));
174
+ }
175
+ }
176
+ const num = (n) => (n < 0 ? `-$${(-n).toString(16)}` : String(n));
177
+ function pushVal(v) {
178
+ if (typeof v === "number")
179
+ return [v === 0 ? "push0" : v === 1 ? "push1" : v === 2 ? "push2" : `pushi ${num(v)}`];
180
+ if (v === "self")
181
+ return ["pushSelf"];
182
+ if ("obj" in v)
183
+ return [`lofsa @${v.obj}`, "push"];
184
+ if ("cls" in v)
185
+ return [`class ${v.cls}`, "push"];
186
+ return [...v.code, "push"];
187
+ }
188
+ /** `to sel1: args..., sel2: args...` as one send. */
189
+ function send(to, calls) {
190
+ const out = [];
191
+ let words = 0;
192
+ for (const [sel, args] of calls) {
193
+ out.push(`pushi #${sel}`, ...pushVal(args.length));
194
+ for (const a of args)
195
+ out.push(...pushVal(a));
196
+ words += 2 + args.length;
197
+ }
198
+ const bytes = words * 2;
199
+ if (to === "self")
200
+ out.push(`self ${bytes}`);
201
+ else if ("superOf" in to)
202
+ out.push(`super ${to.superOf} ${bytes}`);
203
+ else {
204
+ out.push("global" in to ? `lag ${to.global}` : "obj" in to ? `lofsa @${to.obj}` : `class ${to.cls}`);
205
+ out.push(`send ${bytes}`);
206
+ }
207
+ return out;
208
+ }
209
+ class RoomCompiler {
210
+ spec;
211
+ nodes;
212
+ target;
213
+ yarnFile;
214
+ flagNumber;
215
+ name;
216
+ nouns = new Map();
217
+ messages = [];
218
+ instances = [];
219
+ code = [];
220
+ strings = [];
221
+ conversations = [];
222
+ /** Characters by lower-case key and name -> talker number. */
223
+ talkers = new Map();
224
+ /** Verbs whose Yarn sets flags or branches, per thing: verb -> Script. */
225
+ scriptedVerbs = new Map();
226
+ /** Scripts compiled from Yarn: name, steps, and what ends them. */
227
+ sequences = [];
228
+ flagsUsed = new Set();
229
+ nextNoun = 0;
230
+ nextLabel = 0;
231
+ constructor(spec, nodes, target, yarnFile, flagNumber) {
232
+ this.spec = spec;
233
+ this.nodes = nodes;
234
+ this.target = target;
235
+ this.yarnFile = yarnFile;
236
+ this.flagNumber = flagNumber;
237
+ this.name = `rm${spec.room}`;
238
+ }
239
+ compile() {
240
+ const { spec, target } = this;
241
+ const g = target.globals;
242
+ // Nouns: the room first, then everything clickable in the order written.
243
+ this.nouns.set("room", 1);
244
+ for (const name of [...Object.keys(spec.features ?? {}), ...Object.keys(spec.exits ?? {}), ...Object.keys(spec.props ?? {})]) {
245
+ this.nouns.set(name, this.nouns.size + 1);
246
+ }
247
+ this.nextNoun = this.nouns.size + 1;
248
+ const characters = Object.entries(spec.characters ?? {});
249
+ characters.forEach(([key, c], i) => {
250
+ const talker = target.firstRoomTalker + i;
251
+ this.talkers.set(key.toLowerCase(), talker);
252
+ if (c?.name)
253
+ this.talkers.set(c.name.toLowerCase(), talker);
254
+ });
255
+ const enter = this.readNodes();
256
+ // Arrival: walk in (PolyPath cues the script), then room.enter, then hands back.
257
+ const enterSteps = [];
258
+ if (spec.hero?.enterTo) {
259
+ enterSteps.push({ op: "async", code: send({ global: g.ego }, [["setMotion", [{ cls: "PolyPath" }, ...spec.hero.enterTo, "self"]]]) });
260
+ }
261
+ enterSteps.push(...enter);
262
+ if (enterSteps.length) {
263
+ this.sequences.unshift({
264
+ name: "sEnter",
265
+ steps: [{ op: "code", code: send({ global: g.game }, [["handsOff", []]]) }, ...enterSteps],
266
+ end: [...send({ global: g.game }, [["handsOn", []]]), ...send("self", [["dispose", []]])],
267
+ });
268
+ }
269
+ const exitScripts = new Map();
270
+ for (const [name, exit] of Object.entries(spec.exits ?? {})) {
271
+ const script = `sExit${cap(name)}`;
272
+ exitScripts.set(name, script);
273
+ const leave = send({ global: g.curRoom }, [["newRoom", [exit.to]]]);
274
+ this.script(script, exit.walkTo
275
+ ? [
276
+ [...send({ global: g.game }, [["handsOff", []]]), ...send({ global: g.ego }, [["setMotion", [{ cls: "PolyPath" }, ...exit.walkTo, "self"]]])],
277
+ leave,
278
+ ]
279
+ : [[...send({ global: g.game }, [["handsOff", []]]), ...leave]]);
280
+ }
281
+ for (const seq of this.sequences)
282
+ this.sequenceScript(seq.name, seq.steps, seq.end);
283
+ this.roomInstance(enterSteps.length > 0, characters.length > 0);
284
+ for (const [name, f] of Object.entries(spec.features ?? {}))
285
+ this.feature(name, f);
286
+ for (const [name, e] of Object.entries(spec.exits ?? {}))
287
+ this.feature(name, e, exitScripts.get(name));
288
+ for (const [name, p] of Object.entries(spec.props ?? {}))
289
+ this.prop(name, p);
290
+ for (const [key, c] of characters)
291
+ this.character(key, c ?? {});
292
+ for (const conv of this.conversations)
293
+ this.teller(conv);
294
+ return {
295
+ number: spec.room, sca: this.assembly(), msg: this.messageText(), roomTalkers: characters.length > 0,
296
+ flags: [...this.flagsUsed].sort(),
297
+ };
298
+ }
299
+ /** Yarn nodes -> messages and scripts; returns the arrival narration's steps. */
300
+ readNodes() {
301
+ let enter = [];
302
+ const verbNames = Object.keys(this.target.verbs);
303
+ for (const node of this.nodes) {
304
+ const fail = (why, line = node.line) => {
305
+ throw new ContentError(`${this.yarnFile}:${line}: node "${node.title}": ${why}`);
306
+ };
307
+ const m = /^([A-Za-z][A-Za-z0-9]*)\.([A-Za-z][A-Za-z0-9]*)$/.exec(node.title);
308
+ if (!m)
309
+ fail("titles are <thing>.<verb>, e.g. pods.look or room.enter");
310
+ const [, thing, verbName] = m;
311
+ const noun = this.nouns.get(thing);
312
+ if (noun === undefined)
313
+ fail(`no feature, exit or prop called "${thing}" (the room itself is "room")`);
314
+ let verb;
315
+ if (verbName === "enter") {
316
+ if (thing !== "room")
317
+ fail("only the room has .enter (narration on arrival)");
318
+ verb = this.target.narrationVerb;
319
+ }
320
+ else {
321
+ const v = this.target.verbs[verbName.toLowerCase()];
322
+ if (v === undefined)
323
+ fail(`unknown verb "${verbName}" (known: ${verbNames.slice(0, 12).join(", ")}${verbNames.length > 12 ? ", ..." : ""})`);
324
+ verb = v;
325
+ }
326
+ if (!node.body.length)
327
+ fail("has no lines");
328
+ const choices = node.body.filter((x) => x.kind === "choice");
329
+ if (!choices.length) {
330
+ const plain = node.body.every((x) => x.kind === "line");
331
+ if (verbName === "enter") {
332
+ // Plain narration keeps cond 0, like any other message for the room.
333
+ if (plain) {
334
+ node.body.forEach((l, i) => this.message(noun, verb, 0, i + 1, l, node.title));
335
+ enter = [{ op: "say", noun: noun, verb, cond: 0 }];
336
+ }
337
+ else
338
+ enter = this.steps(node.body, noun, verb, counter(1), node.title);
339
+ }
340
+ else if (plain) {
341
+ // The game's doVerb says noun/verb messages by itself: no code at all.
342
+ node.body.forEach((l, i) => this.message(noun, verb, 0, i + 1, l, node.title));
343
+ }
344
+ else {
345
+ const script = `s${cap(thing)}${cap(verbName)}`;
346
+ this.sequences.push({
347
+ name: script,
348
+ steps: [{ op: "code", code: send({ global: this.target.globals.game }, [["handsOff", []]]) }, ...this.steps(node.body, noun, verb, counter(1), node.title)],
349
+ end: [...send({ global: this.target.globals.game }, [["handsOn", []]]), ...send("self", [["dispose", []]])],
350
+ });
351
+ this.scriptedVerbs.set(thing, [...(this.scriptedVerbs.get(thing) ?? []), [verb, script]]);
352
+ }
353
+ continue;
354
+ }
355
+ // A conversation: the target's topic menu (its Teller). Each choice is a topic (the hero's line), the
356
+ // lines under it the answer.
357
+ const stray = node.body.find((x) => x.kind !== "choice");
358
+ if (stray)
359
+ fail("lines or commands outside choices in a conversation aren't supported yet (put them under a choice)", stray.line);
360
+ if (thing === "room" || verbName === "enter")
361
+ fail("conversations need a feature or prop to talk to");
362
+ if (choices.length > 140)
363
+ fail("too many choices (140 at most)");
364
+ const conv = { thing: thing, opensWith: verb, rootNoun: this.nextNoun++, sayNoun: this.nextNoun++, conditional: [], scripted: [] };
365
+ this.conversations.push(conv);
366
+ const tv = this.target.teller.verb;
367
+ let blockNoun;
368
+ const blockConds = counter(1);
369
+ choices.forEach((c, k) => {
370
+ if (!c.body.length)
371
+ fail(`choice "${c.text}" needs at least one line under it`, c.line);
372
+ const topic = k + 1;
373
+ const label = `${node.title}: ${c.text}`;
374
+ this.message(conv.rootNoun, tv, topic, 1, { kind: "line", speaker: "Hero", text: c.text, line: c.line }, label);
375
+ if (c.when)
376
+ conv.conditional.push({ topic, when: c.when, where: `${this.yarnFile}:${c.line}` });
377
+ if (c.body.every((x) => x.kind === "line")) {
378
+ c.body.forEach((l, j) => this.message(conv.sayNoun, tv, topic, j + 1, l, label));
379
+ return;
380
+ }
381
+ // Answered by a script, run by the room with the teller as its caller: when it's
382
+ // done it cues the teller, which shows the menu again.
383
+ blockNoun ??= this.nextNoun++;
384
+ const script = `s${cap(thing)}Topic${topic}`;
385
+ conv.scripted.push({ topic, script });
386
+ this.sequences.push({ name: script, steps: this.steps(c.body, blockNoun, tv, blockConds, label), end: send("self", [["dispose", []]]) });
387
+ });
388
+ }
389
+ return enter;
390
+ }
391
+ /** Yarn items -> steps; runs of lines become one message tuple (one say) each. */
392
+ steps(items, noun, verb, conds, label) {
393
+ const out = [];
394
+ let lines = [];
395
+ const flush = () => {
396
+ if (!lines.length)
397
+ return;
398
+ const cond = conds();
399
+ if (cond > 255)
400
+ throw new ContentError(`${this.yarnFile}:${lines[0].line}: node "${label}" has too many separate passages (255 at most)`);
401
+ lines.forEach((l, i) => this.message(noun, verb, cond, i + 1, l, label));
402
+ out.push({ op: "say", noun, verb, cond });
403
+ lines = [];
404
+ };
405
+ for (const item of items) {
406
+ if (item.kind === "line") {
407
+ lines.push(item);
408
+ continue;
409
+ }
410
+ flush();
411
+ const where = `${this.yarnFile}:${item.line}`;
412
+ if (item.kind === "set")
413
+ out.push({ op: "set", name: item.name, value: item.value, where });
414
+ else if (item.kind === "if") {
415
+ const end = this.label();
416
+ for (const branch of item.branches) {
417
+ const next = this.label();
418
+ if (branch.when)
419
+ out.push({ op: "unless", when: branch.when, label: next, where });
420
+ out.push(...this.steps(branch.body, noun, verb, conds, label), { op: "jump", label: end }, { op: "label", name: next });
421
+ }
422
+ out.push({ op: "label", name: end });
423
+ }
424
+ else if (item.kind === "command")
425
+ out.push(...this.command(item, where));
426
+ else
427
+ throw new ContentError(`${where}: choices can only be the whole of a conversation node`);
428
+ }
429
+ flush();
430
+ return out;
431
+ }
432
+ /**
433
+ * Cutscene commands. Those that take time (walking, turning, waiting, a one-shot
434
+ * animation) cue the script when done; the rest happen at once.
435
+ */
436
+ command(cmd, where) {
437
+ const g = this.target.globals;
438
+ const { name, args } = cmd;
439
+ const fail = (why) => {
440
+ throw new ContentError(`${where}: <<${name}${args.length ? ` ${args.join(" ")}` : ""}>>: ${why}`);
441
+ };
442
+ const arity = (n, usage) => args.length !== n && fail(`expected <<${name} ${usage}>>`);
443
+ const int = (s, what, min = -1000, max = 1000) => {
444
+ const n = Number(s);
445
+ if (!Number.isInteger(n) || n < min || n > max)
446
+ fail(`${what} must be a whole number from ${min} to ${max}`);
447
+ return n;
448
+ };
449
+ const props = this.spec.props ?? {};
450
+ /** hero, or a prop (`moving`: one with moves: true). */
451
+ const who = (s, moving) => {
452
+ if (s === "hero")
453
+ return { global: g.ego };
454
+ const p = s === undefined ? undefined : props[s];
455
+ if (!p)
456
+ fail(`"${s}" isn't hero or a prop of this room`);
457
+ if (moving && !p.moves)
458
+ fail(`"${s}" doesn't move: give it moves: true in the room's YAML`);
459
+ return { obj: s };
460
+ };
461
+ const thingAt = (s) => {
462
+ const p = props[s];
463
+ if (p)
464
+ return p.at;
465
+ const f = this.spec.features?.[s] ?? this.spec.exits?.[s];
466
+ if (!f)
467
+ return undefined;
468
+ return f.at ?? [Math.round((f.rect[0] + f.rect[2]) / 2), Math.round((f.rect[1] + f.rect[3]) / 2)];
469
+ };
470
+ switch (name) {
471
+ case "walk": {
472
+ arity(3, "hero|<prop> x y");
473
+ const to = who(args[0], true);
474
+ const motion = args[0] === "hero" ? "PolyPath" : "MoveTo";
475
+ return [{ op: "async", code: send(to, [["setMotion", [{ cls: motion }, int(args[1], "x"), int(args[2], "y"), "self"]]]) }];
476
+ }
477
+ case "face": {
478
+ if (args.length < 2 || args.length > 3)
479
+ fail("expected <<face hero|<prop> up|down|left|right|<thing>|x y>>");
480
+ const to = who(args[0], true);
481
+ const directions = { up: 0, upright: 45, right: 90, downright: 135, down: 180, downleft: 225, left: 270, upleft: 315 };
482
+ let heading;
483
+ // Props (which may have moved) are faced where they are now; features where they're drawn.
484
+ const pos = (of, sel) => [...send(of, [[sel, []]]), "push"];
485
+ const target = args.length === 3
486
+ ? [...pushVal(int(args[1], "x")), ...pushVal(int(args[2], "y"))]
487
+ : props[args[1]]
488
+ ? [...pos({ obj: args[1] }, "x"), ...pos({ obj: args[1] }, "y")]
489
+ : thingAt(args[1])?.flatMap((n) => pushVal(n));
490
+ if (args.length === 2 && directions[args[1]] !== undefined)
491
+ heading = directions[args[1]];
492
+ else if (target) {
493
+ // GetAngle from where it stands now to the target.
494
+ heading = { code: ["pushi 4", ...pos(to, "x"), ...pos(to, "y"), ...target, "callk GetAngle 8"] };
495
+ }
496
+ else {
497
+ return fail(`expected a direction (${Object.keys(directions).join(", ")}), a thing of this room, or x y`);
498
+ }
499
+ return [{ op: "async", code: send(to, [["setHeading", [heading, "self"]]]) }];
500
+ }
501
+ case "wait": {
502
+ arity(1, "seconds");
503
+ const secs = Number(args[0]);
504
+ if (!(secs > 0 && secs <= 600))
505
+ fail("seconds must be a number from 0 to 600 (e.g. 1.5)");
506
+ return [{ op: "async", code: [`ldi ${Math.max(1, Math.round(secs * 60))}`, "aTop ticks"] }];
507
+ }
508
+ case "hide":
509
+ case "show":
510
+ arity(1, "hero|<prop>");
511
+ return [{ op: "code", code: send(who(args[0], false), [[name, []]]) }];
512
+ case "loop":
513
+ case "cel":
514
+ arity(2, `hero|<prop> ${name}`);
515
+ return [{ op: "code", code: send(who(args[0], false), [[name === "loop" ? "setLoop" : "setCel", [int(args[1], name, 0, 255)]]]) }];
516
+ case "animate": {
517
+ arity(2, "<prop> once|forever");
518
+ const to = who(args[0], false);
519
+ if (args[1] === "once")
520
+ return [{ op: "async", code: send(to, [["setCel", [0]], ["setCycle", [{ cls: "End" }, "self"]]]) }];
521
+ if (args[1] === "forever")
522
+ return [{ op: "code", code: send(to, [["setCycle", [{ cls: this.target.forwardCycle }]]]) }];
523
+ return fail("expected once or forever");
524
+ }
525
+ case "stop":
526
+ arity(1, "<prop>");
527
+ return [{ op: "code", code: send(who(args[0], false), [["setCycle", [0]]]) }];
528
+ case "music":
529
+ arity(1, "number|stop");
530
+ return [{ op: "code", code: args[0] === "stop" ? send({ global: g.music }, [["stop", []]]) : this.playMusic(int(args[0], "music", 0, 65535)) }];
531
+ case "sound": {
532
+ if (args.length < 1 || args.length > 2 || (args[1] !== undefined && args[1] !== "wait"))
533
+ fail("expected <<sound number>> or <<sound number wait>>");
534
+ const n = int(args[0], "sound", 0, 65535);
535
+ // wait: the sound cues the script when it ends.
536
+ return args[1] === "wait"
537
+ ? [{ op: "async", code: send({ global: g.sound }, [["number", [n]], ["loop", [1]], ["play", ["self"]]]) }]
538
+ : [{ op: "code", code: send({ global: g.sound }, [["number", [n]], ["loop", [1]], ["play", []]]) }];
539
+ }
540
+ case "room":
541
+ arity(1, "number");
542
+ return [{ op: "code", code: send({ global: g.curRoom }, [["newRoom", [int(args[0], "room", 0, 65535)]]]) }];
543
+ default:
544
+ return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, room; and set, if)");
545
+ }
546
+ }
547
+ /** Loop music n, unless it's what's playing already. */
548
+ playMusic(n) {
549
+ const music = { global: this.target.globals.music };
550
+ const playing = this.label();
551
+ return [
552
+ ...send(music, [["number", []]]), "push", `ldi ${n}`, "eq?", `bt ${playing}`,
553
+ ...send(music, [["number", [n]], ["setLoop", [-1]], ["play", []]]), `${playing}:`,
554
+ ];
555
+ }
556
+ label() {
557
+ return `.c${this.nextLabel++}`;
558
+ }
559
+ /** A Script running `steps`, then `end`. */
560
+ sequenceScript(name, steps, end) {
561
+ // Where states begin: the start, after each say/async, and at labels.
562
+ const labelState = new Map();
563
+ let state = 0, empty = true;
564
+ for (const step of steps) {
565
+ if (step.op === "label") {
566
+ if (!empty)
567
+ (state++, (empty = true));
568
+ labelState.set(step.name, state);
569
+ continue;
570
+ }
571
+ empty = false;
572
+ if (step.op === "say" || step.op === "async")
573
+ (state++, (empty = true));
574
+ }
575
+ // Jumping sets the state and cues; the rest of this state's code must not run.
576
+ const jump = (label) => [`ldi ${labelState.get(label) - 1}`, "aTop state", ...send("self", [["cue", []]]), `jmp .${name}_done`];
577
+ const states = [];
578
+ let code = [];
579
+ empty = true;
580
+ const close = () => {
581
+ states.push(code);
582
+ code = [];
583
+ empty = true;
584
+ };
585
+ for (const step of steps) {
586
+ switch (step.op) {
587
+ case "label":
588
+ if (!empty)
589
+ (code.push(...jump(step.name)), close());
590
+ break;
591
+ case "say":
592
+ code.push(...send({ global: this.target.globals.messager }, [["say", [step.noun, step.verb, step.cond, 0, "self"]]]));
593
+ close();
594
+ break;
595
+ case "async":
596
+ code.push(...step.code);
597
+ close();
598
+ break;
599
+ case "code":
600
+ code.push(...step.code);
601
+ empty = false;
602
+ break;
603
+ case "set":
604
+ code.push(...this.setFlag(step.name, step.value, step.where));
605
+ empty = false;
606
+ break;
607
+ case "unless": {
608
+ const holds = this.label();
609
+ code.push(...this.expr(step.when, step.where), `bt ${holds}`, ...jump(step.label), `${holds}:`);
610
+ empty = false;
611
+ break;
612
+ }
613
+ case "jump":
614
+ code.push(...jump(step.label));
615
+ empty = false;
616
+ break;
617
+ }
618
+ }
619
+ code.push(...end);
620
+ states.push(code);
621
+ this.script(name, states);
622
+ }
623
+ // --- Flags ---
624
+ flag(name, where) {
625
+ this.flagsUsed.add(name);
626
+ try {
627
+ return this.flagNumber(name);
628
+ }
629
+ catch (e) {
630
+ throw new ContentError(`${where}: $${name}: ${e.message}`);
631
+ }
632
+ }
633
+ flagCall(kind, flag) {
634
+ const f = this.target.flags;
635
+ const call = f.script === 0 ? `callb ${f[kind]} 2` : `calle ${f.script} ${f[kind]} 2`;
636
+ return ["push1", `pushi ${flag}`, call];
637
+ }
638
+ /** The condition's value in acc (non-zero: holds). */
639
+ expr(e, where) {
640
+ switch (e.kind) {
641
+ case "const":
642
+ return [`ldi ${e.value ? 1 : 0}`];
643
+ case "var":
644
+ return this.flagCall("test", this.flag(e.name, where));
645
+ case "not":
646
+ return [...this.expr(e.of, where), "not"];
647
+ case "and":
648
+ case "or": {
649
+ const done = this.label();
650
+ return [...this.expr(e.a, where), `${e.kind === "and" ? "bnt" : "bt"} ${done}`, ...this.expr(e.b, where), `${done}:`];
651
+ }
652
+ }
653
+ }
654
+ setFlag(name, value, where) {
655
+ const flag = this.flag(name, where);
656
+ if (value.kind === "const")
657
+ return this.flagCall(value.value ? "set" : "clear", flag);
658
+ const clear = this.label(), done = this.label();
659
+ return [...this.expr(value, where), `bnt ${clear}`, ...this.flagCall("set", flag), `jmp ${done}`, `${clear}:`, ...this.flagCall("clear", flag), `${done}:`];
660
+ }
661
+ // --- Objects ---
662
+ message(noun, verb, cond, seq, l, label) {
663
+ const where = `${this.yarnFile}:${l.line}`;
664
+ this.messages.push({ noun, verb, cond, seq, talker: this.talkerOf(l.speaker, where), text: plainText(l.text, where), label });
665
+ }
666
+ talkerOf(speaker, where) {
667
+ if (speaker === undefined)
668
+ return this.target.narrator;
669
+ const s = speaker.toLowerCase();
670
+ if (s === "narrator")
671
+ return this.target.narrator;
672
+ if (s === "hero")
673
+ return this.target.heroTalker;
674
+ const t = this.talkers.get(s);
675
+ if (t !== undefined)
676
+ return t;
677
+ const known = ["Narrator", "Hero", ...Object.keys(this.spec.characters ?? {})];
678
+ throw new ContentError(`${where}: unknown speaker "${speaker}" (add them under characters: in the room; known: ${known.join(", ")})`);
679
+ }
680
+ instance(label, cls, props, methods = [], shownName = label) {
681
+ const overrides = this.spec.properties?.[label === this.name ? "room" : label] ?? {};
682
+ const all = { classScript: 0, info: 0, name: `@s_${label}`, ...props, ...overrides };
683
+ this.instances.push(`instance ${label} of ${cls}`, ...Object.entries(all).map(([k, v]) => ` ${k} ${typeof v === "number" ? num(v) : v}`), ...methods.map((m) => ` method ${m} ${label}::${m}`), "");
684
+ this.strings.push(` s_${label} "${shownName.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
685
+ }
686
+ roomInstance(hasEnterScript, hasCharacters) {
687
+ const { spec, target } = this;
688
+ const verbs = this.scriptedVerbs.get("room") ?? [];
689
+ this.instance(this.name, target.roomClass, { noun: 1, modNum: -1, picture: spec.picture ?? spec.room, ...target.roomDefaults }, ["init", ...(hasCharacters ? ["findTalker"] : []), ...(verbs.length ? ["doVerb"] : [])]);
690
+ if (hasCharacters)
691
+ this.findTalker();
692
+ if (verbs.length)
693
+ this.doVerb(this.name, target.roomClass, verbs);
694
+ const c = [`${this.name}::init:`];
695
+ const hero = spec.hero;
696
+ const egoCalls = [["init", []]];
697
+ if (hero?.at)
698
+ egoCalls.push(["x", [hero.at[0]]], ["y", [hero.at[1]]]);
699
+ egoCalls.push(["normalize", []]);
700
+ if (hero?.scale)
701
+ egoCalls.push(["setScaler", [{ cls: "Scaler" }, hero.scale.front, hero.scale.back, hero.scale.frontY, hero.scale.backY]]);
702
+ c.push(...send({ global: target.globals.ego }, egoCalls));
703
+ if (spec.music !== undefined)
704
+ c.push(...this.playMusic(spec.music));
705
+ for (const name of [...Object.keys(spec.features ?? {}), ...Object.keys(spec.exits ?? {})])
706
+ c.push(...send({ obj: name }, [["init", []]]));
707
+ for (const [name, p] of Object.entries(spec.props ?? {})) {
708
+ c.push(...send({ obj: name }, [["init", []], ...(p.cycle === "forward" ? [["setCycle", [{ cls: this.target.forwardCycle }]]] : [])]));
709
+ }
710
+ const polygons = [
711
+ ...(spec.walkable ? [[3, spec.walkable]] : []),
712
+ ...(spec.obstacles ?? []).map((p) => [2, p]),
713
+ ];
714
+ for (const [type, points] of polygons) {
715
+ // curRoom addObstacle: ((Polygon new) type: t, init: points..., yourself): the
716
+ // type/init/yourself words are pushed first, then `Polygon new` leaves the receiver
717
+ // in acc for the outer send.
718
+ const poly = send({ cls: "Polygon" }, [["new", []]]);
719
+ const inner = [];
720
+ let words = 0;
721
+ for (const [sel, args] of [["type", [type]], ["init", points.flat()], ["yourself", []]]) {
722
+ inner.push(`pushi #${sel}`, ...pushVal(args.length), ...args.flatMap((a) => pushVal(a)));
723
+ words += 2 + args.length;
724
+ }
725
+ c.push(`pushi #addObstacle`, "push1", ...inner, ...poly, `send ${words * 2}`, "push", `lag ${target.globals.curRoom}`, "send 6");
726
+ }
727
+ for (const conv of this.conversations) {
728
+ c.push(...send({ obj: `${conv.thing}Teller` }, [["init", [{ obj: conv.thing }, spec.room, conv.sayNoun, target.teller.verb, conv.rootNoun]]]));
729
+ }
730
+ c.push("pushi #init", "push0", "&rest 1", `super ${target.roomClass} 4`);
731
+ if (hasEnterScript)
732
+ c.push(...send("self", [["setScript", [{ obj: "sEnter" }]]]));
733
+ c.push("ret", "");
734
+ this.code.push(...c);
735
+ }
736
+ /** doVerb: given verbs run their script (in the room); anything else as the class does. */
737
+ doVerb(label, superClass, cases) {
738
+ const done = this.label();
739
+ const c = [`${label}::doVerb:`, "lsp 1"];
740
+ for (const [verb, script] of cases) {
741
+ const next = this.label();
742
+ c.push("dup", `ldi ${verb}`, "eq?", `bnt ${next}`, ...send({ global: this.target.globals.curRoom }, [["setScript", [{ obj: script }]]]), `jmp ${done}`, `${next}:`);
743
+ }
744
+ c.push("pushi #doVerb", "push1", "lsp 1", "&rest 2", `super ${superClass} 6`, `${done}:`, "toss", "ret", "");
745
+ this.code.push(...c);
746
+ }
747
+ /** A character's talker: a plain text box, or a portrait talker with its parts. */
748
+ character(key, c) {
749
+ const { target } = this;
750
+ const shown = c.name ?? cap(key);
751
+ if (c.portrait === undefined) {
752
+ this.instance(`${key}Talker`, target.voice.class, target.voice.props, [], shown);
753
+ return;
754
+ }
755
+ const p = target.portrait;
756
+ const [x, y] = p.at;
757
+ this.instance(`${key}Talker`, p.class, p.props, ["init"], shown);
758
+ this.instance(`${key}Bust`, "View", { x, y, view: c.portrait, loop: 0, cel: 0, signal: p.frameSignal });
759
+ this.instance(`${key}Mouth`, "Prop", { x, y, view: c.portrait, loop: 1, cel: 0, signal: p.partSignal });
760
+ this.instance(`${key}Eyes`, "Prop", { x, y, view: c.portrait, loop: 2, cel: 0, signal: p.partSignal });
761
+ // init: mouth bust eyes frame (the bust is drawn as the frame, as the game's own do).
762
+ // Every part gets the talker's priority; the game's portraits sort mouth and eyes above
763
+ // the bust by their y, but ours share one position, so lift them a priority above it.
764
+ this.code.push(`${key}Talker::init:`, ...[
765
+ "pushi #init", "pushi 4", `lofsa @${key}Mouth`, "push", "push0", `lofsa @${key}Eyes`, "push", `lofsa @${key}Bust`, "push",
766
+ "&rest 1", `super ${p.class} 12`,
767
+ "pushi #setPri", "push1", "pTos priority", "ldi 1", "add", "push", `lofsa @${key}Mouth`, "send 6",
768
+ "pushi #setPri", "push1", "pTos priority", "ldi 1", "add", "push", `lofsa @${key}Eyes`, "send 6",
769
+ "ret",
770
+ ], "");
771
+ }
772
+ /** A conversation's Teller, with showCases (conditional topics) and sayMessage (scripted answers). */
773
+ teller(conv) {
774
+ const { target } = this;
775
+ const label = `${conv.thing}Teller`;
776
+ const methods = [...(conv.conditional.length ? ["showCases"] : []), ...(conv.scripted.length ? ["sayMessage"] : [])];
777
+ this.instance(label, target.teller.class, { ...target.teller.props, actionVerb: conv.opensWith }, methods);
778
+ if (conv.conditional.length) {
779
+ // super showCases: topic offered? ... (pairs; a topic whose value is 0 is left out)
780
+ const c = [`${label}::showCases:`, "pushi #showCases", `pushi ${conv.conditional.length * 2}`];
781
+ for (const { topic, when, where } of conv.conditional)
782
+ c.push(`pushi ${topic}`, ...this.expr(when, where), "push");
783
+ c.push(`super ${target.teller.class} ${(2 + conv.conditional.length * 2) * 2}`, "ret", "");
784
+ this.code.push(...c);
785
+ }
786
+ if (conv.scripted.length) {
787
+ const c = [`${label}::sayMessage:`, "pTos iconValue"];
788
+ const other = this.label();
789
+ for (const { topic, script } of conv.scripted) {
790
+ const next = this.label();
791
+ c.push("dup", `ldi ${topic}`, "eq?", `bnt ${next}`, "toss", ...send({ global: target.globals.curRoom }, [["setScript", [{ obj: script }, "self"]]]), "ret", `${next}:`);
792
+ }
793
+ c.push(`${other}:`, "toss", "pushi #sayMessage", "push0", "&rest 1", `super ${target.teller.class} 4`, "ret", "");
794
+ this.code.push(...c);
795
+ }
796
+ }
797
+ /** findTalker: n -> this room's character for talker number n (the game asks for 200+). */
798
+ findTalker() {
799
+ const c = [`${this.name}::findTalker:`, "lsp 1"];
800
+ const done = ".findTalkerDone";
801
+ Object.keys(this.spec.characters ?? {}).forEach((key, i) => {
802
+ const next = `.findTalker${i + 1}`;
803
+ c.push("dup", `ldi ${this.target.firstRoomTalker + i}`, "eq?", `bnt ${next}`, `lofsa @${key}Talker`, `jmp ${done}`, `${next}:`);
804
+ });
805
+ // Not one of ours: the narrator, as the game does for numbers it doesn't know.
806
+ c.push(`lag ${this.target.globals.narratorObject}`, `${done}:`, "toss", "ret", "");
807
+ this.code.push(...c);
808
+ }
809
+ feature(name, f, exitScript) {
810
+ const [l, t, r, b] = f.rect;
811
+ const [x, y] = f.at ?? [Math.round((l + r) / 2), Math.round((t + b) / 2)];
812
+ // Do on an exit leaves (its script); verbs with scripted Yarn run theirs.
813
+ const cases = [...(exitScript ? [[this.target.verbs.do, exitScript]] : []), ...(this.scriptedVerbs.get(name) ?? [])];
814
+ this.instance(name, "Feature", {
815
+ noun: this.nouns.get(name), modNum: -1, nsLeft: l, nsTop: t, nsRight: r, nsBottom: b, sightAngle: f.sightAngle ?? 180,
816
+ ...(f.approach ? { approachX: f.approach[0], approachY: f.approach[1] } : {}), x, y,
817
+ }, cases.length ? ["doVerb"] : []);
818
+ if (cases.length)
819
+ this.doVerb(name, "Feature", cases);
820
+ }
821
+ prop(name, p) {
822
+ const cases = this.scriptedVerbs.get(name) ?? [];
823
+ this.instance(name, p.moves ? "Actor" : "Prop", { noun: this.nouns.get(name), modNum: -1, sightAngle: p.sightAngle ?? 180, x: p.at[0], y: p.at[1], view: p.view, loop: p.loop ?? 0, cel: p.cel ?? 0 }, cases.length ? ["doVerb"] : []);
824
+ if (cases.length)
825
+ this.doVerb(name, p.moves ? "Actor" : "Prop", cases);
826
+ }
827
+ /** A Script instance whose changeState runs `states` in order. */
828
+ script(name, states) {
829
+ this.instance(name, "Script", {}, ["changeState"]);
830
+ const c = [`${name}::changeState:`, "lap 1", "aTop state", "push"];
831
+ const done = `.${name}_done`;
832
+ states.forEach((body, i) => {
833
+ const next = i + 1 < states.length ? `.${name}_${i + 1}` : done;
834
+ if (i > 0)
835
+ c.push(`.${name}_${i}:`);
836
+ c.push("dup", `ldi ${i}`, "eq?", `bnt ${next}`, ...body);
837
+ if (i + 1 < states.length)
838
+ c.push(`jmp ${done}`);
839
+ });
840
+ c.push(`${done}:`, "toss", "ret", "");
841
+ this.code.push(...c);
842
+ }
843
+ assembly() {
844
+ const nounList = [...this.nouns].map(([n, v]) => `${v} ${n}`).join(", ");
845
+ return [
846
+ `; Room ${this.spec.room}, compiled from its .room.yaml and .yarn by @sci-ts/content. Don't edit:`,
847
+ `; change the sources and rebuild. Nouns: ${nounList}.`,
848
+ `script ${this.spec.room}`,
849
+ "",
850
+ "exports",
851
+ ` ${this.name}`,
852
+ "",
853
+ ...this.instances,
854
+ "strings",
855
+ ...this.strings,
856
+ "",
857
+ "code",
858
+ "",
859
+ ...this.code.map((l) => (l.endsWith(":") || l === "" ? l : ` ${l}`)),
860
+ ].join("\n");
861
+ }
862
+ messageText() {
863
+ const lines = [`messages ${this.spec.room} version ${this.target.messageVersion}`, '; noun verb cond seq talker "text" (compiled from Yarn)'];
864
+ for (const m of this.messages) {
865
+ lines.push(`${m.noun} ${m.verb} ${m.cond} ${m.seq} ${m.talker} "${m.text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}" ; ${m.label}`);
866
+ }
867
+ return `${lines.join("\n")}\n`;
868
+ }
869
+ }
870
+ const cap = (s) => s[0].toUpperCase() + s.slice(1);
871
+ const counter = (from) => {
872
+ let n = from;
873
+ return () => n++;
874
+ };
875
+ /** Message text is single bytes: typographic punctuation becomes ASCII, anything else is an error. */
876
+ function plainText(text, where) {
877
+ const out = text
878
+ .replace(/[‘’]/g, "'")
879
+ .replace(/[“”]/g, '"')
880
+ .replace(/[–—]/g, "--")
881
+ .replace(/…/g, "...");
882
+ const bad = /[^\x20-\x7e]/.exec(out);
883
+ if (bad)
884
+ throw new ContentError(`${where}: "${bad[0]}" can't be shown by the game's fonts (plain ASCII only for now)`);
885
+ return out;
886
+ }