@lingfan-engine/create 0.0.0-stage → 0.1.5

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 (122) hide show
  1. package/LICENSE +201 -0
  2. package/bin/create.mjs +18 -0
  3. package/package.json +44 -3
  4. package/src/cli/cli.ts +302 -0
  5. package/src/cli/prompts.ts +115 -0
  6. package/src/generate/dotfiles.ts +27 -0
  7. package/src/generate/index.ts +20 -0
  8. package/src/generate/project.ts +279 -0
  9. package/src/generate/replace.ts +91 -0
  10. package/src/index.ts +46 -0
  11. package/src/overlay/index.ts +10 -0
  12. package/src/overlay/merge.ts +143 -0
  13. package/src/registry/index.ts +15 -0
  14. package/src/registry/template-meta.ts +107 -0
  15. package/src/registry/versions.ts +109 -0
  16. package/src/shared/guards.ts +85 -0
  17. package/src/shared/index.ts +7 -0
  18. package/src/shared/texts.ts +55 -0
  19. package/template/README.md +138 -0
  20. package/template/__PROJECT__/Resources/Audio/.gitkeep +0 -0
  21. package/template/__PROJECT__/Resources/Fonts/README.md +10 -0
  22. package/template/__PROJECT__/Resources/Images/.gitkeep +0 -0
  23. package/template/__PROJECT__/Resources/Lang/.gitkeep +0 -0
  24. package/template/__PROJECT__/Resources/Stories/title/title_main.story +24 -0
  25. package/template/__PROJECT__/Resources/Video/.gitkeep +0 -0
  26. package/template/__PROJECT__/Resources/project.json +7 -0
  27. package/template/__PROJECT__/gitignore +8 -0
  28. package/template/__PROJECT__/index.html +34 -0
  29. package/template/__PROJECT__/package.json +22 -0
  30. package/template/__PROJECT__/pnpm-workspace.yaml +8 -0
  31. package/template/__PROJECT__/src/host/boot.ts +181 -0
  32. package/template/__PROJECT__/src/host/dialogue-view.ts +235 -0
  33. package/template/__PROJECT__/src/host/effects.ts +68 -0
  34. package/template/__PROJECT__/src/host/engine-wiring.ts +206 -0
  35. package/template/__PROJECT__/src/host/frame-loop.ts +42 -0
  36. package/template/__PROJECT__/src/host/index.ts +27 -0
  37. package/template/__PROJECT__/src/host/input-map.ts +51 -0
  38. package/template/__PROJECT__/src/host/layer-z.ts +50 -0
  39. package/template/__PROJECT__/src/host/save-slots.ts +38 -0
  40. package/template/__PROJECT__/src/host/stage-dom.ts +74 -0
  41. package/template/__PROJECT__/src/host/theme-fonts.ts +60 -0
  42. package/template/__PROJECT__/src/host/toast.ts +29 -0
  43. package/template/__PROJECT__/src/main.ts +61 -0
  44. package/template/__PROJECT__/src/styles/stage.css +183 -0
  45. package/template/__PROJECT__/src/vite-env.d.ts +14 -0
  46. package/template/__PROJECT__/tsconfig.json +19 -0
  47. package/template/__PROJECT__/tsconfig.node.json +14 -0
  48. package/template/__PROJECT__/vite.config.ts +64 -0
  49. package/template/overlays/README.md +70 -0
  50. package/template/overlays/miniprogram/README.md +104 -0
  51. package/template/overlays/miniprogram/index.html +15 -0
  52. package/template/overlays/miniprogram/package.patch.json +40 -0
  53. package/template/overlays/miniprogram/scripts/serve-project.mjs +273 -0
  54. package/template/overlays/miniprogram/src/App.vue +23 -0
  55. package/template/overlays/miniprogram/src/components/ChoiceLayer.vue +208 -0
  56. package/template/overlays/miniprogram/src/components/DialogueLayer.vue +183 -0
  57. package/template/overlays/miniprogram/src/components/NodeTree.vue +44 -0
  58. package/template/overlays/miniprogram/src/components/NoticeLayer.vue +87 -0
  59. package/template/overlays/miniprogram/src/components/SaveLoadBar.vue +168 -0
  60. package/template/overlays/miniprogram/src/components/StageView.vue +50 -0
  61. package/template/overlays/miniprogram/src/env.d.ts +1 -0
  62. package/template/overlays/miniprogram/src/main.ts +12 -0
  63. package/template/overlays/miniprogram/src/manifest.json +21 -0
  64. package/template/overlays/miniprogram/src/mp/frame.ts +51 -0
  65. package/template/overlays/miniprogram/src/mp/guards.ts +28 -0
  66. package/template/overlays/miniprogram/src/mp/host.ts +252 -0
  67. package/template/overlays/miniprogram/src/mp/index.ts +9 -0
  68. package/template/overlays/miniprogram/src/mp/platform.ts +210 -0
  69. package/template/overlays/miniprogram/src/mp/ports.ts +84 -0
  70. package/template/overlays/miniprogram/src/mp/project-source.ts +77 -0
  71. package/template/overlays/miniprogram/src/mp/slots.ts +64 -0
  72. package/template/overlays/miniprogram/src/mp/surface.ts +239 -0
  73. package/template/overlays/miniprogram/src/mp/video-surface.ts +104 -0
  74. package/template/overlays/miniprogram/src/mp/view.ts +91 -0
  75. package/template/overlays/miniprogram/src/pages/index/index.vue +299 -0
  76. package/template/overlays/miniprogram/src/pages.json +18 -0
  77. package/template/overlays/miniprogram/tsconfig.json +20 -0
  78. package/template/overlays/miniprogram/vite.config.ts +87 -0
  79. package/template/overlays/react/index.html +16 -0
  80. package/template/overlays/react/package.patch.json +18 -0
  81. package/template/overlays/react/src/App.tsx +32 -0
  82. package/template/overlays/react/src/main.tsx +74 -0
  83. package/template/overlays/react/src/vite-env.d.ts +14 -0
  84. package/template/overlays/react/tsconfig.json +20 -0
  85. package/template/overlays/react/vite.config.ts +67 -0
  86. package/template/overlays/tauri/README.md +59 -0
  87. package/template/overlays/tauri/package.patch.json +14 -0
  88. package/template/overlays/tauri/src/main.ts +58 -0
  89. package/template/overlays/tauri/src-tauri/Cargo.toml +16 -0
  90. package/template/overlays/tauri/src-tauri/build.rs +6 -0
  91. package/template/overlays/tauri/src-tauri/capabilities/default.json +7 -0
  92. package/template/overlays/tauri/src-tauri/icons/128x128.png +0 -0
  93. package/template/overlays/tauri/src-tauri/icons/128x128@2x.png +0 -0
  94. package/template/overlays/tauri/src-tauri/icons/32x32.png +0 -0
  95. package/template/overlays/tauri/src-tauri/icons/Square107x107Logo.png +0 -0
  96. package/template/overlays/tauri/src-tauri/icons/Square142x142Logo.png +0 -0
  97. package/template/overlays/tauri/src-tauri/icons/Square150x150Logo.png +0 -0
  98. package/template/overlays/tauri/src-tauri/icons/Square284x284Logo.png +0 -0
  99. package/template/overlays/tauri/src-tauri/icons/Square30x30Logo.png +0 -0
  100. package/template/overlays/tauri/src-tauri/icons/Square310x310Logo.png +0 -0
  101. package/template/overlays/tauri/src-tauri/icons/Square44x44Logo.png +0 -0
  102. package/template/overlays/tauri/src-tauri/icons/Square71x71Logo.png +0 -0
  103. package/template/overlays/tauri/src-tauri/icons/Square89x89Logo.png +0 -0
  104. package/template/overlays/tauri/src-tauri/icons/StoreLogo.png +0 -0
  105. package/template/overlays/tauri/src-tauri/icons/icon.icns +0 -0
  106. package/template/overlays/tauri/src-tauri/icons/icon.ico +0 -0
  107. package/template/overlays/tauri/src-tauri/icons/icon.png +0 -0
  108. package/template/overlays/tauri/src-tauri/src/lib.rs +26 -0
  109. package/template/overlays/tauri/src-tauri/src/main.rs +7 -0
  110. package/template/overlays/tauri/src-tauri/src/preferences.rs +50 -0
  111. package/template/overlays/tauri/src-tauri/src/save.rs +178 -0
  112. package/template/overlays/tauri/src-tauri/tauri.conf.json +35 -0
  113. package/template/overlays/vue/index.html +16 -0
  114. package/template/overlays/vue/package.patch.json +16 -0
  115. package/template/overlays/vue/src/App.vue +30 -0
  116. package/template/overlays/vue/src/main.ts +68 -0
  117. package/template/overlays/vue/src/vite-env.d.ts +24 -0
  118. package/template/overlays/vue/tsconfig.json +19 -0
  119. package/template/overlays/vue/vite.config.ts +67 -0
  120. package/template/pack-template.ps1 +65 -0
  121. package/template/template-meta.json +40 -0
  122. package/README.md +0 -3
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 帧循环接线:把 `requestAnimationFrame` 接到展示层的帧循环上。
3
+ *
4
+ * 帧源是本宿主唯一认识平台 API 的地方——展示层的帧循环只认「申请下一帧 / 取消」两个方法,
5
+ * 换平台(小游戏运行时、测试用的假帧源)只改这里。
6
+ *
7
+ * 每帧顺序固定:打字机推进 → 上交可见文本 → 媒体位置回写 → 帧驱动表现。
8
+ * 先上屏再写表现,否则同一帧里的动画会看到上一帧的文本。
9
+ */
10
+
11
+ import { createFrameLoop, type FrameLoop } from "@lingfan-engine/ui";
12
+ import type { DialogueView } from "./dialogue-view";
13
+
14
+ /** 帧循环的输入:对话视图(打字机与上屏都经它)与两项每帧回调 */
15
+ export interface HostFrameLoopOptions {
16
+ /** 对话视图:打字机实例与可见文本上屏都经它 */
17
+ dialogue: DialogueView;
18
+ /** 媒体位置帧级回写(音频端口自带的做法) */
19
+ onMediaTick(): void;
20
+ /** 帧驱动表现;传入距上一帧的秒数 */
21
+ onFrame(dt: number): void;
22
+ }
23
+
24
+ /** 启动帧循环并返回句柄(本宿主不主动停帧,句柄留给需要停的场合) */
25
+ export function startHostFrameLoop(options: HostFrameLoopOptions): FrameLoop {
26
+ const loop = createFrameLoop({
27
+ frame: {
28
+ request: (callback) => requestAnimationFrame(callback),
29
+ cancel: (handle) => cancelAnimationFrame(handle),
30
+ },
31
+ // 每帧顺序固定:打字机推进 → 上交可见文本 → 媒体位置回写 → 帧驱动表现
32
+ onTick: (dt) => {
33
+ const typewriter = options.dialogue.readTypewriter();
34
+ typewriter?.tick(dt);
35
+ options.dialogue.onText(typewriter?.visible ?? "");
36
+ options.onMediaTick();
37
+ options.onFrame(dt);
38
+ },
39
+ });
40
+ loop.start();
41
+ return loop;
42
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 模板宿主出口:组合根与外部只从这里取本宿主的零件,不深入具体文件。
3
+ *
4
+ * 转发的都是本宿主的装配入口与契约类型;具体实现留在各自叶子里,
5
+ * 换一处实现不影响调用方的 import 路径。
6
+ */
7
+
8
+ export {
9
+ must,
10
+ createErrorReporter,
11
+ reportBootFailure,
12
+ type StageDom,
13
+ } from "./stage-dom";
14
+ export { createHostLayerZ, type HostLayerZOptions } from "./layer-z";
15
+ export { fillSaveSlots, wireSaveSlots } from "./save-slots";
16
+ export { createToast, type ToastOptions } from "./toast";
17
+ export {
18
+ createDialogueView,
19
+ type DialogueEngine,
20
+ type DialogueView,
21
+ type DialogueViewOptions,
22
+ } from "./dialogue-view";
23
+ export { createHostEffects, type HostEffectsOptions } from "./effects";
24
+ export { wireEngine, type HostWiringOptions } from "./engine-wiring";
25
+ export { startHostFrameLoop, type HostFrameLoopOptions } from "./frame-loop";
26
+ export { wireInput, type HostInputOptions } from "./input-map";
27
+ export { bootHost, type BootPortOverrides, type HostBootOptions } from "./boot";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 输入映射:点击与键盘(空格 / 回车)都汇到同一个 `advance`。
3
+ *
4
+ * 语义归核心层——`advance` 自己处理可跳过性、非等待态与 fail-closed,这里只做映射,
5
+ * 不判断「现在能不能推进」。
6
+ *
7
+ * 两处刻意的不推进:交互层(选项 / 工具条 / 通知)内的点击不推进故事(元素可交互节点
8
+ * 在渲染器内自行 `stopPropagation`,这里再兜一层),输入框内的空格 / 回车不吞键
9
+ * (那是玩家在打字)。
10
+ */
11
+
12
+ import type { StoryEngine } from "@lingfan-engine/core";
13
+ import type { StageDom } from "./stage-dom";
14
+ import type { DialogueView } from "./dialogue-view";
15
+
16
+ /** 输入映射的输入:节点句柄表、引擎推进面与对话视图 */
17
+ export interface HostInputOptions {
18
+ /** 常驻节点句柄表 */
19
+ dom: StageDom;
20
+ /** 引擎:推进命令面的落地 */
21
+ engine: StoryEngine;
22
+ /** 对话视图:打字机未打完时先让打字机瞬间完成 */
23
+ dialogue: DialogueView;
24
+ }
25
+
26
+ /** 接线点击与键盘 */
27
+ export function wireInput(options: HostInputOptions): void {
28
+ const { dom, engine, dialogue } = options;
29
+
30
+ /** 二段式推进:打字未完 → 瞬间完成 / 越过停顿;否则推进故事 */
31
+ function advance(): void {
32
+ const typewriter = dialogue.readTypewriter();
33
+ if (typewriter !== null && !typewriter.done) {
34
+ typewriter.click();
35
+ return;
36
+ }
37
+ engine.advance();
38
+ }
39
+
40
+ dom.root.addEventListener("click", advance);
41
+ for (const el of [dom.choices, dom.toolbar, dom.notifications]) {
42
+ el.addEventListener("click", (event) => event.stopPropagation());
43
+ }
44
+ document.addEventListener("keydown", (event) => {
45
+ if (event.key === " " || event.key === "Enter") {
46
+ if (event.target instanceof HTMLInputElement) return; // 输入框内不吞键
47
+ event.preventDefault();
48
+ advance();
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 层级装配:把展示层的层级视图接到本宿主的 DOM 节点与视频端口上。
3
+ *
4
+ * 展示层管的是「某层最终该是多少」(实例覆盖 > 工程默认 > 内建),本文件管的是
5
+ * 「这个值写到哪个节点上」——这层映射是本宿主独有的,不放进共享面。
6
+ *
7
+ * 视频层的 z 不在 DOM 上而在端口内部,且只在视频层自己变化时才下发(见 `LayerView`),
8
+ * 所以这里用 getter 取端口:装配期先落一次 DOM 层,那时端口还没建。
9
+ */
10
+
11
+ import { createLayerView, type LayerView } from "@lingfan-engine/ui";
12
+ import type { LayerId, LayerZTable, VideoPort } from "@lingfan-engine/core";
13
+ import type { StageDom } from "./stage-dom";
14
+
15
+ /**
16
+ * 层级视图的输入:节点句柄表、工程层默认表与视频端口取值器。
17
+ * 端口用取值器而非实例:装配期要先落一次 DOM 层,那时端口还没建好。
18
+ */
19
+ export interface HostLayerZOptions {
20
+ /** 常驻节点句柄表 */
21
+ dom: StageDom;
22
+ /** 工程层默认(`project.json` 的 `shell.layers`) */
23
+ layerZ: LayerZTable;
24
+ /** 取当前视频端口(尚未建好时返回 null) */
25
+ readVideoPort(): VideoPort | null;
26
+ }
27
+
28
+ /** 某个 DOM 层落到哪个(或哪些)节点上 */
29
+ function writeLayerZ(dom: StageDom, layer: LayerId, z: number): void {
30
+ const value = String(z);
31
+ if (layer === "stage") dom.stage.style.zIndex = value;
32
+ // 对话层的 z 同时管对话框与 NVL 层:两者互斥显示,但要处在同一层级上
33
+ else if (layer === "dialogue") {
34
+ dom.dialogue.style.zIndex = value;
35
+ dom.nvl.style.zIndex = value;
36
+ } else if (layer === "choices") dom.choices.style.zIndex = value;
37
+ else if (layer === "notifications") dom.notifications.style.zIndex = value;
38
+ else if (layer === "toolbar") dom.toolbar.style.zIndex = value;
39
+ }
40
+
41
+ /** 造本宿主的层级视图:DOM 层写节点,视频层的 z 下发到端口内部 */
42
+ export function createHostLayerZ(options: HostLayerZOptions): LayerView {
43
+ return createLayerView({
44
+ layerZ: options.layerZ,
45
+ applyLayerZ: (layer, z) => writeLayerZ(options.dom, layer, z),
46
+ onVideoZ: (z) => {
47
+ options.readVideoPort()?.setZIndex?.(z);
48
+ },
49
+ });
50
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 存档接线:槽位下拉与存 / 读两个按钮。
3
+ *
4
+ * 槽位有几个、叫什么,由引擎按工程配置裁定(`project.json` 的 `shell.saves.slots`),
5
+ * 宿主只负责把 id 放进 `<option>`。槽位口径若搬进宿主,「改工程配置却不生效」就会变成
6
+ * 难查的静默失败。
7
+ *
8
+ * 存 / 读都走引擎既有命令面:宿主不自己拼存档数据;写档失败的可见诊断由引擎出站事件
9
+ * 统一给出(见 `engine-wiring.ts`)。
10
+ */
11
+
12
+ import {
13
+ resolveSavesConfig,
14
+ slotIds,
15
+ type StoryEngine,
16
+ } from "@lingfan-engine/core";
17
+ import type { StageDom } from "./stage-dom";
18
+
19
+ /** 按工程清单里的存档壳配置填充槽位下拉(调用方保证只填一次) */
20
+ export function fillSaveSlots(dom: StageDom, manifest: unknown): void {
21
+ const saves = resolveSavesConfig(manifest);
22
+ for (const id of slotIds(saves.slots)) {
23
+ const option = document.createElement("option");
24
+ option.value = id;
25
+ option.textContent = id;
26
+ dom.slot.append(option);
27
+ }
28
+ }
29
+
30
+ /** 接线存 / 读按钮:槽位取下拉当前选中值 */
31
+ export function wireSaveSlots(dom: StageDom, engine: StoryEngine): void {
32
+ dom.save.addEventListener("click", () => {
33
+ engine.save(dom.slot.value);
34
+ });
35
+ dom.load.addEventListener("click", () => {
36
+ engine.load(dom.slot.value);
37
+ });
38
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 宿主节点契约:`index.html` 里那些固定 id 在宿主侧长什么样,以及出错往哪儿写。
3
+ *
4
+ * id 是宿主与页面的互锁(脚手架测试逐条核对每个 `must()` 的 id 都真的写在 `index.html` 里)。
5
+ * 缺节点必须在**模块求值期立刻抛错**,而不是等某次渲染静默失败——一行
6
+ * 「宿主缺少节点:#text」比在页面里找一个不动的 div 好查得多。所以 `must()` 的调用点
7
+ * 集中在组合根(`main.ts`):那里一眼能看全宿主依赖了页面的哪些节点。
8
+ *
9
+ * 句柄只解析一次(不是每次渲染都查一遍):页面结构是静态的,节点不会在运行期出现或消失。
10
+ * 各视图拿到的都是已确认存在的节点,因此不必自己查、也不必兜空。
11
+ */
12
+
13
+ /** 按选择器取节点;缺失即抛(错误文案带上选择器,便于直接对照 `index.html`) */
14
+ export function must<T extends HTMLElement>(selector: string): T {
15
+ const el = document.querySelector<T>(selector);
16
+ if (el === null) throw new Error(`宿主缺少节点:${selector}`);
17
+ return el;
18
+ }
19
+
20
+ /** 常驻节点句柄表:装配期就要用到、且页面里始终存在的那些 */
21
+ export interface StageDom {
22
+ /** 舞台根:屏幕震动施加位移的对象 */
23
+ root: HTMLElement;
24
+ /** 舞台:元素层容器,也是元素动画节点的查找范围 */
25
+ stage: HTMLElement;
26
+ /** 对话层:说话人 / 正文 / 提示三件套的容器 */
27
+ dialogue: HTMLElement;
28
+ /** 说话人一行 */
29
+ speaker: HTMLElement;
30
+ /** 正文一行 */
31
+ text: HTMLElement;
32
+ /** 等待态提示符(对话等待时显示 ▼) */
33
+ hint: HTMLElement;
34
+ /** NVL 累积层 */
35
+ nvl: HTMLElement;
36
+ /** 等待态层:选项 / 输入 / 提示都渲染进这里 */
37
+ choices: HTMLElement;
38
+ /** 通知层:提示条追加到这里 */
39
+ notifications: HTMLElement;
40
+ /** 工具条:存档槽与存 / 读按钮 */
41
+ toolbar: HTMLElement;
42
+ /** 存档槽下拉 */
43
+ slot: HTMLSelectElement;
44
+ /** 存按钮 */
45
+ save: HTMLButtonElement;
46
+ /** 读按钮 */
47
+ load: HTMLButtonElement;
48
+ /** 全屏转场遮罩 */
49
+ transition: HTMLElement;
50
+ /** 引擎错误横幅 */
51
+ error: HTMLElement;
52
+ }
53
+
54
+ /**
55
+ * 造一个诊断出口:写进页面上的错误条**并**打控制台。
56
+ * 两条都要——页面上的那条给玩家看(黑屏时至少知道发生了什么),控制台那条给排查用。
57
+ * 引擎自己报的错误不走这里(它的控制台行带 `[engine]` 前缀,见 `engine-wiring.ts`)。
58
+ */
59
+ export function createErrorReporter(dom: StageDom): (message: string) => void {
60
+ return (message: string): void => {
61
+ dom.error.textContent = message;
62
+ console.error(`[host] ${message}`);
63
+ };
64
+ }
65
+
66
+ /**
67
+ * 启动失败的可见出口:正文与错误条各写一行「启动失败:…」,原始错误进控制台。
68
+ * 装配中途失败时舞台是空的,这行字是玩家唯一能看到的线索。
69
+ */
70
+ export function reportBootFailure(dom: StageDom, error: unknown): void {
71
+ dom.text.textContent = `启动失败:${String(error)}`;
72
+ dom.error.textContent = `启动失败:${String(error)}`;
73
+ console.error(error);
74
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 项目默认字体(主题面,RenPy `gui.rpy` 的宿主对应物):整条字体分辨链最宽的一档,
3
+ * 所有未显式声明字体的对话都落到它。取值必须是 Resources/Fonts 里的字体名;
4
+ * undefined = 未声明(对话排版走浏览器回退,逐句声明的字体照常生效)。
5
+ *
6
+ * 同时负责注入 @font-face(构建期清单来自 `virtual:story-fonts`,与 stories:build
7
+ * 的字体名注册表同源)——boot 早期一次性调用,纯展示装配,无运行时状态。
8
+ */
9
+ import { storyFonts } from "virtual:story-fonts";
10
+
11
+ /** 项目默认字体(Resources/Fonts 里的字体名;undefined = 未声明) */
12
+ const PROJECT_DEFAULT_FONT: string | undefined = undefined;
13
+
14
+ /** 根 CSS 变量名:对话排版消费它;运行期 setProperty 同名覆盖即显式切换 */
15
+ const FONT_CSS_VAR = "--lf-story-font";
16
+
17
+ /** 扩展名 → @font-face format()(与字体名注册表扩展名集合一致) */
18
+ const FORMAT_BY_EXT: Record<string, string> = {
19
+ ".ttf": "truetype",
20
+ ".otf": "opentype",
21
+ ".woff": "woff",
22
+ ".woff2": "woff2",
23
+ };
24
+
25
+ /**
26
+ * 应用项目默认字体:先校验注册表,再把字体名写进根 CSS 变量。
27
+ * 开发态未注册 = 抛错(作者当面改);生产态 = 告警回退浏览器默认(不拦启动)。
28
+ */
29
+ function applyDefaultFont(): void {
30
+ if (PROJECT_DEFAULT_FONT === undefined) return;
31
+ const registered = storyFonts.some(
32
+ (entry) => entry.name === PROJECT_DEFAULT_FONT,
33
+ );
34
+ if (!registered) {
35
+ const message = `[fonts] 项目默认字体 "${PROJECT_DEFAULT_FONT}" 未在 Resources/Fonts 注册——请补字体文件或改 PROJECT_DEFAULT_FONT`;
36
+ if (import.meta.env.DEV) throw new Error(message);
37
+ console.warn(`${message},已回退浏览器默认字体`);
38
+ return;
39
+ }
40
+ document.documentElement.style.setProperty(
41
+ FONT_CSS_VAR,
42
+ `'${PROJECT_DEFAULT_FONT}'`,
43
+ );
44
+ }
45
+
46
+ /** 注入 @font-face 并应用主题面(boot 一次性调用;空清单 = 不注入) */
47
+ export function installStoryFonts(): void {
48
+ if (storyFonts.length > 0) {
49
+ const style = document.createElement("style");
50
+ style.textContent = storyFonts
51
+ .map(({ file, name }) => {
52
+ const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
53
+ const format = FORMAT_BY_EXT[ext] ?? "truetype";
54
+ return `@font-face { font-family: '${name}'; src: url('/Fonts/${file}') format('${format}'); }`;
55
+ })
56
+ .join("\n");
57
+ document.head.appendChild(style);
58
+ }
59
+ applyDefaultFont();
60
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 提示条:往通知层追加一条 toast,到点自己消失。
3
+ *
4
+ * 驻留时长是**可配置参数**,由调用方具名传入(不传则取展示层的默认名,其值即本宿主的
5
+ * 现状取值);写死在模块里会让「换一个宿主」变成改共享代码。
6
+ */
7
+
8
+ import { DEFAULT_TOAST_MS } from "@lingfan-engine/ui";
9
+ import type { StageDom } from "./stage-dom";
10
+
11
+ /** 提示函数的输入:通知层句柄与驻留毫秒数(缺省 = 展示层默认时长) */
12
+ export interface ToastOptions {
13
+ /** 常驻节点句柄表 */
14
+ dom: StageDom;
15
+ /** 驻留毫秒数;缺省 = 展示层给出的默认时长 */
16
+ durationMs?: number;
17
+ }
18
+
19
+ /** 创建提示函数:多次调用互不影响,各条各自计时 */
20
+ export function createToast(options: ToastOptions): (text: string) => void {
21
+ const durationMs = options.durationMs ?? DEFAULT_TOAST_MS;
22
+ return (text: string): void => {
23
+ const toast = document.createElement("div");
24
+ toast.className = "toast";
25
+ toast.textContent = text;
26
+ options.dom.notifications.append(toast);
27
+ window.setTimeout(() => toast.remove(), durationMs);
28
+ };
29
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 组合根(唯一全知位置:装配适配器 → 加载组装工程 → 建引擎 → 挂载展示层)。
3
+ *
4
+ * 本宿主**不依赖任何 UI 框架**——这是「引擎框架无关」的活证明:渲染直接操作 DOM,
5
+ * 换成 Vue / React / 其他只改本文件与 index.html,引擎与适配器零改动。
6
+ * 形态:**纯 Web**(vite 静态根 `Resources/`)——Tauri 壳的装配(invoke / 资源加密 /
7
+ * 方向锁定 / 宿主信息 / 热重载)见引擎仓库 `apps/playground` 的组合根。
8
+ *
9
+ * 本文件只做三件事,实现都在 `host/` 里:
10
+ * 1. 解析页面节点(`must` 调用点集中在此,与 index.html 的 id 互锁一眼可见);
11
+ * 2. 声明本工程的清单与故事表(可配置项的取值来源);
12
+ * 3. 调用 `bootHost` 装配并启动,失败写进页面。
13
+ *
14
+ * 覆盖范围(本宿主**保证不卡死**:引擎能进入的每个等待态都有 UI 可推进):
15
+ * dialogue / menu / input / wait / video / minigame(可见 fail-closed 提示)+
16
+ * 舞台元素层 + 层级表(shell.layers)+ 存档(shell.saves.slots)。
17
+ * 有意未做(属「少功能」而非「卡死」,见 README 边界表):历史面板与回溯 UI、
18
+ * 玩家偏好面板、I18N 供给、对话框模板注册、存档缩略图合成。
19
+ */
20
+
21
+ import { DEFAULT_TEXT_CPS, DEFAULT_TOAST_MS } from "@lingfan-engine/ui";
22
+ import { bootHost, must, reportBootFailure, type StageDom } from "./host";
23
+
24
+ /**
25
+ * 工程清单与故事文件:**都在应用资源根 `Resources/` 内**。
26
+ * 宿主把 `Resources` 作为静态根,故逻辑路径就是 `project.json` / `Stories/**`。
27
+ * 清单放在资源根内是有意为之——只有资源根内的文件才会进打包产物,dev 与 prod 同机制。
28
+ * **新增故事文件必须同步此表**(Web 形态按显式清单取文件)。
29
+ */
30
+ const MANIFEST = "project.json";
31
+ /** 本工程的故事文件表(相对资源根;新增故事必须同步此表) */
32
+ const STORIES = ["Stories/title/title_main.story"];
33
+
34
+ /** 页面节点句柄表:每个 id 都必须存在于 index.html(缺一个 `must` 当场抛) */
35
+ const dom: StageDom = {
36
+ root: must("#stage-root"),
37
+ stage: must("#stage"),
38
+ dialogue: must("#dialogue"),
39
+ speaker: must("#speaker"),
40
+ text: must("#text"),
41
+ hint: must("#hint"),
42
+ nvl: must("#nvl"),
43
+ choices: must("#choices"),
44
+ notifications: must("#notifications"),
45
+ toolbar: must("#toolbar"),
46
+ slot: must<HTMLSelectElement>("#slot"),
47
+ save: must<HTMLButtonElement>("#save"),
48
+ load: must<HTMLButtonElement>("#load"),
49
+ transition: must("#transition"),
50
+ error: must("#error"),
51
+ };
52
+
53
+ void bootHost({
54
+ dom,
55
+ manifestFile: MANIFEST,
56
+ stories: STORIES,
57
+ defaultCps: DEFAULT_TEXT_CPS,
58
+ toastDurationMs: DEFAULT_TOAST_MS,
59
+ }).catch((error: unknown) => {
60
+ reportBootFailure(dom, error);
61
+ });
@@ -0,0 +1,183 @@
1
+ /* RenderTargets 的落地:stage / dialogue / choices / notifications / toolbar。
2
+ **层级(z 序)不在这里写死**——由 src/main.ts 按「内建默认 × 工程覆盖
3
+ (project.json shell.layers)」解析后内联写入。 */
4
+ :root {
5
+ color-scheme: dark;
6
+ }
7
+ body {
8
+ margin: 0;
9
+ background: #10131a;
10
+ color: #e6e6e6;
11
+ font-family: system-ui, "Microsoft YaHei", sans-serif;
12
+ /* 文本框选:台词、按钮、面板标签只作展示,拖动或连点不该留下选中段。
13
+ 要提供复制能力时,给那个部件加 data-lf-selectable 放行,不在宿主外面破例。 */
14
+ user-select: none;
15
+ -webkit-user-select: none;
16
+ }
17
+ /* 放行名单:输入框要能选词改写;带 data-lf-selectable 的部件由给出复制操作的宿主自行标记 */
18
+ input,
19
+ textarea,
20
+ [data-lf-selectable] {
21
+ user-select: text;
22
+ -webkit-user-select: text;
23
+ }
24
+ /* 舞台根:铺满视口;屏幕震动(shake)施加在它上面 */
25
+ #stage-root {
26
+ position: fixed;
27
+ inset: 0;
28
+ overflow: hidden;
29
+ }
30
+ /* 舞台元素层:容器不阻塞点击(可交互元素自身恢复 pointer-events) */
31
+ #stage {
32
+ position: absolute;
33
+ inset: 0;
34
+ pointer-events: none;
35
+ }
36
+ /* 对话层 */
37
+ #dialogue {
38
+ position: absolute;
39
+ right: 24px;
40
+ bottom: 24px;
41
+ left: 24px;
42
+ box-sizing: border-box;
43
+ border: 1px solid #2a3140;
44
+ border-radius: 10px;
45
+ background: rgba(20, 24, 33, 0.92);
46
+ padding: 16px 20px;
47
+ min-height: 96px;
48
+ }
49
+ #speaker {
50
+ color: #7aa2f7;
51
+ font-weight: 600;
52
+ margin-bottom: 8px;
53
+ }
54
+ #text {
55
+ line-height: 1.7;
56
+ white-space: pre-wrap;
57
+ /* 项目默认字体档:theme-fonts setProperty(--lf-story-font) 装配;未声明时
58
+ 变量无效,属性落到继承值(等价不设),逐句声明的字体以内联样式覆盖 */
59
+ font-family: var(--lf-story-font);
60
+ }
61
+ #hint {
62
+ text-align: right;
63
+ color: #6b7280;
64
+ font-size: 12px;
65
+ }
66
+ /* NVL 累积层:与对话层同域,铺成阅读区 */
67
+ #nvl {
68
+ position: absolute;
69
+ inset: 24px;
70
+ overflow: auto;
71
+ border: 1px solid #2a3140;
72
+ border-radius: 10px;
73
+ background: rgba(20, 24, 33, 0.94);
74
+ padding: 16px 20px;
75
+ line-height: 1.7;
76
+ }
77
+ /* 等待态层(各自渲染进 #choices) */
78
+ #choices {
79
+ position: absolute;
80
+ top: 50%;
81
+ left: 24px;
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 8px;
85
+ align-items: flex-start;
86
+ max-width: min(520px, calc(100% - 48px));
87
+ transform: translateY(-50%);
88
+ }
89
+ #choices:empty {
90
+ display: none;
91
+ }
92
+ .prompt {
93
+ margin: 0 0 4px;
94
+ color: #9aa5ce;
95
+ }
96
+ .choice {
97
+ padding: 8px 16px;
98
+ }
99
+ .input-row {
100
+ display: flex;
101
+ gap: 8px;
102
+ }
103
+ .input-row input {
104
+ min-width: 200px;
105
+ }
106
+ .note {
107
+ color: #6b7280;
108
+ font-size: 13px;
109
+ }
110
+ .banner {
111
+ border: 1px solid #e0af6888;
112
+ border-radius: 6px;
113
+ background: rgba(42, 36, 21, 0.9);
114
+ color: #e0af68;
115
+ padding: 8px 12px;
116
+ }
117
+ /* 通知 toast 层 */
118
+ #notifications {
119
+ position: absolute;
120
+ top: 16px;
121
+ right: 16px;
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 6px;
125
+ align-items: flex-end;
126
+ }
127
+ .toast {
128
+ border: 1px solid #7aa2f755;
129
+ border-radius: 6px;
130
+ background: rgba(36, 40, 59, 0.94);
131
+ padding: 6px 12px;
132
+ font-size: 13px;
133
+ }
134
+ /* 工具条(存/读):槽位数来自 project.json shell.saves.slots */
135
+ #toolbar {
136
+ position: absolute;
137
+ top: 12px;
138
+ left: 12px;
139
+ display: flex;
140
+ gap: 8px;
141
+ align-items: center;
142
+ font-size: 13px;
143
+ }
144
+ button,
145
+ select,
146
+ input {
147
+ font: inherit;
148
+ color: #e6e6e6;
149
+ background: #1b2130;
150
+ border: 1px solid #2f3a4d;
151
+ border-radius: 6px;
152
+ padding: 4px 10px;
153
+ }
154
+ button {
155
+ cursor: pointer;
156
+ }
157
+ /* 全屏转场遮罩(transition):屏幕级,恒在最上;透明度由帧驱动写入 */
158
+ #transition {
159
+ position: absolute;
160
+ inset: 0;
161
+ display: none;
162
+ background: #000;
163
+ opacity: 0;
164
+ pointer-events: none;
165
+ z-index: 5000;
166
+ }
167
+ /* 引擎错误横幅(fail-closed 可见,不静默) */
168
+ #error {
169
+ position: absolute;
170
+ top: 12px;
171
+ right: 12px;
172
+ left: 12px;
173
+ display: none;
174
+ border: 1px solid #f7768e88;
175
+ border-radius: 6px;
176
+ background: rgba(60, 20, 28, 0.94);
177
+ color: #f7768e;
178
+ padding: 8px 12px;
179
+ font-size: 13px;
180
+ }
181
+ #error:not(:empty) {
182
+ display: block;
183
+ }
@@ -0,0 +1,14 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ /**
4
+ * 字体清单的宿主侧声明(脚手架工程自带,与 vite.config 的虚拟模块对应)。
5
+ */
6
+ declare module "virtual:story-fonts" {
7
+ /** 字体清单条目:file = 资源文件名,name = 字体名(文件名去扩展名) */
8
+ export interface StoryFontEntry {
9
+ file: string;
10
+ name: string;
11
+ }
12
+ /** 构建期扫描 Resources/Fonts 的字体清单(与 stories:build 注册表同源) */
13
+ export const storyFonts: readonly StoryFontEntry[];
14
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "moduleResolution": "bundler",
7
+ "types": ["vite/client"],
8
+ "allowImportingTsExtensions": true,
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "skipLibCheck": true,
13
+ "strict": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noFallthroughCasesInSwitch": true
17
+ },
18
+ "include": ["src/**/*.ts"]
19
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "types": ["node"],
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true
12
+ },
13
+ "include": ["vite.config.ts"]
14
+ }