@lingfan-engine/create 0.2.0 → 0.3.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.
- package/bin/create.mjs +1 -1
- package/package.json +1 -1
- package/src/cli/cli.ts +11 -0
- package/src/generate/project.ts +13 -6
- package/src/index.ts +7 -0
- package/src/registry/compat.ts +49 -0
- package/src/registry/index.ts +2 -1
- package/src/shared/index.ts +8 -1
- package/src/shared/semver.ts +115 -0
- package/src/shared/texts.ts +1 -1
- package/template/README.md +59 -21
- package/template/__PROJECT__/Resources/Stories/title/title_main.story +15 -4
- package/template/__PROJECT__/index.html +7 -0
- package/template/__PROJECT__/package.json +5 -3
- package/template/__PROJECT__/src/controls/index.ts +7 -0
- package/template/__PROJECT__/src/controls/store.ts +32 -0
- package/template/__PROJECT__/src/gen/ui_register.g.ts +5 -0
- package/template/__PROJECT__/src/host/boot.ts +200 -5
- package/template/__PROJECT__/src/host/controls.ts +77 -0
- package/template/__PROJECT__/src/host/engine-wiring.ts +75 -0
- package/template/__PROJECT__/src/host/index.ts +37 -1
- package/template/__PROJECT__/src/host/input-map.ts +18 -3
- package/template/__PROJECT__/src/host/layer-z.ts +6 -0
- package/template/__PROJECT__/src/host/mount/index.ts +18 -0
- package/template/__PROJECT__/src/host/mount/renderer.ts +128 -0
- package/template/__PROJECT__/src/host/mount/table.ts +52 -0
- package/template/__PROJECT__/src/host/panels/history.ts +193 -0
- package/template/__PROJECT__/src/host/panels/preferences.ts +216 -0
- package/template/__PROJECT__/src/host/save-slots.ts +52 -4
- package/template/__PROJECT__/src/host/stage-dom.ts +36 -0
- package/template/__PROJECT__/src/host/thumbnails.ts +101 -0
- package/template/__PROJECT__/src/main.ts +30 -8
- package/template/__PROJECT__/src/mounts.ts +22 -0
- package/template/__PROJECT__/src/stories/index.ts +10 -0
- package/template/__PROJECT__/src/styles/stage.css +114 -0
- package/template/__PROJECT__/src/vite-env.d.ts +9 -0
- package/template/__PROJECT__/tsconfig.node.json +2 -1
- package/template/__PROJECT__/vite.config.ts +7 -52
- package/template/__PROJECT__/vite.plugins.ts +49 -0
- package/template/overlays/README.md +9 -3
- package/template/overlays/miniprogram/package.patch.json +1 -1
- package/template/overlays/miniprogram/src/mp/host.ts +5 -1
- package/template/overlays/miniprogram/vite.config.ts +8 -47
- package/template/overlays/react/src/App.tsx +12 -0
- package/template/overlays/react/src/UiHost.tsx +62 -0
- package/template/overlays/react/src/main.tsx +27 -5
- package/template/overlays/react/src/vite-env.d.ts +12 -0
- package/template/overlays/react/vite.config.ts +6 -49
- package/template/overlays/tauri/src/main.ts +23 -4
- package/template/overlays/ts/package.patch.json +1 -2
- package/template/overlays/ts/{Stories.src → src/Stories.src}/title/title_main.ts +6 -0
- package/template/overlays/ts/tsconfig.stories.json +1 -1
- package/template/overlays/vue/src/App.vue +13 -0
- package/template/overlays/vue/src/UiHost.vue +65 -0
- package/template/overlays/vue/src/main.ts +28 -5
- package/template/overlays/vue/src/vite-env.d.ts +12 -0
- package/template/overlays/vue/vite.config.ts +6 -49
- package/template/template-meta.json +8 -8
- package/template/__PROJECT__/src/stories.ts +0 -11
- package/template/overlays/ts/src/stories.ts +0 -10
- /package/template/overlays/ts/{Stories.src → src/Stories.src}/config.ts +0 -0
- /package/template/overlays/ts/{Stories.src → src/Stories.src}/title/look_around.ts +0 -0
- /package/template/overlays/ts/{Stories.src → src}/gen/font.gen.d.ts +0 -0
- /package/template/overlays/ts/{Stories.src → src}/gen/script.gen.d.ts +0 -0
- /package/template/overlays/ts/{Stories.src → src}/gen/state.gen.d.ts +0 -0
- /package/template/overlays/ts/{Stories.src → src}/gen/stories.gen.d.ts +0 -0
package/bin/create.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `src/cli/cli.ts`,也不该被迫装一整套构建链。垫片用本包自带依赖 `tsx` 装载真实 CLI,
|
|
9
9
|
* 呈现与编排逻辑仍然只有一份。
|
|
10
10
|
*
|
|
11
|
-
* 用法:`npm create @lingfan-engine [工程名] [-- --framework
|
|
11
|
+
* 用法:`npm create @lingfan-engine [工程名] [-- --framework vue --shell browser]`;
|
|
12
12
|
* 不带参数即进入交互问答。
|
|
13
13
|
*/
|
|
14
14
|
import { tsImport } from "tsx/esm/api";
|
package/package.json
CHANGED
package/src/cli/cli.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
collectDependencyNames,
|
|
16
16
|
} from "../generate/index.ts";
|
|
17
17
|
import {
|
|
18
|
+
checkEngineCompatibility,
|
|
18
19
|
readTemplateMeta,
|
|
19
20
|
resolveTemplateRoot,
|
|
20
21
|
resolveVersions,
|
|
@@ -218,6 +219,16 @@ async function generate(input: {
|
|
|
218
219
|
if (resolved.size === 0 && names.length > 0) {
|
|
219
220
|
process.stderr.write(`${texts.warnVersionOffline}\n`);
|
|
220
221
|
}
|
|
222
|
+
// 版本判定在写盘之前:模板要求的引擎还没发布时,生成出来的工程装不上,
|
|
223
|
+
// 让它失败在「什么都没写」的时刻,而不是在别人的工程里第一次 pnpm dev 时。
|
|
224
|
+
const incompatible = checkEngineCompatibility(
|
|
225
|
+
input.meta.minEngineVersion,
|
|
226
|
+
resolved,
|
|
227
|
+
);
|
|
228
|
+
if (incompatible !== undefined) {
|
|
229
|
+
process.stderr.write(`${incompatible}\n`);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
221
232
|
|
|
222
233
|
process.stderr.write(`${texts.stageCopy}\n`);
|
|
223
234
|
let result;
|
package/src/generate/project.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface GenerateOptions {
|
|
|
47
47
|
/** 壳叠加层 id */
|
|
48
48
|
readonly shell: string;
|
|
49
49
|
/**
|
|
50
|
-
* 故事源形态叠加层 id;缺省 `
|
|
50
|
+
* 故事源形态叠加层 id;缺省 `ts`(TypeScript 源,编译成故事文件)。
|
|
51
51
|
*
|
|
52
52
|
* 可选是为了不破坏既有调用方——只关心框架与壳的代码不必为一个用不到的参数改签名。
|
|
53
53
|
*/
|
|
@@ -68,8 +68,15 @@ export interface GenerateResult {
|
|
|
68
68
|
readonly sources: string;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* 故事源形态的缺省 id:TypeScript 源(`src/Stories.src/` → 编译成故事文件)。
|
|
73
|
+
*
|
|
74
|
+
* 三种创作形态是**同一个架构的三条入口**,不是三档能力:手写 `.story`、`.json`、
|
|
75
|
+
* TS 源最终都落到资源根下的故事目录,引擎装载时只认那棵树的扫描结果。TS 只是
|
|
76
|
+
* 多一层「写起来更强」的创作面(类型补全、复用 TS 能力),编译产物与手写文件同构,
|
|
77
|
+
* 因此缺省取它不改变任何运行期语义——生成出来的工程开箱即是可扩展的那一种。
|
|
78
|
+
*/
|
|
79
|
+
export const DEFAULT_SOURCES = "ts";
|
|
73
80
|
|
|
74
81
|
/**
|
|
75
82
|
* 生成一个工程。
|
|
@@ -110,12 +117,12 @@ export function generateProject(options: GenerateOptions): GenerateResult {
|
|
|
110
117
|
* 依次列出要合并的目录:骨架在前,框架层次之,故事源形态层再次,壳层最后。
|
|
111
118
|
*
|
|
112
119
|
* 顺序有实际后果,不是随便排的:
|
|
113
|
-
* - 故事源形态层要晚于框架层——它替换 `src/stories.ts`,而各框架入口都从那里取表,
|
|
114
|
-
* 晚于框架层才能覆盖到「框架层是否也带了自己的 stories.ts」这类情况;
|
|
120
|
+
* - 故事源形态层要晚于框架层——它替换 `src/stories/index.ts`,而各框架入口都从那里取表,
|
|
121
|
+
* 晚于框架层才能覆盖到「框架层是否也带了自己的 stories/index.ts」这类情况;
|
|
115
122
|
* - 壳层最后——壳对工程形态有最终发言权(小程序壳会删掉浏览器宿主的 `src/host/`),
|
|
116
123
|
* 让它压过其余各层才不会出现「壳删了、下层又写回来」。
|
|
117
124
|
*
|
|
118
|
-
* 缺席的叠加层直接跳过——缺省项(`none` / `browser
|
|
125
|
+
* 缺席的叠加层直接跳过——缺省项(`none` / `browser`)本就没有目录,
|
|
119
126
|
* 这不是错误,只是「这一层什么都不加」。
|
|
120
127
|
*/
|
|
121
128
|
export function layerBases(options: GenerateOptions): string[] {
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ export {
|
|
|
25
25
|
} from "./overlay/index.ts";
|
|
26
26
|
export type { MergedLayer, PendingFile } from "./overlay/index.ts";
|
|
27
27
|
export {
|
|
28
|
+
checkEngineCompatibility,
|
|
29
|
+
ENGINE_SCOPE,
|
|
28
30
|
applyResolvedVersions,
|
|
29
31
|
fetchLatestVersion,
|
|
30
32
|
overlayDirOf,
|
|
@@ -40,8 +42,13 @@ export type {
|
|
|
40
42
|
} from "./registry/index.ts";
|
|
41
43
|
export {
|
|
42
44
|
caretRange,
|
|
45
|
+
compareSemver,
|
|
46
|
+
floorOfRange,
|
|
43
47
|
isValidProjectName,
|
|
48
|
+
meetsMinimum,
|
|
49
|
+
parseSemver,
|
|
44
50
|
suggestProjectName,
|
|
45
51
|
} from "./shared/index.ts";
|
|
52
|
+
export type { Semver } from "./shared/index.ts";
|
|
46
53
|
export { defaultCreateTexts } from "./shared/index.ts";
|
|
47
54
|
export type { CreateTexts } from "./shared/index.ts";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板与引擎的版本兼容判定。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:`template-meta.json` 的 `minEngineVersion` 曾是纯声明——没有任何代码读它,
|
|
5
|
+
* 于是「模板用了新引擎才有的 API、而生成的工程装的是旧引擎」这种组合可以一路生成成功,
|
|
6
|
+
* 直到使用者 `pnpm dev` 才炸在一条看不懂的报错上。判据放在生成期,是为了让失败发生在
|
|
7
|
+
* 「还没写盘」的时刻,而不是在别人的工程里。
|
|
8
|
+
*
|
|
9
|
+
* 判定只认 registry 上的**实际**版本:模板声明的区间再漂亮,装不到也是空话。查不到版本
|
|
10
|
+
* (离线)时不做判定——那时模板记录的区间就是将要安装的东西,模板自身的自洽性由
|
|
11
|
+
* `tests/template/template.test.ts` 的静态判据守。
|
|
12
|
+
*/
|
|
13
|
+
import { compareSemver } from "../shared/index.ts";
|
|
14
|
+
|
|
15
|
+
/** 引擎包的 scope:本仓发布的包都挂在这里 */
|
|
16
|
+
export const ENGINE_SCOPE = "@lingfan-engine/";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 判定「这次生成会装到的引擎版本」是否满足模板要求。
|
|
20
|
+
*
|
|
21
|
+
* 返回问题描述而不是抛错:措辞归调用方(CLI 与库对同一件事可以说得不一样),
|
|
22
|
+
* 这里只负责说清「谁、要多少、实际是多少」。
|
|
23
|
+
*
|
|
24
|
+
* `resolved` 为空表示一个版本都没查到(离线或 registry 不可达)——此时无从判定,
|
|
25
|
+
* 返回 undefined 放行,离线生成仍然可用。
|
|
26
|
+
*/
|
|
27
|
+
export function checkEngineCompatibility(
|
|
28
|
+
minEngineVersion: string,
|
|
29
|
+
resolved: ReadonlyMap<string, string>,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
if (resolved.size === 0) return undefined;
|
|
32
|
+
const problems: string[] = [];
|
|
33
|
+
for (const [name, version] of [...resolved].sort((a, b) =>
|
|
34
|
+
a[0] < b[0] ? -1 : 1,
|
|
35
|
+
)) {
|
|
36
|
+
if (!name.startsWith(ENGINE_SCOPE)) continue;
|
|
37
|
+
const order = compareSemver(version, minEngineVersion);
|
|
38
|
+
if (order === undefined) {
|
|
39
|
+
problems.push(`${name}@${version} 无法与 ${minEngineVersion} 比较`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (order < 0) problems.push(`${name}@${version}`);
|
|
43
|
+
}
|
|
44
|
+
if (problems.length === 0) return undefined;
|
|
45
|
+
return (
|
|
46
|
+
`模板要求引擎不低于 ${minEngineVersion},但 registry 上的最新版是 ${problems.join("、")}。` +
|
|
47
|
+
"引擎还没发布这个版本,生成的工程装不上模板需要的 API——先发引擎,或把模板的 minEngineVersion 降到已发布版本。"
|
|
48
|
+
);
|
|
49
|
+
}
|
package/src/registry/index.ts
CHANGED
package/src/shared/index.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
/** shared
|
|
1
|
+
/** shared 域唯一出口:文案表、参数校验与版本运算。 */
|
|
2
2
|
export { defaultCreateTexts, type CreateTexts } from "./texts.ts";
|
|
3
3
|
export {
|
|
4
4
|
caretRange,
|
|
5
5
|
isValidProjectName,
|
|
6
6
|
suggestProjectName,
|
|
7
7
|
} from "./guards.ts";
|
|
8
|
+
export {
|
|
9
|
+
compareSemver,
|
|
10
|
+
floorOfRange,
|
|
11
|
+
meetsMinimum,
|
|
12
|
+
parseSemver,
|
|
13
|
+
type Semver,
|
|
14
|
+
} from "./semver.ts";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语义化版本运算:解析、比较、取依赖区间的下界。
|
|
3
|
+
*
|
|
4
|
+
* 为什么自己写:生成器只用到这三件事,装 `semver` 会带进一整棵依赖树——而这个包
|
|
5
|
+
* 是随 `npm create` 下载的,为了比两个版本号先拉一棵树不可接受。
|
|
6
|
+
*
|
|
7
|
+
* 与 `scripts/release/version.mjs` 的边界:那是**发布工具链**的版本运算(递增、
|
|
8
|
+
* dist-tag、发布合法性判定),跑在维护者机器与 CI 上;它既不在本包的 `files` 里,
|
|
9
|
+
* 也不是 TypeScript,发布形态的生成器根本 import 不到。两边共用的是语义化版本规范
|
|
10
|
+
* 本身,不是同一份代码——改这里时请对照那一处,别让两套解读漂移。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 拆开的版本号;`prerelease` 缺席表示正式版 */
|
|
14
|
+
export interface Semver {
|
|
15
|
+
readonly major: number;
|
|
16
|
+
readonly minor: number;
|
|
17
|
+
readonly patch: number;
|
|
18
|
+
readonly prerelease?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 解析 `主.次.修订[-预发布]`。
|
|
23
|
+
*
|
|
24
|
+
* 不接受 `v` 前缀与构建元数据:前者是标签写法不是版本写法,后者 npm 发布时会裁掉,
|
|
25
|
+
* 让「模板里写的」与「registry 上的」对不上。非法输入返回 undefined,由调用方决定
|
|
26
|
+
* 报什么错——本模块不做措辞。
|
|
27
|
+
*/
|
|
28
|
+
export function parseSemver(text: string): Semver | undefined {
|
|
29
|
+
const matched =
|
|
30
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/.exec(
|
|
31
|
+
text.trim(),
|
|
32
|
+
);
|
|
33
|
+
if (matched === null) return undefined;
|
|
34
|
+
const prerelease = matched[4];
|
|
35
|
+
return {
|
|
36
|
+
major: Number(matched[1]),
|
|
37
|
+
minor: Number(matched[2]),
|
|
38
|
+
patch: Number(matched[3]),
|
|
39
|
+
...(prerelease === undefined ? {} : { prerelease }),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 比较两个版本:负数表示前者更小,无法解析时返回 undefined。
|
|
45
|
+
*
|
|
46
|
+
* 预发布按规范排:有预发布 < 无预发布;两者都有时逐段比,纯数字段按数值、
|
|
47
|
+
* 其余按码元序,数字段小于字母段(`0.3.0-rc.2 < 0.3.0-rc.10 < 0.3.0`)。
|
|
48
|
+
*/
|
|
49
|
+
export function compareSemver(left: string, right: string): number | undefined {
|
|
50
|
+
const a = parseSemver(left);
|
|
51
|
+
const b = parseSemver(right);
|
|
52
|
+
if (a === undefined || b === undefined) return undefined;
|
|
53
|
+
for (const key of ["major", "minor", "patch"] as const) {
|
|
54
|
+
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
55
|
+
}
|
|
56
|
+
if (a.prerelease === b.prerelease) return 0;
|
|
57
|
+
if (a.prerelease === undefined) return 1;
|
|
58
|
+
if (b.prerelease === undefined) return -1;
|
|
59
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 逐段比较预发布标识符 */
|
|
63
|
+
function comparePrerelease(left: string, right: string): number {
|
|
64
|
+
const a = left.split(".");
|
|
65
|
+
const b = right.split(".");
|
|
66
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
67
|
+
const one = a[index];
|
|
68
|
+
const other = b[index];
|
|
69
|
+
if (one === undefined) return -1;
|
|
70
|
+
if (other === undefined) return 1;
|
|
71
|
+
if (one === other) continue;
|
|
72
|
+
const oneIsNumber = /^\d+$/.test(one);
|
|
73
|
+
const otherIsNumber = /^\d+$/.test(other);
|
|
74
|
+
if (oneIsNumber && otherIsNumber) {
|
|
75
|
+
return Number(one) < Number(other) ? -1 : 1;
|
|
76
|
+
}
|
|
77
|
+
if (oneIsNumber) return -1;
|
|
78
|
+
if (otherIsNumber) return 1;
|
|
79
|
+
return one < other ? -1 : 1;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 取依赖区间能接受的**最低**版本;认不出的区间返回 undefined。
|
|
86
|
+
*
|
|
87
|
+
* 只服务一个用途:判断「模板声明的引擎区间」是否容得下模板要求的最低引擎版本。
|
|
88
|
+
* 区间下界是够用的判据——区间上界不影响「装得上最低要求」这件事。
|
|
89
|
+
*
|
|
90
|
+
* 认得的写法:裸版本(`0.3.0`)、`^`、`~`、`>=`、`>`、`=`、`*`(无下界)。
|
|
91
|
+
* 复合区间(`>=0.3.0 <0.5.0`)与 `||` 取第一段的下界——模板里不该出现它们,
|
|
92
|
+
* 真出现了也不该因为「太复杂」而静默放过。
|
|
93
|
+
*/
|
|
94
|
+
export function floorOfRange(range: string): string | undefined {
|
|
95
|
+
const trimmed = range.trim();
|
|
96
|
+
if (trimmed === "" || trimmed === "*" || trimmed === "x") return undefined;
|
|
97
|
+
const first = trimmed.split("||")[0]?.trim() ?? "";
|
|
98
|
+
const matched = /^(?:[\^~><=]*)\s*([0-9][0-9A-Za-z.+-]*)/.exec(first);
|
|
99
|
+
if (matched === null) return undefined;
|
|
100
|
+
return parseSemver(matched[1]) === undefined ? undefined : matched[1];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `version` 是否满足「不低于 `minimum`」。
|
|
105
|
+
*
|
|
106
|
+
* 任一版本号解析不出时返回 undefined:调用方要能把「判不了」与「不达标」分开处理,
|
|
107
|
+
* 前者是模板写错,后者是版本太旧。
|
|
108
|
+
*/
|
|
109
|
+
export function meetsMinimum(
|
|
110
|
+
version: string,
|
|
111
|
+
minimum: string,
|
|
112
|
+
): boolean | undefined {
|
|
113
|
+
const order = compareSemver(version, minimum);
|
|
114
|
+
return order === undefined ? undefined : order >= 0;
|
|
115
|
+
}
|
package/src/shared/texts.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const defaultCreateTexts = {
|
|
|
24
24
|
" -h, --help 显示本说明",
|
|
25
25
|
"",
|
|
26
26
|
"示例:",
|
|
27
|
-
" npm create @lingfan-engine my-story -- --framework
|
|
27
|
+
" npm create @lingfan-engine my-story -- --framework vue --shell browser",
|
|
28
28
|
" npm create @lingfan-engine my-story -- --sources ts",
|
|
29
29
|
].join("\n"),
|
|
30
30
|
/** 交互问答的提示语 */
|
package/template/README.md
CHANGED
|
@@ -9,26 +9,27 @@
|
|
|
9
9
|
|
|
10
10
|
### 覆盖范围 / 有意不做(边界表)
|
|
11
11
|
|
|
12
|
-
| 能力 | 模板 | 说明
|
|
13
|
-
| ----------------------------------------------- | ---------------------- |
|
|
14
|
-
| 对话层(打字机 + 内联标记) | 是 | `{p}/{w}` 停顿、样式标记零宽由 `@lingfan-engine/ui` 提供
|
|
15
|
-
| 等待态出口:`menu` / `input` / `wait` / `video` | 是 | 缺任何一个,含该命令的故事都会**永久停滞**
|
|
16
|
-
| 等待态:`minigame` | 是(可见 fail-closed) | 模板不接注册表 → 显示「未注册」并说明接入方式,**不伪造完成**
|
|
17
|
-
| 舞台元素层(36 类型 + 交互 + 12 个表现 op) | 是 | 含 `animate`/`transition`/`shake` 的帧驱动(不驱动会让动画队列只增不减)
|
|
18
|
-
| 层级(z 序,`shell.layers`) | 是 | 各层 z 由 `resolveLayerZ` 解析后内联写入
|
|
19
|
-
| 存档(`shell.saves.slots` 槽位数) | 是 |
|
|
20
|
-
| 存档缩略图(`shell.saves.thumbnail`) |
|
|
21
|
-
| 舞台适配(`shell.stage`) | 否 | 模板按固定视口布局;要接就取 `resolveStageConfig` 结果再算 `computeStageLayout`
|
|
22
|
-
| 游玩界面禁选文本 | 是 | `body` 上 `user-select: none`;要提供复制就给部件加 `data-lf-selectable`
|
|
23
|
-
| 历史面板与回溯 UI |
|
|
24
|
-
| 玩家偏好(音量/文字速度面板) |
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
12
|
+
| 能力 | 模板 | 说明 |
|
|
13
|
+
| ----------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------- |
|
|
14
|
+
| 对话层(打字机 + 内联标记) | 是 | `{p}/{w}` 停顿、样式标记零宽由 `@lingfan-engine/ui` 提供 |
|
|
15
|
+
| 等待态出口:`menu` / `input` / `wait` / `video` | 是 | 缺任何一个,含该命令的故事都会**永久停滞** |
|
|
16
|
+
| 等待态:`minigame` | 是(可见 fail-closed) | 模板不接注册表 → 显示「未注册」并说明接入方式,**不伪造完成** |
|
|
17
|
+
| 舞台元素层(36 类型 + 交互 + 12 个表现 op) | 是 | 含 `animate`/`transition`/`shake` 的帧驱动(不驱动会让动画队列只增不减) |
|
|
18
|
+
| 层级(z 序,`shell.layers`) | 是 | 各层 z 由 `resolveLayerZ` 解析后内联写入 |
|
|
19
|
+
| 存档(`shell.saves.slots` 槽位数) | 是 | 存/读为命令面;缩略图见下行 |
|
|
20
|
+
| 存档缩略图(`shell.saves.thumbnail`) | 是 | `src/host/thumbnails.ts` 合成卡;尺寸/质量/是否带文本按工程配置,合成失败即无图存 |
|
|
21
|
+
| 舞台适配(`shell.stage`) | 否 | 模板按固定视口布局;要接就取 `resolveStageConfig` 结果再算 `computeStageLayout` |
|
|
22
|
+
| 游玩界面禁选文本 | 是 | `body` 上 `user-select: none`;要提供复制就给部件加 `data-lf-selectable` |
|
|
23
|
+
| 历史面板与回溯 UI | 是 | `src/host/panels/history.ts`,槽位 `#history` 可选;点一条即回到那个检查点 |
|
|
24
|
+
| 玩家偏好(音量/文字速度面板) | 是 | `src/host/panels/preferences.ts`,槽位 `#prefs` 可选;未接线 = 音量为 op 值 |
|
|
25
|
+
| 界面列(`type=ui`)的渲染 | 是 | 按 op 渲染即开箱可跑;常驻界面可加 `src/ui/` 挂载(挂载层已接,生成物为空表) |
|
|
26
|
+
| I18N 供给(`Lang/{lang}/` 覆盖) | 否 | 未接 `i18nPort` = 原文直出(引擎契约缺省语义) |
|
|
27
|
+
| 对话框模板注册(`say template=`) | 否 | 未接 = 内建默认骨架 |
|
|
28
|
+
| 屏幕方向锁定(`shell.orientation`) | 不适用 | **纯 Web 形态没有方向 API**;该配置只在 Tauri 壳(playground)生效 |
|
|
29
|
+
| 资源加密 / 热重载 / 宿主信息 | 不适用 | 都是 Tauri 壳能力,见 playground 的组合根 |
|
|
29
30
|
|
|
30
31
|
> 「否」行均指**模板宿主未接**,不代表引擎没有该能力——引擎能力面以 `apps/playground` 为准
|
|
31
|
-
>
|
|
32
|
+
> (I18N / 对话框模板在引擎侧均已具备,仅模板未接线)。
|
|
32
33
|
|
|
33
34
|
> 想升级为全功能实现,看引擎仓库的 `apps/playground`(它同时是模板的第一个实例):
|
|
34
35
|
> 把 `src/main.ts` 换成 Vue/React/原生任意一种都行——**引擎与适配器零改动**。
|
|
@@ -104,11 +105,19 @@ __PROJECT__/
|
|
|
104
105
|
├── package.json
|
|
105
106
|
├── pnpm-workspace.yaml # 只声明 allowBuilds:pnpm 12 起「未批准的构建脚本」是硬错误
|
|
106
107
|
├── tsconfig.json # 浏览器侧:src/ 下的 ts 与 vue
|
|
107
|
-
├── tsconfig.node.json # Node
|
|
108
|
+
├── tsconfig.node.json # Node 侧:vite.config.ts 与 vite.plugins.ts(要 node:fs 与 @types/node)
|
|
108
109
|
├── vite.config.ts # publicDir: "Resources" ← 资源根即静态根
|
|
109
|
-
├──
|
|
110
|
+
├── vite.plugins.ts # 构建期扫描:故事清单与字体清单(虚拟模块,层只换插件数组)
|
|
111
|
+
├── index.html # RenderTargets 落地:stage/dialogue/choices/notifications/toolbar(+ 可选 #prefs / #mounts)
|
|
110
112
|
├── src/main.ts # 组合根:装配适配器 → 组装工程 → 建引擎 → 订阅渲染 + 帧驱动
|
|
111
|
-
├── src/stories
|
|
113
|
+
├── src/stories/ # 故事清单出口(转发 virtual:story-list;清单本身来自磁盘扫描)
|
|
114
|
+
├── src/mounts.ts # 挂载表出口 + mountStore(框架形态从这里订阅挂载值)
|
|
115
|
+
├── src/controls/ # 宿主控制面出口:挂载 UI 拿 pause/navigate/save/load 的地方
|
|
116
|
+
├── src/gen/ # 构建生成物:清单、字体、守卫表、UI 注册表(手改会被覆盖)
|
|
117
|
+
├── src/ui/ # 可挂载界面(`<名>/<名>.<扩展名>`,可缺省:没有就不生成注册表)
|
|
118
|
+
├── src/host/ # 宿主:装配、订阅、帧循环、输入映射、挂载层与面板(不认识 UI 框架)
|
|
119
|
+
│ ├── mount/ # UI 挂载层(原生形态):差量渲染器 + 工厂表契约
|
|
120
|
+
│ └── panels/ # 玩家偏好面板(音量、静音、文字速度)
|
|
112
121
|
├── src/styles/stage.css # 页面与层样式(换框架不动这份:叠加层共用同一张表)
|
|
113
122
|
└── Resources/ # 故事工程根(自包含、可整体搬运)
|
|
114
123
|
├── project.json # 工程清单:id/name/entry/lang/formatVersion
|
|
@@ -123,6 +132,35 @@ __PROJECT__/
|
|
|
123
132
|
|
|
124
133
|
故事里的资源路径**相对 `Resources/`**:`bgm "Audio/main.mp3"`、`video "Video/op.mp4"`。
|
|
125
134
|
|
|
135
|
+
## 界面列:按 op 渲染,或换一套视觉
|
|
136
|
+
|
|
137
|
+
`type=ui` 的列不进历史、不进存档、不建检查点——它是界面,不是剧情。引擎对这类列的认知只有这一条
|
|
138
|
+
(判据看列的类型是不是 `game`),它不知道「页面」是什么,也不管界面长什么样。
|
|
139
|
+
|
|
140
|
+
所以开箱就是按 op 渲染:`say` 出一句、`menu` 出一组选项,和剧情列走同一条渲染路径。
|
|
141
|
+
|
|
142
|
+
想换一套视觉,有两条互相独立的路:
|
|
143
|
+
|
|
144
|
+
- **对局内界面**——常驻在舞台上、跟着故事显隐的那类(状态栏、好感度条、地图角标)。组件放
|
|
145
|
+
`src/ui/<名>/<名>.<扩展名>`,故事源一行 `import` 拿到句柄,再用 `mount(名字, true/false)` 控显隐。
|
|
146
|
+
宿主不用改:挂载层已接好,组件表由构建期扫描产出,宿主只经 `src/mounts.ts` 取。详见文档站的
|
|
147
|
+
[挂载 UI 组件](https://mofeng-02.github.io/LingFan.Engine/guides/ui-mount/)。
|
|
148
|
+
- **普通页面**——登录、大厅、存档选择、设置这类整屏界面。它们属宿主应用层,与引擎同级,引擎完全不参与:
|
|
149
|
+
落 `src/pages/` 或任何你觉得合适的宿主目录,切进切出由宿主自己用 `v-if` / 路由决定。判据只有一条——
|
|
150
|
+
引擎当前停在哪一列(`createStateReader(engine).currentColumnId()`,空串 = 还没开始跑)。详见文档站的
|
|
151
|
+
[普通页面与对局内界面](https://mofeng-02.github.io/LingFan.Engine/guides/pages/)。
|
|
152
|
+
|
|
153
|
+
宿主给 `ui` 列画界面时守两条:
|
|
154
|
+
|
|
155
|
+
- **读走 `StateReader`,写走命令面**(`navigate` / `choose` / `input` / `save` / `load`)。界面不碰状态键,
|
|
156
|
+
也不自己造存档格式——绕过去就等于绕过了保留键守卫与快照一致性。
|
|
157
|
+
- **切走即真销毁**。换界面是完整替换:Vue 调 `app.unmount()`、React 调 `root.unmount()`、
|
|
158
|
+
原生清空容器并解绑监听器。**卸载逻辑归宿主写,引擎不管也不检测。**
|
|
159
|
+
|
|
160
|
+
`#prefs` 与 `#mounts` 都是**可选槽位**:不给就是这一层不接,故事照常跑、`mount` 命令只改状态不出现组件,
|
|
161
|
+
都不报错。`#prefs` 接偏好面板(`src/host/panels/preferences.ts`),`#mounts` 接 UI 挂载层
|
|
162
|
+
(`src/host/mount/`)。
|
|
163
|
+
|
|
126
164
|
## 打包分发
|
|
127
165
|
|
|
128
166
|
```
|
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
// 资源路径相对项目资源根 Resources/:如 bgm "Audio/main.mp3"
|
|
3
3
|
// 一个文件里可以写多列(label / scene);拆文件时把新文件加进 src/main.ts 的 STORIES 列表。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// 标题界面:`type=ui` 的列不进历史、不进存档、不建检查点——它是界面,不是剧情。
|
|
6
|
+
// 引擎对这类列只管一件事:按 op 渲染(say 出一句、menu 出一组选项),所以下面几条开箱即跑,
|
|
7
|
+
// 视图上就是标题界面本身。想换成自己的视觉有两条路:常驻舞台的组件放 src/ui/ 并用 mount 控显隐;
|
|
8
|
+
// 整屏的标题页/设置页属宿主应用,落 src/pages/ 一类宿主目录、由宿主自己切进切出。
|
|
9
|
+
scene title_main type=ui
|
|
6
10
|
say "欢迎来到 __PROJECT__。"
|
|
7
|
-
say "在 Resources/Stories/ 下继续写你的故事;媒体放 Resources/
|
|
11
|
+
say "在 Resources/Stories/ 下继续写你的故事;媒体放 Resources/Audio、Resources/Images、Resources/Video。"
|
|
12
|
+
menu "接下来去哪里?"
|
|
13
|
+
"开始游戏" -> chapter1
|
|
14
|
+
"看舞台元素" -> stage_demo
|
|
15
|
+
|
|
16
|
+
// 游戏正文第一列:进入这里才开始有历史与存档。
|
|
17
|
+
scene chapter1
|
|
8
18
|
input "先问一句:你叫什么名字?" store="player.name"
|
|
9
19
|
say "你好,{player.name}!"
|
|
20
|
+
say "这一列是 game 列:从这里开始,每一步都可回溯、可存档。"
|
|
10
21
|
menu "接下来去哪里?"
|
|
11
22
|
"看舞台元素" -> stage_demo
|
|
12
|
-
"
|
|
23
|
+
"回标题界面" -> title_main
|
|
13
24
|
|
|
14
25
|
// scene 列 = 声明式空间层:进入本列时整体装载,切换列时清空
|
|
15
26
|
scene stage_demo
|
|
@@ -21,4 +32,4 @@ scene stage_demo
|
|
|
21
32
|
window hide
|
|
22
33
|
say "(对话框已隐藏——点击继续)"
|
|
23
34
|
window show
|
|
24
|
-
say "对话框回来了。层级、存档槽位等工程配置见 README 的边界表。"
|
|
35
|
+
say "对话框回来了。层级、存档槽位等工程配置见 README 的边界表。"
|
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
<button id="save">存</button>
|
|
27
27
|
<button id="load">读</button>
|
|
28
28
|
</div>
|
|
29
|
+
<!-- 历史面板槽位:可选节点(缺了游戏照跑,只是回看已走过的对话要另想办法) -->
|
|
30
|
+
<div id="history"></div>
|
|
31
|
+
<!-- 偏好面板槽位:可选节点(缺了游戏照跑,只是没有音量/字速面板) -->
|
|
32
|
+
<div id="prefs"></div>
|
|
33
|
+
<!-- UI 挂载层:`mount` 命令显示/隐藏的组件挂在这里(可选节点,缺了只是没有 UI)。
|
|
34
|
+
位置在对话与工具条之后、转场遮罩之前:UI 盖住对话层,但盖不住转场与错误条。 -->
|
|
35
|
+
<div id="mounts"></div>
|
|
29
36
|
<div id="transition"></div>
|
|
30
37
|
<div id="error"></div>
|
|
31
38
|
</div>
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@lingfan-engine/adapters": "^0.
|
|
14
|
-
"@lingfan-engine/core": "^0.
|
|
15
|
-
"@lingfan-engine/
|
|
13
|
+
"@lingfan-engine/adapters": "^0.3.0",
|
|
14
|
+
"@lingfan-engine/core": "^0.3.0",
|
|
15
|
+
"@lingfan-engine/shell": "^0.3.0",
|
|
16
|
+
"@lingfan-engine/stories-build": "^0.3.0",
|
|
17
|
+
"@lingfan-engine/ui": "^0.3.0"
|
|
16
18
|
},
|
|
17
19
|
"devDependencies": {
|
|
18
20
|
"@types/node": "^26.6.3",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主控制面的宿主侧副本(框架无关)。
|
|
3
|
+
*
|
|
4
|
+
* `bootHost` 造出控制面后经 `onControls` 交给组合根,组合根放进这里;框架形态的挂载
|
|
5
|
+
* 组件订阅它拿最新句柄。与 `mountStore` 同处一层但装的东西不同:那边是故事给的值
|
|
6
|
+
* (JSON 安全、随存档),这边是宿主给的操作句柄(含函数、不进快照)。
|
|
7
|
+
*
|
|
8
|
+
* 复用同一个单元格实现而不是另写一份订阅逻辑:形状一样(一个当前值 + 订阅),
|
|
9
|
+
* 差别只在取出来时的类型,用一层薄转写把 `unknown` 收回来即可。
|
|
10
|
+
*/
|
|
11
|
+
import { createMountStore } from "@lingfan-engine/ui";
|
|
12
|
+
import type { HostControls } from "@lingfan-engine/core";
|
|
13
|
+
|
|
14
|
+
const cell = createMountStore();
|
|
15
|
+
|
|
16
|
+
/** 控制面单元格:`snapshot` 取当下、`subscribe` 追随变化、`set` 在组合根里喂 */
|
|
17
|
+
export const controlsStore = {
|
|
18
|
+
/** 当下的控制面;宿主尚未交付时为 `null` */
|
|
19
|
+
snapshot(): HostControls | null {
|
|
20
|
+
return (cell.snapshot() ?? null) as HostControls | null;
|
|
21
|
+
},
|
|
22
|
+
/** 订阅变化(订阅时回放当下值由调用方自己先取一次 `snapshot`) */
|
|
23
|
+
subscribe(listener: (controls: HostControls | null) => void): () => void {
|
|
24
|
+
return cell.subscribe((raw) => {
|
|
25
|
+
listener((raw ?? null) as HostControls | null);
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
/** 交付控制面(组合根在 `onControls` 回调里调一次) */
|
|
29
|
+
set(controls: HostControls): void {
|
|
30
|
+
cell.set(controls);
|
|
31
|
+
},
|
|
32
|
+
};
|