@lingfan-engine/editor 0.1.5 → 0.3.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/package.json +2 -2
- package/src/chapters/build.ts +3 -5
- package/src/chapters/index.ts +1 -0
- package/src/chapters/types.ts +2 -3
- package/src/contracts/forms.ts +2 -1
- package/src/contracts/texts.ts +4 -4
- package/src/editing/storyOps.ts +3 -3
- package/src/i18n/surfaces.ts +7 -6
- package/src/index.ts +15 -0
- package/src/schema/catalog.ts +30 -0
- package/src/schema/opSchemas/commands.ts +61 -0
- package/src/schema/opSchemas/map.ts +14 -0
- package/src/script/expr/audit/index.ts +1 -1
- package/src/script/expr/audit/parser.ts +1 -1
- package/src/script/index.ts +24 -1
- package/src/script/vocabulary-gen.ts +1 -0
- package/src/script/vocabulary.g.ts +16 -0
- package/src/script/words/audio.ts +119 -0
- package/src/script/words/cells.ts +2 -2
- package/src/script/words/composite.ts +4 -4
- package/src/script/words/index.ts +2 -1
- package/src/script/words/narrative.ts +45 -3
- package/src/script/words/ui.ts +64 -0
- package/src/shared/guards.ts +8 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingfan-engine/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "灵泛叙事作者层:op schema 单一事实源 + 词函数子语言 + 编辑期诊断 + 故事树纯映射器——只读写故事 JSON,不碰运行时与平台",
|
|
6
6
|
"exports": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"sideEffects": false,
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@lingfan-engine/core": "^0.
|
|
16
|
+
"@lingfan-engine/core": "^0.3.0",
|
|
17
17
|
"zod": "^4.6.5"
|
|
18
18
|
},
|
|
19
19
|
"license": "Apache-2.0",
|
package/src/chapters/build.ts
CHANGED
|
@@ -65,15 +65,13 @@ export function chapterGroupOf(
|
|
|
65
65
|
/**
|
|
66
66
|
* 场景类型的**徽标文案**(单一事实源:列侧栏 / 章节树共用,组件不得自写映射)。
|
|
67
67
|
*
|
|
68
|
-
* - `"
|
|
68
|
+
* - `"ui"` → 「界面」
|
|
69
69
|
* - 缺省(`undefined`)与 `"game"` → `null`(**game 是常态不标**——满屏「剧情」徽标是噪音)
|
|
70
70
|
*/
|
|
71
71
|
export function sceneTypeBadgeOf(
|
|
72
72
|
type: StoryColumn["type"],
|
|
73
|
-
): "
|
|
74
|
-
|
|
75
|
-
if (type === "ui") return "界面";
|
|
76
|
-
return null;
|
|
73
|
+
): "界面" | null {
|
|
74
|
+
return type === "ui" ? "界面" : null;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
/** 命令条数(flow 看 commands、scene 看 entry;缺省 0) */
|
package/src/chapters/index.ts
CHANGED
package/src/chapters/types.ts
CHANGED
|
@@ -21,9 +21,8 @@ export interface ChapterNode {
|
|
|
21
21
|
/** 该章节的场景所属分组(由 `type` 决定,**不按目录猜**) */
|
|
22
22
|
readonly group: ChapterGroup;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* 分组判定只吃 `group
|
|
26
|
-
* (`group=ui` 只说明「不可回溯」,不说明是哪一种)。
|
|
24
|
+
* 归约后的场景类型(契约值;`undefined` = 缺省 game)。
|
|
25
|
+
* 分组判定只吃 `group`——`group=ui` 只说明「不可回溯」,不说明作者原本写的是哪一种。
|
|
27
26
|
*/
|
|
28
27
|
readonly type: StoryColumn["type"];
|
|
29
28
|
/** 章节在文件树中的位置(逻辑路径,用于联动资源树选中) */
|
package/src/contracts/forms.ts
CHANGED
package/src/contracts/texts.ts
CHANGED
|
@@ -106,13 +106,13 @@ export interface EditorTexts {
|
|
|
106
106
|
invalidVarHandlePath(prop: string, prefix: string): string;
|
|
107
107
|
/** script/expr:未注册的变量。 */
|
|
108
108
|
unregisteredVariable(key: string): string;
|
|
109
|
-
/** script/expr
|
|
109
|
+
/** script/expr 检查:操作数类型不匹配。 */
|
|
110
110
|
operandKindMismatch(op: string, side: string, want: string, described: string, note: string): string;
|
|
111
|
-
/** script/expr
|
|
111
|
+
/** script/expr 检查:三元条件必须为 boolean。 */
|
|
112
112
|
ternaryCondNotBool(described: string): string;
|
|
113
|
-
/** script/expr
|
|
113
|
+
/** script/expr 检查:不支持跨类型比较。 */
|
|
114
114
|
crossTypeComparison(op: string, left: string, right: string): string;
|
|
115
|
-
/** script/expr
|
|
115
|
+
/** script/expr 检查:未知类型的操作数标签。 */
|
|
116
116
|
unknownOperandKind(): string;
|
|
117
117
|
|
|
118
118
|
/** schema/surface:扩展 op 归组不合法。 */
|
package/src/editing/storyOps.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* ——「列名即标签」,重命名必须保持全部列的引用图一致。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Story } from "@lingfan-engine/core";
|
|
8
|
+
import type { SceneType, Story } from "@lingfan-engine/core";
|
|
9
9
|
import { isSafeFileNameSegment } from "@lingfan-engine/core";
|
|
10
10
|
import {
|
|
11
11
|
getAtPointer,
|
|
@@ -77,11 +77,11 @@ export function addColumn(
|
|
|
77
77
|
id?: string;
|
|
78
78
|
kind?: "scene" | "flow";
|
|
79
79
|
/**
|
|
80
|
-
* 运行语义轴(与 `kind`
|
|
80
|
+
* 运行语义轴(与 `kind` 正交):缺省 = game,非缺省只可能是 ui(界面层)。
|
|
81
81
|
* **缺省与 game 都不写字段**——默认值不显式存储(与写回保真同一口径:
|
|
82
82
|
* 新建列的内存形态必须与重开后的解析形态深等)。
|
|
83
83
|
*/
|
|
84
|
-
type?:
|
|
84
|
+
type?: SceneType;
|
|
85
85
|
/** 语义化建议(仅 id 缺省时参与;见 suggestColumnId) */
|
|
86
86
|
hint?: string | null;
|
|
87
87
|
} = {},
|
package/src/i18n/surfaces.ts
CHANGED
|
@@ -9,16 +9,17 @@
|
|
|
9
9
|
* 路径语法:`a.b` 逐段取值;`a[].b` 的 `[]` = 对数组逐项迭代(`options[].text`)。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
/** 可翻译原文面(运行期 Translate 挂接:say 文本+说话人 / menu prompt+选项 / input prompt / notify 文本。character.screen 见下——仅入编辑器比对面,运行期不翻) */
|
|
12
|
+
/** 可翻译原文面(运行期 Translate 挂接:say 文本+说话人 / menu prompt+选项 / input prompt+候选文本 / notify 文本。character.screen 见下——仅入编辑器比对面,运行期不翻) */
|
|
13
13
|
export const TRANSLATE_SURFACES: Readonly<Record<string, readonly string[]>> = {
|
|
14
14
|
say: ["text", "speaker"],
|
|
15
15
|
menu: ["prompt", "options[].text"],
|
|
16
|
-
|
|
16
|
+
// 候选只翻显示文本;`value` 是要写进变量的原样值,**不进翻译面**(否则多语言下故事逻辑随语言漂移)
|
|
17
|
+
input: ["prompt", "options[].text"],
|
|
17
18
|
notify: ["text"],
|
|
18
|
-
// character.screen
|
|
19
|
-
// translate
|
|
20
|
-
// ops/
|
|
21
|
-
//
|
|
19
|
+
// character.screen 仅入编辑器比对面(原文收集/键抽取),运行期**不翻**:引擎的
|
|
20
|
+
// translate 调用点分布在 runtime/i18n/translate.ts、ops/dialog.ts、ops/interaction.ts
|
|
21
|
+
// 与 ops/media/notify.ts,均不含 screen(说话人显示名解析产物走 say 的 Translate
|
|
22
|
+
// 挂接,查表见 runtime/ops/dialog.ts execSay;docs concepts/i18n.md 同口径)。
|
|
22
23
|
character: ["screen"],
|
|
23
24
|
};
|
|
24
25
|
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,11 @@ export type {
|
|
|
63
63
|
} from "./schema/index.ts";
|
|
64
64
|
/** 词汇层构建期轻类型校验警告(expr/cond 组装时按引擎类型规则产出;drain 取走) */
|
|
65
65
|
export type { ExpressionWarning } from "./script/index.ts";
|
|
66
|
+
/**
|
|
67
|
+
* 挂载 UI 句柄的形状:故事源 `import hub from "…/ui/hub/hub.vue"` 拿到的就是这个。
|
|
68
|
+
* 经包根再导出一次,`stories-build/authoring` 的 ambient 声明才能只 import type 转发。
|
|
69
|
+
*/
|
|
70
|
+
export type { UiHandle } from "./script/index.ts";
|
|
66
71
|
export {
|
|
67
72
|
BUILTIN_OP_SURFACE,
|
|
68
73
|
mergeOpMeta,
|
|
@@ -242,6 +247,8 @@ export {
|
|
|
242
247
|
type ColumnNameRegistry,
|
|
243
248
|
type GuardNameRegistry,
|
|
244
249
|
type InterpolationKeyRegistry,
|
|
250
|
+
type KnownUiName,
|
|
251
|
+
type UiNameRegistry,
|
|
245
252
|
} from "./script/index.ts";
|
|
246
253
|
|
|
247
254
|
/**
|
|
@@ -291,6 +298,7 @@ export {
|
|
|
291
298
|
hide,
|
|
292
299
|
imageElement,
|
|
293
300
|
input,
|
|
301
|
+
inputCandidate,
|
|
294
302
|
interaction,
|
|
295
303
|
jump,
|
|
296
304
|
letVar,
|
|
@@ -298,6 +306,7 @@ export {
|
|
|
298
306
|
localVar,
|
|
299
307
|
menu,
|
|
300
308
|
minigame,
|
|
309
|
+
mount,
|
|
301
310
|
navigate,
|
|
302
311
|
newArray,
|
|
303
312
|
notify,
|
|
@@ -314,6 +323,11 @@ export {
|
|
|
314
323
|
say,
|
|
315
324
|
sceneSetup,
|
|
316
325
|
sceneType,
|
|
326
|
+
audioPlay,
|
|
327
|
+
audioStop,
|
|
328
|
+
audioVolume,
|
|
329
|
+
audioPause,
|
|
330
|
+
audioResume,
|
|
317
331
|
se,
|
|
318
332
|
seekVideo,
|
|
319
333
|
set,
|
|
@@ -323,6 +337,7 @@ export {
|
|
|
323
337
|
staticFor,
|
|
324
338
|
stopAmbient,
|
|
325
339
|
stopBgm,
|
|
340
|
+
stopSe,
|
|
326
341
|
stopVideo,
|
|
327
342
|
stopVoice,
|
|
328
343
|
style,
|
package/src/schema/catalog.ts
CHANGED
|
@@ -22,6 +22,7 @@ export const OP_GROUP_ORDER: readonly OpGroup[] = [
|
|
|
22
22
|
"audio",
|
|
23
23
|
"video",
|
|
24
24
|
"minigame",
|
|
25
|
+
"ui",
|
|
25
26
|
];
|
|
26
27
|
|
|
27
28
|
/** op 分组中文标签(单一事实源) */
|
|
@@ -34,6 +35,7 @@ export const OP_GROUP_LABELS: Readonly<Record<OpGroup, string>> = {
|
|
|
34
35
|
audio: "音频",
|
|
35
36
|
video: "视频",
|
|
36
37
|
minigame: "小游戏",
|
|
38
|
+
ui: "UI",
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
export const OP_META: readonly OpMeta[] = [
|
|
@@ -82,6 +84,12 @@ export const OP_META: readonly OpMeta[] = [
|
|
|
82
84
|
{ op: "stop_ambient", label: "停止环境音", group: "audio" },
|
|
83
85
|
{ op: "voice", label: "语音", group: "audio" },
|
|
84
86
|
{ op: "stop_voice", label: "停止语音", group: "audio" },
|
|
87
|
+
{ op: "stop_se", label: "停止音效", group: "audio" },
|
|
88
|
+
{ op: "audio_play", label: "起播音频实例", group: "audio" },
|
|
89
|
+
{ op: "audio_stop", label: "停止音频实例", group: "audio" },
|
|
90
|
+
{ op: "audio_volume", label: "调音频实例音量", group: "audio" },
|
|
91
|
+
{ op: "audio_pause", label: "暂停音频通道", group: "audio" },
|
|
92
|
+
{ op: "audio_resume", label: "恢复音频通道", group: "audio" },
|
|
85
93
|
{ op: "video", label: "视频", group: "video" },
|
|
86
94
|
{ op: "cutscene", label: "过场", group: "video" },
|
|
87
95
|
{ op: "seek_video", label: "视频跳转", group: "video" },
|
|
@@ -104,6 +112,8 @@ export const OP_META: readonly OpMeta[] = [
|
|
|
104
112
|
{ op: "transition", label: "转场", group: "presentation" },
|
|
105
113
|
{ op: "shake", label: "屏幕震动", group: "presentation" },
|
|
106
114
|
{ op: "text_typewriter", label: "打字机设置", group: "presentation" },
|
|
115
|
+
// 挂载 UI
|
|
116
|
+
{ op: "mount", label: "挂载 UI 显隐", group: "ui" },
|
|
107
117
|
];
|
|
108
118
|
|
|
109
119
|
interface FieldMeta {
|
|
@@ -136,6 +146,9 @@ export const FIELD_META: Readonly<Record<string, FieldMeta>> = {
|
|
|
136
146
|
"menu.z": { label: "层级(实例)", kind: "number" },
|
|
137
147
|
"input.prompt": { label: "提示语", kind: "text" },
|
|
138
148
|
"input.store": { label: "存入变量", kind: "identifier" },
|
|
149
|
+
"input.options": { label: "候选值", kind: "array" },
|
|
150
|
+
"input.options[].text": { label: "候选文本", kind: "text" },
|
|
151
|
+
"input.options[].value": { label: "填入值", kind: "string" },
|
|
139
152
|
"input.z": { label: "层级(实例)", kind: "number" },
|
|
140
153
|
"notify.text": { label: "文本", kind: "text" },
|
|
141
154
|
"notify.type": { label: "类型", kind: "string" },
|
|
@@ -234,6 +247,20 @@ export const FIELD_META: Readonly<Record<string, FieldMeta>> = {
|
|
|
234
247
|
"voice.auto_stop": { label: "推进即停", kind: "boolean" },
|
|
235
248
|
"voice.restart": { label: "回起点重播", kind: "boolean" },
|
|
236
249
|
"stop_voice.fade": { label: "淡出(毫秒)", kind: "number" },
|
|
250
|
+
"stop_se.fade": { label: "淡出(毫秒)", kind: "number" },
|
|
251
|
+
"audio_play.inst": { label: "实例 id", kind: "text" },
|
|
252
|
+
"audio_play.channel": { label: "归属通道", kind: "text" },
|
|
253
|
+
"audio_play.resource": { label: "资源", kind: "text" },
|
|
254
|
+
"audio_play.volume": { label: "音量(0–1)", kind: "number" },
|
|
255
|
+
"audio_play.loop": { label: "循环", kind: "boolean" },
|
|
256
|
+
"audio_play.fade": { label: "淡入(毫秒)", kind: "number" },
|
|
257
|
+
"audio_play.restart": { label: "重新起播", kind: "boolean" },
|
|
258
|
+
"audio_stop.inst": { label: "实例 id(留空 = 全部)", kind: "text" },
|
|
259
|
+
"audio_stop.fade": { label: "淡出(毫秒)", kind: "number" },
|
|
260
|
+
"audio_volume.inst": { label: "实例 id", kind: "text" },
|
|
261
|
+
"audio_volume.volume": { label: "音量(0–1)", kind: "number" },
|
|
262
|
+
"audio_pause.channel": { label: "通道(留空 = 全部常驻)", kind: "text" },
|
|
263
|
+
"audio_resume.channel": { label: "通道(留空 = 全部常驻)", kind: "text" },
|
|
237
264
|
"video.resource": { label: "资源", kind: "resource" },
|
|
238
265
|
"video.volume": { label: "音量", kind: "number" },
|
|
239
266
|
"video.loop": { label: "循环", kind: "boolean" },
|
|
@@ -292,6 +319,9 @@ export const FIELD_META: Readonly<Record<string, FieldMeta>> = {
|
|
|
292
319
|
"shake.duration": { label: "时长(秒)", kind: "number" },
|
|
293
320
|
"text_typewriter.enabled": { label: "启用", kind: "boolean" },
|
|
294
321
|
"text_typewriter.speed": { label: "速度(字符/秒)", kind: "number" },
|
|
322
|
+
"mount.name": { label: "UI 名", kind: "text" },
|
|
323
|
+
"mount.visible": { label: "挂载", kind: "boolean" },
|
|
324
|
+
"mount.props": { label: "入参", kind: "object" },
|
|
295
325
|
};
|
|
296
326
|
|
|
297
327
|
/** 文本插值面(诊断扫描用):text kind 字段集合的查询入口 */
|
|
@@ -56,6 +56,10 @@ const menuSchema = z.strictObject({
|
|
|
56
56
|
const inputSchema = z.strictObject({
|
|
57
57
|
prompt: NonEmpty,
|
|
58
58
|
store: NonEmpty,
|
|
59
|
+
// 候选快捷值:显示文本 + 要填进 store 的值(与 menu 的 {text,target} 刻意不同名)
|
|
60
|
+
options: z
|
|
61
|
+
.array(z.strictObject({ text: NonEmpty, value: NonEmpty }))
|
|
62
|
+
.optional(),
|
|
59
63
|
z: InstanceZ, // choices 层实例 z
|
|
60
64
|
});
|
|
61
65
|
|
|
@@ -227,6 +231,42 @@ const voiceSchema = z.strictObject({
|
|
|
227
231
|
});
|
|
228
232
|
const stopSchema = z.strictObject({ fade: Fade.optional() });
|
|
229
233
|
|
|
234
|
+
/** 音频实例 id:与扩展 / 玩法系统同一条短 id 形态(引擎侧是唯一声明处,此处只校验形态) */
|
|
235
|
+
const AudioInstanceId = z
|
|
236
|
+
.string()
|
|
237
|
+
.regex(/^[a-z][a-z0-9_-]{0,31}$/, "须为小写字母开头的短 id(1..32 位)");
|
|
238
|
+
|
|
239
|
+
const audioChannelSchema = z.enum(["bgm", "se", "ambient", "voice"]);
|
|
240
|
+
|
|
241
|
+
const audioPlaySchema = z.strictObject({
|
|
242
|
+
inst: AudioInstanceId,
|
|
243
|
+
channel: audioChannelSchema,
|
|
244
|
+
resource: NonEmpty,
|
|
245
|
+
volume: Finite.optional(),
|
|
246
|
+
loop: z.boolean().optional(),
|
|
247
|
+
fade: Fade.optional(),
|
|
248
|
+
restart: z.boolean().optional(),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const audioStopSchema = z.strictObject({
|
|
252
|
+
inst: AudioInstanceId.optional(),
|
|
253
|
+
fade: Fade.optional(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const audioVolumeSchema = z.strictObject({
|
|
257
|
+
inst: AudioInstanceId,
|
|
258
|
+
volume: Finite,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// channel 省略 = 全部常驻通道(se 除外),故是 optional 而非缺省成某个通道
|
|
262
|
+
const audioPauseSchema = z.strictObject({
|
|
263
|
+
channel: audioChannelSchema.optional(),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const audioResumeSchema = z.strictObject({
|
|
267
|
+
channel: audioChannelSchema.optional(),
|
|
268
|
+
});
|
|
269
|
+
|
|
230
270
|
const videoSchema = z.strictObject({
|
|
231
271
|
...playFields,
|
|
232
272
|
loop: z.boolean().optional(),
|
|
@@ -314,6 +354,21 @@ const textTypewriterSchema = z.strictObject({
|
|
|
314
354
|
speed: z.number().finite().positive().optional(),
|
|
315
355
|
});
|
|
316
356
|
|
|
357
|
+
/**
|
|
358
|
+
* `mount`:挂载 / 卸载一个 UI 组件。
|
|
359
|
+
*
|
|
360
|
+
* `name` 是 UI 目录名(短标识:小写字母开头,其后小写字母 / 数字 / `_` / `-`,1..32 位)——
|
|
361
|
+
* 与引擎侧 `UI_NAME_PATTERN` 同一形态,名字同时是产物键与目录名,两处必须一致。
|
|
362
|
+
* `props` 只收对象;键值是否 JSON 安全由运行期写入点判定(schema 看不见值语义)。
|
|
363
|
+
*/
|
|
364
|
+
const mountSchema = z.strictObject({
|
|
365
|
+
name: z
|
|
366
|
+
.string()
|
|
367
|
+
.regex(/^[a-z][a-z0-9_-]{0,31}$/, "UI 名须小写字母开头,其后小写字母/数字/_/-,1..32 位"),
|
|
368
|
+
visible: z.boolean(),
|
|
369
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
370
|
+
});
|
|
371
|
+
|
|
317
372
|
export {
|
|
318
373
|
saySchema,
|
|
319
374
|
menuSchema,
|
|
@@ -353,6 +408,11 @@ export {
|
|
|
353
408
|
seSchema,
|
|
354
409
|
voiceSchema,
|
|
355
410
|
stopSchema,
|
|
411
|
+
audioPlaySchema,
|
|
412
|
+
audioStopSchema,
|
|
413
|
+
audioVolumeSchema,
|
|
414
|
+
audioPauseSchema,
|
|
415
|
+
audioResumeSchema,
|
|
356
416
|
videoSchema,
|
|
357
417
|
cutsceneSchema,
|
|
358
418
|
seekVideoSchema,
|
|
@@ -371,4 +431,5 @@ export {
|
|
|
371
431
|
transitionSchema,
|
|
372
432
|
shakeSchema,
|
|
373
433
|
textTypewriterSchema,
|
|
434
|
+
mountSchema,
|
|
374
435
|
};
|
|
@@ -36,6 +36,11 @@ import {
|
|
|
36
36
|
saveDeleteSchema,
|
|
37
37
|
bgmSchema,
|
|
38
38
|
stopSchema,
|
|
39
|
+
audioPlaySchema,
|
|
40
|
+
audioStopSchema,
|
|
41
|
+
audioVolumeSchema,
|
|
42
|
+
audioPauseSchema,
|
|
43
|
+
audioResumeSchema,
|
|
39
44
|
seSchema,
|
|
40
45
|
voiceSchema,
|
|
41
46
|
videoSchema,
|
|
@@ -56,6 +61,7 @@ import {
|
|
|
56
61
|
transitionSchema,
|
|
57
62
|
shakeSchema,
|
|
58
63
|
textTypewriterSchema,
|
|
64
|
+
mountSchema,
|
|
59
65
|
} from "./commands.ts";
|
|
60
66
|
|
|
61
67
|
/** op → 负载 schema(不含 op 键本身);字面量键形态 = `ScriptCommand` 判别联合的派生源。
|
|
@@ -106,6 +112,12 @@ export const OP_SCHEMA_MAP = {
|
|
|
106
112
|
stop_ambient: stopSchema,
|
|
107
113
|
voice: voiceSchema,
|
|
108
114
|
stop_voice: stopSchema,
|
|
115
|
+
stop_se: stopSchema,
|
|
116
|
+
audio_play: audioPlaySchema,
|
|
117
|
+
audio_stop: audioStopSchema,
|
|
118
|
+
audio_volume: audioVolumeSchema,
|
|
119
|
+
audio_pause: audioPauseSchema,
|
|
120
|
+
audio_resume: audioResumeSchema,
|
|
109
121
|
video: videoSchema,
|
|
110
122
|
cutscene: cutsceneSchema,
|
|
111
123
|
seek_video: seekVideoSchema,
|
|
@@ -128,6 +140,8 @@ export const OP_SCHEMA_MAP = {
|
|
|
128
140
|
transition: transitionSchema,
|
|
129
141
|
shake: shakeSchema,
|
|
130
142
|
text_typewriter: textTypewriterSchema,
|
|
143
|
+
// 挂载 UI
|
|
144
|
+
mount: mountSchema,
|
|
131
145
|
};
|
|
132
146
|
|
|
133
147
|
/** 运行期查找面(与 `OP_SCHEMA_MAP` 同一对象,键集恒等);新增 op 自动可用 */
|
package/src/script/index.ts
CHANGED
|
@@ -51,10 +51,24 @@ export {
|
|
|
51
51
|
type KnownInterpolationKey,
|
|
52
52
|
} from "./words/index.ts";
|
|
53
53
|
|
|
54
|
+
// —— 挂载 UI(句柄形状 + 挂载词)——
|
|
55
|
+
export { mount, type KnownUiName, type UiHandle, type UiNameRegistry } from "./words/index.ts";
|
|
56
|
+
|
|
54
57
|
// —— 叙事 ——
|
|
55
|
-
export {
|
|
58
|
+
export {
|
|
59
|
+
say,
|
|
60
|
+
option,
|
|
61
|
+
menu,
|
|
62
|
+
input,
|
|
63
|
+
inputCandidate,
|
|
64
|
+
notify,
|
|
65
|
+
nvl,
|
|
66
|
+
character,
|
|
67
|
+
uncharacter,
|
|
68
|
+
} from "./words/index.ts";
|
|
56
69
|
export type {
|
|
57
70
|
MenuOptionSpec,
|
|
71
|
+
InputCandidateSpec,
|
|
58
72
|
SayOptions,
|
|
59
73
|
NotifyOptions,
|
|
60
74
|
NvlCommandMode,
|
|
@@ -101,6 +115,11 @@ export {
|
|
|
101
115
|
// —— 存档 / 音频 / 视频 / 小游戏 ——
|
|
102
116
|
export { save, load, autoSave, saveDelete } from "./words/index.ts";
|
|
103
117
|
export {
|
|
118
|
+
audioPlay,
|
|
119
|
+
audioStop,
|
|
120
|
+
audioVolume,
|
|
121
|
+
audioPause,
|
|
122
|
+
audioResume,
|
|
104
123
|
bgm,
|
|
105
124
|
stopBgm,
|
|
106
125
|
se,
|
|
@@ -108,8 +127,12 @@ export {
|
|
|
108
127
|
stopAmbient,
|
|
109
128
|
voice,
|
|
110
129
|
stopVoice,
|
|
130
|
+
stopSe,
|
|
111
131
|
} from "./words/index.ts";
|
|
112
132
|
export type {
|
|
133
|
+
AudioPlayOptions,
|
|
134
|
+
AudioStopOptions,
|
|
135
|
+
AudioPauseOptions,
|
|
113
136
|
PlayOptions,
|
|
114
137
|
BgmOptions,
|
|
115
138
|
StopOptions,
|
|
@@ -39,6 +39,7 @@ export function selectWordSources(fileNames: readonly string[]): string[] {
|
|
|
39
39
|
* 未登记的新导出词会让重建失败(见 renderVocabulary 的缺登记核对)。 */
|
|
40
40
|
export const HELPER_WORDS: Readonly<Record<string, string>> = {
|
|
41
41
|
option: "$menu.option",
|
|
42
|
+
inputCandidate: "$input.candidate",
|
|
42
43
|
whenChain: "$if.chain",
|
|
43
44
|
reward: "$minigame.reward",
|
|
44
45
|
extOp: "$ext.op",
|
|
@@ -18,6 +18,11 @@ export const VOCABULARY_OPS = [
|
|
|
18
18
|
"array_pop",
|
|
19
19
|
"array_push",
|
|
20
20
|
"assert",
|
|
21
|
+
"audio_pause",
|
|
22
|
+
"audio_play",
|
|
23
|
+
"audio_resume",
|
|
24
|
+
"audio_stop",
|
|
25
|
+
"audio_volume",
|
|
21
26
|
"auto_save",
|
|
22
27
|
"background",
|
|
23
28
|
"bg_switch",
|
|
@@ -44,6 +49,7 @@ export const VOCABULARY_OPS = [
|
|
|
44
49
|
"local",
|
|
45
50
|
"menu",
|
|
46
51
|
"minigame",
|
|
52
|
+
"mount",
|
|
47
53
|
"navigate",
|
|
48
54
|
"notify",
|
|
49
55
|
"nvl",
|
|
@@ -62,6 +68,7 @@ export const VOCABULARY_OPS = [
|
|
|
62
68
|
"show",
|
|
63
69
|
"stop_ambient",
|
|
64
70
|
"stop_bgm",
|
|
71
|
+
"stop_se",
|
|
65
72
|
"stop_video",
|
|
66
73
|
"stop_voice",
|
|
67
74
|
"style",
|
|
@@ -95,6 +102,12 @@ export const SCRIPT_COVERAGE: Readonly<Record<string, string>> = {
|
|
|
95
102
|
stopAmbient: "stop_ambient",
|
|
96
103
|
voice: "voice",
|
|
97
104
|
stopVoice: "stop_voice",
|
|
105
|
+
stopSe: "stop_se",
|
|
106
|
+
audioPlay: "audio_play",
|
|
107
|
+
audioStop: "audio_stop",
|
|
108
|
+
audioVolume: "audio_volume",
|
|
109
|
+
audioPause: "audio_pause",
|
|
110
|
+
audioResume: "audio_resume",
|
|
98
111
|
// —— elements.ts ——
|
|
99
112
|
show: "show",
|
|
100
113
|
hide: "hide",
|
|
@@ -143,6 +156,8 @@ export const SCRIPT_COVERAGE: Readonly<Record<string, string>> = {
|
|
|
143
156
|
load: "load",
|
|
144
157
|
autoSave: "auto_save",
|
|
145
158
|
saveDelete: "save_delete",
|
|
159
|
+
// —— ui.ts ——
|
|
160
|
+
mount: "mount",
|
|
146
161
|
// —— vars.ts ——
|
|
147
162
|
set: "set",
|
|
148
163
|
define: "define",
|
|
@@ -164,6 +179,7 @@ export const SCRIPT_COVERAGE: Readonly<Record<string, string>> = {
|
|
|
164
179
|
videoSkipable: "video_skipable",
|
|
165
180
|
// —— 辅助词(补充表登记,不产独立 op) ——
|
|
166
181
|
option: "$menu.option",
|
|
182
|
+
inputCandidate: "$input.candidate",
|
|
167
183
|
whenChain: "$if.chain",
|
|
168
184
|
reward: "$minigame.reward",
|
|
169
185
|
extOp: "$ext.op",
|
|
@@ -112,3 +112,122 @@ export function voice(
|
|
|
112
112
|
export function stopVoice(opts?: StopOptions): CommandOf<"stop_voice"> {
|
|
113
113
|
return { op: "stop_voice", ...opts };
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 停止在播音效(`fade` 渐出毫秒,缺省立即)。
|
|
118
|
+
*
|
|
119
|
+
* 音效是「一次性触发 + 可停止」的通道:`se` 每次触发独立叠放,
|
|
120
|
+
* 本命令停掉当前在播的全部音效(已淡出完成的不受影响)。
|
|
121
|
+
*/
|
|
122
|
+
export function stopSe(opts?: StopOptions): CommandOf<"stop_se"> {
|
|
123
|
+
return { op: "stop_se", ...opts };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** `audio_play` 的可选项:音量、循环、淡入毫秒、显式重播 */
|
|
127
|
+
export type AudioPlayOptions = Omit<
|
|
128
|
+
CommandOf<"audio_play">,
|
|
129
|
+
"op" | "inst" | "channel" | "resource"
|
|
130
|
+
>;
|
|
131
|
+
|
|
132
|
+
/** `audio_stop` 的可选项:目标实例(省略 = 停全部)、淡出毫秒 */
|
|
133
|
+
export type AudioStopOptions = Omit<CommandOf<"audio_stop">, "op">;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 起播(或更新)一个**音频实例**:按名字点名的独立音轨,与四通道并存。
|
|
137
|
+
*
|
|
138
|
+
* - `channel` 决定偏好归口(BGM 的音量偏好管 bgm 实例,音效的管 se 实例)。
|
|
139
|
+
* - 同实例重复起播:同资源 = 更新(保留位置),换资源或 `restart: true` = 重播。
|
|
140
|
+
* - 与 {@link bgm} / {@link se} 的区别:通道是「叙事语义上的那一路声音」
|
|
141
|
+
* (各一条、随存档走),实例是「作者点名的编排单元」(可多个、各自独立叠放)。
|
|
142
|
+
*
|
|
143
|
+
* @param inst 实例 id(小写字母开头的短 id,1..32 位)。
|
|
144
|
+
* @param channel 归属通道:`bgm` / `se` / `ambient` / `voice`。
|
|
145
|
+
* @param resource 音频资源路径。
|
|
146
|
+
* @param opts 可选项,见 {@link AudioPlayOptions}。
|
|
147
|
+
* @returns `audio_play` 故事命令。
|
|
148
|
+
* @example
|
|
149
|
+
* audioPlay("rain", "ambient", "audio/rain.ogg", { volume: 0.3 });
|
|
150
|
+
* audioVolume("rain", 0.6);
|
|
151
|
+
*/
|
|
152
|
+
export function audioPlay(
|
|
153
|
+
inst: string,
|
|
154
|
+
channel: "bgm" | "se" | "ambient" | "voice",
|
|
155
|
+
resource: string,
|
|
156
|
+
opts?: AudioPlayOptions,
|
|
157
|
+
): CommandOf<"audio_play"> {
|
|
158
|
+
return { op: "audio_play", inst, channel, resource, ...opts };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 停止音频实例:给了 `inst` 只停那一轨,省略则停掉当前全部实例。
|
|
163
|
+
* @param inst 目标实例 id(省略 = 停全部)。
|
|
164
|
+
* @param opts 可选项:淡出毫秒。
|
|
165
|
+
* @returns `audio_stop` 故事命令。
|
|
166
|
+
*/
|
|
167
|
+
export function audioStop(
|
|
168
|
+
inst?: string,
|
|
169
|
+
opts?: AudioStopOptions,
|
|
170
|
+
): CommandOf<"audio_stop"> {
|
|
171
|
+
return {
|
|
172
|
+
op: "audio_stop",
|
|
173
|
+
...(inst === undefined ? {} : { inst }),
|
|
174
|
+
...opts,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 只改一个音频实例的音量:不动播放位置、不重播。
|
|
180
|
+
*
|
|
181
|
+
* 这是「实时调音」的正规入口——拖动滑块时不要用 {@link audioPlay} 重发,
|
|
182
|
+
* 那会重设位置并打断淡入。
|
|
183
|
+
*
|
|
184
|
+
* @param inst 目标实例 id。
|
|
185
|
+
* @param volume 音量(0–1)。
|
|
186
|
+
* @returns `audio_volume` 故事命令。
|
|
187
|
+
*/
|
|
188
|
+
export function audioVolume(
|
|
189
|
+
inst: string,
|
|
190
|
+
volume: number,
|
|
191
|
+
): CommandOf<"audio_volume"> {
|
|
192
|
+
return { op: "audio_volume", inst, volume };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `audio_pause` / `audio_resume` 的可选项:目标通道(省略 = 全部常驻通道) */
|
|
196
|
+
export type AudioPauseOptions = Omit<CommandOf<"audio_pause">, "op">;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 暂停音频通道:保留播放器与播放位置,只是不出声;恢复从原位置继续。
|
|
200
|
+
*
|
|
201
|
+
* - 省略 `channel` = 全部常驻通道(`bgm` / `ambient` / `voice`)。
|
|
202
|
+
* - `se` 是一次性触发,暂停它没有可恢复的东西——显式传 `se` 不动作。
|
|
203
|
+
* - 只对正在播放的通道生效;已停止或从未播放的通道不受影响(连按两次只算一次)。
|
|
204
|
+
*
|
|
205
|
+
* @param opts 可选项,见 {@link AudioPauseOptions}。
|
|
206
|
+
* @returns `audio_pause` 故事命令。
|
|
207
|
+
* @example
|
|
208
|
+
* audioPause(); // 暂停全部常驻通道
|
|
209
|
+
* audioPause({ channel: "bgm" });
|
|
210
|
+
*/
|
|
211
|
+
export function audioPause(
|
|
212
|
+
opts?: AudioPauseOptions,
|
|
213
|
+
): CommandOf<"audio_pause"> {
|
|
214
|
+
return { op: "audio_pause", ...opts };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 恢复被 {@link audioPause} 暂停的通道:从暂停位置续播,不从头播。
|
|
219
|
+
*
|
|
220
|
+
* 恢复用的是暂停前那个播放态(资源、音量、循环原样),因此不会重新起播、
|
|
221
|
+
* 也不会重设淡入。未处于暂停态的通道不动作。
|
|
222
|
+
*
|
|
223
|
+
* @param opts 可选项,见 {@link AudioPauseOptions}。
|
|
224
|
+
* @returns `audio_resume` 故事命令。
|
|
225
|
+
* @example
|
|
226
|
+
* audioResume(); // 恢复全部常驻通道
|
|
227
|
+
* audioResume({ channel: "ambient" });
|
|
228
|
+
*/
|
|
229
|
+
export function audioResume(
|
|
230
|
+
opts?: AudioPauseOptions,
|
|
231
|
+
): CommandOf<"audio_resume"> {
|
|
232
|
+
return { op: "audio_resume", ...opts };
|
|
233
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Script 词汇层 · **具名实现槽位**(cell —— build-time 声明词)。
|
|
3
3
|
*
|
|
4
|
-
* cell 声明的实现由构建期扫描收集进生成注册物(`
|
|
4
|
+
* cell 声明的实现由构建期扫描收集进生成注册物(`src/gen/fun_register.g.ts`):
|
|
5
5
|
* 故事 JSON 只留名字引用(guard 的 `fn` 字段),实现住生成模块——
|
|
6
6
|
* 「名字在数据、实现在代码、build 做名字闭合」。
|
|
7
7
|
*
|
|
@@ -31,7 +31,7 @@ export type KnownGuardName = keyof GuardNameRegistry extends never
|
|
|
31
31
|
: keyof GuardNameRegistry & string;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* 列名注册表(类型扩充点):构建期生成的 `
|
|
34
|
+
* 列名注册表(类型扩充点):构建期生成的 `src/gen/stories.gen.d.ts`
|
|
35
35
|
* 往本接口塞进工程内每个列 id 的字面量键 ⇒ `jump("…")` 与 `option("…", "…")`
|
|
36
36
|
* 的目标列名获得补全、写错即红。空表退化为普通 string。
|
|
37
37
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Script 词汇层 · **复合词**(多 op/元素的高频组合,纯展开)。
|
|
3
3
|
* 复合词不产出新语义:每个函数 = 既有元素的语法糖。
|
|
4
4
|
*/
|
|
5
|
-
import type { ElementNode } from "@lingfan-engine/core";
|
|
5
|
+
import type { ElementNode, SceneType } from "@lingfan-engine/core";
|
|
6
6
|
|
|
7
7
|
export interface ElementOptions {
|
|
8
8
|
id?: string;
|
|
@@ -72,7 +72,7 @@ export function buttonElement(
|
|
|
72
72
|
* @returns 背景与标题元素节点数组(可省略标题)。
|
|
73
73
|
* @example
|
|
74
74
|
* export default scene("square", () => ({
|
|
75
|
-
* type: "
|
|
75
|
+
* type: "ui",
|
|
76
76
|
* elements: [...sceneSetup("images/square.png", "城镇广场")],
|
|
77
77
|
* columns: { ... },
|
|
78
78
|
* }));
|
|
@@ -117,9 +117,9 @@ export function sceneSetup(
|
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
119
|
* 列的 `type` 字段(game 缺省不写——与序列化口径一致)。
|
|
120
|
-
* @param type
|
|
120
|
+
* @param type 列类型:`ui`(界面层:标题 / 设置 / 覆盖层 / 弹窗)。
|
|
121
121
|
* @returns 带类型标记的对象,展开进列定义。
|
|
122
122
|
*/
|
|
123
|
-
export function sceneType(type:
|
|
123
|
+
export function sceneType(type: SceneType): { type: SceneType } {
|
|
124
124
|
return { type };
|
|
125
125
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Script 词汇层 · 词条实现(作者可用的 builder 与选项类型)。
|
|
3
3
|
* 每个文件对应一族词条:叙事 / 流程 / 变量 / 存档 / 音频 / 视频 / 小游戏 /
|
|
4
|
-
* 元素 / 复合词 / 扩展 / 具名实现槽位 / 交互。
|
|
4
|
+
* 元素 / 复合词 / 扩展 / 具名实现槽位 / 挂载 UI / 交互。
|
|
5
5
|
* 各文件导出名互不重复,故此处整体转发;对外仍由 `script` 出口按族列出名单。
|
|
6
6
|
*
|
|
7
7
|
* 每个词条函数都是**纯数据构造**:只返回一条 `StoryCommand`(与 JSON 同族),
|
|
@@ -19,5 +19,6 @@ export * from "./interaction.ts";
|
|
|
19
19
|
export * from "./minigame.ts";
|
|
20
20
|
export * from "./narrative.ts";
|
|
21
21
|
export * from "./save.ts";
|
|
22
|
+
export * from "./ui.ts";
|
|
22
23
|
export * from "./vars.ts";
|
|
23
24
|
export * from "./video.ts";
|
|
@@ -10,7 +10,7 @@ import type { KnownColumnName, KnownInterpolationKey } from "./cells.ts";
|
|
|
10
10
|
// —— `{key}` 插值键的编译期校验(纯类型层,零运行期成本)——
|
|
11
11
|
//
|
|
12
12
|
// 只在**字符串字面量**上生效:模板串、变量、宽 string 全部放行(动态文本没法查,
|
|
13
|
-
// 也不该吓唬作者);键表来自生成物 `
|
|
13
|
+
// 也不该吓唬作者);键表来自生成物 `src/gen/stories.gen.d.ts`。
|
|
14
14
|
// 富文本行内标记必须豁免——`{b}`/`{color=#FFD700}` 这类与插值共用一个花括号语法,
|
|
15
15
|
// 豁免表对齐引擎 `runtime/expr/interpolate.ts` 的行内标记集。
|
|
16
16
|
type TrimText<S extends string> = S extends ` ${infer R}`
|
|
@@ -146,6 +146,31 @@ export function menu(
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** 输入候选:显示文本 + 要填进变量的值(用 {@link inputCandidate} 构造) */
|
|
150
|
+
export type InputCandidateSpec = NonNullable<CommandOf<"input">["options"]>[number];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 构造一个输入候选,配合 {@link input} 使用:玩家点中它,`value` 就直接当输入值提交,
|
|
154
|
+
* 同时仍然可以自己打字。
|
|
155
|
+
*
|
|
156
|
+
* 与菜单的 {@link option} 不同——候选不跳列,只替玩家填一个值。
|
|
157
|
+
*
|
|
158
|
+
* @param text 候选上显示的文字(支持翻译;翻译只影响显示)。
|
|
159
|
+
* @param value 选中后写进 `store` 的原样值(**不进翻译表**,多语言下不变)。
|
|
160
|
+
* @returns 输入候选数据。
|
|
161
|
+
* @example
|
|
162
|
+
* input("你的身份是?", "role", [
|
|
163
|
+
* inputCandidate("旅人", "traveler"),
|
|
164
|
+
* inputCandidate("商人", "merchant"),
|
|
165
|
+
* ]);
|
|
166
|
+
*/
|
|
167
|
+
export function inputCandidate(
|
|
168
|
+
text: string,
|
|
169
|
+
value: string,
|
|
170
|
+
): InputCandidateSpec {
|
|
171
|
+
return { text, value };
|
|
172
|
+
}
|
|
173
|
+
|
|
149
174
|
/**
|
|
150
175
|
* 向玩家要一段文字:弹出输入框显示 `prompt`,玩家提交后,输入内容写进
|
|
151
176
|
* 状态键 `store`,故事从下一句继续。
|
|
@@ -154,13 +179,30 @@ export function menu(
|
|
|
154
179
|
*
|
|
155
180
|
* @param prompt 输入框上方显示的问题/提示语(必填非空)。
|
|
156
181
|
* @param store 收纳玩家输入的状态变量名(写进全局状态,之后可用 `{store}` 插值显示)。
|
|
182
|
+
* @param options 候选快捷值(可选):给了就在输入框旁列出,玩家点一项即提交该项的 `value`,
|
|
183
|
+
* 也可以不理候选自己打字。用 {@link inputCandidate} 构造。
|
|
157
184
|
* @returns `input` 故事命令。
|
|
158
185
|
* @example
|
|
159
186
|
* input("主角叫什么名字?", "playerName");
|
|
160
187
|
* say("你好,{playerName}!");
|
|
188
|
+
* @example
|
|
189
|
+
* // 带候选:既能点,也能自己打
|
|
190
|
+
* input("你的身份是?", "role", [
|
|
191
|
+
* inputCandidate("旅人", "traveler"),
|
|
192
|
+
* inputCandidate("商人", "merchant"),
|
|
193
|
+
* ]);
|
|
161
194
|
*/
|
|
162
|
-
export function input(
|
|
163
|
-
|
|
195
|
+
export function input(
|
|
196
|
+
prompt: string,
|
|
197
|
+
store: string,
|
|
198
|
+
options?: readonly InputCandidateSpec[],
|
|
199
|
+
): CommandOf<"input"> {
|
|
200
|
+
return {
|
|
201
|
+
op: "input",
|
|
202
|
+
prompt,
|
|
203
|
+
store,
|
|
204
|
+
...(options === undefined ? {} : { options: [...options] }),
|
|
205
|
+
};
|
|
164
206
|
}
|
|
165
207
|
|
|
166
208
|
/** type 保持手写的窄联合(schema 是 string)——作者侧补全优于校验面放宽 */
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script 词汇层 · **挂载 UI**(ui)。
|
|
3
|
+
*
|
|
4
|
+
* 故事源用一行 import 拿到一个 UI 的句柄:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import hub from "../../ui/hub/hub.vue";
|
|
8
|
+
* mount(hub, true);
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* 句柄在编译期就退化成一个名字(`"hub"`),运行期没有对象、没有文件加载——
|
|
12
|
+
* 这正是「静态声明」的含义:写 import 只是告诉编译器「这一列会挂哪个 UI」,
|
|
13
|
+
* 真正把 UI 挂上舞台是 `mount` op 在运行期做的事。
|
|
14
|
+
*
|
|
15
|
+
* 本文件是 `UiHandle` 的**唯一声明处**;`@lingfan-engine/stories-build/authoring`
|
|
16
|
+
* 只做类型转发,所以故事源不需要额外的 tsconfig 配置就能拿到同一份形状。
|
|
17
|
+
*/
|
|
18
|
+
import type { CommandOf } from "../../schema/index.ts";
|
|
19
|
+
|
|
20
|
+
export interface UiHandle<N extends string = string> {
|
|
21
|
+
readonly face: "uis";
|
|
22
|
+
readonly name: N;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* UI 名注册表(类型扩充点):生成物 `src/gen/ui_register.g.ts` 尾部
|
|
27
|
+
* `declare module "@lingfan-engine/editor"` 往本接口塞进每个 UI 名的字面量键 ⇒
|
|
28
|
+
* {@link mount} 的名字参数获得补全与写错即红。空表退化为普通 string。
|
|
29
|
+
*/
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- 类型扩充点:只有接口可被生成物 declare module 合并(type 别名不能),空体是有意形态
|
|
31
|
+
export interface UiNameRegistry {}
|
|
32
|
+
|
|
33
|
+
/** 已声明 UI 名(有声明 = 字面量联合;空 = string) */
|
|
34
|
+
export type KnownUiName = keyof UiNameRegistry extends never
|
|
35
|
+
? string
|
|
36
|
+
: keyof UiNameRegistry & string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 挂载 / 卸载一个 UI 组件:`mount(hub, true)`、`mount(hub, false)`、
|
|
40
|
+
* `mount(map, true, { scene: "forest" })`。
|
|
41
|
+
*
|
|
42
|
+
* 状态写进 `SYS.mounts`,随快照 / 存档 / 回溯自动随行;宿主按这张表决定
|
|
43
|
+
* 渲染树上有哪些组件。**隐藏 = 从渲染树移除**(卸载组件),不是 `display: none`。
|
|
44
|
+
*
|
|
45
|
+
* @param handle UI 句柄(`import hub from "…/hub.vue"`)或其名字字符串。
|
|
46
|
+
* @param visible 是否挂载。必填:没有「切换」写法,显隐写死才能让回溯读到确定的终值。
|
|
47
|
+
* @param props 传给组件的入参(JSON 安全)。给了就**整份替换**,不与上一次合并——
|
|
48
|
+
* 组件的入参集合是它的契约,得能完整指定(含「删掉一个入参」)。
|
|
49
|
+
* @returns `mount` 故事命令。
|
|
50
|
+
* @example
|
|
51
|
+
* import hub from "../../ui/hub/hub.vue";
|
|
52
|
+
* mount(hub, true);
|
|
53
|
+
* mount(hub, false);
|
|
54
|
+
*/
|
|
55
|
+
export function mount<const N extends string>(
|
|
56
|
+
handle: UiHandle<N> | KnownUiName,
|
|
57
|
+
visible: boolean,
|
|
58
|
+
props?: Readonly<Record<string, unknown>>,
|
|
59
|
+
): CommandOf<"mount"> {
|
|
60
|
+
const name = typeof handle === "string" ? handle : handle.name;
|
|
61
|
+
const cmd: CommandOf<"mount"> = { op: "mount", name, visible };
|
|
62
|
+
if (props !== undefined) cmd.props = { ...props };
|
|
63
|
+
return cmd;
|
|
64
|
+
}
|
package/src/shared/guards.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 通用取值判据的**转发口**:编辑器包多个域(诊断、i18n、布局)都要对「这个值是不是一条记录」下判断。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 是否是**普通对象**(JSON 意义上的记录):`null` 与数组都不算。
|
|
4
|
+
* **实现只有一处**,在引擎包 `packages/core/src/shared/guards.ts`(经 `@lingfan-engine/core`
|
|
5
|
+
* 包出口公开,链路由 `core/src/index.ts` → `core/src/contracts/index.ts` → `core/src/shared/index.ts`
|
|
6
|
+
* 三级转发构成)。此处只做转发,不持有实现体。
|
|
11
7
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* 为什么不在这里留第二份实现:判据本身不含业务含义,任何层都可以引用;而它一旦分叉,
|
|
9
|
+
* 分叉点就是静默的行为差异——同一棵树,两处判定不同就会出现「这边认、那边不认」。
|
|
10
|
+
* 缺人少写一个 `!Array.isArray` 就是症状的来源。
|
|
14
11
|
*/
|
|
15
|
-
export
|
|
16
|
-
value: unknown,
|
|
17
|
-
): value is Record<string, unknown> {
|
|
18
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
-
}
|
|
12
|
+
export { isPlainObject } from "@lingfan-engine/core";
|