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 +6 -0
- package/package.json +1 -1
- package/packages/content/src/room.js +12 -1
- package/packages/content/src/room.ts +12 -1
- package/packages/content/src/target.d.ts +10 -0
- package/packages/content/src/target.ts +7 -0
- package/tools/game/build.d.ts +17 -4
- package/tools/game/build.js +68 -22
- package/tools/game/build.ts +64 -20
- package/tools/game/target.d.ts +6 -1
- package/tools/game/target.js +7 -1
- package/tools/game/target.ts +9 -2
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
|
@@ -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. */
|
package/tools/game/build.d.ts
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
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:
|
|
50
|
-
export
|
|
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>;
|
package/tools/game/build.js
CHANGED
|
@@ -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,
|
|
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
|
-
/**
|
|
261
|
-
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
}
|
package/tools/game/build.ts
CHANGED
|
@@ -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
|
|
26
|
-
*
|
|
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,
|
|
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:
|
|
288
|
-
export
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (!/^[a-z][A-Za-z0-9]*$/.test(name
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
}
|
package/tools/game/target.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/tools/game/target.js
CHANGED
|
@@ -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 },
|
package/tools/game/target.ts
CHANGED
|
@@ -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
|
-
|
|
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 },
|