@khorsheed/dsh-game 0.0.0-stage → 0.1.1

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 (101) hide show
  1. package/CHANGELOG.md +25 -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 +30690 -0
  7. package/lib/index.js +4297 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +3876 -0
  10. package/lib/typert.remote-client.d.ts +104 -0
  11. package/lib/typert.remote-client.js +2138 -0
  12. package/lib/types/agent.d.ts +20 -0
  13. package/lib/types/agent.js +43 -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 +195 -0
  20. package/lib/types/client/GameTab.d.ts +14 -0
  21. package/lib/types/client/GameTab.js +261 -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 +163 -0
  26. package/lib/types/client/Preview3D.d.ts +52 -0
  27. package/lib/types/client/Preview3D.js +178 -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 +131 -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 +263 -0
  46. package/lib/types/client/locales.d.ts +379 -0
  47. package/lib/types/client/locales.js +733 -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 +110 -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/reveal.d.ts +43 -0
  59. package/lib/types/client/reveal.js +73 -0
  60. package/lib/types/client/style-samples.d.ts +5 -0
  61. package/lib/types/client/style-samples.js +196 -0
  62. package/lib/types/fsutil.d.ts +26 -0
  63. package/lib/types/fsutil.js +104 -0
  64. package/lib/types/hooks.d.ts +83 -0
  65. package/lib/types/hooks.js +108 -0
  66. package/lib/types/index.d.ts +30 -0
  67. package/lib/types/index.js +22 -0
  68. package/lib/types/kits/canvas2d.d.ts +6 -0
  69. package/lib/types/kits/canvas2d.js +60 -0
  70. package/lib/types/kits/ids.d.ts +11 -0
  71. package/lib/types/kits/ids.js +6 -0
  72. package/lib/types/kits/index.d.ts +26 -0
  73. package/lib/types/kits/index.js +28 -0
  74. package/lib/types/kits/three3d.d.ts +12 -0
  75. package/lib/types/kits/three3d.js +188 -0
  76. package/lib/types/kits/types.d.ts +52 -0
  77. package/lib/types/kits/types.js +6 -0
  78. package/lib/types/look3d.d.ts +20 -0
  79. package/lib/types/look3d.js +175 -0
  80. package/lib/types/panel-session.d.ts +21 -0
  81. package/lib/types/panel-session.js +16 -0
  82. package/lib/types/project.d.ts +138 -0
  83. package/lib/types/project.js +504 -0
  84. package/lib/types/remote.d.ts +184 -0
  85. package/lib/types/remote.js +263 -0
  86. package/lib/types/skills.d.ts +13 -0
  87. package/lib/types/skills.js +141 -0
  88. package/lib/types/store.d.ts +346 -0
  89. package/lib/types/store.js +1383 -0
  90. package/lib/types/tools.d.ts +12 -0
  91. package/lib/types/tools.js +309 -0
  92. package/lib/types/trash.d.ts +5 -0
  93. package/lib/types/trash.js +64 -0
  94. package/lib/types/types.d.ts +548 -0
  95. package/lib/types/types.js +85 -0
  96. package/lib/types/vfs.d.ts +94 -0
  97. package/lib/types/vfs.js +217 -0
  98. package/locale/en.json +6 -0
  99. package/locale/zh.json +6 -0
  100. package/package.json +132 -3
  101. package/runtime/dsh3d.js +1279 -0
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Pure project logic: the creation gate, manifest parsing, chapter status,
3
+ * element validation and which files make up a playable build. No I/O —
4
+ * the store feeds it file contents, the specs feed it strings.
5
+ * @module @khorsheed/dsh-game
6
+ */
7
+ import { isKitId } from "./kits/ids.js";
8
+ import { ART_STYLES, ART_STYLES_3D, isPixelLook, LOOK_CHARS, MANIFEST_FILE, STATE_DIR, } from "./types.js";
9
+ /** Split a list given either as an array or as one 、/,/newline-separated string. */
10
+ export function listOf(value) {
11
+ const raw = typeof value === 'string' ? value.split(/[、,,;;\n]/) : Array.isArray(value) ? value : [];
12
+ const out = [];
13
+ for (const item of raw) {
14
+ const clean = typeof item === 'string' ? item.trim() : '';
15
+ if (clean !== '' && !out.includes(clean))
16
+ out.push(clean);
17
+ }
18
+ return out;
19
+ }
20
+ /**
21
+ * Resolve the art style the user picked: an example id, its Chinese label,
22
+ * or `custom:<description>`. Anything else is not a choice the user made.
23
+ * @param value - what the model passed.
24
+ * @returns the stored spelling, or undefined when it names no style.
25
+ */
26
+ export function resolveArtStyle(value, styles = ART_STYLES) {
27
+ const wanted = (value ?? '').trim();
28
+ if (wanted === '')
29
+ return undefined;
30
+ const known = styles.find(style => style.id === wanted || style.label === wanted);
31
+ if (known !== undefined)
32
+ return known.id;
33
+ if (/^custom[::]\s*\S/.test(wanted))
34
+ return `custom:${wanted.replace(/^custom[::]\s*/, '')}`;
35
+ return undefined;
36
+ }
37
+ /** The style's human label (`custom:` keeps the user's own words). */
38
+ export function artStyleLabel(id) {
39
+ if (id.startsWith('custom:'))
40
+ return id.slice('custom:'.length);
41
+ return [...ART_STYLES, ...ART_STYLES_3D].find(style => style.id === id)?.label ?? id;
42
+ }
43
+ /**
44
+ * A style's short name for tight places (tags, chips): a custom description's
45
+ * lead — up to its first dash, colon, comma or bracket — at most 10 characters.
46
+ * The full label stays for the model and for hover text.
47
+ */
48
+ export function artStyleName(id) {
49
+ const full = artStyleLabel(id).trim();
50
+ const lead = full.split(/——|—|--|[::,,;;((]/)[0]?.trim() || full;
51
+ return [...lead].length > 10 ? `${[...lead].slice(0, 10).join('')}…` : lead;
52
+ }
53
+ /** The kit a create request asks for: `3d` / `3d-three` → 3D, anything else 2D. */
54
+ export function requestedKit(runtime) {
55
+ return /^3d/i.test((runtime ?? '').trim()) ? '3d-three' : '2d-canvas';
56
+ }
57
+ /** The example styles a kit offers. */
58
+ export function stylesFor(kit) {
59
+ return kit === '3d-three' ? ART_STYLES_3D : ART_STYLES;
60
+ }
61
+ /**
62
+ * The light gate: a project needs a title, a one-sentence pitch, at least one
63
+ * element type and a chosen art style. The model gets back exactly what is
64
+ * missing, so it asks the user instead of inventing it.
65
+ * @param request - the create arguments.
66
+ * @returns the manifest body, or the missing items.
67
+ */
68
+ export function gate(request) {
69
+ const missing = [];
70
+ const title = (request.title ?? '').trim().slice(0, 60);
71
+ const pitch = (request.pitch ?? '').trim().slice(0, 400);
72
+ const elementTypes = listOf(request.elementTypes).slice(0, 12);
73
+ const kit = requestedKit(request.runtime);
74
+ const styles = stylesFor(kit);
75
+ const artStyle = resolveArtStyle(request.artStyle, styles);
76
+ if (title === '')
77
+ missing.push('title(游戏名)');
78
+ if (pitch === '')
79
+ missing.push('pitch(一句话:玩家做什么、为什么好玩)');
80
+ if (elementTypes.length === 0)
81
+ missing.push('elementTypes(游戏里有哪几类元素,如 角色、敌人、道具)');
82
+ if (artStyle === undefined) {
83
+ missing.push(`artStyle(用户从示例里选的美术风格:${styles.map(s => `${s.id}=${s.label}`).join('、')},或 custom:描述)`);
84
+ }
85
+ if (missing.length > 0)
86
+ return { ok: false, missing };
87
+ return {
88
+ ok: true,
89
+ manifest: {
90
+ schema: 1,
91
+ title,
92
+ pitch,
93
+ elementTypes,
94
+ artStyle: artStyle,
95
+ pillars: listOf(request.pillars).slice(0, 5),
96
+ autoConfirm: true,
97
+ params: [],
98
+ palette: stylePalette(artStyle),
99
+ // 2D projects keep the pre-kit manifest shape (no runtime key).
100
+ ...(kit === '2d-canvas' ? {} : { runtime: { kit, version: 1 } }),
101
+ },
102
+ };
103
+ }
104
+ /** A style's palette; custom styles start from the pixel palette (the agent may set its own in game.json). */
105
+ export function stylePalette(styleId) {
106
+ return [...([...ART_STYLES, ...ART_STYLES_3D].find(style => style.id === styleId) ?? ART_STYLES[0]).palette];
107
+ }
108
+ const HEX = /^#[0-9a-f]{6}$/i;
109
+ /** Parse `game.json` `params[]`, dropping malformed rows and clamping each value into its range. */
110
+ export function parseParams(raw) {
111
+ if (!Array.isArray(raw))
112
+ return [];
113
+ const out = [];
114
+ for (const row of raw) {
115
+ if (row === null || typeof row !== 'object')
116
+ continue;
117
+ const r = row;
118
+ const key = typeof r.key === 'string' ? r.key.trim() : '';
119
+ const min = Number(r.min), max = Number(r.max);
120
+ if (key === '' || !Number.isFinite(min) || !Number.isFinite(max) || max <= min || out.some(p => p.key === key))
121
+ continue;
122
+ const step = Number(r.step) > 0 ? Number(r.step) : (max - min) / 100;
123
+ const value = Number.isFinite(Number(r.value)) ? Math.min(max, Math.max(min, Number(r.value))) : min;
124
+ out.push({
125
+ key, label: typeof r.label === 'string' && r.label.trim() !== '' ? r.label.trim() : key, min, max, step, value,
126
+ ...(typeof r.unit === 'string' && r.unit !== '' ? { unit: r.unit } : {}),
127
+ });
128
+ }
129
+ return out.slice(0, 24);
130
+ }
131
+ /** What a usable pixel look needs, worded for the model. */
132
+ export const PIXEL_LOOK_HINT = 'look 不可用:需要 palette(#rrggbb 数组)和 states(状态名 → 像素行数组)';
133
+ /**
134
+ * Normalize an element's `look`: a palette of `#rrggbb` colors and pixel
135
+ * rows per state, every row padded/cut to the widest row, characters outside
136
+ * the palette turned transparent. Anything unusable is null.
137
+ */
138
+ export function normalizeLook(raw) {
139
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
140
+ return null;
141
+ const r = raw;
142
+ const palette = Array.isArray(r.palette) ? r.palette.filter((c) => typeof c === 'string' && HEX.test(c)).slice(0, LOOK_CHARS.length) : [];
143
+ if (palette.length === 0 || r.states === null || typeof r.states !== 'object')
144
+ return null;
145
+ const states = {};
146
+ for (const [name, rowsRaw] of Object.entries(r.states).slice(0, 8)) {
147
+ if (!Array.isArray(rowsRaw))
148
+ continue;
149
+ const rows = rowsRaw.filter((row) => typeof row === 'string').slice(0, 64);
150
+ if (rows.length === 0)
151
+ continue;
152
+ const width = Math.min(64, Math.max(...rows.map(row => row.length)));
153
+ states[name] = rows.map(row => [...row.padEnd(width, '.').slice(0, width)].map(ch => {
154
+ const index = LOOK_CHARS.indexOf(ch.toLowerCase());
155
+ return index >= 0 && index < palette.length ? ch.toLowerCase() : '.';
156
+ }).join(''));
157
+ }
158
+ return Object.keys(states).length === 0 ? null : { palette, states };
159
+ }
160
+ /** The look's grid size (first state). */
161
+ export function lookSize(look) {
162
+ const rows = Object.values(look.states)[0] ?? [];
163
+ return { w: rows[0]?.length ?? 0, h: rows.length };
164
+ }
165
+ /**
166
+ * Parse `game.json`. Unknown keys are dropped; a missing optional key takes
167
+ * its default, so a hand-edited manifest still loads.
168
+ * @param text - the file's contents.
169
+ * @returns the manifest, or a Chinese reason.
170
+ */
171
+ export function parseManifest(text) {
172
+ let raw;
173
+ try {
174
+ raw = JSON.parse(text);
175
+ }
176
+ catch (error) {
177
+ return { ok: false, error: `${MANIFEST_FILE} 不是合法 JSON:${error.message}` };
178
+ }
179
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
180
+ return { ok: false, error: `${MANIFEST_FILE} 应是一个对象` };
181
+ const r = raw;
182
+ const str = (v) => (typeof v === 'string' ? v : '');
183
+ const id = str(r.id);
184
+ if (id === '')
185
+ return { ok: false, error: `${MANIFEST_FILE} 缺少 id` };
186
+ return {
187
+ ok: true,
188
+ manifest: {
189
+ schema: 1,
190
+ id,
191
+ title: str(r.title) || id,
192
+ pitch: str(r.pitch),
193
+ elementTypes: listOf(Array.isArray(r.elementTypes) ? r.elementTypes.filter((x) => typeof x === 'string') : str(r.elementTypes)),
194
+ artStyle: str(r.artStyle),
195
+ pillars: listOf(Array.isArray(r.pillars) ? r.pillars.filter((x) => typeof x === 'string') : str(r.pillars)),
196
+ autoConfirm: r.autoConfirm !== false,
197
+ params: parseParams(r.params),
198
+ palette: Array.isArray(r.palette) && r.palette.some(c => typeof c === 'string' && HEX.test(c))
199
+ ? r.palette.filter((c) => typeof c === 'string' && HEX.test(c))
200
+ : stylePalette(str(r.artStyle)),
201
+ createdAt: str(r.createdAt),
202
+ ...(runtimeOf(r.runtime) ?? {}),
203
+ ...(style3dOf(r.style3d) ?? {}),
204
+ },
205
+ };
206
+ }
207
+ /** The manifest's `style3d`, keeping valid colors and clamped numbers; undefined when nothing usable. */
208
+ function style3dOf(raw) {
209
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
210
+ return undefined;
211
+ const r = raw;
212
+ const out = {};
213
+ if (typeof r.preset === 'string' && /^[a-z][\w-]{0,31}$/.test(r.preset))
214
+ out.preset = r.preset;
215
+ Object.assign(out, sceneStyleOf(r));
216
+ if (typeof r.outline === 'string' && HEX.test(r.outline))
217
+ out.outline = r.outline.toLowerCase();
218
+ const width = clampNum(r.outlineWidth, 0, 0.2), shades = clampNum(r.shades, 2, 5);
219
+ if (width !== undefined)
220
+ out.outlineWidth = width;
221
+ if (shades !== undefined)
222
+ out.shades = Math.round(shades);
223
+ if (typeof r.shadows === 'boolean')
224
+ out.shadows = r.shadows;
225
+ const rim = clampNum(r.rim, 0, 2);
226
+ if (rim !== undefined)
227
+ out.rim = rim;
228
+ if (typeof r.rimColor === 'string' && HEX.test(r.rimColor))
229
+ out.rimColor = r.rimColor.toLowerCase();
230
+ // The game's own scenes (any names, a bounded number): each keeps only what a scene may override.
231
+ if (r.scenes !== null && typeof r.scenes === 'object' && !Array.isArray(r.scenes)) {
232
+ const scenes = {};
233
+ for (const [name, raw] of Object.entries(r.scenes).slice(0, 32)) {
234
+ if (name.trim() === '' || name.length > 40 || raw === null || typeof raw !== 'object' || Array.isArray(raw))
235
+ continue;
236
+ scenes[name] = sceneStyleOf(raw);
237
+ }
238
+ if (Object.keys(scenes).length > 0)
239
+ out.scenes = scenes;
240
+ }
241
+ return Object.keys(out).length === 0 ? undefined : { style3d: out };
242
+ }
243
+ const clampNum = (v, min, max) => typeof v === 'number' && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : undefined;
244
+ /** The keys a scene may override (and the game-wide style shares): colors, fog, light. */
245
+ function sceneStyleOf(r) {
246
+ const out = {};
247
+ for (const key of ['bg', 'ground', 'ground2', 'water']) {
248
+ const v = r[key];
249
+ if (typeof v === 'string' && HEX.test(v))
250
+ out[key] = v.toLowerCase();
251
+ }
252
+ const light = clampNum(r.light, 0, 4);
253
+ if (light !== undefined)
254
+ out.light = light;
255
+ if (Array.isArray(r.fog) && r.fog.length === 2) {
256
+ const a = clampNum(r.fog[0], 0, 500), b = clampNum(r.fog[1], 0, 1000);
257
+ if (a !== undefined && b !== undefined && b > a)
258
+ out.fog = [a, b];
259
+ }
260
+ return out;
261
+ }
262
+ /** The manifest's `runtime` when it names a shipped kit with a positive integer version. */
263
+ function runtimeOf(raw) {
264
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
265
+ return undefined;
266
+ const r = raw;
267
+ if (!isKitId(r.kit))
268
+ return undefined;
269
+ const version = typeof r.version === 'number' && Number.isInteger(r.version) && r.version > 0 ? r.version : 1;
270
+ return { runtime: { kit: r.kit, version } };
271
+ }
272
+ /** Serialize a manifest the way the plugin writes it. */
273
+ export function serializeManifest(manifest) {
274
+ return `${JSON.stringify(manifest, null, 2)}\n`;
275
+ }
276
+ /**
277
+ * A chapter's status from its text: empty is `todo`, a `状态:定稿` (or
278
+ * `status: done`) line is `done`, anything else is a `draft`.
279
+ */
280
+ export function chapterStatus(text) {
281
+ if (text === undefined || text.trim() === '')
282
+ return 'todo';
283
+ if (/^\s*(状态[::]\s*定稿|status:\s*done)\s*$/im.test(text))
284
+ return 'done';
285
+ return 'draft';
286
+ }
287
+ /**
288
+ * The one-pager (pitch, element types, style, pillars) lives in `game.json`
289
+ * and the panel pins it; `design/pillars.md` holds only the notes about the
290
+ * pillars (why each matters, how to test it). Projects created before
291
+ * 2026-10-10 got a pillars.md that restated the one-pager: this strips those
292
+ * restated lines so the notes view never shows the card twice.
293
+ * @param text - the chapter's text.
294
+ * @param manifest - the one-pager it may restate.
295
+ * @returns the notes that are not a restatement (trimmed; '' when none).
296
+ */
297
+ export function pillarNotes(text, manifest) {
298
+ if (manifest === null)
299
+ return text.trim();
300
+ const pillars = new Set(manifest.pillars.map(p => p.trim()));
301
+ const kept = text.split('\n').filter(line => {
302
+ const l = line.trim();
303
+ if (/^#\s/.test(l) && /设计支柱|支柱说明/.test(l))
304
+ return false;
305
+ if (l.startsWith('>') && l.replace(/^>\s*/, '') === manifest.pitch.trim())
306
+ return false;
307
+ if (/^(\d+\.|[-*])\s/.test(l) && pillars.has(l.replace(/^(\d+\.|[-*])\s+/, '')))
308
+ return false;
309
+ if (l === `美术风格:${artStyleLabel(manifest.artStyle)}` || l === `元素类型:${manifest.elementTypes.join('、')}`)
310
+ return false;
311
+ if (l.startsWith('(还没定支柱'))
312
+ return false;
313
+ return true;
314
+ });
315
+ return kept.join('\n').replace(/\n{3,}/g, '\n\n').trim();
316
+ }
317
+ /** Element-file scalar values worth tabulating. */
318
+ function scalarStats(r) {
319
+ const out = {};
320
+ for (const [key, value] of Object.entries(r)) {
321
+ if (key === 'id' || key === 'type' || key === 'name' || key === 'look' || key === 'lore')
322
+ continue;
323
+ if (typeof value === 'number' || typeof value === 'boolean' || (typeof value === 'string' && value.length <= 40))
324
+ out[key] = value;
325
+ }
326
+ return out;
327
+ }
328
+ /**
329
+ * Validate the element files. A file holds one element object or an array of
330
+ * them; each needs `id`, `name` and a `type` the manifest declares, and ids
331
+ * are unique across files.
332
+ * @param files - project-relative path → contents, only `elements/*.json`.
333
+ * @param elementTypes - the manifest's declared types (empty = accept any).
334
+ * @param looks - how the project's kit normalizes a `look` (default: 2D pixel rows).
335
+ * @returns the elements that parsed, and one failure per problem.
336
+ */
337
+ export function validateElements(files, elementTypes, looks = { normalizeLook, lookHint: PIXEL_LOOK_HINT }) {
338
+ const elements = [];
339
+ const failures = [];
340
+ const seen = new Map();
341
+ for (const [file, text] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
342
+ let raw;
343
+ try {
344
+ raw = JSON.parse(text);
345
+ }
346
+ catch (error) {
347
+ failures.push({ where: file, message: `不是合法 JSON:${error.message}` });
348
+ continue;
349
+ }
350
+ const items = Array.isArray(raw) ? raw : [raw];
351
+ items.forEach((item, index) => {
352
+ const at = Array.isArray(raw) ? `${file}[${index}]` : file;
353
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) {
354
+ failures.push({ where: at, message: '元素应是对象' });
355
+ return;
356
+ }
357
+ const r = item;
358
+ const id = typeof r.id === 'string' ? r.id.trim() : '';
359
+ const name = typeof r.name === 'string' ? r.name.trim() : '';
360
+ const type = typeof r.type === 'string' ? r.type.trim() : '';
361
+ if (id === '')
362
+ failures.push({ where: at, message: '缺少 id' });
363
+ if (name === '')
364
+ failures.push({ where: at, message: '缺少 name' });
365
+ if (type === '')
366
+ failures.push({ where: at, message: '缺少 type' });
367
+ else if (elementTypes.length > 0 && !elementTypes.includes(type)) {
368
+ failures.push({ where: at, message: `type「${type}」不在 game.json 的 elementTypes(${elementTypes.join('、')})里` });
369
+ }
370
+ if (id !== '') {
371
+ const earlier = seen.get(id);
372
+ if (earlier !== undefined)
373
+ failures.push({ where: at, message: `id「${id}」和 ${earlier} 重复` });
374
+ else
375
+ seen.set(id, at);
376
+ }
377
+ if (r.look !== undefined && looks.normalizeLook(r.look) === null)
378
+ failures.push({ where: at, message: looks.lookHint });
379
+ if (id !== '' && name !== '' && type !== '') {
380
+ const look = looks.normalizeLook(r.look);
381
+ elements.push({
382
+ file, index: Array.isArray(raw) ? index : -1, id, type, name, stats: scalarStats(r),
383
+ look: look !== null && isPixelLook(look) ? look : null,
384
+ look3d: look !== null && !isPixelLook(look) ? look : null,
385
+ lore: typeof r.lore === 'string' ? r.lore.trim() : '',
386
+ });
387
+ }
388
+ });
389
+ }
390
+ return { elements, failures };
391
+ }
392
+ /** The generated 元素与数值 chapter: one table per element type. */
393
+ export function elementsChapter(elements, elementTypes) {
394
+ if (elements.length === 0)
395
+ return '';
396
+ const types = [...elementTypes, ...elements.map(e => e.type).filter(t => !elementTypes.includes(t))];
397
+ const lines = ['# 元素与数值', '', '(由 elements/ 里的文件自动生成,改数值请改元素文件。)', ''];
398
+ for (const type of types) {
399
+ const rows = elements.filter(e => e.type === type);
400
+ if (rows.length === 0)
401
+ continue;
402
+ const keys = [...new Set(rows.flatMap(e => Object.keys(e.stats)))].slice(0, 8);
403
+ lines.push(`## ${type}`, '', `| 名称 | id | ${keys.join(' | ')} |`.replace(/ \| \|$/, ' |'), `|${' --- |'.repeat(2 + keys.length)}`);
404
+ for (const e of rows)
405
+ lines.push(`| ${e.name} | ${e.id} | ${keys.map(k => String(e.stats[k] ?? '')).join(' | ')} |`.replace(/ \| \|$/, ' |'));
406
+ lines.push('');
407
+ }
408
+ return lines.join('\n');
409
+ }
410
+ /** The generated 元素设定 chapter: every element's 设定 by type (elements without one are left out). */
411
+ export function loreChapter(elements, elementTypes) {
412
+ const written = elements.filter(e => e.lore !== '');
413
+ if (written.length === 0)
414
+ return '';
415
+ const types = [...elementTypes, ...written.map(e => e.type).filter(t => !elementTypes.includes(t))];
416
+ const lines = ['# 元素设定', '', '(由元素文件里的设定自动汇总,在「元素」里打开一个元素就能改。)', ''];
417
+ for (const type of types) {
418
+ const rows = written.filter(e => e.type === type);
419
+ if (rows.length === 0)
420
+ continue;
421
+ lines.push(`## ${type}`, '');
422
+ for (const e of rows)
423
+ lines.push(`### ${e.name}`, '', e.lore, '');
424
+ }
425
+ return lines.join('\n');
426
+ }
427
+ /** Directories never part of a playable build. */
428
+ const NON_BUILD_DIRS = new Set([STATE_DIR, 'design', 'tests', 'node_modules', '.git']);
429
+ /**
430
+ * Whether a project-relative path belongs in a playable build: everything
431
+ * except the plugin's state, the design doc, the tests and dot-files.
432
+ */
433
+ export function isBuildFile(path) {
434
+ const segments = path.split('/');
435
+ if (segments.some(s => s.startsWith('.') && s !== '.'))
436
+ return false;
437
+ return !NON_BUILD_DIRS.has(segments[0] ?? '');
438
+ }
439
+ /** File-extension → MIME type for the sandbox loader. */
440
+ const TYPES = {
441
+ html: 'text/html', htm: 'text/html', js: 'text/javascript', mjs: 'text/javascript', css: 'text/css', json: 'application/json',
442
+ txt: 'text/plain', md: 'text/markdown', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
443
+ gif: 'image/gif', webp: 'image/webp', wav: 'audio/wav', ogg: 'audio/ogg', mp3: 'audio/mpeg', m4a: 'audio/mp4',
444
+ woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf', otf: 'font/otf',
445
+ glb: 'model/gltf-binary', gltf: 'model/gltf+json',
446
+ };
447
+ /** Extensions shipped as text (everything else is base64). */
448
+ const TEXT_EXT = new Set(['html', 'htm', 'js', 'mjs', 'css', 'json', 'txt', 'md', 'svg']);
449
+ /** The extension of a path, lower-cased, without the dot. */
450
+ function extOf(path) {
451
+ const name = path.slice(path.lastIndexOf('/') + 1);
452
+ return name.includes('.') ? name.slice(name.lastIndexOf('.') + 1).toLowerCase() : '';
453
+ }
454
+ /** A file's MIME type by extension. */
455
+ export function mimeOf(path) {
456
+ return TYPES[extOf(path)] ?? 'application/octet-stream';
457
+ }
458
+ /** Whether a file ships to the sandbox as text. */
459
+ export function isTextFile(path) {
460
+ return TEXT_EXT.has(extOf(path));
461
+ }
462
+ /**
463
+ * A directory-safe project folder name from a title: ASCII letters and
464
+ * digits kept, other runs collapsed to `-`; a title with none falls back to
465
+ * `game`. The store appends `-2`, `-3` … on collision.
466
+ */
467
+ export function slugOf(title) {
468
+ const slug = title.normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
469
+ return slug === '' ? 'game' : slug;
470
+ }
471
+ /**
472
+ * Apply a patch to one element inside its file's text (an object, or one
473
+ * entry of an array), keeping every other key. `undefined` values delete.
474
+ * @returns the new file text, or undefined when the element is not there.
475
+ */
476
+ export function patchElementText(text, elementId, patch) {
477
+ let raw;
478
+ try {
479
+ raw = JSON.parse(text);
480
+ }
481
+ catch {
482
+ return undefined;
483
+ }
484
+ const items = Array.isArray(raw) ? raw : [raw];
485
+ const target = items.find(item => item !== null && typeof item === 'object' && item.id === elementId);
486
+ if (target === undefined)
487
+ return undefined;
488
+ for (const [key, value] of Object.entries(patch)) {
489
+ if (key === 'id')
490
+ continue;
491
+ if (value === undefined)
492
+ delete target[key];
493
+ else
494
+ target[key] = value;
495
+ }
496
+ return `${JSON.stringify(raw, null, 2)}\n`;
497
+ }
498
+ /** A look as model-facing text: palette, then each state's rows. */
499
+ export function lookText(look) {
500
+ const lines = [`palette: ${look.palette.map((c, i) => `${LOOK_CHARS[i]}=${c}`).join(' ')}`];
501
+ for (const [state, rows] of Object.entries(look.states))
502
+ lines.push(`${state}:`, ...rows.map(row => ` ${row}`));
503
+ return lines.join('\n');
504
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The game Remote service: the panel's data face, namespace `game`. A thin
3
+ * adapter over `ctx.gameStore`. Methods that read or act for a session take
4
+ * its id first and resolve it with {@link panelSession} (subagent sessions
5
+ * included); project-addressed reads take none.
6
+ * @module @khorsheed/dsh-game
7
+ */
8
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
11
+ import type { BundleFile, LiveVersion, CommitResult, DeleteProjectResult, WorkingChanges, GameResult, PlayBundle, ProjectCard, SessionGameState, TurnReport, VersionDiff } from './types.ts';
12
+ declare module '@deepseek-ai/cordis' {
13
+ interface Context {
14
+ /** The game panel's Remote face. */
15
+ gameRemote: GameRemoteService;
16
+ }
17
+ }
18
+ /** Remote construction options (reserved). */
19
+ export interface GameRemoteConfig {
20
+ }
21
+ /** The `game` wire namespace. */
22
+ export declare class GameRemoteService extends TypertRemoteService<GameRemoteConfig> {
23
+ static inject: string[];
24
+ /**
25
+ * @param ctx - host context carrying the service core.
26
+ * @param _config - reserved.
27
+ */
28
+ constructor(ctx: Context, _config?: GameRemoteConfig);
29
+ private get store();
30
+ /** The session's bound project (full view) and the project list. */
31
+ state(sessionId: SessionId): Promise<SessionGameState>;
32
+ /** The bound project's newest version (cheap; the page polls it). */
33
+ liveVersion(sessionId: SessionId): Promise<LiveVersion | null>;
34
+ /** What one of the session's turns changed (null when nothing, or not recorded). */
35
+ turnReport(sessionId: SessionId, request: {
36
+ turn: number;
37
+ }): Promise<TurnReport | null>;
38
+ /** Every project as a switcher card. */
39
+ projectCards(): Promise<ProjectCard[]>;
40
+ /** Delete a project: `trash` moves its folder to the system trash, `forget` only removes it from the plugin. */
41
+ deleteProject(sessionId: SessionId, request: {
42
+ projectId: string;
43
+ mode: 'trash' | 'forget';
44
+ }): Promise<DeleteProjectResult>;
45
+ /** Bind the session to a project, or unbind with `projectId: null`. */
46
+ bind(sessionId: SessionId, request: {
47
+ projectId: string | null;
48
+ }): Promise<GameResult>;
49
+ /** One working build file, for a panel preview ahead of any version. */
50
+ workingFile(request: {
51
+ projectId: string;
52
+ path: string;
53
+ }): Promise<GameResult<{
54
+ readonly ok: true;
55
+ readonly file: BundleFile;
56
+ }>>;
57
+ /** A version's files for the play area. */
58
+ bundle(request: {
59
+ projectId: string;
60
+ which?: 'current' | 'candidate';
61
+ }): Promise<GameResult<{
62
+ readonly ok: true;
63
+ readonly bundle: PlayBundle;
64
+ }>>;
65
+ /** Promote the candidate into the play area. */
66
+ confirm(sessionId: SessionId, request: {
67
+ projectId: string;
68
+ }): Promise<GameResult<{
69
+ readonly ok: true;
70
+ readonly version: number;
71
+ }>>;
72
+ /** What the working files hold that the play area does not (the commit bar polls it). */
73
+ changes(request: {
74
+ projectId: string;
75
+ }): Promise<GameResult<WorkingChanges & {
76
+ readonly ok: true;
77
+ }>>;
78
+ /** 检查并发版: check the working files and cut a version (by the user) carrying every change. */
79
+ commit(sessionId: SessionId, request: {
80
+ projectId: string;
81
+ }): Promise<GameResult<CommitResult>>;
82
+ /** Switch the project's auto-confirm. */
83
+ setAutoConfirm(sessionId: SessionId, request: {
84
+ projectId: string;
85
+ on: boolean;
86
+ }): Promise<GameResult>;
87
+ /** Record one play session into the playtest log. */
88
+ logPlay(sessionId: SessionId, request: {
89
+ projectId: string;
90
+ version: number;
91
+ seconds: number;
92
+ errors: string[];
93
+ }): Promise<GameResult>;
94
+ /** Drop one pending change (the dock's ×). */
95
+ dropPending(sessionId: SessionId, request: {
96
+ projectId: string;
97
+ key: string;
98
+ }): Promise<GameResult>;
99
+ /** Set one slider value (user layer). */
100
+ setParam(sessionId: SessionId, request: {
101
+ projectId: string;
102
+ key: string;
103
+ value: number;
104
+ }): Promise<GameResult>;
105
+ /** 定稿: write slider values into game.json. */
106
+ finalizeParams(sessionId: SessionId, request: {
107
+ projectId: string;
108
+ }): Promise<GameResult>;
109
+ /** 还原: drop the slider overrides. */
110
+ resetParams(sessionId: SessionId, request: {
111
+ projectId: string;
112
+ }): Promise<GameResult>;
113
+ /** Edit one element's look (JSON text) and/or scalar fields (JSON text of an object). */
114
+ editElement(sessionId: SessionId, request: {
115
+ projectId: string;
116
+ elementId: string;
117
+ look?: string;
118
+ fields?: string;
119
+ }): Promise<GameResult<{
120
+ readonly ok: true;
121
+ readonly elementJson: string;
122
+ }>>;
123
+ /** Pick one appearance variant. */
124
+ pickProposal(sessionId: SessionId, request: {
125
+ projectId: string;
126
+ elementId: string;
127
+ at: string;
128
+ index: number;
129
+ }): Promise<GameResult>;
130
+ /** Write a design chapter. */
131
+ writeChapter(sessionId: SessionId, request: {
132
+ projectId: string;
133
+ chapterId: string;
134
+ text: string;
135
+ }): Promise<GameResult>;
136
+ /** Edit the one-pager (game.json pitch / element types / pillars). */
137
+ setOnePager(sessionId: SessionId, request: {
138
+ projectId: string;
139
+ pitch?: string;
140
+ elementTypes?: string;
141
+ pillars?: string;
142
+ }): Promise<GameResult>;
143
+ /** Mark a pending restyle (the next passing version becomes a candidate). */
144
+ requestRestyle(sessionId: SessionId, request: {
145
+ projectId: string;
146
+ styleId: string;
147
+ }): Promise<GameResult>;
148
+ /** Throw the candidate away. */
149
+ reject(sessionId: SessionId, request: {
150
+ projectId: string;
151
+ reason: string;
152
+ }): Promise<GameResult>;
153
+ /** Put an older version into the play area. */
154
+ rollback(sessionId: SessionId, request: {
155
+ projectId: string;
156
+ version: number;
157
+ }): Promise<GameResult>;
158
+ /** Files that differ between two versions. */
159
+ diff(request: {
160
+ projectId: string;
161
+ from: number;
162
+ to: number;
163
+ }): Promise<GameResult<{
164
+ readonly ok: true;
165
+ readonly diff: VersionDiff;
166
+ }>>;
167
+ /** The last check's screenshot. */
168
+ checkShot(request: {
169
+ projectId: string;
170
+ version?: number;
171
+ }): Promise<GameResult<{
172
+ readonly ok: true;
173
+ readonly webp: string;
174
+ }>>;
175
+ /** A specific version's files (rollback preview, export). */
176
+ versionBundle(request: {
177
+ projectId: string;
178
+ version: number;
179
+ }): Promise<GameResult<{
180
+ readonly ok: true;
181
+ readonly bundle: PlayBundle;
182
+ }>>;
183
+ }
184
+ //# sourceMappingURL=remote.d.ts.map