@lingfan-engine/shell 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.
@@ -0,0 +1,258 @@
1
+ /**
2
+ * 叙事视图状态(平台无关):把引擎状态键翻译成视图意图,再落到注入的状态单元上。
3
+ *
4
+ * 引擎的状态键是弱类型的(`value: unknown`),每个键怎么解读、缺省怎么回退,是一套
5
+ * 容易在多个宿主里各写一遍、然后慢慢走样的规则。解读规则本身来自共享的
6
+ * `createStateView`(`@lingfan-engine/ui` 的 host 域);本模块负责两件事:声明订阅哪些键,
7
+ * 以及每条意图落到哪个状态单元上。
8
+ *
9
+ * 共享投影会把非字符串的文本键归一成空串、把非数组的数组键归一成空数组;本模块维持
10
+ * 原判据——类型不符时**不动**已有值。两者只在引擎写出畸形值时可分辨,故这里的守卫
11
+ * 按「保持原行为」保留。
12
+ *
13
+ * 键名与键分类都由装配方注入:订阅哪几个状态键是宿主与引擎之间的接线契约,
14
+ * 共享投影不内置任何键名。
15
+ */
16
+ import {
17
+ INSTANCE_Z_KEYS,
18
+ SYS,
19
+ type ElementInstance,
20
+ } from "@lingfan-engine/core";
21
+ import {
22
+ createStateView,
23
+ type StateIntent,
24
+ type StateKeyTable,
25
+ } from "@lingfan-engine/ui";
26
+ import type {
27
+ MenuChoice,
28
+ NarrativeState,
29
+ NarrativeStateOptions,
30
+ } from "./types.ts";
31
+
32
+ /** 本域订阅的状态键表(键名 → 可读别名) */
33
+ const STATE_KEYS: StateKeyTable = {
34
+ speaker: SYS.currentDialogSpeaker,
35
+ dialogText: SYS.currentDialogText,
36
+ waiting: SYS.waiting,
37
+ nvlMode: SYS.nvlMode,
38
+ nvlBuffer: SYS.nvlBuffer,
39
+ menuOptions: SYS.menuOptions,
40
+ menuTargets: SYS.menuTargets,
41
+ dialogVisible: SYS.dialogVisible,
42
+ elements: SYS.elements,
43
+ };
44
+
45
+ /**
46
+ * 创建叙事视图状态。字段是装配方给的状态单元——框架宿主的单元自带响应性,
47
+ * 绑定层照常消费;无框架宿主可轮询或手动同步。
48
+ */
49
+ export function createNarrativeState(
50
+ options: NarrativeStateOptions,
51
+ ): NarrativeState {
52
+ const { cell } = options.cells;
53
+ const speaker = cell("");
54
+ const text = cell("");
55
+ const shownText = cell("");
56
+ const speakerColor = cell("");
57
+ const canAdvance = cell(false);
58
+ const inMenu = cell(false);
59
+ const inWait = cell(false);
60
+ const inInput = cell(false);
61
+ const inVideo = cell(false);
62
+ const inMinigame = cell(false);
63
+ const inInteraction = cell(false);
64
+ const inExtension = cell(false);
65
+ const dialogHidden = cell(false);
66
+ const menuPrompt = cell("");
67
+ const inputPrompt = cell("");
68
+ const inputValue = cell("");
69
+ const rawTexts = cell<string[]>([]);
70
+ const rawTargets = cell<string[]>([]);
71
+ const dialogTemplateName = cell<string | null>("");
72
+ const dialogFont = cell("");
73
+ const nvlMode = cell("none");
74
+ const nvlBuffer = cell<string[]>([]);
75
+ const elements = cell<ElementInstance[]>([]);
76
+
77
+ /**
78
+ * 选项列表 = 选项文本与选项目标逐位配对。
79
+ * 本模块独占 `rawTexts`/`rawTargets` 的全部写入点,故在写入点直接重算这个
80
+ * 普通单元即可——不要求装配方提供响应式派生(无依赖追踪的框架也能接入)。
81
+ */
82
+ const menuOptions = cell<MenuChoice[]>([]);
83
+ function refreshMenuOptions(): void {
84
+ // 与既有派生语义逐位一致:目标缺失(含两键长度不齐)按空串兜底
85
+ menuOptions.value = rawTexts.value.map((t, i) => ({
86
+ text: t,
87
+ target: rawTargets.value[i] ?? "",
88
+ }));
89
+ }
90
+
91
+ const toIntent = createStateView({
92
+ keys: STATE_KEYS,
93
+ resolveLayer: (key) => options.resolveLayer(key),
94
+ readTypingSetting: () => options.readTypingSetting(),
95
+ readCharacterColor: (name) => options.readCharacterColor(name),
96
+ });
97
+
98
+ /**
99
+ * 重算说话人颜色 = 本句覆盖值优先,其次角色定义。
100
+ *
101
+ * 为什么是函数而不是「在说话人分支里算」:配色是
102
+ * `(speaker, currentDialogColor)` 的派生值——引擎按 `color → speaker` 顺序写两个键,
103
+ * 若只在说话人分支算,本句的 color 可能还没到(算成上一句的颜色,滞后一句)。
104
+ * 派生值不该依赖事件到达顺序,故两个键变更都调它(幂等)。
105
+ */
106
+ function refreshSpeakerColor(): void {
107
+ const def = options.readCharacterColor(speaker.value);
108
+ const override = options.readColorOverride();
109
+ speakerColor.value =
110
+ (override !== null && override !== "" ? override : def) ?? "";
111
+ }
112
+
113
+ /** 把一条意图落到状态单元上;`key` 与 `value` 是原始状态,供类型守卫使用 */
114
+ function applyIntent(intent: StateIntent, key: string, value: unknown): void {
115
+ switch (intent.kind) {
116
+ case "layer":
117
+ // 视频层与 DOM 层同走这一条:该层 z 最终落到哪由出口决定
118
+ options.onLayerZ(intent.layer, intent.z);
119
+ return;
120
+ case "speaker":
121
+ if (typeof value !== "string") return;
122
+ speaker.value = value;
123
+ refreshSpeakerColor();
124
+ return;
125
+ case "dialog-text":
126
+ if (typeof value !== "string") return;
127
+ text.value = value;
128
+ options.onDialogText(value);
129
+ return;
130
+ case "waiting":
131
+ canAdvance.value = value === "dialog";
132
+ inMenu.value = value === "menu";
133
+ inWait.value = value === "wait";
134
+ inInput.value = value === "input";
135
+ inVideo.value = value === "video";
136
+ inMinigame.value = value === "minigame";
137
+ inExtension.value = value === "extension";
138
+ return;
139
+ case "dialog-visible":
140
+ dialogHidden.value = value === "hide";
141
+ return;
142
+ case "choices":
143
+ // 选项与目标两键分别写入,各写各的字段
144
+ if (key === STATE_KEYS.menuOptions) {
145
+ if (Array.isArray(value)) {
146
+ rawTexts.value = value as string[];
147
+ refreshMenuOptions();
148
+ }
149
+ } else if (key === STATE_KEYS.menuTargets) {
150
+ if (Array.isArray(value)) {
151
+ rawTargets.value = value as string[];
152
+ refreshMenuOptions();
153
+ }
154
+ }
155
+ return;
156
+ case "nvl-mode":
157
+ if (typeof value !== "string") return;
158
+ nvlMode.value = value;
159
+ return;
160
+ case "nvl-buffer":
161
+ if (!Array.isArray(value)) return;
162
+ nvlBuffer.value = value as string[];
163
+ return;
164
+ case "elements":
165
+ if (!Array.isArray(value)) return;
166
+ elements.value = value as ElementInstance[];
167
+ return;
168
+ case "none":
169
+ // 表外键:两个提示行、配色覆盖、对话模板名、对话字体
170
+ if (key === SYS.menuPrompt && typeof value === "string") {
171
+ menuPrompt.value = value;
172
+ } else if (key === SYS.inputPrompt && typeof value === "string") {
173
+ inputPrompt.value = value;
174
+ } else if (key === SYS.currentDialogColor) {
175
+ refreshSpeakerColor();
176
+ } else if (key === SYS.dialogTemplate) {
177
+ dialogTemplateName.value = typeof value === "string" ? value : null;
178
+ } else if (key === SYS.dialogFont && typeof value === "string") {
179
+ dialogFont.value = value;
180
+ }
181
+ return;
182
+ }
183
+ }
184
+
185
+ return {
186
+ speaker,
187
+ text,
188
+ shownText,
189
+ speakerColor,
190
+ canAdvance,
191
+ inMenu,
192
+ inWait,
193
+ inInput,
194
+ inVideo,
195
+ inMinigame,
196
+ inInteraction,
197
+ inExtension,
198
+ dialogHidden,
199
+ menuPrompt,
200
+ inputPrompt,
201
+ inputValue,
202
+ rawTexts,
203
+ rawTargets,
204
+ menuOptions,
205
+ dialogTemplateName,
206
+ dialogFont,
207
+ nvlMode,
208
+ nvlBuffer,
209
+ elements,
210
+ handleState(key: string, value: unknown): void {
211
+ applyIntent(toIntent(key, value), key, value);
212
+ },
213
+ syncFromEngine(read: (key: string) => unknown): void {
214
+ // 回放或读档后,渲染状态与引擎对齐(回溯/读档重写了对话与 NVL 系统键)
215
+ const sp = read(SYS.currentDialogSpeaker);
216
+ const tx = read(SYS.currentDialogText);
217
+ speaker.value = typeof sp === "string" ? sp : "";
218
+ text.value = typeof tx === "string" ? tx : "";
219
+ const w = read(SYS.waiting);
220
+ canAdvance.value = w === "dialog";
221
+ inMenu.value = w === "menu";
222
+ inWait.value = w === "wait";
223
+ inInput.value = w === "input";
224
+ inMinigame.value = w === "minigame";
225
+ inInteraction.value = w === "interaction";
226
+ inExtension.value = w === "extension";
227
+ dialogHidden.value = read(SYS.dialogVisible) === "hide";
228
+ const mp = read(SYS.menuPrompt);
229
+ menuPrompt.value = typeof mp === "string" ? mp : "";
230
+ const ip = read(SYS.inputPrompt);
231
+ inputPrompt.value = typeof ip === "string" ? ip : "";
232
+ const opts = read(SYS.menuOptions);
233
+ rawTexts.value = Array.isArray(opts) ? (opts as string[]) : [];
234
+ const tgts = read(SYS.menuTargets);
235
+ rawTargets.value = Array.isArray(tgts) ? (tgts as string[]) : [];
236
+ refreshMenuOptions();
237
+ const nm = read(SYS.nvlMode);
238
+ nvlMode.value = typeof nm === "string" ? nm : "none";
239
+ const nb = read(SYS.nvlBuffer);
240
+ nvlBuffer.value = Array.isArray(nb) ? (nb as string[]) : [];
241
+ const els = read(SYS.elements);
242
+ elements.value = Array.isArray(els) ? (els as ElementInstance[]) : [];
243
+ const dt = read(SYS.dialogTemplate);
244
+ dialogTemplateName.value = typeof dt === "string" ? dt : null;
245
+ const df = read(SYS.dialogFont);
246
+ dialogFont.value = typeof df === "string" ? df : "";
247
+ // 实例级 z:逐层覆盖;本次读数里没有的层回落到工程层默认
248
+ const zNext: Partial<Record<string, number>> = {};
249
+ for (const [layer, zKey] of Object.entries(INSTANCE_Z_KEYS)) {
250
+ const z = read(zKey as string);
251
+ if (typeof z === "number") zNext[layer] = z;
252
+ }
253
+ options.onLayerZRestore(zNext);
254
+ refreshSpeakerColor();
255
+ options.onDialogText(text.value);
256
+ },
257
+ };
258
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 叙事视图状态的形状:状态单元集、装配方需供的外部事实,以及暴露给绑定层的出口。
3
+ *
4
+ * 状态单元由装配方经 StateCells 提供——框架宿主传自己的响应式单元(Vue 的 `ref`
5
+ * 结构上就是 Cell,可直接传),无框架宿主传普通对象。本域不触碰任何框架、DOM
6
+ * 或宿主全局。
7
+ */
8
+ import type {
9
+ ElementInstance,
10
+ LayerId,
11
+ MenuChoice,
12
+ } from "@lingfan-engine/core";
13
+ import type { TypingSetting } from "@lingfan-engine/ui";
14
+ import type { Cell, ReadonlyCell } from "../cell";
15
+
16
+ // 选项形状属于引擎读口,本域只转出:消费方不必为此再引一个包
17
+ export type { MenuChoice };
18
+
19
+ /** 本域的状态单元集:每个视图字段一个单元(框架宿主传响应式单元) */
20
+ export interface StateCells {
21
+ /** 新建一个状态单元;任意框架的响应式盒子或普通对象都满足此形状 */
22
+ cell<T>(initial: T): Cell<T>;
23
+ }
24
+
25
+ /** 视图状态需要宿主提供的外部事实与出口 */
26
+ export interface NarrativeStateOptions {
27
+ /** 状态单元集(字段落点) */
28
+ cells: StateCells;
29
+ /** 键名 → 该键是否承载某一层的实例级 z */
30
+ resolveLayer(key: string): LayerId | undefined;
31
+ /** 读当前故事级打字机设置 */
32
+ readTypingSetting(): TypingSetting | undefined;
33
+ /** 角色配色查询;无名或未定义配色时返回 null */
34
+ readCharacterColor(name: string): string | null;
35
+ /** 本句配色覆盖值(`say color=`);无覆盖时返回 null */
36
+ readColorOverride(): string | null;
37
+ /** 某层实例级 z 变化(`undefined` = 回工程层默认) */
38
+ onLayerZ(layer: LayerId, z: number | undefined): void;
39
+ /** 整体回档:表里没有的层回落到工程层默认 */
40
+ onLayerZRestore(z: Partial<Record<string, number>>): void;
41
+ /** 新的一句正文到达:宿主据此重建打字机 */
42
+ onDialogText(text: string): void;
43
+ }
44
+
45
+ /** 叙事视图状态出口(字段即状态单元,模板/绑定层按平台约定消费) */
46
+ export interface NarrativeState {
47
+ /** 当前说话人(空串 = 无人说话) */
48
+ readonly speaker: Cell<string>;
49
+ /** 本句正文原文 */
50
+ readonly text: Cell<string>;
51
+ /** 本句已上屏的可见前缀(打字机产出) */
52
+ readonly shownText: Cell<string>;
53
+ /** 说话人配色(覆盖值优先,其次角色定义) */
54
+ readonly speakerColor: Cell<string>;
55
+ /** 可以推进(`waiting === "dialog"`) */
56
+ readonly canAdvance: Cell<boolean>;
57
+ /** 等待态:选项 */
58
+ readonly inMenu: Cell<boolean>;
59
+ /** 等待态:无输入等待 */
60
+ readonly inWait: Cell<boolean>;
61
+ /** 等待态:文本输入 */
62
+ readonly inInput: Cell<boolean>;
63
+ /** 等待态:视频 */
64
+ readonly inVideo: Cell<boolean>;
65
+ /** 等待态:小游戏 */
66
+ readonly inMinigame: Cell<boolean>;
67
+ /** 外部玩法系统接管中 */
68
+ readonly inInteraction: Cell<boolean>;
69
+ /** 扩展 op 等待接管中(覆盖层显隐走 mount 事件,此位供对齐与让位判定) */
70
+ readonly inExtension: Cell<boolean>;
71
+ /** 对话框被故事显式隐藏(`window hide`) */
72
+ readonly dialogHidden: Cell<boolean>;
73
+ /** 菜单提示行 */
74
+ readonly menuPrompt: Cell<string>;
75
+ /** 输入提示行 */
76
+ readonly inputPrompt: Cell<string>;
77
+ /** 输入框内容 */
78
+ readonly inputValue: Cell<string>;
79
+ /** 选项文本(引擎原样) */
80
+ readonly rawTexts: Cell<string[]>;
81
+ /** 选项目标(引擎原样) */
82
+ readonly rawTargets: Cell<string[]>;
83
+ /** 选项(文本 + 目标配对后供渲染;形状取自引擎读口的 `MenuChoice`) */
84
+ readonly menuOptions: ReadonlyCell<MenuChoice[]>;
85
+ /** 当前对话模板名(null = 全局默认回退) */
86
+ readonly dialogTemplateName: Cell<string | null>;
87
+ /** 本句对话字体(引擎分辨链结果;空串 = 无声明,回落项目默认) */
88
+ readonly dialogFont: Cell<string>;
89
+ /** NVL 模式(`"none"` = 关闭) */
90
+ readonly nvlMode: Cell<string>;
91
+ /** NVL 缓冲行 */
92
+ readonly nvlBuffer: Cell<string[]>;
93
+ /** 舞台元素表 */
94
+ readonly elements: Cell<ElementInstance[]>;
95
+ /** 处理一条引擎状态变更 */
96
+ handleState(key: string, value: unknown): void;
97
+ /** 按引擎当前状态整体对齐视图(回溯 / 读档 / 重放后调用) */
98
+ syncFromEngine(read: (key: string) => unknown): void;
99
+ }
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.