@lingfan-engine/editor 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +201 -0
  2. package/package.json +39 -3
  3. package/src/chapters/build.ts +180 -0
  4. package/src/chapters/index.ts +34 -0
  5. package/src/chapters/types.ts +71 -0
  6. package/src/contracts/diagnostics.ts +62 -0
  7. package/src/contracts/editing.ts +26 -0
  8. package/src/contracts/forms.ts +69 -0
  9. package/src/contracts/index.ts +22 -0
  10. package/src/contracts/texts.ts +204 -0
  11. package/src/diagnostics/analyze.ts +161 -0
  12. package/src/diagnostics/grouping.ts +164 -0
  13. package/src/diagnostics/index.ts +35 -0
  14. package/src/diagnostics/message.ts +60 -0
  15. package/src/diagnostics/refs.ts +123 -0
  16. package/src/diagnostics/symbols.ts +221 -0
  17. package/src/editing/columnGrouping.ts +331 -0
  18. package/src/editing/graphConnect.ts +88 -0
  19. package/src/editing/index.ts +72 -0
  20. package/src/editing/pointers.ts +291 -0
  21. package/src/editing/saveNormalization.ts +58 -0
  22. package/src/editing/session.ts +134 -0
  23. package/src/editing/storyOps.ts +268 -0
  24. package/src/editing/tsSourceGuard.ts +40 -0
  25. package/src/element/drag.ts +22 -0
  26. package/src/element/dragCreate.ts +60 -0
  27. package/src/element/index.ts +11 -0
  28. package/src/i18n/coverage.ts +191 -0
  29. package/src/i18n/extract.ts +38 -0
  30. package/src/i18n/index.ts +47 -0
  31. package/src/i18n/reconcile.ts +71 -0
  32. package/src/i18n/skeleton.ts +112 -0
  33. package/src/i18n/storyKeys.ts +27 -0
  34. package/src/i18n/surfaces.ts +54 -0
  35. package/src/i18n/table.ts +235 -0
  36. package/src/index.ts +342 -0
  37. package/src/layout/index.ts +14 -0
  38. package/src/layout/steps.ts +378 -0
  39. package/src/schema/catalog.ts +300 -0
  40. package/src/schema/elementForms.ts +307 -0
  41. package/src/schema/elementPalette.ts +100 -0
  42. package/src/schema/forms.ts +196 -0
  43. package/src/schema/index.ts +55 -0
  44. package/src/schema/opSchemas/commands.ts +374 -0
  45. package/src/schema/opSchemas/fragments.ts +24 -0
  46. package/src/schema/opSchemas/index.ts +16 -0
  47. package/src/schema/opSchemas/map.ts +134 -0
  48. package/src/schema/opSchemas/types.ts +42 -0
  49. package/src/schema/opSchemas/validate.ts +184 -0
  50. package/src/schema/surface.ts +101 -0
  51. package/src/schema/validation.ts +92 -0
  52. package/src/schema/walk.ts +167 -0
  53. package/src/script/expr/audit/index.ts +8 -0
  54. package/src/script/expr/audit/parser.ts +370 -0
  55. package/src/script/expr/audit/token.ts +23 -0
  56. package/src/script/expr/build.ts +203 -0
  57. package/src/script/expr/index.ts +17 -0
  58. package/src/script/expr/registry.ts +130 -0
  59. package/src/script/index.ts +168 -0
  60. package/src/script/vocabulary-gen.ts +202 -0
  61. package/src/script/vocabulary.g.ts +175 -0
  62. package/src/script/words/audio.ts +114 -0
  63. package/src/script/words/cells.ts +78 -0
  64. package/src/script/words/compile-time.ts +27 -0
  65. package/src/script/words/composite.ts +125 -0
  66. package/src/script/words/elements.ts +202 -0
  67. package/src/script/words/extension.ts +25 -0
  68. package/src/script/words/flow.ts +391 -0
  69. package/src/script/words/index.ts +23 -0
  70. package/src/script/words/interaction.ts +44 -0
  71. package/src/script/words/minigame.ts +66 -0
  72. package/src/script/words/narrative.ts +261 -0
  73. package/src/script/words/save.ts +54 -0
  74. package/src/script/words/vars.ts +155 -0
  75. package/src/script/words/video.ts +80 -0
  76. package/src/shared/guards.ts +19 -0
  77. package/src/shared/index.ts +6 -0
  78. package/src/shared/pointer.ts +23 -0
  79. package/README.md +0 -3
