@lingfan-engine/core 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.
Files changed (90) hide show
  1. package/package.json +1 -1
  2. package/src/contracts/guard.ts +9 -0
  3. package/src/contracts/index.ts +1 -0
  4. package/src/contracts/media/entities.ts +85 -3
  5. package/src/contracts/media/ports.ts +48 -0
  6. package/src/contracts/param-direction.ts +103 -0
  7. package/src/contracts/runtime/column-summary.ts +24 -0
  8. package/src/contracts/runtime/controls.ts +49 -0
  9. package/src/contracts/runtime/entities.ts +21 -0
  10. package/src/contracts/runtime/index.ts +2 -0
  11. package/src/contracts/runtime/reader.ts +41 -1
  12. package/src/contracts/runtime/state-keys.ts +70 -2
  13. package/src/contracts/runtime/timer.ts +8 -1
  14. package/src/contracts/save.ts +18 -1
  15. package/src/contracts/shell.ts +26 -0
  16. package/src/contracts/story/entities.ts +23 -8
  17. package/src/data/format/column.ts +33 -4
  18. package/src/data/format/validate/dialog.ts +32 -3
  19. package/src/data/index.ts +1 -0
  20. package/src/data/project/assemble.ts +37 -6
  21. package/src/data/project/conflict.ts +4 -3
  22. package/src/data/project/diff.ts +3 -3
  23. package/src/data/project/index.ts +6 -1
  24. package/src/data/project/naming.ts +11 -5
  25. package/src/data/project/serialize.ts +49 -16
  26. package/src/data/text/entry.ts +17 -9
  27. package/src/data/text/generate/command/dialog.ts +15 -3
  28. package/src/data/text/generate/command/index.ts +2 -1
  29. package/src/data/text/generate/command/media.ts +33 -2
  30. package/src/data/text/generate/command/ui.ts +29 -0
  31. package/src/data/text/generate/command.ts +10 -0
  32. package/src/data/text/literals.ts +140 -0
  33. package/src/data/text/parse/statement/dialog.ts +19 -0
  34. package/src/data/text/parse/statement/flow.ts +1 -1
  35. package/src/data/text/parse/statement/index.ts +2 -1
  36. package/src/data/text/parse/statement/media.ts +91 -2
  37. package/src/data/text/parse/statement/ui.ts +47 -0
  38. package/src/data/text/parse/statement.ts +9 -0
  39. package/src/index.ts +8 -1
  40. package/src/runtime/dispatch.ts +24 -0
  41. package/src/runtime/engine/columns.ts +31 -0
  42. package/src/runtime/engine/forwarders.ts +7 -4
  43. package/src/runtime/engine/game-state.ts +29 -0
  44. package/src/runtime/engine/index.ts +2 -0
  45. package/src/runtime/engine/state.ts +13 -1
  46. package/src/runtime/engine.ts +46 -21
  47. package/src/runtime/expr/builtins.ts +1 -1
  48. package/src/runtime/expr/evaluate.ts +1 -1
  49. package/src/runtime/expr/index.ts +1 -1
  50. package/src/runtime/expr/interpolate.ts +1 -1
  51. package/src/runtime/expr/parser/cursor.ts +1 -1
  52. package/src/runtime/history/index.ts +1 -0
  53. package/src/runtime/history/rollback.ts +23 -8
  54. package/src/runtime/index.ts +9 -3
  55. package/src/runtime/internal/context.ts +25 -2
  56. package/src/runtime/internal/helpers.ts +18 -0
  57. package/src/runtime/internal/index.ts +9 -1
  58. package/src/runtime/internal/session.ts +11 -0
  59. package/src/runtime/internal/waiting.ts +51 -3
  60. package/src/runtime/opRegistry.ts +7 -0
  61. package/src/runtime/ops/dialog.ts +2 -11
  62. package/src/runtime/ops/flow.ts +1 -1
  63. package/src/runtime/ops/index.ts +11 -1
  64. package/src/runtime/ops/interaction.ts +14 -1
  65. package/src/runtime/ops/media/audio.ts +230 -0
  66. package/src/runtime/ops/media/index.ts +18 -0
  67. package/src/runtime/ops/media/instances.ts +312 -0
  68. package/src/runtime/ops/media/notify.ts +32 -0
  69. package/src/runtime/ops/media/shared.ts +133 -0
  70. package/src/runtime/ops/media/video.ts +118 -0
  71. package/src/runtime/ops/ui.ts +95 -0
  72. package/src/runtime/paramDirection.ts +35 -0
  73. package/src/runtime/save/export.ts +52 -6
  74. package/src/runtime/save/import.ts +21 -6
  75. package/src/runtime/scope.ts +17 -0
  76. package/src/runtime/session/index.ts +21 -0
  77. package/src/runtime/session/pause.ts +114 -0
  78. package/src/runtime/{waitingOps.ts → session/waitingOps.ts} +1 -1
  79. package/src/runtime/shell.ts +68 -0
  80. package/src/runtime/state/index.ts +2 -1
  81. package/src/runtime/state/reader.ts +47 -1
  82. package/src/runtime/state/writer.ts +1 -1
  83. package/src/shared/audio.ts +106 -0
  84. package/src/shared/guards.ts +38 -4
  85. package/src/shared/index.ts +3 -0
  86. package/src/shared/mounts.ts +42 -0
  87. package/src/shared/ui-name.ts +41 -0
  88. package/src/runtime/ops/media.ts +0 -274
  89. /package/src/runtime/{resolver.ts → expr/resolver.ts} +0 -0
  90. /package/src/runtime/{stateContract.ts → state/stateContract.ts} +0 -0
