feat(bgm): add dialog role and interaction affordances

Add the dialog role and the interaction affordances the free-interaction
layer depends on: Setup.interactions, Package.dialogs, and the Interaction,
DialogAction, and Dialog types. Wire the dialog role through the zod
schemas, package collection (with duplicate checking), and vite
serialization, and update the tabletop serializedToPackage and test
fixtures to carry dialogs.
This commit is contained in:
hyper
2026-08-18 10:24:21 +08:00
parent 8eff44712f
commit 999b7a0771
10 changed files with 604 additions and 312 deletions
+16 -13
View File
@@ -15,17 +15,17 @@
* be mistaken for real installed packages. Editing a game definition
* hot-reloads the app via `addWatchFile`.
*/
import * as path from 'node:path';
import { normalizePath, type ModuleNode, type Plugin } from 'vite';
import { collectPackages, loadDefs } from './collect.js';
import { readDefFiles } from './parse.js';
import type { Package, SerializedPackage } from './types.js';
import * as path from "node:path";
import { normalizePath, type ModuleNode, type Plugin } from "vite";
import { collectPackages, loadDefs } from "./collect.js";
import { readDefFiles } from "./parse.js";
import type { Package, SerializedPackage } from "./types.js";
const VIRTUAL_PREFIX = '\0bgm:';
const VIRTUAL_PREFIX = "\0bgm:";
/** Public specifier for the module that lists every package. */
const PACKAGES = 'virtual:bgm/packages';
const PACKAGES = "virtual:bgm/packages";
/** Public specifier prefix for a single package module. */
const PACKAGE = 'virtual:bgm/package/';
const PACKAGE = "virtual:bgm/package/";
export interface BgmOptions {
/** Absolute path to the games root (e.g. `<repo>/games`). */
@@ -39,23 +39,23 @@ export function bgm(options: BgmOptions): Plugin {
const root = normalizePath(options.root);
const collect = (): Package[] => {
const defMap = loadDefs('', root);
const defMap = loadDefs("", root);
return collectPackages(defMap, root);
};
return {
name: 'bgm',
name: "bgm",
buildStart() {
// Watch every real definition source under the games root so edits
// trigger a re-collect. `defMap.files` only holds the virtual code-block
// files plus real non-markdown files; the markdown files themselves are
// consumed for their code blocks and never appear there, so watch the
// real files on disk too (markdown and anything else the loader reads).
const defMap = loadDefs('', root);
const defMap = loadDefs("", root);
for (const name of defMap.files.keys()) {
this.addWatchFile(path.join(root, name));
}
for (const file of readDefFiles(root, '')) {
for (const file of readDefFiles(root, "")) {
this.addWatchFile(file.source);
}
},
@@ -68,7 +68,9 @@ export function bgm(options: BgmOptions): Plugin {
// changes, so edits hot-reload instead of requiring a manual refresh.
if (!ctx.file.startsWith(root)) return;
const invalidated: ModuleNode[] = [];
const mod = ctx.server.moduleGraph.getModuleById(VIRTUAL_PREFIX + PACKAGES);
const mod = ctx.server.moduleGraph.getModuleById(
VIRTUAL_PREFIX + PACKAGES,
);
if (mod) {
ctx.server.moduleGraph.invalidateModule(mod);
invalidated.push(mod);
@@ -102,5 +104,6 @@ function toJson(pkg: Package): SerializedPackage {
parts: Object.fromEntries(pkg.parts),
surfaces: Object.fromEntries(pkg.surfaces),
setups: Object.fromEntries(pkg.setups),
dialogs: Object.fromEntries(pkg.dialogs),
};
}