@@ -0,0 +1,374 @@
1
+ /**
2
+ * op 负载 schema 全集:每条命令一个 `z.strictObject`,`.strict()` 拒绝未知字段
3
+ * (编辑期 fail-closed)。字段面以执行器为权威(packages/core/src/runtime/ops/)。
4
+ */
5
+ import { z } from "zod";
6
+ import { isValidSayColor } from "@lingfan-engine/core";
7
+ import {
8
+ Body,
9
+ Fade,
10
+ Finite,
11
+ InstanceZ,
12
+ NonEmpty,
13
+ SlotName,
14
+ StateValue,
15
+ Value,
16
+ } from "./fragments.ts";
17
+
18
+ const saySchema = z.strictObject({
19
+ text: NonEmpty,
20
+ speaker: z.string().optional(),
21
+ /**
22
+ * 说话人颜色覆盖(覆盖整句/说话人)。
23
+ *
24
+ * **判据直接复用引擎的 `isValidSayColor`**(不在此重写正则)——
25
+ * 两处各写一份判据 ⇒ 必然漂移——
26
+ * 判据只此一份,投影/校验读同一份。
27
+ * 与**行内标记** `{color=…}`(写在文本内部)是两件事。
28
+ */
29
+ color: z
30
+ .string()
31
+ .refine(isValidSayColor, {
32
+ message: "必须为十六进制颜色(如 #FFD700 / #888 / #FFD700CC)",
33
+ })
34
+ .optional(),
35
+ /**
36
+ * 本句字体(字体名 = Resources/Fonts 文件名去扩展名)。
37
+ * 名字是否在注册表内不在此校验(schema 看不见资源目录)——构建期 fail-closed +
38
+ * 运行期渲染闸门回退负责,这里只收非空字符串。
39
+ */
40
+ font: NonEmpty.optional(),
41
+ clickable: z.boolean().optional(),
42
+ noskip: z.boolean().optional(),
43
+ instant: z.boolean().optional(),
44
+ typewriter: z.number().optional(),
45
+ voice: NonEmpty.optional(),
46
+ template: NonEmpty.optional(),
47
+ z: InstanceZ, // dialogue 层实例 z
48
+ });
49
+
50
+ const menuSchema = z.strictObject({
51
+ prompt: z.string().optional(),
52
+ options: z.array(z.strictObject({ text: NonEmpty, target: NonEmpty })).min(1),
53
+ z: InstanceZ, // choices 层实例 z
54
+ });
55
+
56
+ const inputSchema = z.strictObject({
57
+ prompt: NonEmpty,
58
+ store: NonEmpty,
59
+ z: InstanceZ, // choices 层实例 z
60
+ });
61
+
62
+ const notifySchema = z.strictObject({
63
+ text: NonEmpty,
64
+ type: z.string().optional(),
65
+ duration: z.number().optional(),
66
+ z: InstanceZ, // notifications 层实例 z
67
+ });
68
+
69
+ const nvlSchema = z.strictObject({
70
+ mode: z.enum(["enter", "auto", "clear", "exit"]).optional(),
71
+ });
72
+
73
+ const characterSchema = z.strictObject({
74
+ key: NonEmpty,
75
+ name: z.string().optional(),
76
+ color: z.string().optional(),
77
+ size: z.string().optional(),
78
+ font: z.string().optional(),
79
+ textColor: z.string().optional(),
80
+ screen: NonEmpty.optional(),
81
+ });
82
+
83
+ const uncharacterSchema = z.strictObject({ key: NonEmpty });
84
+
85
+ const waitSchema = z.strictObject({
86
+ seconds: z.number(),
87
+ skipable: z.boolean().optional(),
88
+ });
89
+
90
+ const pauseSchema = z.strictObject({
91
+ seconds: z.number(),
92
+ });
93
+
94
+ const randomSchema = z.strictObject({
95
+ seed: z.number().int(),
96
+ range: z.tuple([z.number(), z.number()]),
97
+ var: NonEmpty,
98
+ });
99
+
100
+ const jumpSchema = z.strictObject({
101
+ target: NonEmpty,
102
+ });
103
+
104
+ const navigateSchema = z.strictObject({
105
+ path: NonEmpty,
106
+ scene: NonEmpty.optional(),
107
+ });
108
+
109
+ /** `call`:target 必填;`into` 为结果绑定名(ret 带值返回时声明成调用方局部变量,无带值 ret 则绑 `null`) */
110
+ const callSchema = z.strictObject({
111
+ target: NonEmpty,
112
+ args: z.array(Value).optional(),
113
+ into: NonEmpty.optional(),
114
+ });
115
+
116
+ const returnSchema = z.strictObject({
117
+ value: Value.optional(),
118
+ });
119
+ /** 函数定义:name/params/body 三者引擎均必填(`data/format/validate/blocks.ts` `case "func"`) */
120
+ const funcSchema = z.strictObject({
121
+ name: NonEmpty,
122
+ params: z.array(NonEmpty),
123
+ body: Body,
124
+ });
125
+
126
+ const ifSchema = z.strictObject({
127
+ cond: NonEmpty,
128
+ then: Body,
129
+ elif: z.array(z.strictObject({ cond: NonEmpty, then: Body })).optional(),
130
+ else: Body.optional(),
131
+ });
132
+
133
+ const whileSchema = z.strictObject({
134
+ cond: NonEmpty,
135
+ body: Body,
136
+ });
137
+
138
+ const assertSchema = z.strictObject({
139
+ cond: NonEmpty,
140
+ message: z.string().optional(),
141
+ });
142
+
143
+ const guardSchema = z.strictObject({
144
+ fn: NonEmpty,
145
+ /**
146
+ * 原样**运输**的宿主参数:嵌套 JSON 合法(同 `minigame.config` 口径;运行期
147
+ * `findJsonValueError` 兜底 JSON 安全)。值口径原则:**被求值的走 `Value`
148
+ * (call args / reward / set),被运输的走 JSON**(guard args / minigame config)。
149
+ */
150
+ args: z.record(z.string(), z.unknown()).optional(),
151
+ });
152
+
153
+ const forSchema = z.strictObject({
154
+ var: NonEmpty,
155
+ in: NonEmpty,
156
+ body: Body,
157
+ });
158
+
159
+ const foreachSchema = z.strictObject({
160
+ var: NonEmpty,
161
+ key: NonEmpty,
162
+ body: Body,
163
+ });
164
+
165
+ const switchSchema = z.strictObject({
166
+ on: NonEmpty,
167
+ cases: z.array(z.strictObject({ value: Value, body: Body })).min(1),
168
+ default: Body.optional(),
169
+ });
170
+
171
+ const emptySchema = z.strictObject({});
172
+
173
+ const assignSchema = z.strictObject({ key: NonEmpty, value: StateValue });
174
+
175
+ const undefSchema = z.strictObject({ key: NonEmpty });
176
+
177
+ const arraySchema = z.strictObject({
178
+ key: NonEmpty,
179
+ items: z.array(StateValue),
180
+ once: z.boolean().optional(),
181
+ });
182
+
183
+ const arrayPushSchema = z.strictObject({ key: NonEmpty, value: StateValue });
184
+
185
+ const arrayPopSchema = z.strictObject({ key: NonEmpty });
186
+
187
+ const dictSchema = z.strictObject({
188
+ key: NonEmpty,
189
+ value: z.record(z.string(), StateValue),
190
+ once: z.boolean().optional(),
191
+ });
192
+
193
+ const dictSetSchema = z.strictObject({
194
+ key: NonEmpty,
195
+ field: NonEmpty,
196
+ value: StateValue,
197
+ });
198
+
199
+ const saveSchema = z.strictObject({
200
+ slot: SlotName,
201
+ title: z.string().optional(),
202
+ });
203
+
204
+ const loadSchema = z.strictObject({ slot: NonEmpty });
205
+
206
+ const autoSaveSchema = z.strictObject({
207
+ enabled: z.boolean(),
208
+ });
209
+
210
+ const saveDeleteSchema = z.strictObject({ slot: SlotName });
211
+
212
+ const playFields = {
213
+ resource: NonEmpty,
214
+ volume: Finite.optional(),
215
+ };
216
+ const bgmSchema = z.strictObject({
217
+ ...playFields,
218
+ loop: z.boolean().optional(),
219
+ fade: Fade.optional(),
220
+ restart: z.boolean().optional(),
221
+ });
222
+ const seSchema = z.strictObject({ ...playFields });
223
+ const voiceSchema = z.strictObject({
224
+ ...playFields,
225
+ auto_stop: z.boolean().optional(),
226
+ restart: z.boolean().optional(),
227
+ });
228
+ const stopSchema = z.strictObject({ fade: Fade.optional() });
229
+
230
+ const videoSchema = z.strictObject({
231
+ ...playFields,
232
+ loop: z.boolean().optional(),
233
+ z: InstanceZ, // video 层实例 z
234
+ });
235
+ const cutsceneSchema = z.strictObject({
236
+ ...playFields,
237
+ skipable: z.boolean().optional(),
238
+ z: InstanceZ, // video 层实例 z
239
+ });
240
+ const seekVideoSchema = z.strictObject({
241
+ seconds: z.number().finite().min(0),
242
+ });
243
+ const videoSkipableSchema = z.strictObject({ value: z.boolean() });
244
+
245
+ /** minigame:config 原样透传 UI(任意 JSON);reward 键值数组(value 执行期求值 → 标量/{expr}) */
246
+ const minigameSchema = z.strictObject({
247
+ game: NonEmpty,
248
+ config: z.record(z.string(), z.unknown()).optional(),
249
+ on_success: NonEmpty.optional(),
250
+ on_fail: NonEmpty.optional(),
251
+ reward: z.array(z.strictObject({ key: NonEmpty, value: Value })).optional(),
252
+ z: InstanceZ, // minigame 层实例 z
253
+ });
254
+
255
+ /** 外部玩法系统接管(与 minigame 同形;无 reward——那是小游戏专属语义) */
256
+ const interactionSchema = z.strictObject({
257
+ system: NonEmpty,
258
+ config: z.record(z.string(), z.unknown()).optional(),
259
+ on_success: NonEmpty.optional(),
260
+ on_fail: NonEmpty.optional(),
261
+ z: InstanceZ, // 与 minigame 同层(外部系统整屏接管)
262
+ });
263
+
264
+ // ====== 元素系统(元素增删改 + 表现类)======
265
+ // 字段面以执行器为权威(runtime/ops/element/visual.ts 的 ELEMENT_OP_FIELDS);x/y 允许数字或 CSS 长度串。
266
+
267
+ /** 元素增删改(`show` / `hide` / `background` / `bg_switch` / `zindex` / `style` / `window`) */
268
+ const showSchema = z.strictObject({
269
+ target: NonEmpty,
270
+ x: z.union([Finite, z.string()]).optional(),
271
+ y: z.union([Finite, z.string()]).optional(),
272
+ id: NonEmpty.optional(),
273
+ name: NonEmpty.optional(),
274
+ background: z.boolean().optional(),
275
+ });
276
+ const hideSchema = z.strictObject({ target: NonEmpty });
277
+ const backgroundSchema = z.strictObject({ resource: NonEmpty });
278
+ const bgSwitchSchema = z.strictObject({ resource: NonEmpty });
279
+ const zindexSchema = z.strictObject({ target: NonEmpty, value: Finite });
280
+ const styleSchema = z.strictObject({
281
+ target: NonEmpty,
282
+ props: z.record(z.string(), Value),
283
+ });
284
+ const windowSchema = z.strictObject({ mode: z.enum(["auto", "show", "hide"]) });
285
+
286
+ /** 帧驱动表现类(`animate` / `animate_block` / `transition` / `shake` / `text_typewriter`) */
287
+ const animateSchema = z.strictObject({
288
+ target: NonEmpty,
289
+ property: NonEmpty,
290
+ value: Finite,
291
+ duration: Fade.optional(),
292
+ easing: NonEmpty.optional(),
293
+ });
294
+ const animateBlockSchema = z.strictObject({
295
+ target: NonEmpty,
296
+ x: Finite.optional(),
297
+ y: Finite.optional(),
298
+ opacity: Finite.optional(),
299
+ rotation: Finite.optional(),
300
+ scale: Finite.optional(),
301
+ duration: Fade.optional(),
302
+ easing: NonEmpty.optional(),
303
+ });
304
+ const transitionSchema = z.strictObject({
305
+ type: NonEmpty,
306
+ duration: Fade.optional(),
307
+ });
308
+ const shakeSchema = z.strictObject({
309
+ intensity: Finite.optional(),
310
+ duration: Fade.optional(),
311
+ });
312
+ const textTypewriterSchema = z.strictObject({
313
+ enabled: z.boolean().optional(),
314
+ speed: z.number().finite().positive().optional(),
315
+ });
316
+
317
+ export {
318
+ saySchema,
319
+ menuSchema,
320
+ inputSchema,
321
+ notifySchema,
322
+ nvlSchema,
323
+ characterSchema,
324
+ uncharacterSchema,
325
+ waitSchema,
326
+ pauseSchema,
327
+ randomSchema,
328
+ jumpSchema,
329
+ navigateSchema,
330
+ callSchema,
331
+ returnSchema,
332
+ funcSchema,
333
+ ifSchema,
334
+ whileSchema,
335
+ assertSchema,
336
+ guardSchema,
337
+ forSchema,
338
+ foreachSchema,
339
+ switchSchema,
340
+ emptySchema,
341
+ assignSchema,
342
+ undefSchema,
343
+ arraySchema,
344
+ arrayPushSchema,
345
+ arrayPopSchema,
346
+ dictSchema,
347
+ dictSetSchema,
348
+ saveSchema,
349
+ loadSchema,
350
+ autoSaveSchema,
351
+ saveDeleteSchema,
352
+ bgmSchema,
353
+ seSchema,
354
+ voiceSchema,
355
+ stopSchema,
356
+ videoSchema,
357
+ cutsceneSchema,
358
+ seekVideoSchema,
359
+ videoSkipableSchema,
360
+ minigameSchema,
361
+ interactionSchema,
362
+ showSchema,
363
+ hideSchema,
364
+ backgroundSchema,
365
+ bgSwitchSchema,
366
+ zindexSchema,
367
+ styleSchema,
368
+ windowSchema,
369
+ animateSchema,
370
+ animateBlockSchema,
371
+ transitionSchema,
372
+ shakeSchema,
373
+ textTypewriterSchema,
374
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * op 负载共享片段:被多条命令 schema 复用的最小判定单元,一处定义防漂移。
3
+ */
4
+ import { z } from "zod";
5
+
6
+ const NonEmpty = z.string().min(1);
7
+ const SlotName = z
8
+ .string()
9
+ .regex(/^[A-Za-z0-9_-]{1,64}$/, "槽位名必须为字母数字/_/-,1..64");
10
+ const Value = z.union([z.string(), z.number(), z.boolean()]);
11
+ /**
12
+ * 状态类 op 的值域 = 共享 Value + null:状态槽允许「空值」占位
13
+ * (初始未定 / 条件性覆盖),变量销毁走 undef(undefined 永不入值域)。
14
+ * 不并入 Value:say/nvl 等叙事字段与 call/return/switch 等共享面
15
+ * 保持标量域,null 只属于状态槽位(set/define/let/local/array 族/dict 族)。
16
+ */
17
+ const StateValue = z.union([z.string(), z.number(), z.boolean(), z.null()]);
18
+ const Body = z.array(z.unknown());
19
+ const Finite = z.number().finite();
20
+ const Fade = z.number().finite().min(0);
21
+ /** 实例级 z:非负有限数;仅拥有独立渲染层的命令接受 */
22
+ const InstanceZ = z.number().finite().min(0).optional();
23
+
24
+ export { NonEmpty, SlotName, Value, StateValue, Body, Finite, Fade, InstanceZ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * op 全集 schema(Zod 单一事实源):一份定义同时驱动属性面板表单派生与
3
+ * 编辑期 fail-closed 校验。字段面以执行器为权威(packages/core/src/runtime/ops/):
4
+ * 未知 op / 未知负载字段 / 缺失必填 / 类型错误在编辑期标红,错误不过夜。
5
+ * 块体字段(then/body/…)按 z.unknown() 数组承载——嵌套命令的校验与指针定位由
6
+ * validateStory 沿表单描述符递归(嵌套命令才拿得到逐条 unknown-op 诊断)。
7
+ */
8
+ export { OP_SCHEMAS } from "./map.ts";
9
+ export type {
10
+ ScriptOpName,
11
+ CommandOf,
12
+ ScriptCommand,
13
+ ScriptValue,
14
+ StateScriptValue,
15
+ } from "./types.ts";
16
+ export { validateCommand } from "./validate.ts";
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+ import {
3
+ saySchema,
4
+ menuSchema,
5
+ inputSchema,
6
+ notifySchema,
7
+ nvlSchema,
8
+ characterSchema,
9
+ uncharacterSchema,
10
+ waitSchema,
11
+ pauseSchema,
12
+ randomSchema,
13
+ jumpSchema,
14
+ navigateSchema,
15
+ funcSchema,
16
+ callSchema,
17
+ returnSchema,
18
+ ifSchema,
19
+ whileSchema,
20
+ forSchema,
21
+ foreachSchema,
22
+ switchSchema,
23
+ emptySchema,
24
+ assertSchema,
25
+ guardSchema,
26
+ assignSchema,
27
+ undefSchema,
28
+ arraySchema,
29
+ arrayPushSchema,
30
+ arrayPopSchema,
31
+ dictSchema,
32
+ dictSetSchema,
33
+ saveSchema,
34
+ loadSchema,
35
+ autoSaveSchema,
36
+ saveDeleteSchema,
37
+ bgmSchema,
38
+ stopSchema,
39
+ seSchema,
40
+ voiceSchema,
41
+ videoSchema,
42
+ cutsceneSchema,
43
+ seekVideoSchema,
44
+ videoSkipableSchema,
45
+ minigameSchema,
46
+ interactionSchema,
47
+ showSchema,
48
+ hideSchema,
49
+ backgroundSchema,
50
+ bgSwitchSchema,
51
+ zindexSchema,
52
+ styleSchema,
53
+ windowSchema,
54
+ animateSchema,
55
+ animateBlockSchema,
56
+ transitionSchema,
57
+ shakeSchema,
58
+ textTypewriterSchema,
59
+ } from "./commands.ts";
60
+
61
+ /** op → 负载 schema(不含 op 键本身);字面量键形态 = `ScriptCommand` 判别联合的派生源。
62
+ * 新增 op = 加条目 = 表单 / 校验 / 类型三面自动出现(单一事实源,零重复定义) */
63
+ export const OP_SCHEMA_MAP = {
64
+ say: saySchema,
65
+ menu: menuSchema,
66
+ input: inputSchema,
67
+ notify: notifySchema,
68
+ nvl: nvlSchema,
69
+ character: characterSchema,
70
+ uncharacter: uncharacterSchema,
71
+ wait: waitSchema,
72
+ pause: pauseSchema,
73
+ random: randomSchema,
74
+ jump: jumpSchema,
75
+ navigate: navigateSchema,
76
+ func: funcSchema,
77
+ call: callSchema,
78
+ return: returnSchema,
79
+ if: ifSchema,
80
+ while: whileSchema,
81
+ for: forSchema,
82
+ foreach: foreachSchema,
83
+ switch: switchSchema,
84
+ break: emptySchema,
85
+ continue: emptySchema,
86
+ assert: assertSchema,
87
+ guard: guardSchema,
88
+ set: assignSchema,
89
+ define: assignSchema,
90
+ let: assignSchema,
91
+ local: assignSchema,
92
+ undef: undefSchema,
93
+ array: arraySchema,
94
+ array_push: arrayPushSchema,
95
+ array_pop: arrayPopSchema,
96
+ dict: dictSchema,
97
+ dict_set: dictSetSchema,
98
+ save: saveSchema,
99
+ load: loadSchema,
100
+ auto_save: autoSaveSchema,
101
+ save_delete: saveDeleteSchema,
102
+ bgm: bgmSchema,
103
+ stop_bgm: stopSchema,
104
+ se: seSchema,
105
+ ambient: bgmSchema,
106
+ stop_ambient: stopSchema,
107
+ voice: voiceSchema,
108
+ stop_voice: stopSchema,
109
+ video: videoSchema,
110
+ cutscene: cutsceneSchema,
111
+ seek_video: seekVideoSchema,
112
+ pause_video: emptySchema,
113
+ resume_video: emptySchema,
114
+ stop_video: emptySchema,
115
+ video_skipable: videoSkipableSchema,
116
+ minigame: minigameSchema,
117
+ interaction: interactionSchema,
118
+ // 元素系统
119
+ show: showSchema,
120
+ hide: hideSchema,
121
+ background: backgroundSchema,
122
+ bg_switch: bgSwitchSchema,
123
+ zindex: zindexSchema,
124
+ style: styleSchema,
125
+ window: windowSchema,
126
+ animate: animateSchema,
127
+ animate_block: animateBlockSchema,
128
+ transition: transitionSchema,
129
+ shake: shakeSchema,
130
+ text_typewriter: textTypewriterSchema,
131
+ };
132
+
133
+ /** 运行期查找面(与 `OP_SCHEMA_MAP` 同一对象,键集恒等);新增 op 自动可用 */
134
+ export const OP_SCHEMAS: Readonly<Record<string, z.ZodType>> = OP_SCHEMA_MAP;
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ import { OP_SCHEMA_MAP } from "./map.ts";
3
+ import { Value, StateValue } from "./fragments.ts";
4
+
5
+ // ====== 类型派生(与 schema 同源 ⇒ 字段面零漂移)======
6
+
7
+ /** 内建 op 名字面量全集 */
8
+ export type ScriptOpName = keyof typeof OP_SCHEMA_MAP & string;
9
+
10
+ /**
11
+ * 单 op 命令类型:`CommandOf<"bgm">` = `{ op: "bgm" } & 负载形状`。
12
+ * 派生自 `OP_SCHEMA_MAP` ⇒ schema 增删字段时类型自动跟随。
13
+ *
14
+ * **条件类型必须保持可分发**(naked `K extends`):不加分发时
15
+ * `CommandOf<ScriptOpName>` 会折叠成 `{ op: 全体字面量 } & (全体 infer 联合)`——
16
+ * 全可选负载(如 nvl)成为逃生舱,缺必填不再报红。
17
+ * 空 schema(strictObject({}) 的 infer = `Record<string, never>`,会与 op 键冲突)
18
+ * 特判为纯 `{ op: K }`。
19
+ */
20
+ export type CommandOf<K extends ScriptOpName> = K extends ScriptOpName
21
+ ? z.infer<(typeof OP_SCHEMA_MAP)[K]> extends Record<string, never>
22
+ ? { op: K }
23
+ : { op: K } & z.infer<(typeof OP_SCHEMA_MAP)[K]>
24
+ : never;
25
+
26
+ /**
27
+ * **内建 op 判别联合**(裸写命令的强类型形态):`op` 决定可用字段——
28
+ * `op: "say"` ⇒ text/speaker/z… 编译期可查;未知 op / 未知字段 / 类型错编译期报红
29
+ * (与编辑期 `validateCommand` 的 unknown-op / unknown-field 同口径,提前到构建期)。
30
+ *
31
+ * **对内建 op 闭合,不带索引签名逃生舱**:联合里任何 `{[k: string]: unknown}`
32
+ * 成员都会让所有字面量可指派到它,废掉整个联合的多余属性检查。
33
+ * 扩展 op 走 `extOp()`(产物 = StoryCommand envelope)或显式 `: StoryCommand`
34
+ * 注解——显式逃生 > 隐式漏洞。
35
+ */
36
+ export type ScriptCommand = CommandOf<ScriptOpName>;
37
+
38
+ /** op 负载标量值(字面量 / `{expr}` 表达式串 / `+=` 复合赋值串——与 zod `Value` 同源) */
39
+ export type ScriptValue = z.infer<typeof Value>;
40
+
41
+ /** 状态类 op 负载值(共享 Value + null——与 zod `StateValue` 同源) */
42
+ export type StateScriptValue = z.infer<typeof StateValue>;