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,368 @@
1
+ import { isSeq, parseDocument, type Node } from "yaml";
2
+
3
+ /**
4
+ * Direct manipulation for the room editor: handles drawn from the room's YAML over the
5
+ * game, which turn drags and brush strokes into edits of the YAML text. Edits replace only
6
+ * the value they change (a `[x, y]`, a `[l, t, r, b]`, a list of points), so comments and
7
+ * layout stay as written.
8
+ */
9
+
10
+ type Point = [number, number];
11
+ type Path = (string | number)[];
12
+
13
+ // --- Editing YAML text in place ----------------------------------------------------------
14
+
15
+ const flow = (v: unknown): string => (Array.isArray(v) ? `[${v.map(flow).join(", ")}]` : String(v));
16
+
17
+ /** `text` with the value at `path` replaced by `value`, keeping the rest as written. */
18
+ export function setYamlValue(text: string, path: Path, value: number[] | Point[]): string {
19
+ const doc = parseDocument(text);
20
+ const node = doc.getIn(path, true) as Node | undefined;
21
+ if (node?.range) {
22
+ const start = node.range[0];
23
+ let end = node.range[1];
24
+ while (end > start && text[end - 1] === "\n") end--;
25
+ let replacement: string;
26
+ if (isSeq(node) && !node.flow && Array.isArray(value[0])) {
27
+ // A block list of points stays one "- [x, y]" per line, at its indentation.
28
+ const indent = " ".repeat(start - (text.lastIndexOf("\n", start - 1) + 1));
29
+ replacement = (value as Point[]).map((p) => `- ${flow(p)}`).join(`\n${indent}`);
30
+ } else replacement = flow(value);
31
+ return text.slice(0, start) + replacement + text.slice(end);
32
+ }
33
+ // Not there yet: let the YAML library add it (it may tidy the layout).
34
+ doc.setIn(path, flowNode(doc, value));
35
+ return doc.toString();
36
+ }
37
+
38
+ /** `text` with `value` added at the end of the list at `path` (made if missing). */
39
+ export function appendYamlItem(text: string, path: Path, value: Point[]): string {
40
+ const doc = parseDocument(text);
41
+ const node = doc.getIn(path, true) as Node | undefined;
42
+ if (node?.range && isSeq(node) && !node.flow) {
43
+ const start = node.range[0];
44
+ let end = node.range[1];
45
+ while (end > start && text[end - 1] === "\n") end--;
46
+ const indent = " ".repeat(start - (text.lastIndexOf("\n", start - 1) + 1));
47
+ return `${text.slice(0, end)}\n${indent}- ${flow(value)}${text.slice(end)}`;
48
+ }
49
+ if (!node) return `${text.replace(/\n*$/, "\n")}\n${path.join(".")}:\n - ${flow(value)}\n`;
50
+ doc.addIn(path, flowNode(doc, value));
51
+ return doc.toString();
52
+ }
53
+
54
+ function flowNode(doc: ReturnType<typeof parseDocument>, value: unknown): Node {
55
+ const node = doc.createNode(value) as Node & { flow?: boolean; items?: Node[] };
56
+ if (isSeq(node)) {
57
+ node.flow = true;
58
+ for (const item of node.items) if (isSeq(item)) item.flow = true;
59
+ }
60
+ return node;
61
+ }
62
+
63
+ // --- The room as handles -----------------------------------------------------------------
64
+
65
+ interface Spec {
66
+ hero?: { at?: Point; enterTo?: Point };
67
+ walkable?: Point[];
68
+ obstacles?: Point[][];
69
+ features?: Record<string, { rect?: number[]; at?: Point; approach?: Point }>;
70
+ exits?: Record<string, { rect?: number[]; at?: Point; approach?: Point }>;
71
+ props?: Record<string, { at?: Point }>;
72
+ }
73
+
74
+ export type EditMode = "play" | "move" | "floor" | "obstacle";
75
+
76
+ export interface EditHost {
77
+ /** The room's YAML as it is in the editor now. */
78
+ yaml(): string;
79
+ /** Replaces the YAML in the editor (an undoable edit). */
80
+ setYaml(text: string): void;
81
+ /** Where the room plane is on screen (game pixels). */
82
+ roomOffset(): Point;
83
+ /** A prop's drawn box right now (room coordinates), to grab it by. */
84
+ spriteBox(name: string): { x: number; y: number; w: number; h: number } | undefined;
85
+ /** Moves a prop in the running game while it's dragged (the rebuild makes it stick). */
86
+ previewProp(name: string, at: Point): void;
87
+ /** Hold rebuilding while a drag or stroke is in progress. */
88
+ holdBuild(hold: boolean): void;
89
+ }
90
+
91
+ const NS = "http://www.w3.org/2000/svg";
92
+ const el = <K extends keyof SVGElementTagNameMap>(tag: K, attrs: Record<string, string | number> = {}, text?: string) => {
93
+ const node = document.createElementNS(NS, tag);
94
+ for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
95
+ if (text) node.textContent = text;
96
+ return node;
97
+ };
98
+ const COLORS = { feature: "#e8b04a", exit: "#5fd08c", prop: "#7fb3e8", hero: "#f08fd0", floor: "#5fa0e8", obstacle: "#e05555" };
99
+ const clampX = (x: number) => Math.max(0, Math.min(319, Math.round(x)));
100
+ const clampY = (y: number) => Math.max(0, Math.min(189, Math.round(y)));
101
+
102
+ /** A drag in progress: what it changes, given how far the pointer has moved. */
103
+ interface Drag {
104
+ start: Point;
105
+ apply(dx: number, dy: number): string;
106
+ /** Shows the change in the game right away, where it can (props). */
107
+ preview?(dx: number, dy: number): void;
108
+ }
109
+
110
+ /**
111
+ * An SVG layer over the game. In "move" mode, everything the YAML places has a handle;
112
+ * "floor" and "obstacle" are brushes that draw a polygon. In "play" it's out of the way.
113
+ */
114
+ export class EditLayer {
115
+ readonly svg: SVGSVGElement;
116
+ mode: EditMode = "play";
117
+ private spec: Spec = {};
118
+ private specText = "";
119
+ private drag: Drag | undefined;
120
+ private stroke: Point[] | undefined;
121
+ private lastWrite = 0;
122
+
123
+ constructor(private readonly host: EditHost) {
124
+ this.svg = el("svg", { viewBox: "0 0 320 200", preserveAspectRatio: "none" });
125
+ this.svg.style.touchAction = "none";
126
+ this.svg.addEventListener("pointerdown", (e) => this.down(e));
127
+ this.svg.addEventListener("pointermove", (e) => this.move(e));
128
+ this.svg.addEventListener("pointerup", (e) => this.up(e));
129
+ this.svg.addEventListener("pointercancel", (e) => this.up(e));
130
+ this.setMode("play");
131
+ }
132
+
133
+ setMode(mode: EditMode): void {
134
+ this.mode = mode;
135
+ this.svg.style.pointerEvents = mode === "play" ? "none" : "auto";
136
+ this.svg.style.cursor = mode === "move" ? "default" : mode === "play" ? "" : "crosshair";
137
+ this.render();
138
+ }
139
+
140
+ /** Room coordinates under the pointer. */
141
+ private point(e: PointerEvent): Point {
142
+ const m = this.svg.getScreenCTM();
143
+ if (!m) return [0, 0];
144
+ const p = new DOMPoint(e.clientX, e.clientY).matrixTransform(m.inverse());
145
+ const [ox, oy] = this.host.roomOffset();
146
+ return [p.x - ox, p.y - oy];
147
+ }
148
+
149
+ private readSpec(): Spec {
150
+ const text = this.host.yaml();
151
+ if (text !== this.specText) {
152
+ this.specText = text;
153
+ try {
154
+ this.spec = (parseDocument(text).toJS() as Spec | null) ?? {};
155
+ } catch {
156
+ /* mid-edit: keep the last good one */
157
+ }
158
+ }
159
+ return this.spec;
160
+ }
161
+
162
+ // --- Drawing ---
163
+
164
+ render(): void {
165
+ this.svg.replaceChildren();
166
+ if (this.mode === "play") return;
167
+ const spec = this.readSpec();
168
+ const [ox, oy] = this.host.roomOffset();
169
+ const g = el("g", { transform: `translate(${ox} ${oy})` });
170
+ this.svg.append(g);
171
+ const handle = (x: number, y: number, color: string, start: () => Drag, shape: "square" | "circle" | "diamond" = "square") => {
172
+ const attrs = { fill: color, stroke: "#000", "stroke-width": 0.4, cursor: "grab" };
173
+ const h = shape === "circle"
174
+ ? el("circle", { cx: x, cy: y, r: 2.4, ...attrs })
175
+ : shape === "diamond"
176
+ ? el("path", { d: `M${x} ${y - 2.8}L${x + 2.8} ${y}L${x} ${y + 2.8}L${x - 2.8} ${y}Z`, ...attrs })
177
+ : el("rect", { x: x - 2, y: y - 2, width: 4, height: 4, ...attrs });
178
+ h.addEventListener("pointerdown", (e) => this.begin(e, start));
179
+ g.append(h);
180
+ return h;
181
+ };
182
+ const label = (x: number, y: number, text: string, color: string) =>
183
+ g.append(el("text", { x, y, fill: color, stroke: "#000", "stroke-width": 1.2, "paint-order": "stroke", "font-size": 5, "font-family": "ui-monospace, Menlo, monospace", "pointer-events": "none" }, text));
184
+
185
+ if (this.mode === "move") {
186
+ // Walk areas: every vertex can be dragged; Alt-click removes one.
187
+ const polygon = (points: Point[], path: Path, color: string) => {
188
+ g.append(el("polygon", { points: points.map((p) => p.join(",")).join(" "), fill: `${color}22`, stroke: color, "stroke-width": 0.6, "pointer-events": "none" }));
189
+ points.forEach((p, i) => {
190
+ const h = handle(p[0], p[1], color, () => ({
191
+ start: p,
192
+ apply: (dx, dy) => setYamlValue(this.host.yaml(), path, points.map((q, j) => (j === i ? [clampX(p[0] + dx), clampY(p[1] + dy)] : q)) as Point[]),
193
+ }), "circle");
194
+ h.addEventListener("click", (e) => {
195
+ if (!e.altKey || points.length <= 3) return;
196
+ this.host.setYaml(setYamlValue(this.host.yaml(), path, points.filter((_, j) => j !== i)));
197
+ });
198
+ });
199
+ };
200
+ if (spec.walkable) polygon(spec.walkable, ["walkable"], COLORS.floor);
201
+ spec.obstacles?.forEach((o, i) => polygon(o, ["obstacles", i], COLORS.obstacle));
202
+
203
+ // Features and exits: drag the box to move it (and its look-at point), corners to resize.
204
+ for (const group of ["features", "exits"] as const) {
205
+ for (const [name, f] of Object.entries(spec[group] ?? {})) {
206
+ const color = group === "exits" ? COLORS.exit : COLORS.feature;
207
+ const r = f.rect;
208
+ if (r?.length === 4) {
209
+ const [l, t, rt, b] = r as [number, number, number, number];
210
+ const box = el("rect", { x: l, y: t, width: rt - l, height: b - t, fill: `${color}22`, stroke: color, "stroke-width": 0.6, cursor: "move" });
211
+ box.addEventListener("pointerdown", (e) => this.begin(e, () => ({
212
+ start: [l, t],
213
+ apply: (dx, dy) => {
214
+ let text = setYamlValue(this.host.yaml(), [group, name, "rect"], [clampX(l + dx), clampY(t + dy), clampX(rt + dx), clampY(b + dy)]);
215
+ if (f.at) text = setYamlValue(text, [group, name, "at"], [clampX(f.at[0] + dx), clampY(f.at[1] + dy)]);
216
+ return text;
217
+ },
218
+ })));
219
+ g.append(box);
220
+ label(l + 2, t + 6, name, color);
221
+ ([[l, t, 0, 1], [rt, t, 2, 1], [l, b, 0, 3], [rt, b, 2, 3]] as const).forEach(([x, y, xi, yi]) => {
222
+ handle(x, y, color, () => ({
223
+ start: [x, y],
224
+ apply: (dx, dy) => {
225
+ const next = [l, t, rt, b];
226
+ next[xi] = clampX(x + dx);
227
+ next[yi] = clampY(y + dy);
228
+ const [nl, nt, nr, nb] = next as [number, number, number, number];
229
+ return setYamlValue(this.host.yaml(), [group, name, "rect"], [Math.min(nl, nr), Math.min(nt, nb), Math.max(nl, nr), Math.max(nt, nb)]);
230
+ },
231
+ }));
232
+ });
233
+ }
234
+ if (f.approach) {
235
+ const [ax, ay] = f.approach;
236
+ label(ax + 3, ay + 2, "approach", color);
237
+ handle(ax, ay, color, () => ({ start: [ax, ay], apply: (dx, dy) => setYamlValue(this.host.yaml(), [group, name, "approach"], [clampX(ax + dx), clampY(ay + dy)]) }), "diamond");
238
+ }
239
+ }
240
+ }
241
+
242
+ // Props: grab the sprite (or its anchor) to move its `at`.
243
+ for (const [name, p] of Object.entries(spec.props ?? {})) {
244
+ if (!p.at) continue;
245
+ const [x, y] = p.at;
246
+ const box = this.host.spriteBox(name);
247
+ const start = (): Drag => ({
248
+ start: [x, y],
249
+ apply: (dx, dy) => setYamlValue(this.host.yaml(), ["props", name, "at"], [clampX(x + dx), clampY(y + dy)]),
250
+ preview: (dx, dy) => this.host.previewProp(name, [clampX(x + dx), clampY(y + dy)]),
251
+ });
252
+ if (box) {
253
+ const r = el("rect", { x: box.x, y: box.y, width: box.w, height: box.h, fill: `${COLORS.prop}22`, stroke: COLORS.prop, "stroke-width": 0.6, "stroke-dasharray": "2 1", cursor: "move" });
254
+ r.addEventListener("pointerdown", (e) => this.begin(e, start));
255
+ g.append(r);
256
+ }
257
+ label(x + 3, y + 2, name, COLORS.prop);
258
+ handle(x, y, COLORS.prop, start, "circle");
259
+ }
260
+
261
+ // The hero: where it appears, and where it walks to on arrival.
262
+ const hero = spec.hero;
263
+ if (hero?.at && hero.enterTo) {
264
+ g.append(el("line", { x1: hero.at[0], y1: hero.at[1], x2: hero.enterTo[0], y2: hero.enterTo[1], stroke: COLORS.hero, "stroke-width": 0.6, "stroke-dasharray": "2 1.5", "pointer-events": "none" }));
265
+ }
266
+ for (const key of ["at", "enterTo"] as const) {
267
+ const p = hero?.[key];
268
+ if (!p) continue;
269
+ label(p[0] + 3, p[1] + 2, key === "at" ? "hero" : "walks to", COLORS.hero);
270
+ handle(p[0], p[1], COLORS.hero, () => ({ start: p, apply: (dx, dy) => setYamlValue(this.host.yaml(), ["hero", key], [clampX(p[0] + dx), clampY(p[1] + dy)]) }), key === "at" ? "circle" : "diamond");
271
+ }
272
+ } else if (this.stroke) {
273
+ // A brush stroke in progress.
274
+ const color = this.mode === "floor" ? COLORS.floor : COLORS.obstacle;
275
+ g.append(el("polyline", { points: this.stroke.map((p) => p.join(",")).join(" "), fill: `${color}33`, stroke: color, "stroke-width": 1.2, "pointer-events": "none" }));
276
+ }
277
+ }
278
+
279
+ // --- Pointer handling ---
280
+
281
+ private begin(e: PointerEvent, start: () => Drag): void {
282
+ if (this.mode !== "move" || e.button !== 0 || e.altKey) return;
283
+ e.stopPropagation();
284
+ e.preventDefault();
285
+ this.drag = start();
286
+ (this.drag as Drag & { from?: Point }).from = this.point(e);
287
+ this.capture(e, true);
288
+ this.host.holdBuild(true);
289
+ }
290
+
291
+ /** Keep getting the pointer's moves outside the layer (not for synthetic events). */
292
+ private capture(e: PointerEvent, on: boolean): void {
293
+ try {
294
+ if (on) this.svg.setPointerCapture(e.pointerId);
295
+ else if (this.svg.hasPointerCapture(e.pointerId)) this.svg.releasePointerCapture(e.pointerId);
296
+ } catch {
297
+ /* no such pointer */
298
+ }
299
+ }
300
+
301
+ private down(e: PointerEvent): void {
302
+ if (this.mode !== "floor" && this.mode !== "obstacle") return;
303
+ e.preventDefault();
304
+ this.capture(e, true);
305
+ this.stroke = [this.point(e).map(Math.round) as Point];
306
+ this.host.holdBuild(true);
307
+ this.render();
308
+ }
309
+
310
+ private move(e: PointerEvent): void {
311
+ const p = this.point(e);
312
+ if (this.drag) {
313
+ const from = (this.drag as Drag & { from: Point }).from;
314
+ const [dx, dy] = [Math.round(p[0] - from[0]), Math.round(p[1] - from[1])];
315
+ this.drag.preview?.(dx, dy);
316
+ const text = this.drag.apply(dx, dy);
317
+ // Show it as it moves: the YAML (and these handles) follow the pointer.
318
+ if (text !== this.host.yaml()) this.host.setYaml(text);
319
+ this.render();
320
+ } else if (this.stroke) {
321
+ const last = this.stroke.at(-1)!;
322
+ if (Math.hypot(p[0] - last[0], p[1] - last[1]) < 4) return;
323
+ this.stroke.push([clampX(p[0]), clampY(p[1])]);
324
+ this.render();
325
+ // The list in the editor follows the brush, a few times a second.
326
+ if (performance.now() - this.lastWrite > 120) this.writeStroke(false);
327
+ }
328
+ }
329
+
330
+ private up(e: PointerEvent): void {
331
+ this.capture(e, false);
332
+ if (this.drag) {
333
+ this.drag = undefined;
334
+ this.host.holdBuild(false);
335
+ this.render();
336
+ } else if (this.stroke) {
337
+ this.writeStroke(true);
338
+ this.stroke = undefined;
339
+ this.host.holdBuild(false);
340
+ this.render();
341
+ }
342
+ }
343
+
344
+ /** The stroke so far as a polygon in the YAML (simplified to its corners). */
345
+ private writeStroke(final: boolean): void {
346
+ this.lastWrite = performance.now();
347
+ const points = simplify(this.stroke!, final ? 2.5 : 2);
348
+ if (points.length < 3) return;
349
+ const yaml = this.host.yaml();
350
+ if (this.mode === "floor") this.host.setYaml(setYamlValue(yaml, ["walkable"], points));
351
+ else if (final) this.host.setYaml(appendYamlItem(yaml, ["obstacles"], points));
352
+ }
353
+ }
354
+
355
+ /** Ramer-Douglas-Peucker: drop points within `epsilon` of the line through their neighbours. */
356
+ export function simplify(points: Point[], epsilon: number): Point[] {
357
+ if (points.length < 3) return points;
358
+ const [a, b] = [points[0]!, points.at(-1)!];
359
+ let index = 0, max = 0;
360
+ for (let i = 1; i < points.length - 1; i++) {
361
+ const p = points[i]!;
362
+ const len = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
363
+ const d = Math.abs((b[0] - a[0]) * (a[1] - p[1]) - (a[0] - p[0]) * (b[1] - a[1])) / len;
364
+ if (d > max) (max = d), (index = i);
365
+ }
366
+ if (max <= epsilon) return [a, b];
367
+ return [...simplify(points.slice(0, index + 1), epsilon).slice(0, -1), ...simplify(points.slice(index), epsilon)];
368
+ }
@@ -0,0 +1,23 @@
1
+ import { StreamLanguage } from "@codemirror/language";
2
+ /**
3
+ * Highlighting for the room editor: Yarn (headers, speakers, choices, <<commands>>,
4
+ * $variables, comments, #tags) and the compiled SCI assembly. Token names are
5
+ * CodeMirror's legacy names, which its highlight styles already colour.
6
+ */
7
+ interface YarnState {
8
+ /** Before a node's --- : `key: value` headers. */
9
+ header: boolean;
10
+ /** Inside <<...>>. */
11
+ command: boolean;
12
+ }
13
+ export declare const yarn: StreamLanguage<YarnState>;
14
+ /** The compiled .sca: directives, labels, mnemonics, #selectors, @heap labels, comments. */
15
+ export declare const sciAssembly: StreamLanguage<{
16
+ code: boolean;
17
+ }>;
18
+ /** Message text: noun verb cond seq talker "text" ; note */
19
+ export declare const sciMessages: StreamLanguage<unknown>;
20
+ export declare const sciScript: StreamLanguage<{
21
+ braces: number;
22
+ }>;
23
+ export {};
@@ -0,0 +1,157 @@
1
+ import { StreamLanguage } from "@codemirror/language";
2
+ const yarnParser = {
3
+ name: "yarn",
4
+ startState: () => ({ header: true, command: false }),
5
+ token(stream, state) {
6
+ if (stream.sol()) {
7
+ if (stream.match(/^---\s*$/))
8
+ return (state.header = false), "meta";
9
+ if (stream.match(/^===\s*$/))
10
+ return (state.header = true), "meta";
11
+ }
12
+ if (state.command) {
13
+ if (stream.match(">>"))
14
+ return (state.command = false), "keyword";
15
+ if (stream.match(/^\$[A-Za-z_]\w*/))
16
+ return "variableName";
17
+ if (stream.match(/^(true|false)\b/))
18
+ return "atom";
19
+ if (stream.match(/^(not|and|or|to|is|eq|neq)\b/) || stream.match(/^(==|!=|&&|\|\||!)/))
20
+ return "operator";
21
+ if (stream.match(/^-?\d+(\.\d+)?/))
22
+ return "number";
23
+ if (stream.match(/^[A-Za-z_]\w*/))
24
+ return "variableName.special";
25
+ stream.next();
26
+ return null;
27
+ }
28
+ if (stream.match("//"))
29
+ return stream.skipToEnd(), "comment";
30
+ if (state.header && stream.sol()) {
31
+ if (stream.match(/^[A-Za-z_][\w.-]*\s*:/))
32
+ return "propertyName";
33
+ }
34
+ if (state.header)
35
+ return stream.skipToEnd(), "string";
36
+ if (stream.match("<<")) {
37
+ state.command = true;
38
+ stream.eatSpace();
39
+ return stream.match(/^(if|elseif|else|endif|set)\b/) ? "controlKeyword" : stream.match(/^\w+/) ? "keyword" : "keyword";
40
+ }
41
+ // A line's start: a choice, or "Speaker:".
42
+ if (stream.column() === stream.indentation() || stream.sol()) {
43
+ stream.eatSpace();
44
+ if (stream.match("->"))
45
+ return "operator";
46
+ if (stream.match(/^[A-Za-z][\w ]*?:(?=\s)/))
47
+ return "typeName";
48
+ }
49
+ if (stream.match(/^\s#[^\s#]+/))
50
+ return "meta";
51
+ stream.next();
52
+ while (!stream.eol() && !stream.match(/^(?=<<|\s#|\/\/)/, false))
53
+ stream.next();
54
+ return "content";
55
+ },
56
+ };
57
+ export const yarn = StreamLanguage.define(yarnParser);
58
+ const DIRECTIVES = /^(script|exports|instance|class|method|strings|code|locals|of|owner)\b/;
59
+ /** The compiled .sca: directives, labels, mnemonics, #selectors, @heap labels, comments. */
60
+ export const sciAssembly = StreamLanguage.define({
61
+ name: "sca",
62
+ startState: () => ({ code: false }),
63
+ token(stream, state) {
64
+ if (stream.match(";"))
65
+ return stream.skipToEnd(), "comment";
66
+ if (stream.sol() && stream.match(/^\S+:\s*$/))
67
+ return "labelName";
68
+ if (stream.eatSpace())
69
+ return null;
70
+ if (stream.match(DIRECTIVES)) {
71
+ if (stream.current() === "code")
72
+ state.code = true;
73
+ return "keyword";
74
+ }
75
+ if (stream.match(/^#\w+/))
76
+ return "propertyName";
77
+ if (stream.match(/^@\w+/) || stream.match(/^\.\w+/))
78
+ return "labelName";
79
+ if (stream.match(/^"(?:[^"\\]|\\.)*"/))
80
+ return "string";
81
+ if (stream.match(/^-?\$[0-9a-f]+|^-?\d+/i))
82
+ return "number";
83
+ if (stream.match(/^[\w?&.]+/)) {
84
+ const word = stream.current();
85
+ return stream.column() === stream.indentation() && state.code && !word.includes("::") ? "operatorKeyword" : null;
86
+ }
87
+ stream.next();
88
+ return null;
89
+ },
90
+ });
91
+ /** Message text: noun verb cond seq talker "text" ; note */
92
+ export const sciMessages = StreamLanguage.define({
93
+ name: "msg",
94
+ token(stream) {
95
+ if (stream.match(";"))
96
+ return stream.skipToEnd(), "comment";
97
+ if (stream.match(/^"(?:[^"\\]|\\.)*"/))
98
+ return "string";
99
+ if (stream.match(/^\d+/))
100
+ return "number";
101
+ if (stream.match(/^\w+/))
102
+ return "keyword";
103
+ stream.next();
104
+ return null;
105
+ },
106
+ });
107
+ /** SCI script source (.sc, .sh): the Lisp-like language games are written in. */
108
+ const SCRIPT_KEYWORDS = new Set([
109
+ "script", "include", "define", "enum", "public", "local", "global", "extern", "class", "instance", "of", "kindof",
110
+ "properties", "method", "procedure", "if", "else", "cond", "switch", "switchto", "while", "repeat", "for", "break",
111
+ "continue", "return", "and", "or", "not", "send", "super", "self", "argc", "&rest", "&tmp",
112
+ ]);
113
+ const scriptParser = {
114
+ name: "sci",
115
+ startState: () => ({ braces: 0 }),
116
+ token(stream, state) {
117
+ if (state.braces) {
118
+ // Inside a {string}: to its end.
119
+ while (!stream.eol())
120
+ if (stream.next() === "}")
121
+ return (state.braces = 0), "string";
122
+ return "string";
123
+ }
124
+ if (stream.eatSpace())
125
+ return null;
126
+ if (stream.match(";"))
127
+ return stream.skipToEnd(), "comment";
128
+ if (stream.match(/^"(?:[^"\\]|\\.)*"?/))
129
+ return "string";
130
+ if (stream.eat("{"))
131
+ return (state.braces = 1), "string";
132
+ if (stream.match(/^(-?\$[0-9a-fA-F]+|%[01]+|-?\d+)(?=[\s()[\]]|$)/) || stream.match(/^`\^?./))
133
+ return "number";
134
+ if (stream.match(/^#[\w-]+/))
135
+ return "atom";
136
+ if (stream.match(/^@\w+/))
137
+ return "variableName.special";
138
+ if (stream.match(/^[()[\]]/))
139
+ return "bracket";
140
+ const word = stream.match(/^[^\s()[\]"{;]+/);
141
+ if (!word)
142
+ return stream.next(), null;
143
+ const w = word[0];
144
+ if (/^[\w-]+[:?]$/.test(w))
145
+ return "propertyName";
146
+ if (SCRIPT_KEYWORDS.has(w))
147
+ return "keyword";
148
+ if (/^[A-Z][A-Z0-9_]+$/.test(w))
149
+ return "atom";
150
+ if (/^[A-Z]/.test(w))
151
+ return "typeName";
152
+ if (/^[-+*/=<>!&|^~u]+$|^mod$/.test(w))
153
+ return "operator";
154
+ return "variableName";
155
+ },
156
+ };
157
+ export const sciScript = StreamLanguage.define(scriptParser);
@@ -0,0 +1,135 @@
1
+ import { StreamLanguage, type StreamParser } from "@codemirror/language";
2
+
3
+ /**
4
+ * Highlighting for the room editor: Yarn (headers, speakers, choices, <<commands>>,
5
+ * $variables, comments, #tags) and the compiled SCI assembly. Token names are
6
+ * CodeMirror's legacy names, which its highlight styles already colour.
7
+ */
8
+
9
+ interface YarnState {
10
+ /** Before a node's --- : `key: value` headers. */
11
+ header: boolean;
12
+ /** Inside <<...>>. */
13
+ command: boolean;
14
+ }
15
+
16
+ const yarnParser: StreamParser<YarnState> = {
17
+ name: "yarn",
18
+ startState: () => ({ header: true, command: false }),
19
+ token(stream, state) {
20
+ if (stream.sol()) {
21
+ if (stream.match(/^---\s*$/)) return (state.header = false), "meta";
22
+ if (stream.match(/^===\s*$/)) return (state.header = true), "meta";
23
+ }
24
+ if (state.command) {
25
+ if (stream.match(">>")) return (state.command = false), "keyword";
26
+ if (stream.match(/^\$[A-Za-z_]\w*/)) return "variableName";
27
+ if (stream.match(/^(true|false)\b/)) return "atom";
28
+ if (stream.match(/^(not|and|or|to|is|eq|neq)\b/) || stream.match(/^(==|!=|&&|\|\||!)/)) return "operator";
29
+ if (stream.match(/^-?\d+(\.\d+)?/)) return "number";
30
+ if (stream.match(/^[A-Za-z_]\w*/)) return "variableName.special";
31
+ stream.next();
32
+ return null;
33
+ }
34
+ if (stream.match("//")) return stream.skipToEnd(), "comment";
35
+ if (state.header && stream.sol()) {
36
+ if (stream.match(/^[A-Za-z_][\w.-]*\s*:/)) return "propertyName";
37
+ }
38
+ if (state.header) return stream.skipToEnd(), "string";
39
+ if (stream.match("<<")) {
40
+ state.command = true;
41
+ stream.eatSpace();
42
+ return stream.match(/^(if|elseif|else|endif|set)\b/) ? "controlKeyword" : stream.match(/^\w+/) ? "keyword" : "keyword";
43
+ }
44
+ // A line's start: a choice, or "Speaker:".
45
+ if (stream.column() === stream.indentation() || stream.sol()) {
46
+ stream.eatSpace();
47
+ if (stream.match("->")) return "operator";
48
+ if (stream.match(/^[A-Za-z][\w ]*?:(?=\s)/)) return "typeName";
49
+ }
50
+ if (stream.match(/^\s#[^\s#]+/)) return "meta";
51
+ stream.next();
52
+ while (!stream.eol() && !stream.match(/^(?=<<|\s#|\/\/)/, false)) stream.next();
53
+ return "content";
54
+ },
55
+ };
56
+
57
+ export const yarn = StreamLanguage.define(yarnParser);
58
+
59
+ const DIRECTIVES = /^(script|exports|instance|class|method|strings|code|locals|of|owner)\b/;
60
+
61
+ /** The compiled .sca: directives, labels, mnemonics, #selectors, @heap labels, comments. */
62
+ export const sciAssembly = StreamLanguage.define<{ code: boolean }>({
63
+ name: "sca",
64
+ startState: () => ({ code: false }),
65
+ token(stream, state) {
66
+ if (stream.match(";")) return stream.skipToEnd(), "comment";
67
+ if (stream.sol() && stream.match(/^\S+:\s*$/)) return "labelName";
68
+ if (stream.eatSpace()) return null;
69
+ if (stream.match(DIRECTIVES)) {
70
+ if (stream.current() === "code") state.code = true;
71
+ return "keyword";
72
+ }
73
+ if (stream.match(/^#\w+/)) return "propertyName";
74
+ if (stream.match(/^@\w+/) || stream.match(/^\.\w+/)) return "labelName";
75
+ if (stream.match(/^"(?:[^"\\]|\\.)*"/)) return "string";
76
+ if (stream.match(/^-?\$[0-9a-f]+|^-?\d+/i)) return "number";
77
+ if (stream.match(/^[\w?&.]+/)) {
78
+ const word = stream.current();
79
+ return stream.column() === stream.indentation() && state.code && !word.includes("::") ? "operatorKeyword" : null;
80
+ }
81
+ stream.next();
82
+ return null;
83
+ },
84
+ });
85
+
86
+ /** Message text: noun verb cond seq talker "text" ; note */
87
+ export const sciMessages = StreamLanguage.define({
88
+ name: "msg",
89
+ token(stream) {
90
+ if (stream.match(";")) return stream.skipToEnd(), "comment";
91
+ if (stream.match(/^"(?:[^"\\]|\\.)*"/)) return "string";
92
+ if (stream.match(/^\d+/)) return "number";
93
+ if (stream.match(/^\w+/)) return "keyword";
94
+ stream.next();
95
+ return null;
96
+ },
97
+ });
98
+
99
+ /** SCI script source (.sc, .sh): the Lisp-like language games are written in. */
100
+ const SCRIPT_KEYWORDS = new Set([
101
+ "script", "include", "define", "enum", "public", "local", "global", "extern", "class", "instance", "of", "kindof",
102
+ "properties", "method", "procedure", "if", "else", "cond", "switch", "switchto", "while", "repeat", "for", "break",
103
+ "continue", "return", "and", "or", "not", "send", "super", "self", "argc", "&rest", "&tmp",
104
+ ]);
105
+
106
+ const scriptParser: StreamParser<{ braces: number }> = {
107
+ name: "sci",
108
+ startState: () => ({ braces: 0 }),
109
+ token(stream, state) {
110
+ if (state.braces) {
111
+ // Inside a {string}: to its end.
112
+ while (!stream.eol()) if (stream.next() === "}") return (state.braces = 0), "string";
113
+ return "string";
114
+ }
115
+ if (stream.eatSpace()) return null;
116
+ if (stream.match(";")) return stream.skipToEnd(), "comment";
117
+ if (stream.match(/^"(?:[^"\\]|\\.)*"?/)) return "string";
118
+ if (stream.eat("{")) return (state.braces = 1), "string";
119
+ if (stream.match(/^(-?\$[0-9a-fA-F]+|%[01]+|-?\d+)(?=[\s()[\]]|$)/) || stream.match(/^`\^?./)) return "number";
120
+ if (stream.match(/^#[\w-]+/)) return "atom";
121
+ if (stream.match(/^@\w+/)) return "variableName.special";
122
+ if (stream.match(/^[()[\]]/)) return "bracket";
123
+ const word = stream.match(/^[^\s()[\]"{;]+/) as RegExpMatchArray | null;
124
+ if (!word) return stream.next(), null;
125
+ const w = word[0];
126
+ if (/^[\w-]+[:?]$/.test(w)) return "propertyName";
127
+ if (SCRIPT_KEYWORDS.has(w)) return "keyword";
128
+ if (/^[A-Z][A-Z0-9_]+$/.test(w)) return "atom";
129
+ if (/^[A-Z]/.test(w)) return "typeName";
130
+ if (/^[-+*/=<>!&|^~u]+$|^mod$/.test(w)) return "operator";
131
+ return "variableName";
132
+ },
133
+ };
134
+
135
+ export const sciScript = StreamLanguage.define(scriptParser);