sci2-ts 0.5.0 → 0.6.0

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project. Versions follow [Semantic Versioning](https
4
4
  and the entries are generated from commit messages
5
5
  ([Conventional Commits](https://www.conventionalcommits.org/)) when a release is made.
6
6
 
7
+ ## [0.6.0](https://github.com/rlueder/sci-ts/compare/v0.5.0...v0.6.0) (2026-10-01)
8
+
9
+ ### Added
10
+
11
+ * **content:** items made from items.yaml, given and taken in Yarn ([#4](https://github.com/rlueder/sci-ts/issues/4)) ([129c439](https://github.com/rlueder/sci-ts/commit/129c43965f318d8815957e6efd6ce9290eced619))
12
+
7
13
  ## [0.5.0](https://github.com/rlueder/sci-ts/compare/v0.4.0...v0.5.0) (2026-10-01)
8
14
 
9
15
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sci2-ts",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "An SCI2 adventure game engine in TypeScript, and the tools to make games with it",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -549,11 +549,22 @@ class RoomCompiler {
549
549
  "pushi #show", "pushi 4", ...pushVal(int(view, "view", 0, 65535)), ...pushVal(int(loop, "loop", 0, 255)), ...pushVal(int(cel, "cel", 0, 255)), "pushSelf", "send 12",
550
550
  ] }];
551
551
  }
552
+ case "get":
553
+ case "drop": {
554
+ arity(1, "<item>");
555
+ const { items, globals } = this.target;
556
+ const n = items?.exports[args[0]];
557
+ if (n === undefined || globals.inventory === undefined) {
558
+ return fail(`no item called "${args[0]}"${items && Object.keys(items.exports).length ? ` (items: ${Object.keys(items.exports).join(", ")})` : " (items with a view in items.yaml)"}`);
559
+ }
560
+ const item = { code: [`pushi 2`, `pushi ${items.script}`, `pushi ${n}`, "callk ScriptID 4"] };
561
+ return [{ op: "code", code: send({ global: globals.inventory }, [[name === "get" ? "add" : "delete", [item]]]) }];
562
+ }
552
563
  case "room":
553
564
  arity(1, "number");
554
565
  return [{ op: "code", code: send({ global: g.curRoom }, [["newRoom", [int(args[0], "room", 0, 65535)]]]) }];
555
566
  default:
556
- return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, closeup, room; and set, if)");
567
+ return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, closeup, get, drop, room; and set, if)");
557
568
  }
558
569
  }
559
570
  /** Loop music n, unless it's what's playing already. */
@@ -646,11 +646,22 @@ class RoomCompiler {
646
646
  "pushi #show", "pushi 4", ...pushVal(int(view, "view", 0, 65535)), ...pushVal(int(loop, "loop", 0, 255)), ...pushVal(int(cel, "cel", 0, 255)), "pushSelf", "send 12",
647
647
  ] }];
648
648
  }
649
+ case "get":
650
+ case "drop": {
651
+ arity(1, "<item>");
652
+ const { items, globals } = this.target;
653
+ const n = items?.exports[args[0]!];
654
+ if (n === undefined || globals.inventory === undefined) {
655
+ return fail(`no item called "${args[0]}"${items && Object.keys(items.exports).length ? ` (items: ${Object.keys(items.exports).join(", ")})` : " (items with a view in items.yaml)"}`);
656
+ }
657
+ const item: Val = { code: [`pushi 2`, `pushi ${items!.script}`, `pushi ${n}`, "callk ScriptID 4"] };
658
+ return [{ op: "code", code: send({ global: globals.inventory }, [[name === "get" ? "add" : "delete", [item]]]) }];
659
+ }
649
660
  case "room":
650
661
  arity(1, "number");
651
662
  return [{ op: "code", code: send({ global: g.curRoom }, [["newRoom", [int(args[0], "room", 0, 65535)]]]) }];
652
663
  default:
653
- return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, closeup, room; and set, if)");
664
+ return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, closeup, get, drop, room; and set, if)");
654
665
  }
655
666
  }
656
667
 
@@ -20,6 +20,8 @@ export interface Target {
20
20
  /** The Sound object playing the music, and the one for sound effects. */
21
21
  music: number;
22
22
  sound: number;
23
+ /** The collection of what the hero carries (`add:`, `delete:`), if the target has one. */
24
+ inventory?: number;
23
25
  };