@@ -1,274 +0,0 @@
1
- /**
2
- * 媒体命令的执行:音频通道、视频命令流、出站通知。
3
- *
4
- * 这一族的共同点是「核心只写状态,播放交给宿主」——音频写四通道的通道对象,
5
- * 视频写单调递增的命令流,通知只发出站事件。渲染器按状态与事件执行,
6
- * 核心不持有播放器,也不关心播放是否成功。
7
- */
8
- import {
9
- SYS,
10
- type AudioChannelState,
11
- type OutboundEvent,
12
- type OutboundPayload,
13
- type StoryCommand,
14
- type VideoCommand,
15
- } from "../../contracts/index.ts";
16
- import { interpolateText } from "../expr/index.ts";
17
- import type { Frame, OpContext } from "../internal/index.ts";
18
-
19
- /** 音频 op 已知负载字段(未知字段 fail-closed) */
20
- const AUDIO_FIELDS: Record<string, ReadonlySet<string>> = {
21
- bgm: new Set(["op", "resource", "volume", "loop", "fade", "restart"]),
22
- se: new Set(["op", "resource", "volume"]),
23
- ambient: new Set(["op", "resource", "volume", "loop", "fade", "restart"]),
24
- stop_bgm: new Set(["op", "fade"]),
25
- stop_ambient: new Set(["op", "fade"]),
26
- voice: new Set(["op", "resource", "volume", "auto_stop", "restart"]),
27
- stop_voice: new Set(["op", "fade"]),
28
- };
29
-
30
- /** 停止类 op → 目标常驻通道系统键 */
31
- const AUDIO_STOP_KEY: Record<string, string> = {
32
- stop_bgm: SYS.audioBgm,
33
- stop_ambient: SYS.audioAmbient,
34
- stop_voice: SYS.audioVoice,
35
- };
36
-
37
- /** 音频通道 op → 常驻通道系统键(se 为一次性触发,不在表内) */
38
- const AUDIO_CHANNEL_KEY: Record<string, string> = {
39
- bgm: SYS.audioBgm,
40
- ambient: SYS.audioAmbient,
41
- voice: SYS.audioVoice,
42
- };
43
-
44
- /** 视频族已知负载字段(未知字段 fail-closed) */
45
- const VIDEO_FIELDS: Record<string, ReadonlySet<string>> = {
46
- video: new Set(["op", "resource", "volume", "loop", "z"]),
47
- cutscene: new Set(["op", "resource", "volume", "skipable", "z"]),
48
- seek_video: new Set(["op", "seconds"]),
49
- pause_video: new Set(["op"]),
50
- resume_video: new Set(["op"]),
51
- stop_video: new Set(["op"]),
52
- video_skipable: new Set(["op", "value"]),
53
- };
54
-
55
- /**
56
- * 四音频通道:核心只写状态,播放由 UI 适配器落地。
57
- * - bgm/ambient/voice 为常驻通道(写状态对象,随快照/存档随行);se 为一次性触发(单调 seq)
58
- * - 同资源重写保留播放位置:重放不打断当前曲目(回滚 seek / 读档续播)
59
- * - stop_* 写 stop 形态(带淡出参数);未知字段/非法负载 fail-closed
60
- */
61
- export function execAudio(ctx: OpContext, cmd: StoryCommand): boolean {
62
- const unknown = Object.keys(cmd).filter(
63
- (k) => !(AUDIO_FIELDS[cmd.op]?.has(k) ?? false),
64
- );
65
- if (unknown.length > 0) {
66
- ctx.fail(
67
- `${cmd.op}-unknown-field`,
68
- `${cmd.op} 未知负载字段:${unknown.join(", ")}`,
69
- );
70
- return false;
71
- }
72
- const fade = audioFade(ctx, cmd.fade, cmd.op);
73
- if (fade === null) return false;
74
- if (
75
- cmd.op === "stop_bgm" ||
76
- cmd.op === "stop_ambient" ||
77
- cmd.op === "stop_voice"
78
- ) {
79
- // 清通道为停止形态(淡出参数随状态走,不被丢弃)
80
- const stopKey = AUDIO_STOP_KEY[cmd.op]!;
81
- ctx.setSystem(stopKey, {
82
- kind: "stop",
83
- fadeMs: fade,
84
- } satisfies AudioChannelState);
85
- // 停止背景乐:播放位置归零(帧级键静默写)
86
- if (cmd.op === "stop_bgm") ctx.state.set(SYS.bgmPosition, 0);
87
- return true;
88
- }
89
- if (typeof cmd.resource !== "string" || cmd.resource === "") {
90
- ctx.fail(
91
- `${cmd.op}-invalid-resource`,
92
- `${cmd.op} 需要非空 resource 字符串`,
93
- );
94
- return false;
95
- }
96
- const volume = audioVolume(ctx, cmd.volume, cmd.op);
97
- if (volume === null) return false;
98
- if (cmd.op === "se") {
99
- ctx.mediaSeq += 1;
100
- ctx.setSystem(SYS.audioSe, {
101
- kind: "play",
102
- resource: cmd.resource,
103
- volume,
104
- loop: false,
105
- fadeMs: 0,
106
- seq: ctx.mediaSeq,
107
- } satisfies AudioChannelState);
108
- return true;
109
- }
110
- const channelKey = AUDIO_CHANNEL_KEY[cmd.op]!;
111
- const previous = ctx.get(channelKey) as AudioChannelState | null | undefined;
112
- const restart = cmd.restart === true;
113
- if (cmd.op === "bgm") {
114
- // 首播/换曲/显式重播归零;同曲静默续播(回滚 seek / 读档续播)。
115
- // 帧级键静默写(高频键不进事件流),UI 经通道事件重读位置。
116
- const sameTrack =
117
- previous?.kind === "play" && previous.resource === cmd.resource;
118
- if (!sameTrack || restart) ctx.state.set(SYS.bgmPosition, 0);
119
- }
120
- const state: AudioChannelState = {
121
- kind: "play",
122
- resource: cmd.resource,
123
- volume,
124
- loop: cmd.op === "voice" ? false : cmd.loop !== false,
125
- fadeMs: fade,
126
- };
127
- if (cmd.op === "voice") state.autoStop = cmd.auto_stop !== false;
128
- if (restart) {
129
- // 单调序号:同曲连续两次 restart 也是两次独立事件(渲染层据此重播)
130
- ctx.mediaSeq += 1;
131
- state.restart = true;
132
- state.seq = ctx.mediaSeq;
133
- }
134
- ctx.setSystem(channelKey, state);
135
- return true;
136
- }
137
-
138
- /** volume 窄化:缺省 1;非有限数字 fail-closed,越界按物理范围钳制 */
139
- function audioVolume(ctx: OpContext, raw: unknown, op: string): number | null {
140
- if (raw === undefined) return 1;
141
- if (typeof raw !== "number" || !Number.isFinite(raw)) {
142
- ctx.fail(`${op}-invalid-volume`, `${op} 的 volume 必须是有限数字`);
143
- return null;
144
- }
145
- return Math.min(1, Math.max(0, raw));
146
- }
147
-
148
- /** fade 窄化:缺省 0;负数/非有限数字 fail-closed */
149
- function audioFade(ctx: OpContext, raw: unknown, op: string): number | null {
150
- if (raw === undefined) return 0;
151
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) {
152
- ctx.fail(`${op}-invalid-fade`, `${op} 的 fade 必须是非负有限数字(毫秒)`);
153
- return null;
154
- }
155
- return raw;
156
- }
157
-
158
- /**
159
- * 视频族:核心只写命令流(`__video`,seq 单调),渲染器按序执行。
160
- * - `video` = 非阻塞播放(故事继续);`cutscene` = 阻塞过场(等待建立时提交检查点,
161
- * 同 menu/wait/input;ended/跳过解除)。
162
- * - `seek_video`/`pause_video`/`resume_video`/`stop_video` 为离散命令;
163
- * `video_skipable` 设后续 video/cutscene 的缺省 skipable。
164
- */
165
- export function execVideo(
166
- ctx: OpContext,
167
- frame: Frame,
168
- cmd: StoryCommand,
169
- cutscene = false,
170
- ): boolean {
171
- const known = VIDEO_FIELDS[cmd.op]!;
172
- const unknown = Object.keys(cmd).filter((k) => !known.has(k));
173
- if (unknown.length > 0) {
174
- ctx.fail(
175
- `${cmd.op}-unknown-field`,
176
- `${cmd.op} 未知负载字段:${unknown.join(", ")}`,
177
- );
178
- return false;
179
- }
180
- ctx.videoSeq += 1;
181
- const seq = ctx.videoSeq;
182
- if (cmd.op === "video" || cmd.op === "cutscene") {
183
- if (ctx.rejectBadInstanceZ(cmd)) return false; // 先拒非法 z(不动状态)
184
- ctx.setInstanceZ(SYS.videoZ, cmd.z); // 视频层实例 z(宿主解析后交 VideoPort.setZIndex)
185
- if (typeof cmd.resource !== "string" || cmd.resource === "") {
186
- ctx.fail(
187
- `${cmd.op}-invalid-resource`,
188
- `${cmd.op} 需要非空 resource 字符串`,
189
- );
190
- return false;
191
- }
192
- const volume = audioVolume(ctx, cmd.volume, cmd.op);
193
- if (volume === null) return false;
194
- // skipable:cutscene 显式参数 > video_skipable 持久开关(缺省可跳)
195
- const skipable =
196
- cutscene && cmd.skipable !== undefined
197
- ? cmd.skipable === true
198
- : ctx.get(SYS.videoSkipable) !== false;
199
- ctx.setSystem(SYS.video, {
200
- kind: "play",
201
- resource: cmd.resource,
202
- volume,
203
- loop: cmd.loop === true,
204
- cutscene,
205
- skipable,
206
- seq,
207
- } satisfies VideoCommand);
208
- if (cutscene) {
209
- // 过场等待建立时提交检查点(同 menu/wait/input);重放期同坐标原位替换
210
- ctx.setSystem(SYS.waiting, "video");
211
- ctx.commitCheckpoint(ctx.takeSnapshot(ctx.checkpointCoord(frame)));
212
- ctx.liveCheckpointed = true;
213
- ctx.autoSaveAtCheckpoint(); // cutscene 等待画面建立 = auto_save 消费点
214
- frame.index += 1;
215
- }
216
- return true;
217
- }
218
- if (cmd.op === "seek_video") {
219
- const seconds = cmd.seconds;
220
- if (
221
- typeof seconds !== "number" ||
222
- !Number.isFinite(seconds) ||
223
- seconds < 0
224
- ) {
225
- ctx.fail(
226
- "seek_video-invalid-seconds",
227
- "seek_video 需要非负有限数字(秒)",
228
- );
229
- return false;
230
- }
231
- ctx.setSystem(SYS.video, {
232
- kind: "seek",
233
- seconds,
234
- seq,
235
- } satisfies VideoCommand);
236
- return true;
237
- }
238
- if (cmd.op === "pause_video") {
239
- ctx.setSystem(SYS.video, { kind: "pause", seq } satisfies VideoCommand);
240
- return true;
241
- }
242
- if (cmd.op === "resume_video") {
243
- ctx.setSystem(SYS.video, { kind: "resume", seq } satisfies VideoCommand);
244
- return true;
245
- }
246
- if (cmd.op === "stop_video") {
247
- ctx.setSystem(SYS.video, { kind: "stop", seq } satisfies VideoCommand);
248
- return true;
249
- }
250
- // video_skipable
251
- ctx.setSystem(SYS.videoSkipable, cmd.value !== false);
252
- return true;
253
- }
254
-
255
- /** notify:出站 toast 事件(覆盖层);文本插值与 say 同语义 */
256
- export function execNotify(ctx: OpContext, cmd: StoryCommand): void {
257
- if (ctx.rejectBadInstanceZ(cmd)) return; // 先拒非法 z(不动状态)
258
- const { text, errors } = interpolateText(
259
- ctx.translate(cmd.text as string),
260
- ctx.resolveName,
261
- ctx.draw,
262
- );
263
- for (const e of errors)
264
- ctx.fail(e.code, ctx.texts.interpolationFallback(e.message));
265
- ctx.setInstanceZ(SYS.notificationsZ, cmd.z); // 本条通知的实例 z(notifications 层)
266
- const payload: OutboundPayload = {
267
- kind: "notify",
268
- text,
269
- ...(typeof cmd.type === "string" ? { notifyType: cmd.type } : {}),
270
- ...(typeof cmd.duration === "number" ? { duration: cmd.duration } : {}),
271
- };
272
- const event: OutboundEvent = { v: 1, kind: "event", payload };
273
- for (const listener of ctx.eventListeners) listener(event);
274
- }
File without changes