@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.
- package/CHANGELOG.md +17 -0
- package/README.en.md +53 -0
- package/README.i18n.yaml +6 -0
- package/README.md +52 -2
- package/cordis.patch.yml +23 -0
- package/lib/client.js +30348 -0
- package/lib/index.js +4154 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3769 -0
- package/lib/typert.remote-client.d.ts +101 -0
- package/lib/typert.remote-client.js +2082 -0
- package/lib/types/agent.d.ts +20 -0
- package/lib/types/agent.js +42 -0
- package/lib/types/checks.d.ts +44 -0
- package/lib/types/checks.js +267 -0
- package/lib/types/client/DesignView.d.ts +22 -0
- package/lib/types/client/DesignView.js +134 -0
- package/lib/types/client/ElementsView.d.ts +21 -0
- package/lib/types/client/ElementsView.js +187 -0
- package/lib/types/client/GameTab.d.ts +14 -0
- package/lib/types/client/GameTab.js +256 -0
- package/lib/types/client/PlayFrame.d.ts +54 -0
- package/lib/types/client/PlayFrame.js +129 -0
- package/lib/types/client/PlayView.d.ts +19 -0
- package/lib/types/client/PlayView.js +161 -0
- package/lib/types/client/Preview3D.d.ts +50 -0
- package/lib/types/client/Preview3D.js +176 -0
- package/lib/types/client/ProjectSwitcher.d.ts +20 -0
- package/lib/types/client/ProjectSwitcher.js +75 -0
- package/lib/types/client/Recipe3DEditor.d.ts +19 -0
- package/lib/types/client/Recipe3DEditor.js +44 -0
- package/lib/types/client/StyleGallery.d.ts +18 -0
- package/lib/types/client/StyleGallery.js +27 -0
- package/lib/types/client/TurnCard.d.ts +62 -0
- package/lib/types/client/TurnCard.js +100 -0
- package/lib/types/client/VersionsView.d.ts +17 -0
- package/lib/types/client/VersionsView.js +68 -0
- package/lib/types/client/chat-node.d.ts +42 -0
- package/lib/types/client/chat-node.js +77 -0
- package/lib/types/client/contract.d.ts +125 -0
- package/lib/types/client/contract.js +6 -0
- package/lib/types/client/definition.d.ts +22 -0
- package/lib/types/client/definition.js +28 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/index.js +229 -0
- package/lib/types/client/locales.d.ts +376 -0
- package/lib/types/client/locales.js +727 -0
- package/lib/types/client/looks.d.ts +36 -0
- package/lib/types/client/looks.js +105 -0
- package/lib/types/client/markdown.d.ts +11 -0
- package/lib/types/client/markdown.js +52 -0
- package/lib/types/client/preset-visibility.d.ts +94 -0
- package/lib/types/client/preset-visibility.js +115 -0
- package/lib/types/client/preview-entry.d.ts +24 -0
- package/lib/types/client/preview-entry.js +107 -0
- package/lib/types/client/refs.d.ts +36 -0
- package/lib/types/client/refs.js +44 -0
- package/lib/types/client/style-samples.d.ts +5 -0
- package/lib/types/client/style-samples.js +196 -0
- package/lib/types/fsutil.d.ts +26 -0
- package/lib/types/fsutil.js +104 -0
- package/lib/types/hooks.d.ts +83 -0
- package/lib/types/hooks.js +108 -0
- package/lib/types/index.d.ts +30 -0
- package/lib/types/index.js +22 -0
- package/lib/types/kits/canvas2d.d.ts +6 -0
- package/lib/types/kits/canvas2d.js +60 -0
- package/lib/types/kits/ids.d.ts +11 -0
- package/lib/types/kits/ids.js +6 -0
- package/lib/types/kits/index.d.ts +26 -0
- package/lib/types/kits/index.js +28 -0
- package/lib/types/kits/three3d.d.ts +12 -0
- package/lib/types/kits/three3d.js +188 -0
- package/lib/types/kits/types.d.ts +52 -0
- package/lib/types/kits/types.js +6 -0
- package/lib/types/look3d.d.ts +20 -0
- package/lib/types/look3d.js +175 -0
- package/lib/types/panel-session.d.ts +21 -0
- package/lib/types/panel-session.js +16 -0
- package/lib/types/project.d.ts +132 -0
- package/lib/types/project.js +494 -0
- package/lib/types/remote.d.ts +182 -0
- package/lib/types/remote.js +256 -0
- package/lib/types/skills.d.ts +13 -0
- package/lib/types/skills.js +140 -0
- package/lib/types/store.d.ts +325 -0
- package/lib/types/store.js +1285 -0
- package/lib/types/tools.d.ts +12 -0
- package/lib/types/tools.js +301 -0
- package/lib/types/trash.d.ts +5 -0
- package/lib/types/trash.js +64 -0
- package/lib/types/types.d.ts +522 -0
- package/lib/types/types.js +85 -0
- package/lib/types/vfs.d.ts +92 -0
- package/lib/types/vfs.js +217 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +132 -3
- package/runtime/dsh3d.js +1279 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kit registry: which runtime kits this build ships, and which one a
|
|
3
|
+
* project runs on.
|
|
4
|
+
* @module @khorsheed/dsh-game
|
|
5
|
+
*/
|
|
6
|
+
import type { BundleFile } from '../types.ts';
|
|
7
|
+
import type { KitId, KitRef, RuntimeKit } from './types.ts';
|
|
8
|
+
export { isKitId, KIT_IDS } from './ids.ts';
|
|
9
|
+
export * from './types.ts';
|
|
10
|
+
/** Every shipped kit, by id. */
|
|
11
|
+
export declare const KITS: Readonly<Record<KitId, RuntimeKit>>;
|
|
12
|
+
/** The kit a project without a `runtime` field runs on. */
|
|
13
|
+
export declare const DEFAULT_KIT: RuntimeKit;
|
|
14
|
+
/**
|
|
15
|
+
* The kit a project runs on.
|
|
16
|
+
* @param ref - the manifest's `runtime` (absent on projects made before kits).
|
|
17
|
+
* @returns the kit; unknown ids fall back to the default.
|
|
18
|
+
*/
|
|
19
|
+
export declare function kitOf(ref: KitRef | undefined): RuntimeKit;
|
|
20
|
+
/**
|
|
21
|
+
* A bundle with the kit's runtime files added. The project's own files win
|
|
22
|
+
* on a path clash (a project should never write under `_dsh/`, but if it
|
|
23
|
+
* does, its file is what it meant to ship).
|
|
24
|
+
*/
|
|
25
|
+
export declare function withKitRuntime(files: Readonly<Record<string, BundleFile>>, kit: RuntimeKit): Record<string, BundleFile>;
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CANVAS_2D } from "./canvas2d.js";
|
|
2
|
+
import { THREE_3D } from "./three3d.js";
|
|
3
|
+
import { isKitId } from "./ids.js";
|
|
4
|
+
export { isKitId, KIT_IDS } from "./ids.js";
|
|
5
|
+
export * from "./types.js";
|
|
6
|
+
/** Every shipped kit, by id. */
|
|
7
|
+
export const KITS = {
|
|
8
|
+
'2d-canvas': CANVAS_2D,
|
|
9
|
+
'3d-three': THREE_3D,
|
|
10
|
+
};
|
|
11
|
+
/** The kit a project without a `runtime` field runs on. */
|
|
12
|
+
export const DEFAULT_KIT = CANVAS_2D;
|
|
13
|
+
/**
|
|
14
|
+
* The kit a project runs on.
|
|
15
|
+
* @param ref - the manifest's `runtime` (absent on projects made before kits).
|
|
16
|
+
* @returns the kit; unknown ids fall back to the default.
|
|
17
|
+
*/
|
|
18
|
+
export function kitOf(ref) {
|
|
19
|
+
return ref !== undefined && isKitId(ref.kit) ? KITS[ref.kit] : DEFAULT_KIT;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A bundle with the kit's runtime files added. The project's own files win
|
|
23
|
+
* on a path clash (a project should never write under `_dsh/`, but if it
|
|
24
|
+
* does, its file is what it meant to ship).
|
|
25
|
+
*/
|
|
26
|
+
export function withKitRuntime(files, kit) {
|
|
27
|
+
return { ...kit.runtimeFiles(files), ...files };
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type BundleFile } from '../types.ts';
|
|
2
|
+
import { type RuntimeKit } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The add-on files a project's scripts import (`three/addons/x`), plus the
|
|
5
|
+
* add-on files those import relatively, as bundle entries under `_dsh/addons/`.
|
|
6
|
+
* @param project - the project's bundle files.
|
|
7
|
+
* @returns the entries to add to the bundle.
|
|
8
|
+
*/
|
|
9
|
+
export declare function addonFiles(project: Readonly<Record<string, BundleFile>>): Record<string, BundleFile>;
|
|
10
|
+
/** The 3D kit. */
|
|
11
|
+
export declare const THREE_3D: RuntimeKit;
|
|
12
|
+
//# sourceMappingURL=three3d.d.ts.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `3d-three` kit (proposal 2026-10-21-game-3d §4): three.js plus the
|
|
3
|
+
* package's own thin runtime `dsh3d` (parts-recipe models with an open
|
|
4
|
+
* body/part set, style as data with two starting presets, a 45° camera, and
|
|
5
|
+
* the workbench hooks pick / pause / highlight).
|
|
6
|
+
*
|
|
7
|
+
* Both modules ride every bundle under `_dsh/` and resolve through the
|
|
8
|
+
* loader's bare-import map, so a game writes `import * as THREE from 'three'`
|
|
9
|
+
* and `import { createWorld, build } from 'dsh3d'`. They are read from disk
|
|
10
|
+
* once, on first use: three.js from the pinned `three` dependency, dsh3d from
|
|
11
|
+
* this package's `runtime/` folder. A missing file degrades to a runtime that
|
|
12
|
+
* reports why, so the smoke check names the problem instead of crashing.
|
|
13
|
+
*
|
|
14
|
+
* three's own add-ons (`three/addons/…`: GLTFLoader, EffectComposer, …) map
|
|
15
|
+
* to `_dsh/addons/` too, but only the ones a project's code imports ride its
|
|
16
|
+
* bundle, with whatever they import in turn: the whole add-on tree is far too
|
|
17
|
+
* big to carry on every load.
|
|
18
|
+
* @module @khorsheed/dsh-game
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { dirname, join, normalize, posix } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { LOOK3D_HINT, normalizeLook3d } from "../look3d.js";
|
|
25
|
+
import { ART_STYLES_3D, BODY_BASES } from "../types.js";
|
|
26
|
+
import { BLANK_PROBE } from "./canvas2d.js";
|
|
27
|
+
import { KIT_IMPORTS_FILE, KIT_RUNTIME_FILE } from "./types.js";
|
|
28
|
+
const THREE_FILE = '_dsh/three.module.js';
|
|
29
|
+
/** The 2D probe after one render: under the checks dsh3d's \`step\` advances logic only, so the canvas is drawn on demand here. */
|
|
30
|
+
const BLANK_PROBE_3D = `(window.dsh3dWorld && window.dsh3dWorld.render(), ${BLANK_PROBE})`;
|
|
31
|
+
const DSH3D_FILE = '_dsh/dsh3d.js';
|
|
32
|
+
/** `runtime/dsh3d.js` beside the package root: one level up from `lib/`, two from `src/kits/`. */
|
|
33
|
+
function dsh3dPath() {
|
|
34
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
return [join(here, '..', 'runtime', 'dsh3d.js'), join(here, '..', '..', 'runtime', 'dsh3d.js')].find(p => existsSync(p));
|
|
36
|
+
}
|
|
37
|
+
/** The pinned three.js module build (the package's exports only expose the unminified one). */
|
|
38
|
+
function threePath() {
|
|
39
|
+
try {
|
|
40
|
+
const entry = createRequire(import.meta.url).resolve('three');
|
|
41
|
+
const min = join(dirname(entry), 'three.module.min.js');
|
|
42
|
+
return existsSync(min) ? min : undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let cached;
|
|
49
|
+
function load() {
|
|
50
|
+
if (cached !== undefined)
|
|
51
|
+
return cached;
|
|
52
|
+
const three = threePath(), dsh3d = dsh3dPath();
|
|
53
|
+
if (three === undefined || dsh3d === undefined) {
|
|
54
|
+
// Not cached: a later install can still fix it without a restart.
|
|
55
|
+
const missing = [three === undefined ? 'three.js' : '', dsh3d === undefined ? 'dsh3d' : ''].filter(Boolean).join('、');
|
|
56
|
+
return { [KIT_RUNTIME_FILE]: { type: 'text/javascript', text: `console.error(${JSON.stringify(`3D 套件缺文件:${missing}(插件安装不完整)`)})` } };
|
|
57
|
+
}
|
|
58
|
+
cached = {
|
|
59
|
+
[THREE_FILE]: { type: 'text/javascript', text: readFileSync(three, 'utf8') },
|
|
60
|
+
[DSH3D_FILE]: { type: 'text/javascript', text: readFileSync(dsh3d, 'utf8') },
|
|
61
|
+
// dsh3d loads glTF looks with three's GLTFLoader: always aboard, since a panel preview may
|
|
62
|
+
// load a model the live version does not carry yet.
|
|
63
|
+
...addonFiles({ 'dsh3d.js': { type: 'text/javascript', text: "import 'three/addons/loaders/GLTFLoader.js'" } }),
|
|
64
|
+
// A key ending in '/' maps a prefix (import-map style).
|
|
65
|
+
[KIT_IMPORTS_FILE]: { type: 'application/json', text: JSON.stringify({ three: THREE_FILE, dsh3d: DSH3D_FILE, 'three/addons/': ADDONS_DIR }) },
|
|
66
|
+
};
|
|
67
|
+
return cached;
|
|
68
|
+
}
|
|
69
|
+
const ADDONS_DIR = '_dsh/addons/';
|
|
70
|
+
/** An add-on set larger than this is cut off (the loader then reports the missing module). */
|
|
71
|
+
const ADDONS_MAX_BYTES = 6 * 1024 * 1024;
|
|
72
|
+
/** three's `examples/jsm` folder (what `three/addons/` names). */
|
|
73
|
+
function addonsRoot() {
|
|
74
|
+
try {
|
|
75
|
+
const dir = join(dirname(createRequire(import.meta.url).resolve('three')), '..', 'examples', 'jsm');
|
|
76
|
+
return existsSync(dir) ? dir : undefined;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const SPECIFIER = /(?:\bimport\s*\(\s*|\bfrom\s*|\bimport\s+)(['"])([^'"]+)\1/g;
|
|
83
|
+
/**
|
|
84
|
+
* The add-on files a project's scripts import (`three/addons/x`), plus the
|
|
85
|
+
* add-on files those import relatively, as bundle entries under `_dsh/addons/`.
|
|
86
|
+
* @param project - the project's bundle files.
|
|
87
|
+
* @returns the entries to add to the bundle.
|
|
88
|
+
*/
|
|
89
|
+
export function addonFiles(project) {
|
|
90
|
+
const root = addonsRoot();
|
|
91
|
+
if (root === undefined)
|
|
92
|
+
return {};
|
|
93
|
+
const queue = [];
|
|
94
|
+
for (const [path, file] of Object.entries(project)) {
|
|
95
|
+
if (!/\.m?js$/.test(path) || !('text' in file))
|
|
96
|
+
continue;
|
|
97
|
+
for (const m of file.text.matchAll(SPECIFIER))
|
|
98
|
+
if (m[2].startsWith('three/addons/'))
|
|
99
|
+
queue.push(m[2].slice('three/addons/'.length));
|
|
100
|
+
}
|
|
101
|
+
const out = {};
|
|
102
|
+
let total = 0;
|
|
103
|
+
while (queue.length > 0) {
|
|
104
|
+
const rel = posix.normalize(queue.shift());
|
|
105
|
+
if (rel.startsWith('..') || rel.startsWith('/') || out[ADDONS_DIR + rel] !== undefined)
|
|
106
|
+
continue;
|
|
107
|
+
const full = normalize(join(root, rel));
|
|
108
|
+
if (!full.startsWith(root) || !existsSync(full))
|
|
109
|
+
continue;
|
|
110
|
+
const text = readFileSync(full, 'utf8');
|
|
111
|
+
if (total + text.length > ADDONS_MAX_BYTES)
|
|
112
|
+
break;
|
|
113
|
+
total += text.length;
|
|
114
|
+
out[ADDONS_DIR + rel] = { type: 'text/javascript', text };
|
|
115
|
+
for (const m of text.matchAll(SPECIFIER)) {
|
|
116
|
+
const spec = m[2];
|
|
117
|
+
if (spec.startsWith('./') || spec.startsWith('../'))
|
|
118
|
+
queue.push(posix.join(posix.dirname(rel), spec));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
const STYLE_LINES = ART_STYLES_3D.map(s => `- \`${s.id}\` ${s.label}:${s.look}`).join('\n');
|
|
124
|
+
/** The 3D kit. */
|
|
125
|
+
export const THREE_3D = {
|
|
126
|
+
id: '3d-three',
|
|
127
|
+
version: 1,
|
|
128
|
+
label: '3D(three.js)',
|
|
129
|
+
runtimeFiles: project => (project === undefined ? load() : { ...load(), ...addonFiles(project) }),
|
|
130
|
+
normalizeLook: normalizeLook3d,
|
|
131
|
+
lookHint: LOOK3D_HINT,
|
|
132
|
+
// dsh3d's renderer keeps its drawing buffer, so the 2D read-back works on the WebGL canvas too.
|
|
133
|
+
blankProbe: BLANK_PROBE_3D,
|
|
134
|
+
styles: ART_STYLES_3D,
|
|
135
|
+
skillSection: `## 运行环境 · 3D 套件(game.json 的 runtime.kit 是 3d-three)
|
|
136
|
+
- three.js 0.170 已内置,再加插件的薄运行时 dsh3d。代码里直接写 \`import * as THREE from 'three'\` 和 \`import { createWorld, build, defineBase, definePart, ink, toon, flat } from 'dsh3d'\`(不要写路径、不要引 CDN)。
|
|
137
|
+
- 题材、世界、角色全由用户的描述决定;插件只给机制和几个起点,别往用户没提的题材上靠。
|
|
138
|
+
- **风格是数据**:写在 game.json 的 \`style3d\`,按用户描述的画风生成,createWorld 自动读取:
|
|
139
|
+
\`{ "preset": "ink", "bg": "#efe7d6", "ground": "#e5dbc5", "ground2": "#d6c9ac", "water": "#a7bab4", "outline": "#2a2622", "outlineWidth": 0.032, "shades": 3, "fog": [26, 62], "light": 1.1 }\`
|
|
140
|
+
preset 是起点(下面两个),其余键覆盖它;outlineWidth 为 0 就没有描边,shades 是卡通明暗层数(2 硬朗 … 5 柔和)。用户说「霓虹赛博」「糖果色」「黑白剪影」之类,就据此调颜色、描边和层数;artStyle 写 custom:用户的描述。现成起点:
|
|
141
|
+
${STYLE_LINES}
|
|
142
|
+
自己加的物体用 \`ink(geometry, toon('#颜色'))\`(卡通材质 + 描边),地面起伏、石头、树这类场景用 \`flat('#颜色')\`,这样换 style3d 时全场跟着变。别用写实贴图和 PBR 材质。
|
|
143
|
+
- 世界:\`const world = createWorld({ region: (x, z) => '区域名' })\`,然后
|
|
144
|
+
- \`world.terrain({ size, height: (x, z) => 高度 })\` 地形;\`world.place(obj, x, z)\` 放到地面上
|
|
145
|
+
- **能站上去的模型**(楼梯、平台、桥、屋顶):\`world.add(obj, { walkable: true })\` 或 \`world.walkable(obj)\`。角色移动时用 \`world.groundAt(x, z, 脚底高度 + 台阶高)\` 取落脚高度:只认这个高度以下最高的面,所以能一级级爬楼梯,不会被吸到头顶的屋顶上。要挡墙就再问一次不设上限的 \`groundAt(x, z)\`,比脚底高出一个台阶以上就别走过去。它只管「站在哪」,不管撞墙、滑坡。
|
|
146
|
+
- \`world.add(obj, { id: 元素id, inst: 实例id, name, state: () => '当前在干什么' })\` 登记实体(右栏「指一下」靠它认出东西)
|
|
147
|
+
- \`world.scatter(元素id, 数量或位置列表, { seed, spread, center, scale: [0.85, 1.15] })\` 把一种元素批量撒在地面上:外观只生成一次、批量绘制,每棵按地形高度落地、随机朝向和大小(同一个 seed 每次摆得一样);点到任意一棵,「指一下」认出是哪个元素、第几棵。树林、石滩、草地都用它,别自己循环 clone。加 \`sway: { amplitude: 0.06, period: 2.4 }\` 让草木随风摆(越高处摆得越大,每棵错开;影子不跟着摆);除此之外撒出来的是静物,没有逐棵动效。
|
|
148
|
+
- **多个场景**(地图上的几座山、室内外):\`world.enterScene('场景名', w => { w.terrain(…); w.scatter(…); w.add(…); w.onUpdate(…) })\`。切到下一个场景时,上一个场景里登记的地形、实体、批量物件、onUpdate 全部移除并释放;在任何场景之外登记的(玩家、镜头跟随)保留。各场景的底色、地面颜色、水色、雾、光照写在 game.json 的 \`style3d.scenes\`:\`{ "北坡": { "bg": "#cfd6dc", "ground": "#d8dcd6", "fog": [12, 40], "light": 0.8 } }\`(名字由游戏自己定;明暗层数和描边全局共用,不能按场景换)。别在同一个场景里堆所有区域,也别手动去删 scene 里的东西。
|
|
149
|
+
- \`world.follow(玩家)\` 镜头跟随(俯视斜 45° 正交镜头);\`world.onUpdate((dt, t) => {...})\` 写游戏逻辑
|
|
150
|
+
- \`world.step(dt)\` 推进一帧(dshGame.step 就调它),\`world.start()\` 开始实时播放
|
|
151
|
+
- \`world.pick(x, y)\`、\`world.pause(on)\`、\`world.highlight(list)\` 是给面板用的,dshGame 里转发即可
|
|
152
|
+
- 外观 look(3D):部件配方,\`build(元素id或look)\` 拼出模型并自带程序动效。例子(一只长尾猫):
|
|
153
|
+
\`{ "kind": "parts3d", "base": "quadruped", "proportions": { "bodyLength": 0.9, "bodyHeight": 0.4, "legLength": 0.3, "headSize": 0.34, "snout": 0.1 }, "parts": [{ "type": "ear", "count": 2 }, { "type": "tail", "count": 1, "length": 0.9, "thickness": 0.06 }], "palette": { "body": "#f2a65a", "accent": "#7a4a2a", "eye": "#1d1d1f" }, "motion": { "tailSway": { "amplitude": 20, "period": 1.4 }, "breath": { "amplitude": 0.02, "period": 2.4 } } }\`
|
|
154
|
+
- 内置身体 base:${BODY_BASES.join(' / ')}(四足、禽、鱼、蛇形、人形)。比例:bodyLength、bodyHeight、legLength、headSize、snout、legs(足数)、heads(头数);禽另有 beak;蛇形是 length、thickness、segments;人形是 height。
|
|
155
|
+
- 内置部件 parts:ear、horn、tail(count/length/fan/thickness)、wing(count/size)、fin、eye(count)、face(style: "human")。color 写 palette 的键或 #rrggbb。
|
|
156
|
+
- palette 的键:body、accent、eye,另可 belly、horn、wing、beak、face、skin。motion:tailSway、breath、flap、swim、bob(amplitude 角度,breath/bob 是比例;period 秒)。
|
|
157
|
+
- **内置的不够就自己注册**(机器人、史莱姆、车、建筑……)。注册代码一律放 \`src/looks.js\`(入口先 \`import './looks.js'\` 再 build)——右栏的外观预览只加载这个文件,写在别处面板就拼不出来:
|
|
158
|
+
\`defineBase('robot', (look, kit) => { const g = new kit.THREE.Group(), body = new kit.THREE.Group(); g.add(body); body.add(kit.ink(new kit.THREE.BoxGeometry(0.8, 0.6, 0.5), kit.toon(kit.color('body', '#888888')))); return { g, body } })\`
|
|
159
|
+
\`definePart('antenna', ({ spec, anchors, kit }) => { const a = kit.ink(new kit.THREE.CylinderGeometry(0.02, 0.02, spec.length ?? 0.4), kit.toon(kit.color(spec.color ?? 'accent', '#ff0000'))); a.position.y = 0.5; anchors.body.add(a); return { update: t => { a.rotation.z = 0.2 * Math.sin(t * 3) } } })\`
|
|
160
|
+
之后配方里就能写 \`"base": "robot"\`、\`{ "type": "antenna", "length": 0.5 }\`(自定义部件的其他参数在 spec.extra 里)。注册过的名字用户在面板里同样能调。
|
|
161
|
+
- **场景物件也走配方**:树、石头、草丛、远山、建筑这类有形状的元素,同样在 \`src/looks.js\` 里 defineBase 注册造型(用 \`kit.flat(kit.color('body', '#…'))\`,尺寸读 \`look.proportions\`),元素文件写 \`look\`,成片的用 \`world.scatter(元素id, …)\` 撒开、单个的 \`build(元素id)\`(数量、分布范围这类仍是元素字段)。别在 main.js 里直接手写几何体——那样右栏看不到它的样子,用户也没法调。只有地面高度场(world.terrain)和水面这种铺满整个场景的不算元素。
|
|
162
|
+
- 面数预算:单个元素 1 万三角面以内,同屏一个场景 15 万以内(\`world.stats()\` 可查,批量物件按份数算)。这是建议不是硬限制,超了先看帧率。
|
|
163
|
+
- 贴图与素材:图片放 \`assets/\`,\`new THREE.TextureLoader().load('assets/xx.png')\` 就能用(贴到 toon/flat 材质的 map 上);纸纹、墨晕这类也可以用 canvas 现画,再 \`new THREE.CanvasTexture(canvas)\`,不用另存文件。
|
|
164
|
+
- **模型文件做外观(glTF)**:Blender 做的角色、建筑导出成 .glb 放进 \`assets/\`,元素文件写 \`"look": { "kind": "gltf", "src": "assets/yumin.glb", "scale": 1, "material": "toon" }\`。代码里照样 \`build('yumin')\`:它立刻返回,模型加载完自己出现(要等就 \`await model.userData.loaded\` 或 \`await loaded()\`)。\`material\` 取 \`toon\`(默认:套当前风格的卡通明暗和描边,保留模型的颜色、顶点色和贴图)或 \`original\`(模型自己的材质,默认不描边);\`outline: true/false\` 单独开关描边。右栏能预览、切换材质、调大小。导出建议:Y 轴朝上、单位米、原点在脚底、**脸朝 +X**(插件的约定,内置的异兽也是;在 Blender 里就是让角色面向 +X 轴,glTF 默认的 +Z 朝向要转过来)、贴图打包进 .glb、单个模型 1 万面以内。右栏预览按这个约定拍正面偏 3/4;某个元素想换默认角度,写 \`"preview": { "yaw": 35, "pitch": 18 }\`(度数:yaw 0 正面、90 侧面、180 背面,pitch 往下看的角度),配方外观也能写。检查会等模型加载完再截图、跑测试;外观引用的文件不在项目里,检查会拦下。用现成的 CC0 素材时,在 \`assets/LICENSES.md\` 记下来源和授权。
|
|
165
|
+
- 骨骼动画:Blender 里每个动作做成一个 Action(名字就是动画名,如 idle、walk),导出时勾选动画。模型加载后自动循环播放 \`look.animation\` 指定的那个,没写就挑名字带 idle / 待机 / stand 的,再没有就第一个。代码里 \`model.userData.play('walk')\` 切换(默认 0.2 秒过渡;\`{ loop: false }\` 只播一遍),\`model.userData.animations\` 是全部动画名(加载完才有)。动画跟着 world 的时钟走:模型要 \`world.add\` 登记(或者自己每帧调 \`model.userData.update(world.time)\`),测试里 \`step\` 照样推进动画。描边跟着骨骼动;「指一下」能点中摆着姿势的角色。右栏转盘下面列出全部动画,点一下就播。
|
|
166
|
+
- 按名字微调(游戏和右栏一致,都由 build 生效):\`"tint": { "冠": "#c9a23a" }\` 按材质名换基础色(材质用了顶点色时是相乘,会偏暗);\`"bones": { "head": { "scale": 1.2, "rotate": [0, 10, 0] } }\` 按骨头名缩放(一个数或 [x,y,z],子骨头跟着变)、加旋转(度),叠在动画之上,播动作也不会被冲掉;\`"morphs": { "smile": 0.6 }\` 按形态键名设权重,描边跟着变形。所以 Blender 里材质、骨头、形态键要起有意义的名字(羽_青、冠、head、wing.L、smile……)。右栏「细调外观」读出模型里的这些名字,列成色块和滑块(带 DEF-/MCH-/ORG- 前缀的绑定辅助骨默认不列,可以搜)。代码里加载后改了的东西右栏看不到,能写进外观的就写进外观。
|
|
167
|
+
- three 自带的扩展直接引:\`import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'\`,后期处理 \`three/addons/postprocessing/EffectComposer.js\` 等都行(只打包你引用到的文件)。
|
|
168
|
+
- 镜头:默认是 45° 俯视的正交镜头。第三人称跟随直接用现成的:\`const rig = world.followCamera({ target: player, distance: 7, height: 1.4, mouse: true })\`(\`mouse: true\` 点画面锁定鼠标、移动鼠标转视角,Esc 退出),\`rig.yaw\` / \`rig.pitch\` / \`rig.distance\` 随时可改,dshGame 上写 \`look: (dx, dy) => rig.look(dx, dy)\`,测试里用 \`drag\` 模拟;角色朝镜头前方走时用 \`rig.yaw\` 换算方向。要别的镜头就自己建 \`new THREE.PerspectiveCamera(…)\`,\`world.setCamera(cam)\`,然后每帧(onUpdate 里)自己摆位置、朝向;「指一下」、高亮圈都跟着这台镜头走,\`world.setCamera(null)\` 换回默认。鼠标转视角可以在点击里 \`canvas.requestPointerLock()\`(Esc 退出),并在 dshGame 上提供 \`look(dx, dy)\`,测试里用 \`drag(dx, dy)\` 模拟拖动。
|
|
169
|
+
- 光与投影:\`world.lights.hemi\`(天光)、\`world.lights.sun\`(太阳)可以直接调;style3d 里写 \`"shadows": true\` 开太阳投影(之后 world.add / scatter 进来的东西都投影、受影);\`"rim": 0.6\`(0–2)加轮廓光,角色边缘亮一圈、从地面上「浮」出来,\`"rimColor"\` 换颜色。
|
|
170
|
+
- 后期处理(泛光、调色):\`world.onRender(({ renderer, scene, camera }) => composer.render())\` 接管绘制,返回的函数交还。泛光(狐火、灯笼)这样接:\`import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'\`、\`RenderPass\`、\`UnrealBloomPass\`、\`OutputPass\`(同目录),建 composer 依次 addPass(new RenderPass(world.scene, world.camera))、addPass(new UnrealBloomPass(new THREE.Vector2(w, h), 强度 0.6, 半径 0.4, 阈值 0.85))、addPass(new OutputPass()),然后 onRender 里 \`composer.render()\`;发光的东西用 \`toon(颜色, { own: true, emissive: 颜色 })\`。换镜头后记得把 RenderPass 的 camera 一起换。
|
|
171
|
+
- 材质:toon / flat 按颜色共用一份。要让某一个东西单独发光、变色、淡出:\`toon('#ff8a3d', { own: true, emissive: '#ff8a3d' })\` 拿一份自己的。
|
|
172
|
+
- 加载资源时(glb、大贴图)先挂 \`window.dshGame = { ready: false, loading: true, … }\`,加载完再设 \`ready = true\`:检查会等最多 20 秒(不写 loading 只等 4 秒)。
|
|
173
|
+
- 检查里(\`window.__GAMEDEV_TEST__\`)\`step\` 推进逻辑和镜头但不画画面,测试可以放心多跑帧;要读像素时先 \`world.render()\`。
|
|
174
|
+
- dshGame 约定照旧,3D 推荐这样接:
|
|
175
|
+
\`\`\`js
|
|
176
|
+
window.dshGame = {
|
|
177
|
+
ready: true,
|
|
178
|
+
step: dt => world.step(dt),
|
|
179
|
+
input(key, down) { /* 记到按键状态里,onUpdate 里读 */ },
|
|
180
|
+
state: () => ({ player: { x: player.position.x, z: player.position.z } }),
|
|
181
|
+
pick: (x, y) => world.pick(x, y),
|
|
182
|
+
pause: on => world.pause(on),
|
|
183
|
+
highlight: list => world.highlight(list),
|
|
184
|
+
}
|
|
185
|
+
world.start()
|
|
186
|
+
\`\`\`
|
|
187
|
+
`,
|
|
188
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime-kit seam (proposal 2026-10-21-game-3d §10.2): everything that
|
|
3
|
+
* depends on HOW a game draws — the runtime the frame gets, the look formats
|
|
4
|
+
* the panel and the checks accept, the blank-screen probe, the skill section
|
|
5
|
+
* and the art styles — lives behind one kit per runtime. The workbench core
|
|
6
|
+
* (projects, versions, checks, the panel) asks the project's kit instead of
|
|
7
|
+
* assuming 2D canvas.
|
|
8
|
+
* @module @khorsheed/dsh-game
|
|
9
|
+
*/
|
|
10
|
+
import type { AnyLook, ArtStyle, BundleFile } from '../types.ts';
|
|
11
|
+
/** The kits this build ships. */
|
|
12
|
+
export type KitId = '2d-canvas' | '3d-three';
|
|
13
|
+
/** A project's pinned runtime (`game.json` `runtime`); absent means `2d-canvas` version 1. */
|
|
14
|
+
export interface KitRef {
|
|
15
|
+
readonly kit: KitId;
|
|
16
|
+
readonly version: number;
|
|
17
|
+
}
|
|
18
|
+
/** Bundle path prefix the kit's runtime files ride under (never written into a project). */
|
|
19
|
+
export declare const KIT_DIR = "_dsh";
|
|
20
|
+
/** The runtime script the loader injects before the game's own scripts. */
|
|
21
|
+
export declare const KIT_RUNTIME_FILE = "_dsh/runtime.js";
|
|
22
|
+
/** Bare module specifier → bundle path, read by the loader (e.g. `three` → `_dsh/three.module.js`). */
|
|
23
|
+
export declare const KIT_IMPORTS_FILE = "_dsh/imports.json";
|
|
24
|
+
/** One runtime kit. */
|
|
25
|
+
export interface RuntimeKit {
|
|
26
|
+
readonly id: KitId;
|
|
27
|
+
/** The runtime version new projects pin. */
|
|
28
|
+
readonly version: number;
|
|
29
|
+
readonly label: string;
|
|
30
|
+
/**
|
|
31
|
+
* Files added to every bundle of a project on this kit: the runtime
|
|
32
|
+
* script ({@link KIT_RUNTIME_FILE}), vendored libraries, and the bare
|
|
33
|
+
* import map ({@link KIT_IMPORTS_FILE}). Kept out of version snapshots.
|
|
34
|
+
* Given the project's own files, a kit may add what they import from it
|
|
35
|
+
* (the 3D kit's `three/addons/…`).
|
|
36
|
+
*/
|
|
37
|
+
runtimeFiles(project?: Readonly<Record<string, BundleFile>>): Readonly<Record<string, BundleFile>>;
|
|
38
|
+
/** Normalize an element's `look`; null when unusable. */
|
|
39
|
+
normalizeLook(raw: unknown): AnyLook | null;
|
|
40
|
+
/** What a usable look needs, worded for the model when one fails. */
|
|
41
|
+
readonly lookHint: string;
|
|
42
|
+
/**
|
|
43
|
+
* In-page expression (evaluated after the smoke steps) answering whether
|
|
44
|
+
* the screen is blank: `true` blank, `false` drawn, `null` no canvas.
|
|
45
|
+
*/
|
|
46
|
+
readonly blankProbe: string;
|
|
47
|
+
/** The kit's part of the `gamedev` skill: the runtime, looks and drawing helpers. */
|
|
48
|
+
readonly skillSection: string;
|
|
49
|
+
/** The art styles the gate offers on this kit. */
|
|
50
|
+
readonly styles: readonly ArtStyle[];
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Bundle path prefix the kit's runtime files ride under (never written into a project). */
|
|
2
|
+
export const KIT_DIR = '_dsh';
|
|
3
|
+
/** The runtime script the loader injects before the game's own scripts. */
|
|
4
|
+
export const KIT_RUNTIME_FILE = `${KIT_DIR}/runtime.js`;
|
|
5
|
+
/** Bare module specifier → bundle path, read by the loader (e.g. `three` → `_dsh/three.module.js`). */
|
|
6
|
+
export const KIT_IMPORTS_FILE = `${KIT_DIR}/imports.json`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation for 3D looks (`parts3d` recipes and `gltf` files). Pure data,
|
|
3
|
+
* no three.js: the store, the tools and the browser half all use it, and the
|
|
4
|
+
* 3d-three kit's `dsh3d.build` trusts what passes here.
|
|
5
|
+
*
|
|
6
|
+
* Normalizing is forgiving the way the pixel-look normalizer is: unknown
|
|
7
|
+
* keys drop, numbers clamp into range, bad colors drop; only a look with no
|
|
8
|
+
* usable body (an unknown base, or a gltf path that leaves the project) is
|
|
9
|
+
* rejected outright.
|
|
10
|
+
* @module @khorsheed/dsh-game
|
|
11
|
+
*/
|
|
12
|
+
import { type Look3D } from './types.ts';
|
|
13
|
+
/** What a usable 3D look needs, worded for the model. */
|
|
14
|
+
export declare const LOOK3D_HINT: string;
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a 3D look; null when it has no usable body.
|
|
17
|
+
* @param raw - the element's `look` as written.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeLook3d(raw: unknown): Look3D | null;
|
|
20
|
+
//# sourceMappingURL=look3d.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation for 3D looks (`parts3d` recipes and `gltf` files). Pure data,
|
|
3
|
+
* no three.js: the store, the tools and the browser half all use it, and the
|
|
4
|
+
* 3d-three kit's `dsh3d.build` trusts what passes here.
|
|
5
|
+
*
|
|
6
|
+
* Normalizing is forgiving the way the pixel-look normalizer is: unknown
|
|
7
|
+
* keys drop, numbers clamp into range, bad colors drop; only a look with no
|
|
8
|
+
* usable body (an unknown base, or a gltf path that leaves the project) is
|
|
9
|
+
* rejected outright.
|
|
10
|
+
* @module @khorsheed/dsh-game
|
|
11
|
+
*/
|
|
12
|
+
import { BODY_BASES, PART_TYPES, } from "./types.js";
|
|
13
|
+
const HEX = /^#[0-9a-f]{6}$/i;
|
|
14
|
+
const MOTIONS = ['tailSway', 'breath', 'flap', 'swim', 'bob'];
|
|
15
|
+
/** Proportions that are counts (六足, 多首, a serpent's segments), with their integer ranges. */
|
|
16
|
+
const COUNTS = { legs: [0, 8], heads: [1, 9], segments: [3, 40] };
|
|
17
|
+
/** What a usable 3D look needs, worded for the model. */
|
|
18
|
+
export const LOOK3D_HINT = 'look 不可用:3D 外观要么是 { kind: "parts3d", base: '
|
|
19
|
+
+ `${BODY_BASES.map(b => `"${b}"`).join(' / ')} 或项目里 defineBase 注册的名字, proportions, parts, palette, motion },`
|
|
20
|
+
+ '要么是 { kind: "gltf", src: "assets/….glb" }(路径在项目里)';
|
|
21
|
+
function clamp(value, min, max) {
|
|
22
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : undefined;
|
|
23
|
+
}
|
|
24
|
+
function record(raw) {
|
|
25
|
+
return raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : undefined;
|
|
26
|
+
}
|
|
27
|
+
/** A name a project may register with `defineBase` / `definePart`: lowercase identifier. */
|
|
28
|
+
const NAME = /^[a-z][\w-]{0,31}$/;
|
|
29
|
+
function part(raw) {
|
|
30
|
+
const r = record(raw);
|
|
31
|
+
// Built-in part types, or a name the project registers in its own code (dsh3d.definePart).
|
|
32
|
+
if (r === undefined || typeof r.type !== 'string' || !NAME.test(r.type))
|
|
33
|
+
return undefined;
|
|
34
|
+
const out = { type: r.type };
|
|
35
|
+
const count = clamp(r.count, 1, 12);
|
|
36
|
+
if (count !== undefined)
|
|
37
|
+
out.count = Math.round(count);
|
|
38
|
+
for (const [key, min, max] of [['size', 0.01, 3], ['length', 0.01, 5], ['fan', 0, 360], ['thickness', 0.005, 1]]) {
|
|
39
|
+
const v = clamp(r[key], min, max);
|
|
40
|
+
if (v !== undefined)
|
|
41
|
+
out[key] = v;
|
|
42
|
+
}
|
|
43
|
+
if (typeof r.color === 'string' && r.color.length <= 24)
|
|
44
|
+
out.color = r.color;
|
|
45
|
+
if (typeof r.style === 'string' && r.style.length <= 24)
|
|
46
|
+
out.style = r.style;
|
|
47
|
+
if (!PART_TYPES.includes(r.type)) {
|
|
48
|
+
// A project-defined part keeps its own extra numbers and short strings.
|
|
49
|
+
const extra = {};
|
|
50
|
+
for (const [k, v] of Object.entries(r).slice(0, 16)) {
|
|
51
|
+
if (k in out || k === 'type' || !NAME.test(k))
|
|
52
|
+
continue;
|
|
53
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
54
|
+
extra[k] = Math.min(1000, Math.max(-1000, v));
|
|
55
|
+
else if (typeof v === 'string' && v.length <= 40)
|
|
56
|
+
extra[k] = v;
|
|
57
|
+
}
|
|
58
|
+
if (Object.keys(extra).length > 0)
|
|
59
|
+
out.extra = extra;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function parts3d(r) {
|
|
64
|
+
// A built-in body, or one the project registers (dsh3d.defineBase); an
|
|
65
|
+
// unregistered name fails loudly at build time, which the smoke check reports.
|
|
66
|
+
if (typeof r.base !== 'string' || !NAME.test(r.base))
|
|
67
|
+
return null;
|
|
68
|
+
const proportions = {};
|
|
69
|
+
for (const [k, v] of Object.entries(record(r.proportions) ?? {}).slice(0, 16)) {
|
|
70
|
+
const count = COUNTS[k];
|
|
71
|
+
const n = count === undefined ? clamp(v, 0.01, 5) : clamp(v, count[0], count[1]);
|
|
72
|
+
if (n !== undefined && k.length <= 24)
|
|
73
|
+
proportions[k] = count === undefined ? n : Math.round(n);
|
|
74
|
+
}
|
|
75
|
+
const palette = {};
|
|
76
|
+
for (const [k, v] of Object.entries(record(r.palette) ?? {}).slice(0, 12)) {
|
|
77
|
+
if (typeof v === 'string' && HEX.test(v) && k.length <= 24)
|
|
78
|
+
palette[k] = v.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
const motion = {};
|
|
81
|
+
for (const name of MOTIONS) {
|
|
82
|
+
const m = record(record(r.motion)?.[name]);
|
|
83
|
+
if (m === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
const amplitude = clamp(m.amplitude, 0, 90), period = clamp(m.period, 0.1, 20);
|
|
86
|
+
motion[name] = { ...(amplitude === undefined ? {} : { amplitude }), ...(period === undefined ? {} : { period }) };
|
|
87
|
+
}
|
|
88
|
+
const refine = {};
|
|
89
|
+
for (const [k, v] of Object.entries(record(r.refine) ?? {}).slice(0, 8)) {
|
|
90
|
+
const n = clamp(v, 0, 100_000);
|
|
91
|
+
if (n !== undefined)
|
|
92
|
+
refine[k] = n;
|
|
93
|
+
}
|
|
94
|
+
const scale = clamp(r.scale, 0.05, 50);
|
|
95
|
+
return {
|
|
96
|
+
kind: 'parts3d',
|
|
97
|
+
base: r.base,
|
|
98
|
+
proportions,
|
|
99
|
+
parts: (Array.isArray(r.parts) ? r.parts : []).slice(0, 16).map(part).filter((p) => p !== undefined),
|
|
100
|
+
palette,
|
|
101
|
+
motion,
|
|
102
|
+
...(scale === undefined ? {} : { scale }),
|
|
103
|
+
...previewOf(r),
|
|
104
|
+
...(Object.keys(refine).length === 0 ? {} : { refine }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** A look's `preview` angle (degrees), when it has a usable one. */
|
|
108
|
+
function previewOf(r) {
|
|
109
|
+
const p = record(r.preview);
|
|
110
|
+
if (p === undefined)
|
|
111
|
+
return {};
|
|
112
|
+
const yaw = clamp(p.yaw, -360, 360), pitch = clamp(p.pitch, -10, 80);
|
|
113
|
+
const out = { ...(yaw === undefined ? {} : { yaw }), ...(pitch === undefined ? {} : { pitch }) };
|
|
114
|
+
return Object.keys(out).length === 0 ? {} : { preview: out };
|
|
115
|
+
}
|
|
116
|
+
/** Names the model chose (materials, bones, shape keys): any text, bounded. */
|
|
117
|
+
const PART_NAME = /^[^\u0000-\u001f]{1,60}$/;
|
|
118
|
+
/** A glTF look's named tweaks: tint, bones, morphs (each kept only when usable, at most 64 names). */
|
|
119
|
+
function tweaksOf(r) {
|
|
120
|
+
const entries = (raw) => Object.entries(record(raw) ?? {}).filter(([k]) => PART_NAME.test(k)).slice(0, 64);
|
|
121
|
+
const tint = Object.fromEntries(entries(r.tint).filter(([, v]) => typeof v === 'string' && HEX.test(v)).map(([k, v]) => [k, v.toLowerCase()]));
|
|
122
|
+
const morphs = Object.fromEntries(entries(r.morphs).map(([k, v]) => [k, clamp(v, -1, 2)]).filter(([, v]) => v !== undefined));
|
|
123
|
+
const bones = {};
|
|
124
|
+
for (const [k, v] of entries(r.bones)) {
|
|
125
|
+
const b = record(v);
|
|
126
|
+
if (b === undefined)
|
|
127
|
+
continue;
|
|
128
|
+
const triple = (raw, min, max) => {
|
|
129
|
+
if (!Array.isArray(raw) || raw.length !== 3)
|
|
130
|
+
return undefined;
|
|
131
|
+
const t = raw.map(x => clamp(x, min, max));
|
|
132
|
+
return t.every(x => x !== undefined) ? t : undefined;
|
|
133
|
+
};
|
|
134
|
+
const scale = typeof b.scale === 'number' ? clamp(b.scale, 0.1, 5) : triple(b.scale, 0.1, 5);
|
|
135
|
+
const rotate = triple(b.rotate, -180, 180);
|
|
136
|
+
if (scale !== undefined || rotate !== undefined)
|
|
137
|
+
bones[k] = { ...(scale === undefined ? {} : { scale }), ...(rotate === undefined ? {} : { rotate }) };
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...(Object.keys(tint).length === 0 ? {} : { tint }),
|
|
141
|
+
...(Object.keys(bones).length === 0 ? {} : { bones }),
|
|
142
|
+
...(Object.keys(morphs).length === 0 ? {} : { morphs }),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function gltf(r) {
|
|
146
|
+
const src = typeof r.src === 'string' ? r.src.trim().replace(/^\.\//, '') : '';
|
|
147
|
+
if (!/\.(glb|gltf)$/i.test(src) || src.startsWith('/') || src.split('/').includes('..'))
|
|
148
|
+
return null;
|
|
149
|
+
const scale = clamp(r.scale, 0.05, 50);
|
|
150
|
+
const recipe = record(r.recipe);
|
|
151
|
+
const baked = recipe === undefined ? null : parts3d(recipe);
|
|
152
|
+
const material = r.material === 'original' || r.material === 'toon' ? r.material : undefined;
|
|
153
|
+
return {
|
|
154
|
+
kind: 'gltf', src, ...(scale === undefined ? {} : { scale }),
|
|
155
|
+
...(material === undefined ? {} : { material }), ...(typeof r.outline === 'boolean' ? { outline: r.outline } : {}),
|
|
156
|
+
...(typeof r.animation === 'string' && r.animation.trim() !== '' ? { animation: r.animation.trim().slice(0, 80) } : {}),
|
|
157
|
+
...previewOf(r),
|
|
158
|
+
...tweaksOf(r),
|
|
159
|
+
...(baked === null ? {} : { recipe: baked }),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Normalize a 3D look; null when it has no usable body.
|
|
164
|
+
* @param raw - the element's `look` as written.
|
|
165
|
+
*/
|
|
166
|
+
export function normalizeLook3d(raw) {
|
|
167
|
+
const r = record(raw);
|
|
168
|
+
if (r === undefined)
|
|
169
|
+
return null;
|
|
170
|
+
if (r.kind === 'parts3d')
|
|
171
|
+
return parts3d(r);
|
|
172
|
+
if (r.kind === 'gltf')
|
|
173
|
+
return gltf(r);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session a panel call names, by id. The wire's `agent`/`session` lookups
|
|
3
|
+
* refuse identities owned by subagent routing (a room's CLI member, a native
|
|
4
|
+
* subagent), so a panel opened on such a session could not even read. Panel
|
|
5
|
+
* calls take the session id instead: an attached session — subagent ones
|
|
6
|
+
* included — answers directly; anything else goes through the host's own
|
|
7
|
+
* `session` lookup, which resumes an ordinary cold session and raises the
|
|
8
|
+
* same errors a lookup parameter did.
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
12
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the session a panel call names.
|
|
15
|
+
* @param ctx - host context carrying the session store and the typert lookups.
|
|
16
|
+
* @param sessionId - the session the panel shows.
|
|
17
|
+
* @returns the session.
|
|
18
|
+
* @throws the host lookup's refusal, or when no session has that id.
|
|
19
|
+
*/
|
|
20
|
+
export declare function panelSession(ctx: Context, sessionId: SessionId): Promise<Session>;
|
|
21
|
+
//# sourceMappingURL=panel-session.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the session a panel call names.
|
|
3
|
+
* @param ctx - host context carrying the session store and the typert lookups.
|
|
4
|
+
* @param sessionId - the session the panel shows.
|
|
5
|
+
* @returns the session.
|
|
6
|
+
* @throws the host lookup's refusal, or when no session has that id.
|
|
7
|
+
*/
|
|
8
|
+
export async function panelSession(ctx, sessionId) {
|
|
9
|
+
const attached = ctx.get('sessions')?.get(sessionId);
|
|
10
|
+
if (attached !== undefined)
|
|
11
|
+
return attached;
|
|
12
|
+
const resolved = await ctx.get('typert')?.lookups.get('session')?.resolve(sessionId);
|
|
13
|
+
if (resolved === undefined)
|
|
14
|
+
throw new Error(`session "${sessionId}" not found`);
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|