@khorsheed/dsh-game 0.0.0-stage → 0.1.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.en.md +53 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +52 -2
  5. package/cordis.patch.yml +23 -0
  6. package/lib/client.js +30348 -0
  7. package/lib/index.js +4154 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +3769 -0
  10. package/lib/typert.remote-client.d.ts +101 -0
  11. package/lib/typert.remote-client.js +2082 -0
  12. package/lib/types/agent.d.ts +20 -0
  13. package/lib/types/agent.js +42 -0
  14. package/lib/types/checks.d.ts +44 -0
  15. package/lib/types/checks.js +267 -0
  16. package/lib/types/client/DesignView.d.ts +22 -0
  17. package/lib/types/client/DesignView.js +134 -0
  18. package/lib/types/client/ElementsView.d.ts +21 -0
  19. package/lib/types/client/ElementsView.js +187 -0
  20. package/lib/types/client/GameTab.d.ts +14 -0
  21. package/lib/types/client/GameTab.js +256 -0
  22. package/lib/types/client/PlayFrame.d.ts +54 -0
  23. package/lib/types/client/PlayFrame.js +129 -0
  24. package/lib/types/client/PlayView.d.ts +19 -0
  25. package/lib/types/client/PlayView.js +161 -0
  26. package/lib/types/client/Preview3D.d.ts +50 -0
  27. package/lib/types/client/Preview3D.js +176 -0
  28. package/lib/types/client/ProjectSwitcher.d.ts +20 -0
  29. package/lib/types/client/ProjectSwitcher.js +75 -0
  30. package/lib/types/client/Recipe3DEditor.d.ts +19 -0
  31. package/lib/types/client/Recipe3DEditor.js +44 -0
  32. package/lib/types/client/StyleGallery.d.ts +18 -0
  33. package/lib/types/client/StyleGallery.js +27 -0
  34. package/lib/types/client/TurnCard.d.ts +62 -0
  35. package/lib/types/client/TurnCard.js +100 -0
  36. package/lib/types/client/VersionsView.d.ts +17 -0
  37. package/lib/types/client/VersionsView.js +68 -0
  38. package/lib/types/client/chat-node.d.ts +42 -0
  39. package/lib/types/client/chat-node.js +77 -0
  40. package/lib/types/client/contract.d.ts +125 -0
  41. package/lib/types/client/contract.js +6 -0
  42. package/lib/types/client/definition.d.ts +22 -0
  43. package/lib/types/client/definition.js +28 -0
  44. package/lib/types/client/index.d.ts +21 -0
  45. package/lib/types/client/index.js +229 -0
  46. package/lib/types/client/locales.d.ts +376 -0
  47. package/lib/types/client/locales.js +727 -0
  48. package/lib/types/client/looks.d.ts +36 -0
  49. package/lib/types/client/looks.js +105 -0
  50. package/lib/types/client/markdown.d.ts +11 -0
  51. package/lib/types/client/markdown.js +52 -0
  52. package/lib/types/client/preset-visibility.d.ts +94 -0
  53. package/lib/types/client/preset-visibility.js +115 -0
  54. package/lib/types/client/preview-entry.d.ts +24 -0
  55. package/lib/types/client/preview-entry.js +107 -0
  56. package/lib/types/client/refs.d.ts +36 -0
  57. package/lib/types/client/refs.js +44 -0
  58. package/lib/types/client/style-samples.d.ts +5 -0
  59. package/lib/types/client/style-samples.js +196 -0
  60. package/lib/types/fsutil.d.ts +26 -0
  61. package/lib/types/fsutil.js +104 -0
  62. package/lib/types/hooks.d.ts +83 -0
  63. package/lib/types/hooks.js +108 -0
  64. package/lib/types/index.d.ts +30 -0
  65. package/lib/types/index.js +22 -0
  66. package/lib/types/kits/canvas2d.d.ts +6 -0
  67. package/lib/types/kits/canvas2d.js +60 -0
  68. package/lib/types/kits/ids.d.ts +11 -0
  69. package/lib/types/kits/ids.js +6 -0
  70. package/lib/types/kits/index.d.ts +26 -0
  71. package/lib/types/kits/index.js +28 -0
  72. package/lib/types/kits/three3d.d.ts +12 -0
  73. package/lib/types/kits/three3d.js +188 -0
  74. package/lib/types/kits/types.d.ts +52 -0
  75. package/lib/types/kits/types.js +6 -0
  76. package/lib/types/look3d.d.ts +20 -0
  77. package/lib/types/look3d.js +175 -0
  78. package/lib/types/panel-session.d.ts +21 -0
  79. package/lib/types/panel-session.js +16 -0
  80. package/lib/types/project.d.ts +132 -0
  81. package/lib/types/project.js +494 -0
  82. package/lib/types/remote.d.ts +182 -0
  83. package/lib/types/remote.js +256 -0
  84. package/lib/types/skills.d.ts +13 -0
  85. package/lib/types/skills.js +140 -0
  86. package/lib/types/store.d.ts +325 -0
  87. package/lib/types/store.js +1285 -0
  88. package/lib/types/tools.d.ts +12 -0
  89. package/lib/types/tools.js +301 -0
  90. package/lib/types/trash.d.ts +5 -0
  91. package/lib/types/trash.js +64 -0
  92. package/lib/types/types.d.ts +522 -0
  93. package/lib/types/types.js +85 -0
  94. package/lib/types/vfs.d.ts +92 -0
  95. package/lib/types/vfs.js +217 -0
  96. package/locale/en.json +6 -0
  97. package/locale/zh.json +6 -0
  98. package/package.json +132 -3
  99. package/runtime/dsh3d.js +1279 -0
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The `./agent` composition entry: the game mode's tools, their guidance
3
+ * section and the `gamedev` / `game-design` skills. The package's
4
+ * `cordis.patch.yml` inserts this row DISABLED at the profile root; the game
5
+ * preset (`preset-dsh-gamedev`) names it in its own composition, so the tools
6
+ * exist for that preset's sessions only. Never both live at once — the tools
7
+ * would register twice (the canvas `./agent` pattern).
8
+ * @module @khorsheed/dsh-game/agent
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ /** Cordis plugin name used by loader diagnostics. */
12
+ export declare const name = "game-agent";
13
+ /** No hard injects: the core service is probed, the registries join through deferred injection. */
14
+ export declare const inject: never[];
15
+ /**
16
+ * Register the tools, the guidance and the skills, or nothing when the core is absent.
17
+ * @param ctx - the mounting scope (a preset's plane, normally).
18
+ */
19
+ export declare function apply(ctx: Context): void;
20
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1,42 @@
1
+ import { GAME_SKILLS } from "./skills.js";
2
+ import { gameToolDefinitions } from "./tools.js";
3
+ /** Cordis plugin name used by loader diagnostics. */
4
+ export const name = 'game-agent';
5
+ /** No hard injects: the core service is probed, the registries join through deferred injection. */
6
+ export const inject = [];
7
+ /** One pointer paragraph; the rules live in the skills. */
8
+ const GUIDANCE = 'This session is the game-development mode. The user designs and plays a small web game (2D or 3D) with you: '
9
+ + 'the conversation decides what the game is, the right-sidebar game tab shows the design doc and plays the latest '
10
+ + 'version that passed the checks. Use game_open / game_read for the project; write its files with the '
11
+ + 'ordinary file tools. Whenever you changed something playable, call game_publish before you report or ask anything, '
12
+ + 'without waiting to be asked — a question does not end the turn, so the play area would still show the old version. '
13
+ + 'When a CLI member reports its changes, publish them for it (members have no publish tool). Load the game-design skill when shaping the game with the user, and the gamedev skill before '
14
+ + 'writing game code. Answer in the user\'s language.';
15
+ /**
16
+ * Register the tools, the guidance and the skills, or nothing when the core is absent.
17
+ * @param ctx - the mounting scope (a preset's plane, normally).
18
+ */
19
+ export function apply(ctx) {
20
+ const store = ctx.get('gameStore');
21
+ if (store === undefined) {
22
+ ctx.logger.warn('game-agent: the gameStore service is absent — the game tools are not registered');
23
+ return;
24
+ }
25
+ ctx.inject(['tools'], (toolsCtx) => {
26
+ for (const definition of gameToolDefinitions(store)) {
27
+ toolsCtx.effect(() => toolsCtx.tools.register(definition), `game-agent: ${definition.name} tool`);
28
+ }
29
+ });
30
+ ctx.inject(['systemPrompt'], (promptCtx) => {
31
+ const sections = promptCtx.get('systemPrompt');
32
+ sections?.section({ name: 'game:mode', order: 152, text: GUIDANCE });
33
+ });
34
+ ctx.inject(['skills'], (skillCtx) => {
35
+ const skills = skillCtx.get('skills');
36
+ if (skills === undefined)
37
+ return;
38
+ for (const skill of GAME_SKILLS) {
39
+ skillCtx.effect(() => skills.register({ ...skill, source: 'runtime', provider: '@khorsheed/dsh-game' }), `game-agent: ${skill.name} skill`);
40
+ }
41
+ });
42
+ }
@@ -0,0 +1,44 @@
1
+ import type { BundleFile, CheckFailure } from './types.ts';
2
+ /** What the browser part reports. */
3
+ export interface BrowserCheckResult {
4
+ readonly failures: readonly CheckFailure[];
5
+ readonly passed: number;
6
+ /** Set when the browser part did not run at all. */
7
+ readonly skipped?: string;
8
+ /** A webp screenshot of the first frames, base64, when the smoke run got that far. */
9
+ readonly screenshot?: string;
10
+ }
11
+ /** Per-run inputs that come from the project's runtime kit. */
12
+ export interface BrowserCheckOptions {
13
+ /** The kit's blank-screen expression (see `RuntimeKit.blankProbe`). */
14
+ readonly blankProbe: string;
15
+ }
16
+ /** The browser-check seam (specs stand in a fake). */
17
+ export interface BrowserChecker {
18
+ run(files: Readonly<Record<string, BundleFile>>, tests: Readonly<Record<string, string>>, options: BrowserCheckOptions): Promise<BrowserCheckResult>;
19
+ dispose(): Promise<void>;
20
+ }
21
+ /**
22
+ * The Chrome executable to drive: the configured path, `CHROME_PATH`, or the
23
+ * first common install location that exists.
24
+ * @param configured - the plugin config's `chromePath`.
25
+ * @returns the path, or undefined when none is found.
26
+ */
27
+ export declare function findChrome(configured?: string): string | undefined;
28
+ /** The headless-Chrome checker. */
29
+ export declare class ChromeChecker implements BrowserChecker {
30
+ private readonly chromePath?;
31
+ private browser;
32
+ private idle;
33
+ private profile;
34
+ /** @param chromePath - configured executable (optional). */
35
+ constructor(chromePath?: string | undefined);
36
+ private launch;
37
+ private page;
38
+ private armIdle;
39
+ /** Load the build into a fresh page with CSP bypassed and wait for ready. */
40
+ private load;
41
+ run(files: Readonly<Record<string, BundleFile>>, tests: Readonly<Record<string, string>>, options: BrowserCheckOptions): Promise<BrowserCheckResult>;
42
+ dispose(): Promise<void>;
43
+ }
44
+ //# sourceMappingURL=checks.d.ts.map
@@ -0,0 +1,267 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /**
10
+ * The end-of-turn checks (proposal §6): data validation in plain node, then
11
+ * a smoke run and the project's own test scripts in headless Chrome, all on
12
+ * the same sandbox loader the panel uses.
13
+ *
14
+ * The browser is the machine's installed Chrome (or `chromePath`), driven by
15
+ * `puppeteer-core` loaded on first use — a host without Chrome still runs
16
+ * the data checks and reports the browser part as skipped instead of
17
+ * failing. One browser stays up between turns and shuts down after a minute
18
+ * idle.
19
+ *
20
+ * Test scripts are `tests/*.test.js` the agent writes beside the code. Each
21
+ * file runs on a fresh load of the build, as the body of an async function
22
+ * receiving `test`, `assert`, `game` (= `window.dshGame`), `step`, `press`
23
+ * and `drag` (calls the game's optional `look(dx, dy)`); `test(name, fn)`
24
+ * registers a case and every case runs in order.
25
+ * @module @khorsheed/dsh-game
26
+ */
27
+ import { existsSync } from 'node:fs';
28
+ import { mkdtemp, rm } from 'node:fs/promises';
29
+ import { tmpdir } from 'node:os';
30
+ import { join } from 'node:path';
31
+ import { shellHtml } from "./vfs.js";
32
+ /** Common Chrome install locations, tried when no path is configured. */
33
+ const CHROME_CANDIDATES = [
34
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
35
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
36
+ '/usr/bin/google-chrome',
37
+ '/usr/bin/google-chrome-stable',
38
+ '/usr/bin/chromium',
39
+ '/usr/bin/chromium-browser',
40
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
41
+ ];
42
+ /**
43
+ * The Chrome executable to drive: the configured path, `CHROME_PATH`, or the
44
+ * first common install location that exists.
45
+ * @param configured - the plugin config's `chromePath`.
46
+ * @returns the path, or undefined when none is found.
47
+ */
48
+ export function findChrome(configured) {
49
+ for (const candidate of [configured, process.env.CHROME_PATH, ...CHROME_CANDIDATES]) {
50
+ if (candidate !== undefined && candidate !== '' && existsSync(candidate))
51
+ return candidate;
52
+ }
53
+ return undefined;
54
+ }
55
+ /** How long the game gets to set `dshGame.ready`. */
56
+ const READY_TIMEOUT_MS = 4000;
57
+ /** …and how long while it says it is still loading assets (`dshGame.loading`). */
58
+ const LOADING_TIMEOUT_MS = 20_000;
59
+ /** How long one test file may run. */
60
+ const TEST_TIMEOUT_MS = 8000;
61
+ /** The viewport checks render at. */
62
+ const VIEWPORT = { width: 640, height: 400 };
63
+ /** The in-page wait for `window.dshGame.ready`; resolves true/false. */
64
+ const WAIT_READY = `(async () => {
65
+ const start = Date.now();
66
+ for (;;) {
67
+ const g = window.dshGame;
68
+ if (g && g.ready) {
69
+ // Models still loading (dsh3d's glTF looks): smoke, tests and the screenshot wait for them.
70
+ if (window.__dshPending) await Promise.race([window.__dshPending(), new Promise(r => setTimeout(r, ${LOADING_TIMEOUT_MS}))]);
71
+ return true;
72
+ }
73
+ const limit = g && g.loading ? ${LOADING_TIMEOUT_MS} : ${READY_TIMEOUT_MS};
74
+ if (Date.now() - start >= limit) return false;
75
+ await new Promise(r => setTimeout(r, 40));
76
+ }
77
+ })()`;
78
+ /**
79
+ * The in-page smoke run: 2 s of game time, then the kit's blank-screen probe.
80
+ * @param blankProbe - the kit's expression (true blank, false drawn, null no canvas).
81
+ */
82
+ function smokeExpression(blankProbe) {
83
+ return `(async () => {
84
+ const g = window.dshGame;
85
+ const out = { stepped: false, blank: null };
86
+ if (g && typeof g.step === 'function') { for (let i = 0; i < 120; i++) g.step(1 / 60); out.stepped = true; }
87
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
88
+ out.blank = ${blankProbe};
89
+ return out;
90
+ })()`;
91
+ }
92
+ /**
93
+ * The in-page test harness for one file. The body is spliced in as the
94
+ * function body — CSP is bypassed for the check page, never for the panel.
95
+ * @param body - the test file's text.
96
+ * @returns the expression to evaluate.
97
+ */
98
+ function testExpression(body) {
99
+ return `(async () => {
100
+ const cases = [];
101
+ const test = (name, fn) => { cases.push({ name: String(name), fn }); };
102
+ const fail = (msg) => { throw new Error(msg); };
103
+ const assert = (cond, msg) => { if (!cond) fail(msg || 'assert 失败'); };
104
+ assert.equal = (a, b, msg) => { if (a !== b) fail((msg ? msg + ':' : '') + '期望 ' + JSON.stringify(b) + ',实际 ' + JSON.stringify(a)); };
105
+ assert.near = (a, b, eps, msg) => { if (!(Math.abs(a - b) <= (eps ?? 1e-6))) fail((msg ? msg + ':' : '') + '期望约 ' + b + ',实际 ' + a); };
106
+ const game = window.dshGame;
107
+ const step = (n = 1, dt = 1 / 60) => { for (let i = 0; i < n; i++) game.step(dt); };
108
+ const press = (key, frames = 1) => { game.input(key, true); step(frames); game.input(key, false); };
109
+ const drag = (dx, dy, frames = 1) => { if (typeof game.look !== 'function') fail('window.dshGame.look 不是函数,没法模拟鼠标拖动'); game.look(dx, dy); step(frames); };
110
+ try {
111
+ await (async (test, assert, game, step, press, drag) => {\n${body}\n})(test, assert, game, step, press, drag);
112
+ } catch (e) { return { load: String(e && e.message || e), results: [] }; }
113
+ const results = [];
114
+ for (const c of cases) {
115
+ try { await c.fn(); results.push({ name: c.name, ok: true }); }
116
+ catch (e) { results.push({ name: c.name, ok: false, msg: String(e && e.message || e).slice(0, 300) }); }
117
+ }
118
+ return { results };
119
+ })()`;
120
+ }
121
+ /** The errors the bridge collected, worded for the model. */
122
+ const READ_ERRORS = '(window.__gamedevErrors || []).slice(0, 5)';
123
+ /** Race a promise against a timeout. */
124
+ async function within(ms, work, what) {
125
+ let timer;
126
+ try {
127
+ return await Promise.race([
128
+ work,
129
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${what}超时(${ms / 1000} 秒)`)), ms); }),
130
+ ]);
131
+ }
132
+ finally {
133
+ if (timer !== undefined)
134
+ clearTimeout(timer);
135
+ }
136
+ }
137
+ /** The headless-Chrome checker. */
138
+ export class ChromeChecker {
139
+ chromePath;
140
+ browser;
141
+ idle;
142
+ profile;
143
+ /** @param chromePath - configured executable (optional). */
144
+ constructor(chromePath) {
145
+ this.chromePath = chromePath;
146
+ }
147
+ async launch(executablePath) {
148
+ // A variable specifier keeps the typert overlay (a harness clone without
149
+ // puppeteer) from resolving it, and the driver loads on first use only.
150
+ const specifier = 'puppeteer-core';
151
+ const { launch } = (await import(__rewriteRelativeImportExtension(specifier)));
152
+ this.profile = await mkdtemp(join(tmpdir(), 'dsh-game-chrome-'));
153
+ const browser = await launch({
154
+ executablePath,
155
+ headless: true,
156
+ pipe: true,
157
+ userDataDir: this.profile,
158
+ args: ['--no-first-run', '--no-default-browser-check', '--disable-background-networking', '--mute-audio'],
159
+ timeout: 30_000,
160
+ });
161
+ browser.on('disconnected', () => { this.browser = undefined; });
162
+ return browser;
163
+ }
164
+ async page(executablePath) {
165
+ if (this.idle !== undefined)
166
+ clearTimeout(this.idle);
167
+ this.browser ??= this.launch(executablePath).catch((error) => {
168
+ this.browser = undefined;
169
+ throw error;
170
+ });
171
+ return (await this.browser).newPage();
172
+ }
173
+ armIdle() {
174
+ if (this.idle !== undefined)
175
+ clearTimeout(this.idle);
176
+ this.idle = setTimeout(() => { void this.dispose(); }, 60_000);
177
+ this.idle.unref?.();
178
+ }
179
+ /** Load the build into a fresh page with CSP bypassed and wait for ready. */
180
+ async load(executablePath, files) {
181
+ const page = await this.page(executablePath);
182
+ await page.setBypassCSP(true);
183
+ await page.setViewport(VIEWPORT);
184
+ await page.evaluateOnNewDocument(`window.__VFS_FILES__ = ${JSON.stringify(files)};`);
185
+ const shell = shellHtml('<script>window.__GAMEDEV_TEST__=true</script>');
186
+ await page.goto(`data:text/html;base64,${Buffer.from(shell).toString('base64')}`, { waitUntil: 'load', timeout: 10_000 });
187
+ const ready = await page.evaluate(WAIT_READY);
188
+ return { page, ready };
189
+ }
190
+ async run(files, tests, options) {
191
+ const executablePath = findChrome(this.chromePath);
192
+ if (executablePath === undefined)
193
+ return { failures: [], passed: 0, skipped: '没找到 Chrome,只跑了数据校验' };
194
+ const failures = [];
195
+ let passed = 0;
196
+ let screenshot;
197
+ try {
198
+ const { page, ready } = await this.load(executablePath, files);
199
+ try {
200
+ if (!ready) {
201
+ failures.push({ where: 'smoke', message: `游戏没在 ${READY_TIMEOUT_MS / 1000} 秒内就绪(加载资源时设 dshGame.loading = true 可放宽到 ${LOADING_TIMEOUT_MS / 1000} 秒):window.dshGame.ready 一直不是 true` });
202
+ }
203
+ else {
204
+ const smoke = await within(5000, page.evaluate(smokeExpression(options.blankProbe)), '冒烟运行');
205
+ if (!smoke.stepped)
206
+ failures.push({ where: 'smoke', message: 'window.dshGame.step 不是函数,测试没法推进游戏时间' });
207
+ if (smoke.blank === true)
208
+ failures.push({ where: 'smoke', message: '跑了 2 秒后画布仍是同一种颜色(什么都没画出来)' });
209
+ }
210
+ const errors = await page.evaluate(READ_ERRORS);
211
+ for (const e of errors)
212
+ failures.push({ where: 'smoke', message: `运行报错(${e.kind}):${e.msg.slice(0, 300)}` });
213
+ screenshot = await page.screenshot({ type: 'webp', quality: 70, encoding: 'base64', captureBeyondViewport: false });
214
+ }
215
+ finally {
216
+ await page.close().catch(() => { });
217
+ }
218
+ // Tests only make sense on a build that loads.
219
+ if (failures.length === 0) {
220
+ for (const [file, body] of Object.entries(tests).sort(([a], [b]) => a.localeCompare(b))) {
221
+ const { page, ready } = await this.load(executablePath, files);
222
+ try {
223
+ if (!ready) {
224
+ failures.push({ where: file, message: '这次加载游戏没有就绪,测试没跑' });
225
+ continue;
226
+ }
227
+ const outcome = await within(TEST_TIMEOUT_MS, page.evaluate(testExpression(body)), '测试');
228
+ if (outcome.load !== undefined)
229
+ failures.push({ where: file, message: `测试文件本身出错:${outcome.load}` });
230
+ if (outcome.load === undefined && outcome.results.length === 0)
231
+ failures.push({ where: file, message: '没有用 test(名称, 函数) 登记任何用例' });
232
+ for (const r of outcome.results) {
233
+ if (r.ok)
234
+ passed++;
235
+ else
236
+ failures.push({ where: `${file} › ${r.name}`, message: r.msg ?? '失败' });
237
+ }
238
+ }
239
+ catch (error) {
240
+ failures.push({ where: file, message: error.message.split('\n')[0] ?? '出错' });
241
+ }
242
+ finally {
243
+ await page.close().catch(() => { });
244
+ }
245
+ }
246
+ }
247
+ }
248
+ catch (error) {
249
+ return { failures, passed, skipped: `浏览器检查没跑起来:${error.message.split('\n')[0]?.slice(0, 200)}` };
250
+ }
251
+ finally {
252
+ this.armIdle();
253
+ }
254
+ return { failures, passed, ...(screenshot === undefined ? {} : { screenshot }) };
255
+ }
256
+ async dispose() {
257
+ if (this.idle !== undefined)
258
+ clearTimeout(this.idle);
259
+ const pending = this.browser;
260
+ this.browser = undefined;
261
+ if (pending !== undefined)
262
+ await pending.then(b => b.close()).catch(() => { });
263
+ if (this.profile !== undefined)
264
+ await rm(this.profile, { recursive: true, force: true }).catch(() => { });
265
+ this.profile = undefined;
266
+ }
267
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 策划: the living design doc. The one-pager and the pillars sit pinned on
3
+ * top (every later idea is weighed against them); the chapters fold below,
4
+ * each with its status, a 刚更新 mark when it changed since the user last
5
+ * opened it, and in-place editing (the user's words win — the agent reads
6
+ * the file). 元素与数值 is drawn from the element files with a bar chart per
7
+ * number; 试玩记录 is the log the plugin keeps.
8
+ */
9
+ import { type ReactNode } from 'react';
10
+ import type { ProjectView } from '../types.ts';
11
+ import { type ViewProps } from './contract.ts';
12
+ /** Props of the design view. */
13
+ export interface DesignViewProps extends ViewProps {
14
+ readonly project: ProjectView;
15
+ readonly onStyle: () => void;
16
+ /** The chapter a jump asked to open. */
17
+ readonly openChapter?: string | null;
18
+ readonly onOpenChapter?: (id: string | null) => void;
19
+ }
20
+ /** The design view. */
21
+ export declare function DesignView(props: DesignViewProps): ReactNode;
22
+ //# sourceMappingURL=DesignView.d.ts.map
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * 策划: the living design doc. The one-pager and the pillars sit pinned on
4
+ * top (every later idea is weighed against them); the chapters fold below,
5
+ * each with its status, a 刚更新 mark when it changed since the user last
6
+ * opened it, and in-place editing (the user's words win — the agent reads
7
+ * the file). 元素与数值 is drawn from the element files with a bar chart per
8
+ * number; 试玩记录 is the log the plugin keeps.
9
+ */
10
+ import { useEffect, useMemo, useState } from 'react';
11
+ import { artStyleLabel } from "../project.js";
12
+ import { failureOf } from "./contract.js";
13
+ import { Markdown } from "./markdown.js";
14
+ import css from './GameTab.module.css';
15
+ const STATUS_KEY = { todo: 'design.todo', draft: 'design.draft', done: 'design.done' };
16
+ /** Per-project "last seen" times of chapters (localStorage, per viewer). */
17
+ function seenKey(projectId) {
18
+ return `dsh-game:seen:${projectId}`;
19
+ }
20
+ function readSeen(projectId) {
21
+ try {
22
+ return JSON.parse(localStorage.getItem(seenKey(projectId)) ?? '{}');
23
+ }
24
+ catch {
25
+ return {};
26
+ }
27
+ }
28
+ function writeSeen(projectId, seen) {
29
+ try {
30
+ localStorage.setItem(seenKey(projectId), JSON.stringify(seen));
31
+ }
32
+ catch {
33
+ // Per-viewer convenience only.
34
+ }
35
+ }
36
+ /** The design view. */
37
+ export function DesignView(props) {
38
+ const { t, project } = props;
39
+ const manifest = project.manifest;
40
+ const [open, setOpen] = useState(props.openChapter ?? null);
41
+ useEffect(() => { if (props.openChapter != null)
42
+ setOpen(props.openChapter); }, [props.openChapter]);
43
+ const [seen, setSeen] = useState(() => readSeen(project.id));
44
+ useEffect(() => { setSeen(readSeen(project.id)); }, [project.id]);
45
+ const toggle = (chapter) => {
46
+ setOpen(o => (o === chapter.id ? null : chapter.id));
47
+ const next = { ...seen, [chapter.id]: Date.now() };
48
+ setSeen(next);
49
+ writeSeen(project.id, next);
50
+ };
51
+ const chapters = project.chapters.filter(c => c.id !== 'pillars');
52
+ const pillarsChapter = project.chapters.find(c => c.id === 'pillars');
53
+ const done = project.chapters.filter(c => !c.generated && c.status === 'done').length;
54
+ const authored = project.chapters.filter(c => !c.generated).length;
55
+ return _jsxs("div", { className: css.scroll, children: [_jsx(OnePager, { ...props }), pillarsChapter !== undefined && _jsxs("section", { className: `${css.chapter} ${css.notesChapter}`, "data-open": open === 'pillars' ? 'true' : 'false', children: [_jsxs("button", { type: "button", className: css.chapterHead, "aria-expanded": open === 'pillars', onClick: () => { toggle(pillarsChapter); }, children: [_jsx("span", { className: css.chevron, "aria-hidden": "true", children: open === 'pillars' ? '▾' : '▸' }), _jsx("span", { className: css.chapterTitle, children: pillarsChapter.title }), _jsx("span", { className: css.metaLine, children: t('design.pillarNotesHint') }), _jsx("span", { className: css.spacer }), _jsx("span", { className: css.status, "data-status": pillarsChapter.status, children: t(STATUS_KEY[pillarsChapter.status]) })] }), open === 'pillars' && _jsx(ChapterBody, { ...props, chapter: pillarsChapter })] }), _jsxs("div", { className: css.progressRow, children: [_jsx("span", { className: css.metaLine, children: t('design.progress', { done, total: authored }) }), _jsx("span", { className: css.progressBar, children: _jsx("span", { style: { width: `${authored === 0 ? 0 : done / authored * 100}%` } }) })] }), _jsx("div", { className: css.chapters, children: chapters.map(c => {
56
+ const fresh = c.mtime > 0 && c.text !== '' && c.mtime > (seen[c.id] ?? 0) && Date.now() - c.mtime < 1000 * 60 * 60 * 24 * 7;
57
+ const isOpen = open === c.id;
58
+ return _jsxs("section", { className: css.chapter, "data-open": isOpen ? 'true' : 'false', children: [_jsxs("button", { type: "button", className: css.chapterHead, "aria-expanded": isOpen, onClick: () => { toggle(c); }, children: [_jsx("span", { className: css.chevron, "aria-hidden": "true", children: isOpen ? '▾' : '▸' }), _jsx("span", { className: css.chapterTitle, children: c.title }), fresh && _jsx("span", { className: css.freshTag, children: t('design.fresh') }), _jsx("span", { className: css.spacer }), _jsx("span", { className: css.status, "data-status": c.generated ? 'generated' : c.status, children: c.generated ? t('design.generated') : t(STATUS_KEY[c.status]) })] }), isOpen && (c.id === 'elements'
59
+ ? _jsx(NumbersChapter, { t: t, elements: project.elements, types: manifest?.elementTypes ?? [] })
60
+ : _jsx(ChapterBody, { ...props, chapter: c }))] }, c.id);
61
+ }) })] });
62
+ }
63
+ /** The one-pager (game.json): pitch, element types, style, pillars — read, or edited in place. */
64
+ function OnePager(props) {
65
+ const { t, sessionId, api, project, onStyle, refresh } = props;
66
+ const manifest = project.manifest;
67
+ const [editing, setEditing] = useState(false);
68
+ const [pitch, setPitch] = useState('');
69
+ const [types, setTypes] = useState('');
70
+ const [pillars, setPillars] = useState('');
71
+ const [error, setError] = useState(null);
72
+ const begin = () => {
73
+ setPitch(manifest?.pitch ?? '');
74
+ setTypes((manifest?.elementTypes ?? []).join('、'));
75
+ setPillars((manifest?.pillars ?? []).join('\n'));
76
+ setError(null);
77
+ setEditing(true);
78
+ };
79
+ return _jsxs("section", { className: css.onePager, children: [_jsxs("div", { className: css.onePagerHead, children: [_jsx("span", { className: css.eyebrow, children: t('design.onePager') }), _jsx("span", { className: css.metaLine, children: "game.json" }), _jsx("span", { className: css.spacer }), _jsx("button", { type: "button", className: css.styleChip, onClick: onStyle, title: t('style.change'), children: t('design.style', { style: manifest === null ? '—' : artStyleLabel(manifest.artStyle) }) }), !editing && manifest !== null && _jsx("button", { type: "button", className: css.ghost, onClick: begin, children: t('design.editOnePager') })] }), editing
80
+ ? _jsxs("div", { className: css.onePagerForm, children: [_jsxs("label", { className: css.field, children: [_jsx("span", { className: css.fieldKey, children: t('design.pitchLabel') }), _jsx("textarea", { className: css.textarea, rows: 2, value: pitch, onChange: e => { setPitch(e.target.value); } })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.fieldKey, children: t('design.typesLabel') }), _jsx("input", { className: css.input, value: types, onChange: e => { setTypes(e.target.value); } })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.fieldKey, children: t('design.pillarsLabel') }), _jsx("textarea", { className: css.textarea, rows: 4, value: pillars, onChange: e => { setPillars(e.target.value); } })] }), error !== null && _jsx("p", { className: css.warnText, children: error }), _jsxs("div", { className: css.rowEnd, children: [_jsx("button", { type: "button", className: css.ghost, onClick: () => { setEditing(false); }, children: t('common.cancel') }), _jsx("button", { type: "button", className: css.primary, onClick: () => {
81
+ void api.setOnePager(sessionId, project.id, { pitch, elementTypes: types, pillars }).then(r => {
82
+ if (failureOf(r) !== undefined) {
83
+ setError(t('design.onePagerInvalid'));
84
+ return;
85
+ }
86
+ setEditing(false);
87
+ refresh();
88
+ });
89
+ }, children: t('common.save') })] })] })
90
+ : _jsxs(_Fragment, { children: [_jsx("p", { className: css.pitch, children: manifest?.pitch || t('design.noPitch') }), _jsx("div", { className: css.typeRow, children: (manifest?.elementTypes ?? []).map(type => _jsx("span", { className: css.typeTag, children: type }, type)) }), _jsxs("div", { className: css.pillars, children: [_jsx("span", { className: css.eyebrow, children: t('design.pillars') }), (manifest?.pillars.length ?? 0) > 0
91
+ ? _jsx("ol", { className: css.pillarList, children: manifest.pillars.map((p, i) => _jsx("li", { children: p }, i)) })
92
+ : _jsx("p", { className: css.metaLine, children: t('design.noPillars') })] })] })] });
93
+ }
94
+ /** One chapter's body: read, or edit in place. */
95
+ function ChapterBody(props) {
96
+ const { t, sessionId, api, project, chapter, refresh } = props;
97
+ const [editing, setEditing] = useState(false);
98
+ const [text, setText] = useState(chapter.text);
99
+ useEffect(() => { if (!editing)
100
+ setText(chapter.text); }, [chapter.text, editing]);
101
+ if (editing) {
102
+ return _jsxs("div", { className: css.chapterBody, children: [_jsx("textarea", { className: css.textarea, rows: Math.min(18, Math.max(6, text.split('\n').length + 1)), value: text, onChange: e => { setText(e.target.value); } }), _jsxs("div", { className: css.rowEnd, children: [_jsx("span", { className: css.metaLine, children: t('design.editNote') }), _jsx("span", { className: css.spacer }), _jsx("button", { type: "button", className: css.ghost, onClick: () => { setEditing(false); setText(chapter.text); }, children: t('common.cancel') }), _jsx("button", { type: "button", className: css.primary, onClick: () => {
103
+ void api.writeChapter(sessionId, project.id, chapter.id, text).then(r => {
104
+ if (failureOf(r) === undefined) {
105
+ setEditing(false);
106
+ refresh();
107
+ }
108
+ });
109
+ }, children: t('common.save') })] })] });
110
+ }
111
+ return _jsxs("div", { className: css.chapterBody, children: [chapter.text === ''
112
+ ? _jsx("p", { className: css.lead, children: chapter.generated ? t('design.emptyGenerated') : chapter.id === 'pillars' ? t('design.pillarNotesEmpty') : t('design.empty', { file: chapter.file }) })
113
+ : _jsx(Markdown, { text: chapter.text }), !chapter.generated && _jsxs("div", { className: css.rowEnd, children: [_jsx("span", { className: css.metaLine, children: chapter.file }), _jsx("span", { className: css.spacer }), _jsx("button", { type: "button", className: css.ghost, onClick: () => { setEditing(true); }, children: t('design.edit') })] })] });
114
+ }
115
+ /** 元素与数值: one table per type plus a bar chart per numeric column. */
116
+ function NumbersChapter({ t, elements, types }) {
117
+ const groups = useMemo(() => {
118
+ const all = [...types, ...elements.map(e => e.type).filter(type => !types.includes(type))];
119
+ return [...new Set(all)].map(type => ({ type, rows: elements.filter(e => e.type === type) })).filter(g => g.rows.length > 0);
120
+ }, [elements, types]);
121
+ if (groups.length === 0)
122
+ return _jsx("div", { className: css.chapterBody, children: _jsx("p", { className: css.lead, children: t('design.emptyGenerated') }) });
123
+ return _jsxs("div", { className: css.chapterBody, children: [_jsx("p", { className: css.metaLine, children: t('design.numbersNote') }), groups.map(({ type, rows }) => {
124
+ const keys = [...new Set(rows.flatMap(e => Object.keys(e.stats)))].slice(0, 6);
125
+ const numeric = keys.filter(k => rows.filter(e => typeof e.stats[k] === 'number').length >= 2);
126
+ return _jsxs("div", { className: css.numbers, children: [_jsxs("h5", { className: css.numbersTitle, children: [type, _jsx("span", { className: css.count, children: rows.length })] }), _jsx("div", { className: css.tableWrap, children: _jsxs("table", { className: css.table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: t('elements.name') }), keys.map(k => _jsx("th", { children: k }, k))] }) }), _jsx("tbody", { children: rows.map(e => _jsxs("tr", { children: [_jsx("td", { children: e.name }), keys.map(k => _jsx("td", { children: String(e.stats[k] ?? '') }, k))] }, e.id)) })] }) }), numeric.slice(0, 2).map(k => {
127
+ const max = Math.max(...rows.map(e => (typeof e.stats[k] === 'number' ? Math.abs(e.stats[k]) : 0)), 1e-9);
128
+ return _jsxs("div", { className: css.bars, "aria-label": t('design.chart', { key: k }), children: [_jsx("span", { className: css.barsKey, children: k }), rows.map(e => {
129
+ const v = typeof e.stats[k] === 'number' ? e.stats[k] : 0;
130
+ return _jsxs("div", { className: css.barRow, children: [_jsx("span", { className: css.barName, children: e.name }), _jsx("span", { className: css.barTrack, children: _jsx("span", { className: css.barFill, style: { width: `${Math.abs(v) / max * 100}%` } }) }), _jsx("span", { className: css.barValue, children: v })] }, e.id);
131
+ })] }, k);
132
+ })] }, type);
133
+ })] });
134
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 元素: every element of the game as a card grid (look, name, key numbers),
3
+ * filtered by type, multi-selectable to hand to the model as references.
4
+ * One card opens the element: its look large (a 3D turntable, or the pixel
5
+ * editor), right under it a box to ask the model for a new look and the
6
+ * model's A/B/C proposals, its 设定 (lore), the 3D recipe's fine-tuning
7
+ * folded away, and its fields. Every edit lands in the working files; the
8
+ * tab's commit bar turns them into a version for the play area.
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ import { type ProjectView } from '../types.ts';
12
+ import { type ViewProps } from './contract.ts';
13
+ /** Props of the elements view. */
14
+ export interface ElementsViewProps extends ViewProps {
15
+ readonly project: ProjectView;
16
+ readonly openId: string | null;
17
+ readonly onOpen: (id: string | null) => void;
18
+ }
19
+ /** The elements view. */
20
+ export declare function ElementsView(props: ElementsViewProps): ReactNode;
21
+ //# sourceMappingURL=ElementsView.d.ts.map