@lingfan-engine/editor 0.0.0-stage → 0.1.2

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,261 @@
1
+ /**
2
+ * Script 词汇层 · **叙事域**(say / menu / input / notify / nvl / character)。
3
+ * 产物 = StoryCommand 数据(与 JSON 同族);形状知识只在此处,作者只说人话。
4
+ * 返回类型 = `CommandOf<op>`(派生自 OP_SCHEMA_MAP,与 validateCommand 同源)——
5
+ * builder 形状漂移(漏字段 / 错字段)编译期报红,不再只靠互锁测试兜底。
6
+ */
7
+ import type { CommandOf } from "../../schema/index.ts";
8
+ import type { KnownColumnName, KnownInterpolationKey } from "./cells.ts";
9
+
10
+ // —— `{key}` 插值键的编译期校验(纯类型层,零运行期成本)——
11
+ //
12
+ // 只在**字符串字面量**上生效:模板串、变量、宽 string 全部放行(动态文本没法查,
13
+ // 也不该吓唬作者);键表来自生成物 `Stories.src/gen/stories.gen.d.ts`。
14
+ // 富文本行内标记必须豁免——`{b}`/`{color=#FFD700}` 这类与插值共用一个花括号语法,
15
+ // 豁免表对齐引擎 `runtime/expr/interpolate.ts` 的行内标记集。
16
+ type TrimText<S extends string> = S extends ` ${infer R}`
17
+ ? TrimText<R>
18
+ : S extends `${infer R} `
19
+ ? TrimText<R>
20
+ : S;
21
+
22
+ type InlineTextTag =
23
+ | "b"
24
+ | "/b"
25
+ | "i"
26
+ | "/i"
27
+ | "u"
28
+ | "/u"
29
+ | "w"
30
+ | "fast"
31
+ | "p"
32
+ | `color=${string}`
33
+ | "/color"
34
+ | `font=${string}`
35
+ | "/font"
36
+ | `size=${string}`
37
+ | "/size";
38
+
39
+ /** `{key:00}` 的 `:格式` 后缀先剥掉再查表 */
40
+ type StripTextFormat<S extends string> = S extends `${infer K}:${string}`
41
+ ? TrimText<K>
42
+ : TrimText<S>;
43
+
44
+ /** 查表失败时把这个键留在参数类型里,报错信息即可读出是哪个键 */
45
+ type BadInterpolationKey<K extends string> = {
46
+ readonly __插值键未登记: K;
47
+ };
48
+
49
+ type CheckTextAt<
50
+ S extends string,
51
+ Whole extends string,
52
+ > = S extends `${string}{${infer Inner}}${infer Rest}`
53
+ ? StripTextFormat<Inner> extends InlineTextTag
54
+ ? CheckTextAt<Rest, Whole>
55
+ : StripTextFormat<Inner> extends KnownInterpolationKey
56
+ ? CheckTextAt<Rest, Whole>
57
+ : BadInterpolationKey<StripTextFormat<Inner>>
58
+ : Whole;
59
+
60
+ /** 文本参数类型:字面量按 `{…}` 逐键校验,非字面量原样放行 */
61
+ export type InterpolatedText<S extends string> = S & CheckTextAt<S, S>;
62
+
63
+ /** say 的可选项(say 负载中除 text / speaker 外的全部字段,均可省略) */
64
+ export type SayOptions = Omit<CommandOf<"say">, "op" | "text" | "speaker">;
65
+
66
+ /**
67
+ * 让角色说一句话:玩家看到一句对话,点击或按键后故事才继续。
68
+ *
69
+ * - 文本先查翻译表,再做 `{var}` 插值(`{var:00}` 保留两位小数);插值失败
70
+ * 保留原文显示并发出错误事件,不中断故事。
71
+ * - 进入等待的这一刻就是回溯检查点;开启 autoSave 时这里是自动存档点。
72
+ * - `color` 只覆盖本句说话人颜色(优先于角色定义);`voice` 播放本句语音,
73
+ * 句子推进后自动停止;`instant` / `typewriter` 控制打字机显示;
74
+ * `template` 指定本句对话框模板(优先于角色定义的 screen)。
75
+ *
76
+ * @param text 台词;可含 `{var}` 插值,在显示时求值。必填非空。
77
+ * 字面量文本的插值键在编译期对表(生成物注册表),拼错即红;富文本行内标记豁免。
78
+ * @param speaker 说话人显示名(同样支持翻译与插值);省略则本句不显示说话人名。
79
+ * @param opts 其余可选项,见 {@link SayOptions}。
80
+ * @returns `say` 故事命令,写进当前列即为一句对话。
81
+ * @example
82
+ * say("欢迎来到 {place}!", "向导");
83
+ * say("上好绸缎,客官……", "商人", { color: "#FFD700", voice: "audio/hi.ogg" });
84
+ */
85
+ export function say<const S extends string>(
86
+ text: InterpolatedText<S>,
87
+ speaker?: string,
88
+ opts?: SayOptions,
89
+ ): CommandOf<"say"> {
90
+ return {
91
+ op: "say",
92
+ text,
93
+ ...(speaker === undefined ? {} : { speaker }),
94
+ ...opts,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * 菜单选项的**命令参数**形态:一项显示文字 + 选中后跳去的目标列名(列名收窄到已知列)。
100
+ *
101
+ * 与运行期的读取面(引擎 `MenuChoice`:文本与目标列成对)不是同一条链:这里供作者写脚本,
102
+ * 目标列名会在生成物存在时收窄成字面量联合,故单独命名,避免与读取面的同形状类型混用。
103
+ */
104
+ export type MenuOptionSpec = CommandOf<"menu">["options"][number];
105
+
106
+ /**
107
+ * 构造一个菜单选项,配合 {@link menu} 使用:玩家点中这一项后,
108
+ * 故事跳进 `target` 指向的那一列(列名即标签)继续。
109
+ * @param text 选项上显示的文字(支持翻译与插值)。
110
+ * @param target 选中后进入的目标列名(`stories.gen.d.ts` 存在时收窄为列名字面量联合)。
111
+ * @returns 菜单选项数据。
112
+ * @example
113
+ * menu("去哪儿?", [
114
+ * option("酒馆", "to_inn"),
115
+ * option("广场", "to_square"),
116
+ * ]);
117
+ */
118
+ export function option(text: string, target: KnownColumnName): MenuOptionSpec {
119
+ return { text, target };
120
+ }
121
+
122
+ /**
123
+ * 弹出一张选项菜单:玩家看到 `prompt`(可省略)与若干选项,选中一项后
124
+ * 故事跳进该选项的目标列继续。
125
+ *
126
+ * - 选项文字与提示语都走翻译表;等待期间建立回溯检查点。
127
+ * - 菜单出现前会清掉上一句对话的残留画面。
128
+ *
129
+ * @param prompt 菜单上方的问题/提示语;省略则只显示选项。
130
+ * @param options 选项列表(至少一项),用 {@link option} 构造。
131
+ * @returns `menu` 故事命令。
132
+ * @example
133
+ * menu("先做什么?", [
134
+ * option("买药水", "shop"),
135
+ * option("打听消息", "gossip"),
136
+ * ]);
137
+ */
138
+ export function menu(
139
+ prompt: string | undefined,
140
+ options: readonly MenuOptionSpec[],
141
+ ): CommandOf<"menu"> {
142
+ return {
143
+ op: "menu",
144
+ ...(prompt === undefined ? {} : { prompt }),
145
+ options: [...options],
146
+ };
147
+ }
148
+
149
+ /**
150
+ * 向玩家要一段文字:弹出输入框显示 `prompt`,玩家提交后,输入内容写进
151
+ * 状态键 `store`,故事从下一句继续。
152
+ *
153
+ * 等待期间建立回溯检查点;开启 autoSave 时此处自动存档。
154
+ *
155
+ * @param prompt 输入框上方显示的问题/提示语(必填非空)。
156
+ * @param store 收纳玩家输入的状态变量名(写进全局状态,之后可用 `{store}` 插值显示)。
157
+ * @returns `input` 故事命令。
158
+ * @example
159
+ * input("主角叫什么名字?", "playerName");
160
+ * say("你好,{playerName}!");
161
+ */
162
+ export function input(prompt: string, store: string): CommandOf<"input"> {
163
+ return { op: "input", prompt, store };
164
+ }
165
+
166
+ /** type 保持手写的窄联合(schema 是 string)——作者侧补全优于校验面放宽 */
167
+ export interface NotifyOptions {
168
+ type?: "info" | "success" | "warning" | "error";
169
+ duration?: number;
170
+ z?: number;
171
+ }
172
+
173
+ /**
174
+ * 弹一条系统通知(toast):在画面上短暂显示 `text` 后自动消失,
175
+ * 故事不等待、立即执行下一句。
176
+ *
177
+ * - 通知是一次性画面效果,不进存档/回溯状态。
178
+ * - 文本先查翻译表再做 `{var}` 插值(同 {@link say})。
179
+ *
180
+ * @param text 通知内容(必填非空);插值键同 {@link say} 在编译期对表。
181
+ * @param opts 可选项:type 决定样式(info/success/warning/error,缺省 info)、
182
+ * duration 控制停留时长、z 控制层级。
183
+ * @returns `notify` 故事命令。
184
+ * @example
185
+ * notify("已保存", { type: "success" });
186
+ */
187
+ export function notify<const S extends string>(
188
+ text: InterpolatedText<S>,
189
+ opts?: NotifyOptions,
190
+ ): CommandOf<"notify"> {
191
+ return { op: "notify", text, ...opts };
192
+ }
193
+
194
+ /**
195
+ * NVL 命令的模式参数:作者在脚本里写的动作(`enter` 进入累积层 / `auto` 跟随 / `clear` 清屏 / `exit` 退出)。
196
+ *
197
+ * 与引擎的 `NvlMode`(`none` / `active` / `clear` / `exit`)**不是同一套取值**:那是运行期
198
+ * `__nvl_mode` 的状态值,这里是命令参数,两者语义不同、不可互换。
199
+ */
200
+ export type NvlCommandMode = NonNullable<CommandOf<"nvl">["mode"]>;
201
+
202
+ /**
203
+ * 切换 NVL(整屏文本)模式。本命令立即生效、故事继续,不等待。
204
+ *
205
+ * - `"enter"`(缺省):激活 NVL,此后每句 {@link say} 的文本都会累积
206
+ * 显示在整屏文本框里,直到退出。
207
+ * - `"auto"`:同 enter,激活累积层。
208
+ * - `"clear"`:清空已累积的文本,但仍处于 NVL 模式。
209
+ * - `"exit"`:清空并退出 NVL,回到普通对话框显示。
210
+ *
211
+ * 累积文本属于核心状态:回溯、存档/读档自动保持一致。
212
+ *
213
+ * @param mode NVL 动作,缺省 `"enter"`。
214
+ * @returns `nvl` 故事命令。
215
+ * @example
216
+ * nvl(); // 进入整屏文本模式
217
+ * say("第一段……");
218
+ * nvl("clear"); // 清屏,继续整屏
219
+ * nvl("exit"); // 退出整屏,恢复对话框
220
+ */
221
+ export function nvl(mode?: NvlCommandMode): CommandOf<"nvl"> {
222
+ return { op: "nvl", ...(mode === undefined ? {} : { mode }) };
223
+ }
224
+
225
+ export type CharacterOptions = Omit<CommandOf<"character">, "op" | "key">;
226
+
227
+ /**
228
+ * 注册(或更新)一名角色:说话人名字、颜色、字号、字体、文字颜色、
229
+ * 对话框模板。立即生效、故事继续,不等待。
230
+ *
231
+ * 注意:对同一 `key` 重复调用会**整体替换**定义——本次没传的字段
232
+ * 会回到无定义状态,而不是保留上次值。
233
+ *
234
+ * @param key 角色键(say 的 speaker 名按它查表取样式)。
235
+ * @param opts 角色属性:name 显示名、color 说话人颜色、size 字号、
236
+ * font 字体、textColor 台词文字颜色、screen 对话框模板。
237
+ * @returns `character` 故事命令。
238
+ * @example
239
+ * character("向导", { name: "阿岚", color: "#7FD4FF" });
240
+ * say("跟上我。", "向导");
241
+ */
242
+ export function character(
243
+ key: string,
244
+ opts?: CharacterOptions,
245
+ ): CommandOf<"character"> {
246
+ return { op: "character", key, ...opts };
247
+ }
248
+
249
+ /**
250
+ * 注销一名角色:之后以该 `key` 作说话人的 say 不再套角色样式。
251
+ * 键不存在时发 `character.missing` 知情事件(故事继续,不停机)——
252
+ * 创作者可在宿主的事件回调里自行处置(记日志/提示等)。
253
+ * 注销会随回溯自然回滚(回到注册后的检查点即恢复角色)。
254
+ * @param key 角色键(与 character 的 key 同一面)。
255
+ * @returns `uncharacter` 故事命令。
256
+ * @example
257
+ * uncharacter("向导");
258
+ */
259
+ export function uncharacter(key: string): CommandOf<"uncharacter"> {
260
+ return { op: "uncharacter", key };
261
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Script 词汇层 · **存档域**(save / load / auto_save / save_delete)。
3
+ */
4
+ import type { CommandOf } from "../../schema/index.ts";
5
+
6
+ /**
7
+ * 声明一个存档点:把「存进槽位 `slot`」记下来,等到**下一个玩家所见
8
+ * 等待画面**(对话/菜单/输入等)出现时才真正写盘——保证存档画面
9
+ * 可重放重建。声明本身立即继续,不等待。
10
+ * @param slot 槽位名(字母/数字/_/-,1–64 字符)。
11
+ * @param title 存档显示名(可省略)。
12
+ * @returns `save` 故事命令。
13
+ * @example
14
+ * say("要休息了吗?");
15
+ * save("inn", "客栈过夜");
16
+ */
17
+ export function save(slot: string, title?: string): CommandOf<"save"> {
18
+ return { op: "save", slot, ...(title === undefined ? {} : { title }) };
19
+ }
20
+
21
+ /**
22
+ * 读档传送:从槽位 `slot` 异步读档,画面与状态整体回到存档时刻,
23
+ * 故事从存档时的等待画面继续。
24
+ * @param slot 槽位名。
25
+ * @returns `load` 故事命令。
26
+ * @example
27
+ * menu("回到存档点?", [option("是", "do_load"), option("否", "stay")]);
28
+ */
29
+ export function load(slot: string): CommandOf<"load"> {
30
+ return { op: "load", slot };
31
+ }
32
+
33
+ /**
34
+ * 开关自动存档:开启后,每个玩家所见等待画面建立时都会自动写入
35
+ * 自动存档槽。开关本身随存档/读档一并恢复。
36
+ * @param enabled true = 开启自动存档。
37
+ * @returns `auto_save` 故事命令。
38
+ * @example
39
+ * autoSave(true);
40
+ */
41
+ export function autoSave(enabled: boolean): CommandOf<"auto_save"> {
42
+ return { op: "auto_save", enabled };
43
+ }
44
+
45
+ /**
46
+ * 删除槽位 `slot` 的存档(异步执行;不影响回档基准)。
47
+ * @param slot 槽位名。
48
+ * @returns `save_delete` 故事命令。
49
+ * @example
50
+ * saveDelete("inn");
51
+ */
52
+ export function saveDelete(slot: string): CommandOf<"save_delete"> {
53
+ return { op: "save_delete", slot };
54
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Script 词汇层 · **变量域**(set / define / let / local / undef / array / dict)。
3
+ * value 参数 = 引擎 StateValue(字面量 / null / `{expr}` 表达式串 / `+=` 复合赋值串——
4
+ * 口径同 JSON v1;状态槽允许 null 空值,销毁走 undef)。
5
+ */
6
+ import type { CommandOf, StateScriptValue } from "../../schema/index.ts";
7
+
8
+ /**
9
+ * 把值写进状态变量:沿作用域链找到 `key` 已声明的层写入;
10
+ * 找不到声明层就写进全局状态(全局变量随快照/存档/回溯走)。
11
+ *
12
+ * - `value` 传字面量直接存;传 `"{expr}"` 表达式串则在运行到这里时求值。
13
+ * - 支持复合赋值串 `"+= 1"` / `"-= 2"` / `"*= 3"` / `"/= 4"` / `"%= 5"`
14
+ * (只对数字有意义;除以 0 直接报错停机)。
15
+ *
16
+ * @param key 变量名(之后用 `{key}` 插值显示)。
17
+ * @param value 字面量(含 null 空值)或 `"{expr}"` 表达式串。
18
+ * @returns `set` 故事命令。
19
+ * @example
20
+ * set("gold", 100);
21
+ * set("medal", null); // 状态槽空值占位
22
+ * set("gold", "{gold + 50}"); // 表达式在运行期求值
23
+ * set("gold", "+= 10");
24
+ */
25
+ export function set(key: string, value: StateScriptValue): CommandOf<"set"> {
26
+ return { op: "set", key, value };
27
+ }
28
+
29
+ /**
30
+ * 声明全局变量的初值:**只在变量不存在时生效**(重复执行/重复经过
31
+ * 不会覆盖已有值)。适合做关卡开场的默认设定。
32
+ * @param key 全局变量名。
33
+ * @param value 初值(字面量或 `"{expr}"` 表达式串)。
34
+ * @returns `define` 故事命令。
35
+ * @example
36
+ * define("visitedInn", false);
37
+ */
38
+ export function define(key: string, value: StateScriptValue): CommandOf<"define"> {
39
+ return { op: "define", key, value };
40
+ }
41
+
42
+ /**
43
+ * 在**当前最内层作用域**声明一个临时变量(块/列级):出了这段作用域
44
+ * 自动销毁,不进存档。适合循环计数、分支内临时值。
45
+ * @param key 变量名。
46
+ * @param value 初值(字面量或 `"{expr}"` 表达式串)。
47
+ * @returns `let` 故事命令。
48
+ * @example
49
+ * letVar("i", 0);
50
+ */
51
+ export function letVar(key: string, value: StateScriptValue): CommandOf<"let"> {
52
+ return { op: "let", key, value };
53
+ }
54
+
55
+ /**
56
+ * 同 {@link letVar}:在当前最内层作用域声明临时变量(不进存档)。
57
+ * @param key 变量名。
58
+ * @param value 初值(字面量或 `"{expr}"` 表达式串)。
59
+ * @returns `local` 故事命令。
60
+ * @example
61
+ * localVar("tmp", "{gold * 2}");
62
+ */
63
+ export function localVar(key: string, value: StateScriptValue): CommandOf<"local"> {
64
+ return { op: "local", key, value };
65
+ }
66
+
67
+ /**
68
+ * 销毁变量(沿作用域链与全局层查找)。变量不存在时故事报错停机
69
+ * (fail-closed),便于暴露拼写错误。
70
+ * @param key 要销毁的变量名。
71
+ * @returns `undef` 故事命令。
72
+ */
73
+ export function undef(key: string): CommandOf<"undef"> {
74
+ return { op: "undef", key };
75
+ }
76
+
77
+ /**
78
+ * 创建全局数组:`items` 逐项求值(项可为 `"{expr}"`)后存进全局状态。
79
+ * `once: true` 时数组已存在则跳过,不覆盖(幂等声明)。
80
+ * @param key 数组变量名。
81
+ * @param items 初始元素列表。
82
+ * @param opts 可选项:once 已存在则跳过。
83
+ * @returns `array` 故事命令。
84
+ * @example
85
+ * newArray("party", ["alice", "bob"], { once: true });
86
+ */
87
+ export function newArray(
88
+ key: string,
89
+ items: readonly StateScriptValue[],
90
+ opts?: { once?: boolean },
91
+ ): CommandOf<"array"> {
92
+ return { op: "array", key, items: [...items], ...opts };
93
+ }
94
+
95
+ /**
96
+ * 向**已存在**的数组末尾追加一个值(值可为 `"{expr}"`)。目标不存在或
97
+ * 不是数组时故事报错停机。逐个压入:多件就多次调用。
98
+ * @param key 目标数组变量名。
99
+ * @param value 要追加的值。
100
+ * @returns `array_push` 故事命令。
101
+ * @example
102
+ * arrayPush("party", "carol");
103
+ */
104
+ export function arrayPush(
105
+ key: string,
106
+ value: StateScriptValue,
107
+ ): CommandOf<"array_push"> {
108
+ return { op: "array_push", key, value };
109
+ }
110
+
111
+ /**
112
+ * 移除数组末尾一个元素(移除即丢弃)。目标不存在、非数组或已空时
113
+ * 故事报错停机。
114
+ * @param key 目标数组变量名。
115
+ * @returns `array_pop` 故事命令。
116
+ */
117
+ export function arrayPop(key: string): CommandOf<"array_pop"> {
118
+ return { op: "array_pop", key };
119
+ }
120
+
121
+ /**
122
+ * 创建全局字典(JSON 对象:字段名 → 值,值可为 `"{expr}"`)。
123
+ * `once: true` 时字典已存在则跳过,不覆盖。
124
+ * @param key 字典变量名。
125
+ * @param record 字段与初值的映射。
126
+ * @param opts 可选项:once 已存在则跳过。
127
+ * @returns `dict` 故事命令。
128
+ * @example
129
+ * dict("hero", { hp: 30, mp: 10 }, { once: true });
130
+ */
131
+ export function dict(
132
+ key: string,
133
+ record: Record<string, StateScriptValue>,
134
+ opts?: { once?: boolean },
135
+ ): CommandOf<"dict"> {
136
+ return { op: "dict", key, value: { ...record }, ...opts };
137
+ }
138
+
139
+ /**
140
+ * 修改字典的单个字段(值可为 `"{expr}"`)。目标不存在或不是字典时
141
+ * 故事报错停机。
142
+ * @param key 目标字典变量名。
143
+ * @param field 字段名。
144
+ * @param value 新值。
145
+ * @returns `dict_set` 故事命令。
146
+ * @example
147
+ * dictSet("hero", "hp", "{hero.hp - 5}");
148
+ */
149
+ export function dictSet(
150
+ key: string,
151
+ field: string,
152
+ value: StateScriptValue,
153
+ ): CommandOf<"dict_set"> {
154
+ return { op: "dict_set", key, field, value };
155
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Script 词汇层 · **视频域**(video / cutscene / seek / pause / resume / stop / skipable)。
3
+ * Options 派生自 schema(零漂移);返回类型 = `CommandOf<op>` 判别联合成员。
4
+ */
5
+ import type { CommandOf } from "../../schema/index.ts";
6
+
7
+ /** video / cutscene 共用的可选项:volume 音量、loop 循环、z 视频层层级 */
8
+ export type VideoOptions = Omit<CommandOf<"video">, "op" | "resource">;
9
+
10
+ /**
11
+ * 播放视频(非阻塞):画面切到视频层,故事**立即继续**执行下一句,
12
+ * 视频在背景里继续播。
13
+ * @param resource 视频资源路径。
14
+ * @param opts 可选项:volume 音量、loop 循环、z 层级。
15
+ * @returns `video` 故事命令。
16
+ * @example
17
+ * video("video/opening.mp4");
18
+ */
19
+ export function video(
20
+ resource: string,
21
+ opts?: VideoOptions,
22
+ ): CommandOf<"video"> {
23
+ return { op: "video", resource, ...opts };
24
+ }
25
+
26
+ export type CutsceneOptions = Omit<CommandOf<"cutscene">, "op" | "resource">;
27
+
28
+ /**
29
+ * 播放过场(阻塞):故事**等待**视频播完(或被玩家跳过)才继续。
30
+ * 过场开始时建立回溯检查点;开启 autoSave 时此处自动存档。
31
+ *
32
+ * 可跳过性:显式传 `skipable` 优先;否则用 {@link videoSkipable} 的全局
33
+ * 开关;两者都未设置时缺省可跳过。
34
+ *
35
+ * @param resource 视频资源路径。
36
+ * @param opts 可选项:volume 音量、loop 循环、skipable 可点击跳过、z 层级。
37
+ * @returns `cutscene` 故事命令。
38
+ * @example
39
+ * cutscene("video/intro.mp4", { skipable: true });
40
+ */
41
+ export function cutscene(
42
+ resource: string,
43
+ opts?: CutsceneOptions,
44
+ ): CommandOf<"cutscene"> {
45
+ return { op: "cutscene", resource, ...opts };
46
+ }
47
+
48
+ /**
49
+ * 把当前视频跳转到第 `seconds` 秒(立即生效,故事继续)。
50
+ * @param seconds 目标时刻(秒)。
51
+ * @returns `seek_video` 故事命令。
52
+ */
53
+ export function seekVideo(seconds: number): CommandOf<"seek_video"> {
54
+ return { op: "seek_video", seconds };
55
+ }
56
+
57
+ /** 暂停当前视频(立即生效,故事继续)。@returns `pause_video` 故事命令。 */
58
+ export function pauseVideo(): CommandOf<"pause_video"> {
59
+ return { op: "pause_video" };
60
+ }
61
+
62
+ /** 继续播放已暂停的视频(立即生效,故事继续)。@returns `resume_video` 故事命令。 */
63
+ export function resumeVideo(): CommandOf<"resume_video"> {
64
+ return { op: "resume_video" };
65
+ }
66
+
67
+ /** 停止并关闭当前视频层(立即生效,故事继续)。@returns `stop_video` 故事命令。 */
68
+ export function stopVideo(): CommandOf<"stop_video"> {
69
+ return { op: "stop_video" };
70
+ }
71
+
72
+ /**
73
+ * 设置「视频可跳过」全局开关(持久生效,影响之后的视频与过场;
74
+ * 过场显式传 skipable 时以其为准)。立即生效、故事继续。
75
+ * @param value true = 允许玩家跳过视频。
76
+ * @returns `video_skipable` 故事命令。
77
+ */
78
+ export function videoSkipable(value: boolean): CommandOf<"video_skipable"> {
79
+ return { op: "video_skipable", value };
80
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 通用取值判据:编辑器包多个域(诊断、i18n、布局)都要对「这个值是不是一条记录」下判断。
3
+ *
4
+ * 为什么放在共享层:判据本身不含业务含义,任何层都可以引用;而它一旦分叉,
5
+ * 分叉点就是静默的行为差异——同一棵树,两处判定不同就会出现「这边认、那边不认」。
6
+ * 所以编辑器包只有这一处实现,缺人少写一个 `!Array.isArray` 就是症状的来源。
7
+ */
8
+
9
+ /**
10
+ * 是否是**普通对象**(JSON 意义上的记录):`null` 与数组都不算。
11
+ *
12
+ * 关键在最后那个 `!Array.isArray`:数组也是 `typeof === "object"`,
13
+ * 少了它就会让数组通过「记录」校验,下游会拿下标当字段名继续走。
14
+ */
15
+ export function isPlainObject(
16
+ value: unknown,
17
+ ): value is Record<string, unknown> {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 共享层出口:没有业务含义、任何域都可以引用的取值判据与小工具。
3
+ * 实现按用途分文件,域外消费方只从这里取。
4
+ */
5
+ export { escapePointerToken, joinPointer } from "./pointer.ts";
6
+ export { isPlainObject } from "./guards.ts";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * JSON Pointer(RFC 6901)的拼装工具:转义单段、拼出整串。
3
+ *
4
+ * 为什么放在共享层:诊断、结构校验、步骤布局都要产出指向 Story 树的 pointer,
5
+ * 契约层只声明「pointer 是一个字符串」,拼法本身是可执行逻辑。
6
+ * 规则只有这一处实现——改口径则全部消费方同步,不会出现半套转义。
7
+ */
8
+
9
+ /**
10
+ * 转义 JSON Pointer 的单段:`~` 写 `~0`,`/` 写 `~1`。
11
+ *
12
+ * 顺序要紧:必须先替换 `~`。反过来的话,先产出的 `~1` 会被随后的 `~` 替换
13
+ * 二次转义成 `~01`,解析回去得到的是另一条路径。
14
+ */
15
+ export function escapePointerToken(token: string | number): string {
16
+ return String(token).replaceAll("~", "~0").replaceAll("/", "~1");
17
+ }
18
+
19
+ /** 由段序列组装 JSON Pointer:没有段即根(空串),否则每段前加 `/` 并逐段转义 */
20
+ export function joinPointer(...segments: (string | number)[]): string {
21
+ if (segments.length === 0) return "";
22
+ return "/" + segments.map(escapePointerToken).join("/");
23
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.