@lingfan-engine/ui 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 (63) hide show
  1. package/LICENSE +201 -0
  2. package/package.json +38 -4
  3. package/src/audio/audio.ts +331 -0
  4. package/src/audio/index.ts +11 -0
  5. package/src/choices/index.ts +10 -0
  6. package/src/choices/templates.ts +74 -0
  7. package/src/commands/index.ts +6 -0
  8. package/src/commands/registry.ts +54 -0
  9. package/src/dialogue/index.ts +24 -0
  10. package/src/dialogue/inline.ts +136 -0
  11. package/src/dialogue/templates.ts +70 -0
  12. package/src/dialogue/textView.ts +35 -0
  13. package/src/dialogue/typewriter.ts +181 -0
  14. package/src/dom/index.ts +7 -0
  15. package/src/dom/materialize.ts +88 -0
  16. package/src/dom/overlay-surface.ts +278 -0
  17. package/src/element/animation.ts +141 -0
  18. package/src/element/index.ts +38 -0
  19. package/src/element/interaction.ts +123 -0
  20. package/src/element/registry.ts +69 -0
  21. package/src/element/render.ts +77 -0
  22. package/src/element/renderers/base.ts +56 -0
  23. package/src/element/renderers/bindings.ts +178 -0
  24. package/src/element/renderers/container.ts +141 -0
  25. package/src/element/renderers/controls.ts +80 -0
  26. package/src/element/renderers/decoration.ts +28 -0
  27. package/src/element/renderers/index.ts +5 -0
  28. package/src/element/renderers/media.ts +71 -0
  29. package/src/element/renderers/register.ts +77 -0
  30. package/src/element/renderers/text.ts +20 -0
  31. package/src/element/resource.ts +55 -0
  32. package/src/element/spec.ts +77 -0
  33. package/src/element/style.ts +150 -0
  34. package/src/host/duration.ts +25 -0
  35. package/src/host/effects.ts +174 -0
  36. package/src/host/frame-loop.ts +84 -0
  37. package/src/host/index.ts +47 -0
  38. package/src/host/layer-view.ts +74 -0
  39. package/src/host/state-view.ts +190 -0
  40. package/src/index.ts +161 -0
  41. package/src/input/index.ts +11 -0
  42. package/src/input/scope.ts +100 -0
  43. package/src/minigame/index.ts +2 -0
  44. package/src/minigame/registry.ts +35 -0
  45. package/src/notify/index.ts +11 -0
  46. package/src/notify/templates.ts +60 -0
  47. package/src/overlay/choices-view.ts +76 -0
  48. package/src/overlay/create.ts +254 -0
  49. package/src/overlay/dialogue-view.ts +72 -0
  50. package/src/overlay/element-tree.ts +109 -0
  51. package/src/overlay/index.ts +16 -0
  52. package/src/overlay/layer-z.ts +73 -0
  53. package/src/overlay/notify-view.ts +32 -0
  54. package/src/overlay/projector.ts +286 -0
  55. package/src/overlay/surface.ts +125 -0
  56. package/src/overlay/types.ts +124 -0
  57. package/src/resources/index.ts +10 -0
  58. package/src/resources/urlCache.ts +79 -0
  59. package/src/templates/index.ts +7 -0
  60. package/src/templates/registry.ts +79 -0
  61. package/src/video/index.ts +6 -0
  62. package/src/video/videoView.ts +122 -0
  63. package/README.md +0 -3
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 挂载点模板注册表(通用形态:语义骨架挂点 + 皮肤类)。
3
+ *
4
+ * 每个渲染挂载点(对话 / 选择 / 通知 / …)都有自己的 `TemplateInput`(宿主投影的渲染状态)
5
+ * 与 `TemplateView`(各挂点内容 + 根皮肤类)。本类只承载**注册与解析语义**,
6
+ * 与具体挂载点无关:
7
+ * - 空名 = 「无模板」哨兵,不可被注册占用(`resolve("")` 始终走默认)
8
+ * - 未知名 / null / undefined = 回退默认(展示层缺失应有兜底)
9
+ * - 未设默认 = `null`(宿主用自己的内建实现兜底)
10
+ *
11
+ * **与元素/小游戏注册表的口径差异**:那些是 `fail-closed`(未知类型/未注册命令不伪造),
12
+ * 本表是 `fail-soft`——模板只影响「长什么样」,缺失回退默认不会伪造语义。
13
+ */
14
+ export interface TemplateViewBase {
15
+ /** 根皮肤类(宿主布局类之上叠加) */
16
+ rootClass: string;
17
+ }
18
+
19
+ /** 模板函数:吃宿主投影出的渲染状态,吐该挂点的内容与根皮肤类(纯函数,不碰 DOM、无副作用) */
20
+ export type TemplateFn<TInput, TView extends TemplateViewBase> = (
21
+ input: TInput,
22
+ ) => TView;
23
+
24
+ /**
25
+ * 模板注册与解析:按名存模板,解析时未知名 / null / 空串一律回退默认,未设默认则返回 `null`。
26
+ *
27
+ * 用法:宿主装配期 `register` 一批模板(可指定其中之一为兜底),渲染挂载点时用 `resolve` 取函数。
28
+ * 模板缺失不算错误——由宿主的内建实现兜底,因此本表是 fail-soft 的。
29
+ * 本类不持有渲染状态,同一实例可跨挂载点共享。
30
+ */
31
+ export class TemplateRegistry<TInput, TView extends TemplateViewBase> {
32
+ /**
33
+ * 模板表:模板名 → 模板函数。`register` 写入(同名覆盖),`resolve` / `has` / `names` 读取。
34
+ * 空串是「无模板」哨兵,`register` 直接拒收,所以键集合恰好等于真正注册过的模板名;
35
+ * Map 保插入序,`names()` 就按这个顺序返回。引用只读,内容随注册变化。
36
+ */
37
+ private readonly templates = new Map<string, TemplateFn<TInput, TView>>();
38
+ /** 默认模板名(`register` 传 `makeDefault: true` 时改成它);`null` = 未设默认,解析时返回 `null` */
39
+ private defaultName: string | null = null;
40
+
41
+ /** 注册模板(同名覆盖更新——注册即生效的可选语义) */
42
+ register(
43
+ name: string,
44
+ fn: TemplateFn<TInput, TView>,
45
+ opts: { makeDefault?: boolean } = {},
46
+ ): void {
47
+ if (name === "") return; // 空名 = 「无模板」哨兵,不可被注册占用
48
+ this.templates.set(name, fn);
49
+ if (opts.makeDefault === true) this.defaultName = name;
50
+ }
51
+
52
+ /** 该名是否已被注册(**不走回退**:问的是表里有没有这个名字) */
53
+ has(name: string): boolean {
54
+ return this.templates.has(name);
55
+ }
56
+
57
+ /** 已注册模板名(诊断 / 帮助面板用) */
58
+ names(): string[] {
59
+ return [...this.templates.keys()];
60
+ }
61
+
62
+ /** 按名解析:未知名/null/空串回退默认;未设默认 = null(宿主兜底) */
63
+ resolve(name?: string | null): TemplateFn<TInput, TView> | null {
64
+ if (name !== null && name !== undefined && name !== "") {
65
+ const hit = this.templates.get(name);
66
+ if (hit !== undefined) return hit;
67
+ }
68
+ if (this.defaultName === null) return null;
69
+ return this.templates.get(this.defaultName) ?? null;
70
+ }
71
+ }
72
+
73
+ /** 造一张空的模板注册表(未设默认);默认模板需在 `register` 时用 `makeDefault: true` 指定 */
74
+ export function createTemplateRegistry<
75
+ TInput,
76
+ TView extends TemplateViewBase,
77
+ >(): TemplateRegistry<TInput, TView> {
78
+ return new TemplateRegistry<TInput, TView>();
79
+ }
@@ -0,0 +1,6 @@
1
+ /** 视频渲染域出口:命令流 → VideoPort 执行(可测纯编排)。 */
2
+ export {
3
+ createVideoRenderer,
4
+ type VideoRenderer,
5
+ type VideoRendererOptions,
6
+ } from "./videoView";
@@ -0,0 +1,122 @@
1
+ /**
2
+ * 视频渲染:引擎的 `__video` 命令流 → VideoPort 执行(可测纯编排)。
3
+ * - seq 去重:同一命令只执行一次(重放/读档恢复后的同 seq 不重放)
4
+ * - 资源经 ResourcePort 解析 + 代际取消(迟到的旧解析不误播)
5
+ * - cutscene 自然播放结束 → onVideoFinished(引擎解除 video 等待)
6
+ * 性能:sync 仅由 `__video` 事件与显式调用触发(回溯/读档后对齐),非逐帧。
7
+ */
8
+ import type {
9
+ ResourcePort,
10
+ VideoCommand,
11
+ VideoPort,
12
+ StoryEngine,
13
+ } from "@lingfan-engine/core";
14
+ import { createStateReader, SYS } from "@lingfan-engine/core";
15
+ import { createResourceUrlCache } from "../resources";
16
+
17
+ /**
18
+ * 宿主侧回调:只收「出事了」与「放完了」两件本层无法自行处理的事。
19
+ * 都不传也能跑——只是解析失败会静默不播、cutscene 结束后引擎会一直等。
20
+ */
21
+ export interface VideoRendererOptions {
22
+ /** 资源解析失败诊断(报错诊断:不静默吞错) */
23
+ onError?: (message: string) => void;
24
+ /** cutscene 自然播放结束(引擎据此解除 video 等待) */
25
+ onVideoFinished?: () => void;
26
+ }
27
+
28
+ /**
29
+ * 视频渲染器:命令的订阅与执行都在工厂内建好,外部只需处理偶尔的显式对齐。
30
+ */
31
+ export interface VideoRenderer {
32
+ /** 与引擎命令流对齐(订阅外的显式同步,如回溯/读档完成) */
33
+ sync(): void;
34
+ dispose(): void;
35
+ }
36
+
37
+ /**
38
+ * 造一个视频渲染器并立即订阅引擎的 `__video` 命令流(构造末尾先自同步一次)。
39
+ *
40
+ * 它按 seq 去重、按代际取消迟到的解析结果;资源 URL 经共享缓存取,`dispose` 时逐个
41
+ * `resources.release` 并退订——释放与订阅成对,调用方不必自己记账。
42
+ */
43
+ export function createVideoRenderer(
44
+ engine: StoryEngine,
45
+ port: VideoPort,
46
+ resources: ResourcePort,
47
+ options: VideoRendererOptions = {},
48
+ ): VideoRenderer {
49
+ let lastSeq = -1;
50
+ /** 解析代际:异步解析落地时若已被更新命令取代,丢弃该次播放(防错播) */
51
+ let generation = 0;
52
+ const urls = createResourceUrlCache((resource) =>
53
+ resources.resolve(resource),
54
+ );
55
+ const reader = createStateReader(engine);
56
+
57
+ async function resolveUrl(resource: string): Promise<string | null> {
58
+ try {
59
+ return await urls.resolve(resource);
60
+ } catch (e) {
61
+ options.onError?.(`资源解析失败:${resource}(${String(e)})`);
62
+ return null; // fail-closed:不播放(诊断已上报,不静默)
63
+ }
64
+ }
65
+
66
+ function execute(command: VideoCommand): void {
67
+ generation += 1;
68
+ const gen = generation;
69
+ if (command.kind === "play") {
70
+ void resolveUrl(command.resource).then((url) => {
71
+ if (url === null || generation !== gen) return; // 已被更新命令取代
72
+ port.play(
73
+ url,
74
+ { volume: command.volume, loop: command.loop },
75
+ command.cutscene ? () => options.onVideoFinished?.() : undefined,
76
+ );
77
+ });
78
+ return;
79
+ }
80
+ if (command.kind === "pause") {
81
+ port.pause();
82
+ return;
83
+ }
84
+ if (command.kind === "resume") {
85
+ port.resume();
86
+ return;
87
+ }
88
+ if (command.kind === "seek") {
89
+ port.seek(command.seconds);
90
+ return;
91
+ }
92
+ port.stop();
93
+ }
94
+
95
+ function sync(): void {
96
+ const command = reader.videoCommand();
97
+ if (command === null) {
98
+ if (lastSeq >= 0) {
99
+ port.stop(); // 命令流被清空(重启/重建):停播
100
+ lastSeq = -1;
101
+ }
102
+ return;
103
+ }
104
+ if (command.seq === lastSeq) return; // 同命令已执行(重放同 seq 不重放)
105
+ lastSeq = command.seq;
106
+ execute(command);
107
+ }
108
+
109
+ const off = engine.onStateChanged((change) => {
110
+ if (change.key === SYS.video) sync();
111
+ });
112
+ sync();
113
+
114
+ return {
115
+ sync,
116
+ dispose(): void {
117
+ off();
118
+ for (const url of urls.values()) resources.release(url);
119
+ urls.clear();
120
+ },
121
+ };
122
+ }
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.