24
26
  /** Message talker number for narration (no speaker). */
25
27
  narrator: number;
@@ -64,6 +66,14 @@ export interface Target {
64
66
  * instance takes `show: view loop cel caller` and cues the caller when it's dismissed.
65
67
  */
66
68
  closeUp?: string;
69
+ /**
70
+ * Items rooms can give and take (<<get lens>>, <<drop lens>>): objects exported by
71
+ * `script`, by name, added to and deleted from the inventory global.
72
+ */
73
+ items?: {
74
+ script: number;
75
+ exports: Record<string, number>;
76
+ };
67
77
  /** Verb number for "said on arrival" messages. */
68
78
  narrationVerb: number;
69
79
  /** Verb names as used in Yarn node titles (lower case) -> verb numbers. */
@@ -20,6 +20,8 @@ export interface Target {
20
20
  /** The Sound object playing the music, and the one for sound effects. */
21
21
  music: number;
22
22
  sound: number;
23
+ /** The collection of what the hero carries (`add:`, `delete:`), if the target has one. */
24
+ inventory?: number;
23
25
  };
24
26
  /** Message talker number for narration (no speaker). */
25
27
  narrator: number;
@@ -51,6 +53,11 @@ export interface Target {
51
53
  * instance takes `show: view loop cel caller` and cues the caller when it's dismissed.
52
54
  */
53
55
  closeUp?: string;
56
+ /**
57
+ * Items rooms can give and take (<<get lens>>, <<drop lens>>): objects exported by
58
+ * `script`, by name, added to and deleted from the inventory global.
59
+ */
60
+ items?: { script: number; exports: Record<string, number> };
54
61
  /** Verb number for "said on arrival" messages. */
55
62
  narrationVerb: number;
56
63
  /** Verb names as used in Yarn node titles (lower case) -> verb numbers. */
@@ -9,8 +9,9 @@ import { type ResourceData } from "../../packages/sci/src/index.ts";
9
9
  * games/<name>/rooms/<n>.room.yaml (+ <n>.yarn) rooms as data (packages/content), compiled
10
10
  * for the class library; their Yarn variables are flags
11
11
  * numbered in flags.yaml
12
- * games/<name>/items.yaml optional: the verbs of the things the hero can carry
13
- * (`lens: 10`), so rooms' Yarn can answer them (filings.lens)
12
+ * games/<name>/items.yaml optional: the things the hero can carry: their verbs, so
13
+ * rooms' Yarn can answer them (filings.lens), and with a view
14
+ * the item itself, made in script ITEMS_SCRIPT (<<get lens>>)
14
15
  * games/<name>/music/<n>.mid music: sound n, from a Standard MIDI File (General MIDI)
15
16
  * games/<name>/sounds/<n>.wav digital effects: sound n plays this instead of music
16
17
  * games/<name>/resources.ts optional: `export default () => ResourceData[]` for art,
@@ -46,5 +47,17 @@ export declare const LIBRARY_DIR: string;
46
47
  export declare function buildGame(dir: string, options?: {
47
48
  library?: boolean;
48
49
  }): Promise<BuiltGame>;
49
- /** items.yaml: `name: verb` lines, each item's verb from 10 to 255 (1 to 4 are the player's). */
50
- export declare function itemVerbs(dir: string): Record<string, number>;
50
+ /** An item from items.yaml: its verb, and if it has a view, everything the build needs to make it. */
51
+ export interface GameItem {
52
+ verb: number;
53
+ view?: number;
54
+ description?: string;
55
+ }
56
+ /**
57
+ * items.yaml: each thing the hero can carry, by name. `lens: 10` names just its verb (10 to
58
+ * 255; 1 to 4 are the player's), for a game whose scripts make the item; with a view, the
59
+ * build makes it too, in script ITEMS_SCRIPT, and rooms can give it (`<<get lens>>`):
60
+ *
61
+ * lens: { verb: 10, view: 250, description: "Holmes's lens." }
62
+ */
63
+ export declare function gameItems(dir: string): Record<string, GameItem>;
@@ -9,12 +9,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { existsSync, readFileSync, readdirSync } from "node:fs";
10
10
  import { basename, dirname, join, relative, resolve } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
+ import { parse as parseYaml } from "yaml";
12
13
  import { AsmError, CompileError, GLOBAL_NAMES_VOCAB, OBJECT_HEADER, ResourceType, assemble, compileScripts, kernelNames, messagesFromText, parseScript, resourceKey, writeClassTable, writeEffects, writeMessages, writeSelectorNames, writeSol, writeSound, } from "../../packages/sci/src/index.js";
13
14
  import { ContentError, compileRoom } from "../../packages/content/src/index.js";
14
15
  import { modFlags } from "../mod-build.js";
15
16
  import { AudioFileError, readMidiFile, readWav } from "./audio.js";
16
17
  import { defaultResources } from "./defaults.js";
17
- import { libraryTarget } from "./target.js";
18
+ import { ITEMS_SCRIPT, libraryTarget } from "./target.js";
18
19
  const HEADER = OBJECT_HEADER;
19
20
  export class GameBuildError extends Error {
20
21
  }
@@ -36,6 +37,14 @@ export async function buildGame(dir, options = {}) {
36
37
  const repeated = numbers.find((n, i) => numbers.indexOf(n) !== i);
37
38
  if (repeated !== undefined)
38
39
  throw new GameBuildError(`${dir}: script ${repeated} is there twice`);
40
+ // Items described in items.yaml are made by a script of their own.
41
+ const items = gameItems(dir);
42
+ const madeItems = library ? itemsScript(items) : undefined;
43
+ if (madeItems) {
44
+ if (numbers.includes(ITEMS_SCRIPT))
45
+ throw new GameBuildError(`${dir}: script ${ITEMS_SCRIPT} is where the build makes the items in items.yaml`);
46
+ compiled.push({ file: join(dir, `${ITEMS_SCRIPT}.items.sc`), text: madeItems });
47
+ }
39
48
  if (library) {
40
49
  const lib = load(LIBRARY_DIR, /^\d+\.sc$/);
41
50
  const clash = lib.find((l) => numbers.includes(numberOf(l.file)));
@@ -108,7 +117,7 @@ export async function buildGame(dir, options = {}) {
108
117
  const roomsDir = join(dir, "rooms");
109
118
  if (!existsSync(roomsDir))
110
119
  return [];
111
- const target = libraryTarget(globals, itemVerbs(dir));
120
+ const target = libraryTarget(globals, items);
112
121
  const flags = modFlags(dir, { content: target });
113
122
  const out = [];
114
123
  for (const { file, text } of load(roomsDir, /^\d+\.room\.yaml$/)) {
@@ -257,29 +266,66 @@ export async function buildGame(dir, options = {}) {
257
266
  warnings: [...new Set(warnings)],
258
267
  };
259
268
  }
260
- /** items.yaml: `name: verb` lines, each item's verb from 10 to 255 (1 to 4 are the player's). */
261
- export function itemVerbs(dir) {
269
+ /**
270
+ * items.yaml: each thing the hero can carry, by name. `lens: 10` names just its verb (10 to
271
+ * 255; 1 to 4 are the player's), for a game whose scripts make the item; with a view, the
272
+ * build makes it too, in script ITEMS_SCRIPT, and rooms can give it (`<<get lens>>`):
273
+ *
274
+ * lens: { verb: 10, view: 250, description: "Holmes's lens." }
275
+ */
276
+ export function gameItems(dir) {
262
277
  const file = join(dir, "items.yaml");
263
278
  if (!existsSync(file))
264
279
  return {};
265
280
  const where = relative(process.cwd(), file);
266
- const verbs = {};
267
- readFileSync(file, "utf8").split("\n").forEach((raw, i) => {
268
- const line = raw.replace(/#.*/, "").trim();
269
- if (!line)
270
- return;
271
- const at = `${where}:${i + 1}`;
272
- const m = /^([A-Za-z]\w*)\s*:\s*(\d+)$/.exec(line);
273
- if (!m)
274
- throw new GameBuildError(`${at}: expected item: verb`);
275
- const [, name, n] = m, verb = Number(n);
281
+ const fail = (message) => { throw new GameBuildError(`${where}: ${message}`); };
282
+ let data;
283
+ try {
284
+ data = parseYaml(readFileSync(file, "utf8"));
285
+ }
286
+ catch (e) {
287
+ return fail(e.message.split("\n")[0]);
288
+ }
289
+ if (data === null || data === undefined)
290
+ return {};
291
+ if (typeof data !== "object" || Array.isArray(data))
292
+ return fail("expected item: verb, or item: { verb, view, description }");
293
+ const items = {};
294
+ for (const [name, value] of Object.entries(data)) {
276
295
  if (!/^[a-z][A-Za-z0-9]*$/.test(name) || ["look", "talk", "walk", "do", "enter"].includes(name))
277
- throw new GameBuildError(`${at}: "${name}" can't name an item (lower camelCase, and not a verb)`);
278
- if (verb < 10 || verb > 255)
279
- throw new GameBuildError(`${at}: ${name}: the verb is a number from 10 to 255`);
280
- if (Object.values(verbs).includes(verb))
281
- throw new GameBuildError(`${at}: ${name}: verb ${verb} is used twice`);
282
- verbs[name] = verb;
283
- });
284
- return verbs;
296
+ fail(`"${name}" can't name an item (lower camelCase, and not a verb)`);
297
+ const spec = typeof value === "number" ? { verb: value } : value;
298
+ if (!spec || typeof spec !== "object" || Array.isArray(spec))
299
+ return fail(`${name}: expected a verb, or { verb, view, description }`);
300
+ const { verb, view, description, ...rest } = spec;
301
+ const unknown = Object.keys(rest)[0];
302
+ if (unknown)
303
+ fail(`${name}: unknown key ${unknown}`);
304
+ if (typeof verb !== "number" || !Number.isInteger(verb) || verb < 10 || verb > 255)
305
+ fail(`${name}: the verb is a number from 10 to 255`);
306
+ if (Object.values(items).some((i) => i.verb === verb))
307
+ fail(`${name}: verb ${verb} is used twice`);
308
+ if (view !== undefined && (typeof view !== "number" || !Number.isInteger(view) || view < 0 || view > 65535))
309
+ fail(`${name}: view is a view number`);
310
+ if (description !== undefined && (typeof description !== "string" || /["\\]/.test(description)))
311
+ fail(`${name}: description is text without quotes or backslashes`);
312
+ if (description !== undefined && view === undefined)
313
+ fail(`${name}: an item the build makes needs a view`);
314
+ items[name] = { verb: verb, view: view, description: description };
315
+ }
316
+ return items;
317
+ }
318
+ /** The script making the items that have views, exported in items.yaml's order. */
319
+ function itemsScript(items) {
320
+ const made = Object.entries(items).filter(([, i]) => i.view !== undefined);
321
+ if (!made.length)
322
+ return undefined;
323
+ return [
324
+ ";;; The items in items.yaml, made by the build.",
325
+ `(script ${ITEMS_SCRIPT})`,
326
+ '(include "system.sh")',
327
+ `(public ${made.map(([name], i) => `${name} ${i}`).join(" ")})`,
328
+ ...made.map(([name, i]) => `(instance ${name} of InvItem (properties view ${i.view} verb ${i.verb}${i.description ? ` description "${i.description}"` : ""}))`),
329
+ "",
330
+ ].join("\n");
285
331
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { basename, dirname, join, relative, resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
+ import { parse as parseYaml } from "yaml";
4
5
  import {
5
6
  AsmError, CompileError, GLOBAL_NAMES_VOCAB, OBJECT_HEADER, ResourceType, assemble, compileScripts, kernelNames, messagesFromText, parseScript,
6
7
  resourceKey, writeClassTable, writeEffects, writeMessages, writeSelectorNames, writeSol, writeSound, type AsmContext,
@@ -10,7 +11,7 @@ import { ContentError, compileRoom } from "../../packages/content/src/index.ts";
10
11
  import { modFlags } from "../mod-build.ts";
11
12
  import { AudioFileError, readMidiFile, readWav } from "./audio.ts";
12
13
  import { defaultResources } from "./defaults.ts";
13
- import { libraryTarget } from "./target.ts";
14
+ import { ITEMS_SCRIPT, libraryTarget } from "./target.ts";
14
15
 
15
16
  /**
16
17
  * Builds a game from its sources, needing nothing from any other game:
@@ -22,8 +23,9 @@ import { libraryTarget } from "./target.ts";
22
23
  * games/<name>/rooms/<n>.room.yaml (+ <n>.yarn) rooms as data (packages/content), compiled
23
24
  * for the class library; their Yarn variables are flags
24
25
  * numbered in flags.yaml
25
- * games/<name>/items.yaml optional: the verbs of the things the hero can carry
26
- * (`lens: 10`), so rooms' Yarn can answer them (filings.lens)
26
+ * games/<name>/items.yaml optional: the things the hero can carry: their verbs, so
27
+ * rooms' Yarn can answer them (filings.lens), and with a view
28
+ * the item itself, made in script ITEMS_SCRIPT (<<get lens>>)
27
29
  * games/<name>/music/<n>.mid music: sound n, from a Standard MIDI File (General MIDI)
28
30
  * games/<name>/sounds/<n>.wav digital effects: sound n plays this instead of music
29
31
  * games/<name>/resources.ts optional: `export default () => ResourceData[]` for art,
@@ -71,6 +73,13 @@ export async function buildGame(dir: string, options: { library?: boolean } = {}
71
73
  if (!numbers.includes(0)) throw new GameBuildError(`${dir}: no scripts/0.sc or 0.sca (script 0 exports the game object)`);
72
74
  const repeated = numbers.find((n, i) => numbers.indexOf(n) !== i);
73
75
  if (repeated !== undefined) throw new GameBuildError(`${dir}: script ${repeated} is there twice`);
76
+ // Items described in items.yaml are made by a script of their own.
77
+ const items = gameItems(dir);
78
+ const madeItems = library ? itemsScript(items) : undefined;
79
+ if (madeItems) {
80
+ if (numbers.includes(ITEMS_SCRIPT)) throw new GameBuildError(`${dir}: script ${ITEMS_SCRIPT} is where the build makes the items in items.yaml`);
81
+ compiled.push({ file: join(dir, `${ITEMS_SCRIPT}.items.sc`), text: madeItems });
82
+ }
74
83
  if (library) {
75
84
  const lib = load(LIBRARY_DIR, /^\d+\.sc$/);
76
85
  const clash = lib.find((l) => numbers.includes(numberOf(l.file)));
@@ -142,7 +151,7 @@ export async function buildGame(dir: string, options: { library?: boolean } = {}
142
151
  const compileRooms = (): { file: string; text: string }[] => {
143
152
  const roomsDir = join(dir, "rooms");
144
153
  if (!existsSync(roomsDir)) return [];
145
- const target = libraryTarget(globals, itemVerbs(dir));
154
+ const target = libraryTarget(globals, items);
146
155
  const flags = modFlags(dir, { content: target });
147
156
  const out: { file: string; text: string }[] = [];
148
157
  for (const { file, text } of load(roomsDir, /^\d+\.room\.yaml$/)) {
@@ -284,23 +293,58 @@ export async function buildGame(dir: string, options: { library?: boolean } = {}
284
293
  };
285
294
  }
286
295
 
287
- /** items.yaml: `name: verb` lines, each item's verb from 10 to 255 (1 to 4 are the player's). */
288
- export function itemVerbs(dir: string): Record<string, number> {
296
+ /** An item from items.yaml: its verb, and if it has a view, everything the build needs to make it. */
297
+ export interface GameItem {
298
+ verb: number;
299
+ view?: number;
300
+ description?: string;
301
+ }
302
+
303
+ /**
304
+ * items.yaml: each thing the hero can carry, by name. `lens: 10` names just its verb (10 to
305
+ * 255; 1 to 4 are the player's), for a game whose scripts make the item; with a view, the
306
+ * build makes it too, in script ITEMS_SCRIPT, and rooms can give it (`<<get lens>>`):
307
+ *
308
+ * lens: { verb: 10, view: 250, description: "Holmes's lens." }
309
+ */
310
+ export function gameItems(dir: string): Record<string, GameItem> {
289
311
  const file = join(dir, "items.yaml");
290
312
  if (!existsSync(file)) return {};
291
313
  const where = relative(process.cwd(), file);
292
- const verbs: Record<string, number> = {};
293
- readFileSync(file, "utf8").split("\n").forEach((raw, i) => {
294
- const line = raw.replace(/#.*/, "").trim();
295
- if (!line) return;
296
- const at = `${where}:${i + 1}`;
297
- const m = /^([A-Za-z]\w*)\s*:\s*(\d+)$/.exec(line);
298
- if (!m) throw new GameBuildError(`${at}: expected item: verb`);
299
- const [, name, n] = m, verb = Number(n);
300
- if (!/^[a-z][A-Za-z0-9]*$/.test(name!) || ["look", "talk", "walk", "do", "enter"].includes(name!)) throw new GameBuildError(`${at}: "${name}" can't name an item (lower camelCase, and not a verb)`);
301
- if (verb < 10 || verb > 255) throw new GameBuildError(`${at}: ${name}: the verb is a number from 10 to 255`);
302
- if (Object.values(verbs).includes(verb)) throw new GameBuildError(`${at}: ${name}: verb ${verb} is used twice`);
303
- verbs[name!] = verb;
304
- });
305
- return verbs;
314
+ const fail = (message: string): never => { throw new GameBuildError(`${where}: ${message}`); };
315
+ let data: unknown;
316
+ try { data = parseYaml(readFileSync(file, "utf8")); }
317
+ catch (e) { return fail((e as Error).message.split("\n")[0]!); }
318
+ if (data === null || data === undefined) return {};
319
+ if (typeof data !== "object" || Array.isArray(data)) return fail("expected item: verb, or item: { verb, view, description }");
320
+ const items: Record<string, GameItem> = {};
321
+ for (const [name, value] of Object.entries(data as Record<string, unknown>)) {
322
+ if (!/^[a-z][A-Za-z0-9]*$/.test(name) || ["look", "talk", "walk", "do", "enter"].includes(name)) fail(`"${name}" can't name an item (lower camelCase, and not a verb)`);
323
+ const spec = typeof value === "number" ? { verb: value } : value;
324
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) return fail(`${name}: expected a verb, or { verb, view, description }`);
325
+ const { verb, view, description, ...rest } = spec as Record<string, unknown>;
326
+ const unknown = Object.keys(rest)[0];
327
+ if (unknown) fail(`${name}: unknown key ${unknown}`);
328
+ if (typeof verb !== "number" || !Number.isInteger(verb) || verb < 10 || verb > 255) fail(`${name}: the verb is a number from 10 to 255`);
329
+ if (Object.values(items).some((i) => i.verb === verb)) fail(`${name}: verb ${verb} is used twice`);
330
+ if (view !== undefined && (typeof view !== "number" || !Number.isInteger(view) || view < 0 || view > 65535)) fail(`${name}: view is a view number`);
331
+ if (description !== undefined && (typeof description !== "string" || /["\\]/.test(description))) fail(`${name}: description is text without quotes or backslashes`);
332
+ if (description !== undefined && view === undefined) fail(`${name}: an item the build makes needs a view`);
333
+ items[name] = { verb: verb as number, view: view as number | undefined, description: description as string | undefined };
334
+ }
335
+ return items;
336
+ }
337
+
338
+ /** The script making the items that have views, exported in items.yaml's order. */
339
+ function itemsScript(items: Record<string, GameItem>): string | undefined {
340
+ const made = Object.entries(items).filter(([, i]) => i.view !== undefined);
341
+ if (!made.length) return undefined;
342
+ return [
343
+ ";;; The items in items.yaml, made by the build.",
344
+ `(script ${ITEMS_SCRIPT})`,
345
+ '(include "system.sh")',
346
+ `(public ${made.map(([name], i) => `${name} ${i}`).join(" ")})`,
347
+ ...made.map(([name, i]) => `(instance ${name} of InvItem (properties view ${i.view} verb ${i.verb}${i.description ? ` description "${i.description}"` : ""}))`),
348
+ "",
349
+ ].join("\n");
306
350
  }
@@ -4,4 +4,9 @@ import type { Target } from "../../packages/content/src/index.ts";
4
4
  * (packages/content). Global numbers depend on the game's script 0, so the target is made
5
5
  * for each build from the compiled globals' names.
6
6
  */
7
- export declare function libraryTarget(globals: readonly string[], items?: Record<string, number>): Target;
7
+ /** The script the build makes for the items items.yaml describes (InvItem instances). */
8
+ export declare const ITEMS_SCRIPT = 989;
9
+ export declare function libraryTarget(globals: readonly string[], items?: Record<string, {
10
+ verb: number;
11
+ view?: number;
12
+ }>): Target;
@@ -3,7 +3,10 @@
3
3
  * (packages/content). Global numbers depend on the game's script 0, so the target is made
4
4
  * for each build from the compiled globals' names.
5
5
  */
6
+ /** The script the build makes for the items items.yaml describes (InvItem instances). */
7
+ export const ITEMS_SCRIPT = 989;
6
8
  export function libraryTarget(globals, items = {}) {
9
+ const made = Object.entries(items).filter(([, i]) => i.view !== undefined).map(([name]) => name);
7
10
  const g = (name) => {
8
11
  const n = globals.indexOf(name);
9
12
  if (n < 0)
@@ -21,6 +24,7 @@ export function libraryTarget(globals, items = {}) {
21
24
  narratorObject: g("narrator"),
22
25
  music: g("music"),
23
26
  sound: g("sfx"),
27
+ inventory: g("inventory"),
24
28
  },
25
29
  // As in lib/system.sh.
26
30
  narrator: 99,
@@ -35,7 +39,9 @@ export function libraryTarget(globals, items = {}) {
35
39
  // room.enter lines: a verb the player can't use.
36
40
  narrationVerb: 0,
37
41
  // And the game's items (items.yaml): using one is its own verb.
38
- verbs: { look: 1, talk: 2, walk: 3, do: 4, ...items },
42
+ verbs: { look: 1, talk: 2, walk: 3, do: 4, ...Object.fromEntries(Object.entries(items).map(([name, i]) => [name, i.verb])) },
43
+ // Items the build makes (exported in items.yaml's order): <<get>> and <<drop>>.
44
+ items: { script: ITEMS_SCRIPT, exports: Object.fromEntries(made.map((name, i) => [name, i])) },
39
45
  messageVersion: 4321,
40
46
  // SetFlag, ClearFlag and IsFlag: script 999's exports 0, 1 and 2.
41
47
  flags: { script: 999, set: 0, clear: 1, test: 2, first: 0, last: 1023 },
@@ -5,7 +5,11 @@ import type { Target } from "../../packages/content/src/index.ts";
5
5
  * (packages/content). Global numbers depend on the game's script 0, so the target is made
6
6
  * for each build from the compiled globals' names.
7
7
  */
8
- export function libraryTarget(globals: readonly string[], items: Record<string, number> = {}): Target {
8
+ /** The script the build makes for the items items.yaml describes (InvItem instances). */
9
+ export const ITEMS_SCRIPT = 989;
10
+
11
+ export function libraryTarget(globals: readonly string[], items: Record<string, { verb: number; view?: number }> = {}): Target {
12
+ const made = Object.entries(items).filter(([, i]) => i.view !== undefined).map(([name]) => name);
9
13
  const g = (name: string) => {
10
14
  const n = globals.indexOf(name);
11
15
  if (n < 0) throw new Error(`the library's global ${name} isn't there: is script 0 compiled with the library?`);
@@ -22,6 +26,7 @@ export function libraryTarget(globals: readonly string[], items: Record<string,
22
26
  narratorObject: g("narrator"),
23
27
  music: g("music"),
24
28
  sound: g("sfx"),
29
+ inventory: g("inventory"),
25
30
  },
26
31
  // As in lib/system.sh.
27
32
  narrator: 99,
@@ -36,7 +41,9 @@ export function libraryTarget(globals: readonly string[], items: Record<string,
36
41
  // room.enter lines: a verb the player can't use.
37
42
  narrationVerb: 0,
38
43
  // And the game's items (items.yaml): using one is its own verb.
39
- verbs: { look: 1, talk: 2, walk: 3, do: 4, ...items },
44
+ verbs: { look: 1, talk: 2, walk: 3, do: 4, ...Object.fromEntries(Object.entries(items).map(([name, i]) => [name, i.verb])) },
45
+ // Items the build makes (exported in items.yaml's order): <<get>> and <<drop>>.
46
+ items: { script: ITEMS_SCRIPT, exports: Object.fromEntries(made.map((name, i) => [name, i])) },
40
47
  messageVersion: 4321,
41
48
  // SetFlag, ClearFlag and IsFlag: script 999's exports 0, 1 and 2.
42
49
  flags: { script: 999, set: 0, clear: 1, test: 2, first: 0, last: 1023 },