@momoyu-ink/kit 0.5.0 → 0.5.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.
package/AGENTS.md ADDED
@@ -0,0 +1,413 @@
1
+ # @momoyu-ink/kit — Agent 指南
2
+
3
+ 本文档面向 AI coding agent 和开发者,描述末语引擎 JavaScript SDK(`@momoyu-ink/kit`)的架构与开发规范。
4
+
5
+ > 引擎核心与通用规范见仓库根 [AGENTS.md](../../AGENTS.md)。本文件只覆盖 kit 包内约定。
6
+
7
+ ---
8
+
9
+ ## 概述
10
+
11
+ `@momoyu-ink/kit` 是末语引擎的官方 React SDK,提供:
12
+
13
+ - 自定义 **React Reconciler**,将 React 组件树映射到引擎节点树
14
+ - 引擎通信层(`moyu.executeNodeCommand` / `executePluginCommand` 等)
15
+ - 事件系统(冒泡事件 + 全局事件)
16
+ - 基于 `react-spring` 的动画
17
+ - Stage / Scenario hooks(剧情运行时集成)
18
+ - Stack 风格的导航组件(pages + overlays)
19
+ - 基于 `valtio` 的 sandbox(JS 沙盒变量访问)
20
+ - 由 `ts-rs` 从 Rust 生成的类型绑定
21
+
22
+ **关键环境:**
23
+
24
+ - **浏览器(Web)**:完整 Web API
25
+ - **QuickJS(Native)**:受限运行时,kit 内置必要 polyfill
26
+
27
+ ---
28
+
29
+ ## 目录结构
30
+
31
+ ```
32
+ packages/kit/src/
33
+ ├── lib.ts # 主入口,重新导出公共 API
34
+ ├── moyu.ts # 引擎通信底层(createInstance / updateProps / executeXxxCommand ...)
35
+ ├── node.ts # Node 节点封装类
36
+ ├── react.ts # React Reconciler 实现
37
+ ├── declaration.ts # JSX 命名空间与 IntrinsicElements
38
+ ├── commands.ts # 重新导出 bindings 中的 Command 类型
39
+ ├── events.ts # 事件系统总入口
40
+ ├── events/ # 事件实现
41
+ │ ├── base.ts # BubbleEvent 基础类型
42
+ │ ├── mouse.ts / touch.ts / keyboard.ts / wheel.ts
43
+ │ ├── globals.ts # 全局事件分发
44
+ │ └── listener.ts # addEventListener 实现
45
+ ├── hooks.ts # hooks 总入口
46
+ ├── hooks/
47
+ │ ├── useStage.ts # createStage / StageContextProvider / GameControl
48
+ │ ├── useScenario.ts # useScenario / nextLine / setWaiting
49
+ │ ├── useFadeInOut.ts # 淡入淡出辅助
50
+ │ └── useSoundEffect.ts # 音效便捷 hook
51
+ ├── components/
52
+ │ └── navigation.tsx # createStackNavigator / Navigation / useNavigation
53
+ ├── spring/ # react-spring 集成
54
+ │ ├── index.ts
55
+ │ ├── animated.ts # animated.<element> 原语
56
+ │ └── primitives.ts
57
+ ├── state.ts # 内部状态
58
+ ├── sandbox.ts # ARCHIVE/GLOBAL 变量 Proxy(通过 scenario 插件访问剧本变量)
59
+ ├── runtime-globals.ts # Console / 全局 API 类型声明
60
+ ├── utils.ts # 工具函数
61
+ ├── zod-patch.ts # Zod 兼容补丁
62
+ ├── jsx-runtime.ts # React 19 自动 JSX runtime
63
+ ├── jsx-dev-runtime.ts # dev 模式 JSX runtime
64
+ └── bindings/ # 由 `yarn generate:bindings` 从 Rust 生成,勿手工改
65
+ ├── NodeProps.ts / SpriteProps.ts / TextProps.ts / VideoProps.ts / ...
66
+ ├── AudioCommand.ts / ScenarioCommand.ts / SystemCommand.ts / ...
67
+ └── ...
68
+ ```
69
+
70
+ **重要规则:**
71
+
72
+ - `bindings/` 由 `yarn generate:bindings`(运行 `cargo test export_bindings --workspace`)自动生成,**禁止手工修改**。修改对应的 Rust 结构体后必须重新生成。
73
+ - `lib.ts` 会同时重新导出 `./spring` 与 `@react-spring/core`,并通过 package exports 暴露 ESM / CJS 双入口;改动打包或导出配置时要同时检查 `tsup.config.ts` 与 `package.json`。
74
+
75
+ ---
76
+
77
+ ## 核心模块
78
+
79
+ ### 1. React Reconciler(`react.ts`)
80
+
81
+ kit 实现了完整的 React Reconciler,将 React 组件映射到引擎节点:
82
+
83
+ ```typescript
84
+ const hostConfig: HostConfig<...> = {
85
+ createInstance(type, props) {
86
+ return Node.create(props.label ?? '', type, props);
87
+ },
88
+ appendChild(parent, child) { parent.addChild(child); },
89
+ // ...
90
+ };
91
+ ```
92
+
93
+ **重要约束:**
94
+
95
+ - **不支持原生 HTML 元素**(`div` / `span` 等会崩溃)
96
+ - 只支持 Moyu 特有元素(见下方 "内置组件")
97
+ - 使用 `createRoot()` 而非 `react-dom` 的 `createRoot`
98
+
99
+ ### 2. 引擎通信层(`moyu.ts`)
100
+
101
+ 通过全局 `moyu` 对象与 Rust 引擎通信:
102
+
103
+ ```typescript
104
+ // 底层命令
105
+ moyu.pushCommand(name, args, callback); // 通用命令
106
+ moyu.executeNodeCommand(nodeId, payload); // 节点命令
107
+ moyu.executePluginCommand(pluginName, payload); // 插件命令
108
+ ```
109
+
110
+ **封装 API(kit 导出):**
111
+
112
+ - 节点生命周期:`createInstance` / `destroyInstance`
113
+ - 节点树:`addChild` / `insertChild` / `insertChildBefore` / `removeChildAt` / `removeChild`
114
+ - 属性:`updateProps`
115
+ - 命令:`executeNodeCommand` / `executePluginCommand`
116
+
117
+ ### 3. 事件系统
118
+
119
+ 引擎从 Rust 层通过全局 `__moyu_receive_event` 回调把事件送到 JS 层,kit 负责分发。
120
+
121
+ **事件类别:**
122
+
123
+ - **冒泡事件**:`MouseEvent` / `TouchEvent` / `KeyboardEvent` / `WheelEvent`,从目标节点向上冒泡
124
+ - **节点事件**:`NodeEvent`(创建、销毁)
125
+ - **全局事件**:通过 `addEventListener(type, handler)` 监听
126
+
127
+ **支持的全局事件类型**(`addEventListener(type, handler)`,`type` 为纯字符串,分发逻辑见 `events/globals.ts`):
128
+
129
+ - **冒泡事件的全局落点**:引擎 top-level name 为 `mouseevent` / `touchevent` / `keyboardevent` / `wheelevent`;分发时按 `body.kind`(PascalCase)调节点 `on<Kind>`,冒泡完成后按 `kind.toLowerCase()` 触发全局监听器。因此全局监听用小写名,如 `click` / `mousedown` / `mousemove` / `keydown` / `wheel` / `touchstart` 等。
130
+ - **自定义事件**(`customevent`):按 `body.name` 分发;`targetId === 0` 时触发 `globalEventListeners[name.toLowerCase()]`,否则派发到节点的 `on<Name>`。
131
+ - **节点生命周期**(`nodeevent`):内部用于清理 `nodeMap`,**不暴露给用户监听**。
132
+ - **RAF 驱动**(`animationframecallbackevent`):消耗 `globalRequestAnimationFrameListeners`,支撑 `requestAnimationFrame` polyfill,**不暴露给用户监听**。
133
+ - **其它由引擎下发的 name**:通过 `default` 分支直接查 `globalEventListeners[name]`(如 `ready`、`resize`、`fullscreen`、`beforeunload`、`gamepad` 等,具体取决于引擎发射集合)。
134
+
135
+ 具体事件字段的定义见 `bindings/`(`KeyboardEventKind` / `MouseEventKind` / `TouchEventKind` / `GameEvent` / `ResizeEvent` 等)。
136
+
137
+ ```typescript
138
+ import { addEventListener } from '@momoyu-ink/kit';
139
+
140
+ const unregister = addEventListener('keydown', (event) => {
141
+ // handle keyboard event
142
+ });
143
+ ```
144
+
145
+ ### 4. 动画系统
146
+
147
+ 基于 `react-spring` 的声明式动画:
148
+
149
+ ```typescript
150
+ import { animated, useSpring } from '@momoyu-ink/kit';
151
+
152
+ function Fade() {
153
+ const styles = useSpring({ opacity: 1, from: { opacity: 0 } });
154
+ return <animated.sprite style={styles} src="image.png" />;
155
+ }
156
+ ```
157
+
158
+ **关键原则:由 valtio / 外部 state 驱动的动画使用 reactive 模式。**
159
+
160
+ ```typescript
161
+ // ✅ 推荐:reactive 模式,state 变化自动重新动画
162
+ const springs = useSpring({
163
+ x: character.x,
164
+ y: character.y,
165
+ config: { duration: 500 },
166
+ });
167
+
168
+ // ⚠️ factory 模式(`useSpring(() => ({...}))`)需要手动 `api.start()`
169
+ // 触发,与 valtio 频繁重渲染配合时容易出现时序问题。仅在按钮点击、
170
+ // 动画链等手动触发的场景使用。
171
+ ```
172
+
173
+ `useTransition` 只跟踪 item 的 enter / leave / update by key,不会传播已有 item 的属性变化。如需响应 item 属性变化,在子组件内部用 `useSnapshot()` 读最新 state。
174
+
175
+ ### 5. Stage & Scenario(`hooks/useStage.ts` + `hooks/useScenario.ts`)
176
+
177
+ Stage 是剧情驱动 UI 的核心抽象:命令调度、流程控制、skip / auto 状态,以及自动模式下的 barrier / ticket 协调。
178
+
179
+ **常用 API:**
180
+
181
+ - `createStage()` — 创建 stage 实例,负责注册命令处理器、文本处理器和模式控制逻辑
182
+ - `StageContextProvider` / `useStageContext()` — 在 actor 树中共享同一个 stage
183
+ - `useScenario(stories, startName?, entryName?, goNextOnLoad = false)` — 加载剧情并接管生命周期
184
+ - `nextLine()` / `setWaiting(time, skippable)` — 直接调用 scenario 插件推进或进入等待
185
+ - `useSkipCallback(fn)` / `useInterruptCallback(fn)` — 注册 skip 收尾逻辑和用户点击打断逻辑
186
+ - `useSkipBlocker(fn)` / `useAutoBlocker(fn)` — 返回 `true` 时阻止 skip / auto 启动或继续
187
+ - `useBeforeHandleCommandCallback(fn)` — 在每条 `scenariocommandline` 分发前执行
188
+ - `useAutoTicket()` — 为当前 auto barrier 发放完成 ticket
189
+ - `useIsSkipping()` / `useIsAutoing()` — 读取 `skipState` / `autoState` 的响应式快照
190
+ - `setDefaultAutoTailMs(ms)` — 设置 auto 模式默认尾延迟,用于 fallback 或新 ticket 的 `tailMs`
191
+
192
+ **GameControl**(传给命令 handler):
193
+
194
+ | 方法 | 作用 |
195
+ |------|------|
196
+ | `control.hold()` | 暂停直到用户操作;在 auto 模式下会打开 `hold` barrier |
197
+ | `control.setWaiting(ms, skippable)` | 定时等待;在 auto 模式下会打开 `wait` barrier |
198
+ | `control.nextLine()` | 立即推进 |
199
+ | `control.unskippable()` | 将本次 dispatch 标记为不可跳过 |
200
+ | `control.record(meta)` | 记录当前运行时快照到 backlog,并返回 record id |
201
+
202
+ **实现要点:**
203
+
204
+ - Stage 当前只封装 `scenariocommandline` 和 `scenariotext` 两类事件;`ResolvedSystemCallLine` 虽然从 `events.ts` 导出,但若要处理 system call,需要自己显式监听对应事件。
205
+ - auto 模式下,`hold()` / `setWaiting()` 不会直接把控制权交给普通等待逻辑,而是打开一个 barrier。只有当 barrier 的 ticket 全部 `done()` / `cancel()`,并且各自 `tailMs` 都结算完成后,Stage 才会恢复 `nextLine()`。
206
+ - `useAutoTicket()` 允许 actor 参与 auto barrier 协调。若 ticket 在 barrier 打开前的短暂采集窗口内创建,Stage 会先把它放入 pending 集合,再在下一个 barrier 打开时收编,解决语音等副作用早于文本 barrier 注册的问题。
207
+ - skip / auto blocker 用于表达“当前流程禁止模式继续”的业务条件,适合选项菜单、模态交互等必须等待用户操作的场景。
208
+
209
+ `useScenario` 内部维护一个带 session key 的 refcount session。key 由 `stories`、`startName`、`entryName`、`goNextOnLoad` 共同决定;相同 key 的瞬时 remount 会复用同一个 live session。cleanup 时只会减少 `refCount`,真正的 `terminateStory` 会被排入串行生命周期队列,并推迟到下一个宏任务,这样 Fast Refresh 之类的短暂卸载不会重置剧情;只有 key 变化或最终卸载时,旧 session 才会被终止。
210
+
211
+ ### 6. 导航(`components/navigation.tsx`)
212
+
213
+ Stack 风格导航,区分 **pages**(全屏)和 **overlays**(模态):
214
+
215
+ ```typescript
216
+ import { createStackNavigator, createStaticNavigation, RegisterNavigator } from '@momoyu-ink/kit';
217
+
218
+ const navigator = createStackNavigator({
219
+ pages: { title: TitlePage, stage: StagePage },
220
+ overlays: { menu: MenuOverlay, settings: SettingsOverlay },
221
+ initialPage: 'title',
222
+ });
223
+
224
+ export const Navigation = createStaticNavigation(navigator);
225
+
226
+ declare module '@momoyu-ink/kit' {
227
+ interface RootNavigatorList extends RegisterNavigator<typeof navigator> {}
228
+ }
229
+ ```
230
+
231
+ - `useNavigation()` / `getNavigator()` — 组件内/外部导航
232
+ - `useNavigationParams<T>()` — 读取当前页面/overlay 参数
233
+ - 通过 module augmentation 扩展公开模块 `@momoyu-ink/kit` 中的 `RootNavigatorList`,不要去扩展内部相对路径模块
234
+
235
+ ### 7. Sandbox(`sandbox.ts`)
236
+
237
+ Sandbox 为剧本表达式提供受控求值环境:
238
+
239
+ - `ARCHIVE` 代理到 scenario 插件的普通变量(`getVariable` / `setVariable`)
240
+ - `GLOBAL` 代理到 scenario 插件的永久变量(`getPermanentVariable` / `setPermanentVariable`)
241
+ - `__moyu_eval_sandbox` 会在代理作用域里执行表达式,保留 JS 内建全局的透传
242
+ - 在 sandbox 内部,`window` 和 `globalThis` 都会指向 sandbox proxy 自身,而不是浏览器真实全局对象;编写表达式或运行时辅助逻辑时不能假设它们等同于宿主环境
243
+
244
+ ### 8. Bindings(`bindings/`)
245
+
246
+ 由 Rust 用 `ts-rs` 自动生成的 TypeScript 类型。涵盖:
247
+
248
+ - 节点 Props(`NodeProps` / `SpriteProps` / `TextProps` / `VideoProps` / `ClipProps` / `FilterProps` / `BackdropProps` / `AnimationProps`)
249
+ - 命令(`AudioCommand` / `TextCommand` / `ScenarioCommand` / `SystemCommand` / `GamepadCommand` / `VideoCommand`)
250
+ - 事件(`KeyboardEvent` / `MouseEvent` / `TouchEvent` / `WheelEvent` / `GameEvent` / `GamepadEvent` / `ScenarioEvent` / `TextEvent` / ...)
251
+ - 其它类型(`WindowState` / `AudioSettings` / `MoyuCursor` / `SpriteMode` / `NineSliceMode` / ...)
252
+
253
+ ---
254
+
255
+ ## 内置组件
256
+
257
+ ### JSX IntrinsicElements
258
+
259
+ | 组件 | 用途 | 关键 Props |
260
+ | ------------- | -------- | -------------------------------------------- |
261
+ | `<container>` | 容器 | 基础变换属性 |
262
+ | `<sprite>` | 图片精灵 | `src`, `area`, `mode`, `bounds` |
263
+ | `<text>` | 文本 | `text`, `fontSize`, `fillColor`, `printMode`, `onFinish`, `onProgress` |
264
+ | `<clip>` | 裁剪 | 区域尺寸 |
265
+ | `<filter>` | 滤镜 | filter 类型与参数 |
266
+ | `<backdrop>` | 背景滤镜 | filter 类型与参数 |
267
+ | `<animation>` | 帧动画 | 动画配置 |
268
+ | `<video>` | 视频 | `src`, `onEnded`, `onStateChange` |
269
+
270
+ ### 通用节点属性(`MoyuNodeAttributes`)
271
+
272
+ `MoyuNodeAttributes` = `MoyuListenerAttributes` + `NodeProps`(由 bindings 生成)+ `children`。
273
+
274
+ **来自 `NodeProps`(`bindings/NodeProps.ts`):**
275
+
276
+ ```typescript
277
+ type NodeProps = {
278
+ label?: string;
279
+ anchor?: [number, number]; // 0.0-1.0
280
+ pivot?: [number, number]; // pixels
281
+ x?: number; y?: number;
282
+ scale?: number; scaleX?: number; scaleY?: number;
283
+ rotation?: number;
284
+ skew?: number; skewX?: number; skewY?: number;
285
+ visible?: boolean;
286
+ tint?: string;
287
+ opacity?: number;
288
+ interactive?: boolean;
289
+ cursor?: MoyuCursor;
290
+ };
291
+ ```
292
+
293
+ **来自 `MoyuListenerAttributes`(`declaration.ts`):**
294
+
295
+ ```typescript
296
+ interface MoyuListenerAttributes {
297
+ onClick?: MoyuEventHandler<MouseEvent>;
298
+ onMouseEnter?: MoyuEventHandler<MouseEvent>;
299
+ onMouseLeave?: MoyuEventHandler<MouseEvent>;
300
+ onMouseDown?: MoyuEventHandler<MouseEvent>;
301
+ onMouseUp?: MoyuEventHandler<MouseEvent>;
302
+ onMouseMove?: MoyuEventHandler<MouseEvent>;
303
+ onKeyDown?: MoyuEventHandler<KeyboardEvent>;
304
+ onKeyUp?: MoyuEventHandler<KeyboardEvent>;
305
+ onKeyPress?: MoyuEventHandler<KeyboardEvent>;
306
+ onTouchStart?: MoyuEventHandler<TouchEvent>;
307
+ onTouchMove?: MoyuEventHandler<TouchEvent>;
308
+ onTouchEnd?: MoyuEventHandler<TouchEvent>;
309
+ onTouchCancel?: MoyuEventHandler<TouchEvent>;
310
+ }
311
+ ```
312
+
313
+ 注意:`declaration.ts` 当前并未暴露 `onWheel`,即使 wheel 事件会冲出 JS 层;如需需在节点上绑定 wheel,目前的做法是全局 `addEventListener('wheel', ...)`。这应当在未来得到改进,改进后本处描述应当更新。
314
+
315
+ ---
316
+
317
+ ## 开发规范
318
+
319
+ ### 新增内置组件(需 Rust + kit 配合)
320
+
321
+ 1. Rust 端:在 `crates/nodes` 添加节点类型(见根 AGENTS.md),用 `#[derive(TS)]` 标注 Props。
322
+ 2. 重新生成 bindings:`yarn generate:bindings`。
323
+ 3. 在 `declaration.ts` 中添加 JSX 声明:
324
+
325
+ ```typescript
326
+ // declaration.ts
327
+ export type MoyuMyComponentAttributes = MyComponentProps & MoyuNodeAttributes;
328
+
329
+ declare namespace JSX {
330
+ interface IntrinsicElements {
331
+ mycomponent: DetailedMoyuProps<MoyuMyComponentAttributes>;
332
+ }
333
+ }
334
+ ```
335
+
336
+ 4. 如需 spring 支持,在 `spring/primitives.ts` 中加入对应 `animated.mycomponent`。
337
+
338
+ ### 新增全局事件类型
339
+
340
+ 1. 在 `events/` 下新建事件类型文件(如果不是 bindings 自动生成的)。
341
+ 2. 在 `events/globals.ts` / `events/listener.ts` 中处理事件分发。
342
+ 3. 在 `events.ts` 中 re-export。
343
+
344
+ ### 新增 hook
345
+
346
+ - 落在 `hooks/` 下单独文件,在 `hooks.ts` 中 re-export。
347
+ - 优先复用 `useStage` / `useScenario` / valtio snapshot 等既有能力。
348
+
349
+ ### 修改 bindings
350
+
351
+ **绝对不要**直接编辑 `bindings/` 下的文件。流程:
352
+
353
+ 1. 修改 Rust 侧的 `#[derive(TS)]` 结构体。
354
+ 2. 在仓库根目录运行 `yarn generate:bindings`。
355
+ 3. 检查 diff,确认改动符合预期。
356
+
357
+ ---
358
+
359
+ ## 环境兼容性
360
+
361
+ kit 设计为同时支持:
362
+
363
+ - **浏览器环境**:完整 Web API
364
+ - **QuickJS 环境**(原生平台):受限 JS 运行时
365
+
366
+ QuickJS 环境的 polyfill / 类型垫片:
367
+
368
+ - `requestAnimationFrame` / `cancelAnimationFrame`(`events/globals.ts`,非浏览器环境下基于引擎 `animationframecallbackevent` 实现)
369
+ - `console` 与其它 Web 标准 API 类型声明(`runtime-globals.ts`)
370
+
371
+ **环境感知:**
372
+
373
+ 编写代码时假设代码可能同时运行在两种环境;不要直接依赖 `window` / `document` 之类只在浏览器存在的全局。
374
+
375
+ ---
376
+
377
+ ## 构建
378
+
379
+ ```bash
380
+ # 仓库根目录
381
+ yarn build # 构建所有 packages
382
+ yarn generate:bindings # 重新生成 bindings
383
+
384
+ # packages/kit 目录
385
+ yarn build # 仅构建 kit(tsup)
386
+ yarn dev # watch 模式
387
+ ```
388
+
389
+ `tsup` 配置输出双入口:ESM(`dist/lib.mjs`)和 CJS(`dist/lib.cjs`),同时提供 `jsx-runtime` / `jsx-dev-runtime` 子路径导出。
390
+
391
+ ---
392
+
393
+ ## 技术选型概览
394
+
395
+ - **React 19 + react-reconciler**:提供自定义渲染器基础,负责把 React 树映射到 Moyu 节点树。
396
+ - **react-spring**:提供 `animated.<element>`、spring host 和时间调度能力,用于声明式动画与交互动画。
397
+ - **valtio**:用于 Stage 的 skip / auto 响应式状态,以及与 sandbox / 运行时协作的轻量状态读取。
398
+ - **zod**:用于命令 schema、运行时解析和上层项目中的命令验证。
399
+
400
+ 具体版本、导出格式和构建细节以 `package.json`、`tsup.config.ts` 与源码为准。
401
+
402
+ ---
403
+
404
+ ## 注意事项
405
+
406
+ 1. **不支持 HTML 元素**:Reconciler 仅接受 Moyu JSX elements。
407
+ 2. **事件冒泡**:遵循类似 DOM 的冒泡机制。
408
+ 3. **属性命名**:JS 端使用 camelCase,与 Rust 端 snake_case 自动转换。
409
+ 4. **类型安全**:充分利用 bindings 与 TS 类型系统。
410
+ 5. **性能**:避免在渲染循环/事件回调中进行大量分配或同步计算。
411
+ 6. **bindings 只读**:一切类型变更走 Rust + `yarn generate:bindings`。
412
+ 7. **HMR 与 scenario**:`useScenario` 依赖 session key、refcount 和串行生命周期队列实现 HMR 弹性;相同 key 的瞬时 remount 会复用 live session,只有 key 变化或最终卸载时才会真正 `terminateStory`。
413
+ 8. **CJS/ESM 分发**:改动打包配置前确认消费者(框架、SDK 用户)不会因运行时重复化(如 valtio)而失效。
@@ -1,4 +1,4 @@
1
1
  export { Fragment, jsxDEV } from 'react/jsx-dev-runtime';
2
- export { J as JSX } from './jsx-dev-runtime--ZXLAgUi.mjs';
2
+ export { J as JSX } from './jsx-dev-runtime-C7RQHtUh.mjs';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
@@ -1,4 +1,4 @@
1
1
  export { Fragment, jsxDEV } from 'react/jsx-dev-runtime';
2
- export { J as JSX } from './jsx-dev-runtime--ZXLAgUi.js';
2
+ export { J as JSX } from './jsx-dev-runtime-C7RQHtUh.js';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
@@ -1,4 +1,4 @@
1
1
  export { Fragment, jsx, jsxs } from 'react/jsx-runtime';
2
- export { J as JSX } from './jsx-dev-runtime--ZXLAgUi.mjs';
2
+ export { J as JSX } from './jsx-dev-runtime-C7RQHtUh.mjs';
3
3
  import 'react/jsx-dev-runtime';
4
4
  import 'react';
@@ -1,4 +1,4 @@
1
1
  export { Fragment, jsx, jsxs } from 'react/jsx-runtime';
2
- export { J as JSX } from './jsx-dev-runtime--ZXLAgUi.js';
2
+ export { J as JSX } from './jsx-dev-runtime-C7RQHtUh.js';
3
3
  import 'react/jsx-dev-runtime';
4
4
  import 'react';
package/dist/lib.cjs CHANGED
@@ -31,7 +31,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/lib.ts
32
32
  var lib_exports = {};
33
33
  __export(lib_exports, {
34
- MoyuRenderer: () => MoyuRenderer,
35
34
  OverlayParamsContext: () => OverlayParamsContext,
36
35
  PageParamsContext: () => PageParamsContext,
37
36
  StageContextProvider: () => StageContextProvider,
@@ -390,7 +389,7 @@ function createRoot(options) {
390
389
  });
391
390
  return {
392
391
  render: (reactElement, callback) => {
393
- return MoyuRenderer.updateContainer(reactElement, rootElement, null, callback);
392
+ MoyuRenderer.updateContainer(reactElement, rootElement, null, callback);
394
393
  }
395
394
  };
396
395
  }
@@ -855,6 +854,138 @@ function useFadeOut(outTime, pause = false, onFinished) {
855
854
 
856
855
  // src/hooks/useScenario.ts
857
856
  var import_react4 = require("react");
857
+ var currentScenarioSession = null;
858
+ var scenarioSessionId = 0;
859
+ var scenarioLifecycleQueue = Promise.resolve();
860
+ function normalizeStories(stories) {
861
+ return stories.filter((story) => Boolean(story));
862
+ }
863
+ function createScenarioSessionKey(config) {
864
+ return JSON.stringify({
865
+ stories: config.stories,
866
+ startName: config.startName ?? null,
867
+ entryName: config.entryName ?? null,
868
+ goNextOnLoad: config.goNextOnLoad
869
+ });
870
+ }
871
+ function isCurrentScenarioSession(session) {
872
+ return currentScenarioSession?.id === session.id && !session.disposed;
873
+ }
874
+ function enqueueScenarioLifecycleTask(task) {
875
+ const nextTask = scenarioLifecycleQueue.then(task, task);
876
+ scenarioLifecycleQueue = nextTask.catch(() => {
877
+ });
878
+ return nextTask;
879
+ }
880
+ function clearScenarioDisposeTimer(session) {
881
+ if (session.disposeTimer !== null) {
882
+ clearTimeout(session.disposeTimer);
883
+ session.disposeTimer = null;
884
+ }
885
+ }
886
+ async function terminateCurrentScenario() {
887
+ try {
888
+ await executePluginCommand("scenario", {
889
+ subCommand: "terminateStory"
890
+ });
891
+ } catch (error) {
892
+ console.error("Failed to terminate scenario:", error);
893
+ }
894
+ }
895
+ async function startScenarioSession(session, config) {
896
+ for (const story of config.stories) {
897
+ if (!isCurrentScenarioSession(session)) return;
898
+ try {
899
+ await executePluginCommand("scenario", {
900
+ subCommand: "addStory",
901
+ name: story
902
+ });
903
+ } catch (error) {
904
+ console.error("Failed to load scenario:", error);
905
+ }
906
+ }
907
+ if (!isCurrentScenarioSession(session)) return;
908
+ try {
909
+ if (config.startName) {
910
+ await executePluginCommand("scenario", {
911
+ subCommand: "startStory",
912
+ name: config.startName,
913
+ entry: config.entryName
914
+ });
915
+ }
916
+ if (!isCurrentScenarioSession(session)) return;
917
+ if (config.goNextOnLoad) {
918
+ await nextLine();
919
+ }
920
+ } catch (error) {
921
+ console.error(`Failed to start scenario ${config.startName}:`, error);
922
+ }
923
+ }
924
+ function ensureScenarioSessionStarted(session, config) {
925
+ if (session.started || session.startPromise !== null) {
926
+ return;
927
+ }
928
+ const startPromise = enqueueScenarioLifecycleTask(async () => {
929
+ if (!isCurrentScenarioSession(session)) return;
930
+ await startScenarioSession(session, config);
931
+ if (isCurrentScenarioSession(session)) {
932
+ session.started = true;
933
+ }
934
+ });
935
+ session.startPromise = startPromise;
936
+ void startPromise.finally(() => {
937
+ if (session.startPromise === startPromise) {
938
+ session.startPromise = null;
939
+ }
940
+ });
941
+ }
942
+ function disposeScenarioSession(session, delayMs) {
943
+ clearScenarioDisposeTimer(session);
944
+ session.disposeTimer = setTimeout(() => {
945
+ if (!isCurrentScenarioSession(session) || session.refCount > 0) {
946
+ return;
947
+ }
948
+ session.disposeTimer = null;
949
+ session.disposed = true;
950
+ currentScenarioSession = null;
951
+ void enqueueScenarioLifecycleTask(terminateCurrentScenario);
952
+ }, delayMs);
953
+ }
954
+ function acquireScenarioSession(config) {
955
+ const key = createScenarioSessionKey(config);
956
+ if (currentScenarioSession !== null && currentScenarioSession.key !== key) {
957
+ const previousSession = currentScenarioSession;
958
+ clearScenarioDisposeTimer(previousSession);
959
+ previousSession.disposed = true;
960
+ currentScenarioSession = null;
961
+ void enqueueScenarioLifecycleTask(terminateCurrentScenario);
962
+ }
963
+ if (currentScenarioSession === null) {
964
+ currentScenarioSession = {
965
+ id: ++scenarioSessionId,
966
+ key,
967
+ refCount: 0,
968
+ started: false,
969
+ startPromise: null,
970
+ disposeTimer: null,
971
+ disposed: false
972
+ };
973
+ }
974
+ clearScenarioDisposeTimer(currentScenarioSession);
975
+ currentScenarioSession.refCount += 1;
976
+ ensureScenarioSessionStarted(currentScenarioSession, config);
977
+ return currentScenarioSession;
978
+ }
979
+ function releaseScenarioSession(session) {
980
+ if (!isCurrentScenarioSession(session)) {
981
+ return;
982
+ }
983
+ session.refCount = Math.max(0, session.refCount - 1);
984
+ if (session.refCount > 0) {
985
+ return;
986
+ }
987
+ disposeScenarioSession(session, 0);
988
+ }
858
989
  function nextLine() {
859
990
  return executePluginCommand("scenario", { subCommand: "nextLine" });
860
991
  }
@@ -863,39 +994,14 @@ function setWaiting(time, skippable) {
863
994
  }
864
995
  function useScenario(stories, startName, entryName, goNextOnLoad = false) {
865
996
  (0, import_react4.useEffect)(() => {
866
- const loadScenario = async () => {
867
- for (const story of stories) {
868
- if (!story) continue;
869
- try {
870
- await executePluginCommand("scenario", {
871
- subCommand: "addStory",
872
- name: story
873
- });
874
- } catch (error) {
875
- console.error(`Failed to load scenario:`, error);
876
- }
877
- }
878
- };
879
- void loadScenario().then(async () => {
880
- try {
881
- if (startName) {
882
- await executePluginCommand("scenario", {
883
- subCommand: "startStory",
884
- name: startName,
885
- entry: entryName
886
- });
887
- }
888
- if (goNextOnLoad) {
889
- await nextLine();
890
- }
891
- } catch (error) {
892
- console.error(`Failed to start scenario ${startName}:`, error);
893
- }
997
+ const session = acquireScenarioSession({
998
+ stories: normalizeStories(stories),
999
+ startName,
1000
+ entryName,
1001
+ goNextOnLoad
894
1002
  });
895
1003
  return () => {
896
- executePluginCommand("scenario", {
897
- subCommand: "terminateStory"
898
- });
1004
+ releaseScenarioSession(session);
899
1005
  };
900
1006
  }, [stories, startName, entryName, goNextOnLoad]);
901
1007
  }
package/dist/lib.d.mts CHANGED
@@ -1,8 +1,7 @@
1
1
  import * as React$1 from 'react';
2
- import React__default, { Component, ReactElement, ForwardRefExoticComponent, CSSProperties, FC } from 'react';
3
- import Reconciler, { BaseErrorInfo } from 'react-reconciler';
4
- import { N as Node, M as MouseEvent, a as MoyuEvent, C as Command, b as MaybePromise, J as JSX } from './jsx-dev-runtime--ZXLAgUi.mjs';
5
- export { A as AudioCommand, c as AudioSettings, B as BubbleEvent, D as DetailedMoyuProps, E as EffectParams, G as GamepadCommand, K as KeyboardEvent, d as KeyboardEventKind, e as MakeNullOptional, f as MouseEventKind, g as MoyuAnimationAttributes, h as MoyuBackdropAttributes, i as MoyuClipAttributes, j as MoyuContainerAttributes, k as MoyuEventHandler, l as MoyuFilterAttributes, m as MoyuListenerAttributes, n as MoyuNodeAttributes, o as MoyuSpriteAttributes, p as MoyuTextAttributes, q as MoyuVideoAttributes, S as ScenarioCommand, r as SystemCommand, T as TextCommand, s as TouchEvent, t as TouchEventKind, u as Tuple2, v as Tuple3, w as Tuple4, W as WindowState, x as createBubbleEvent, y as getStageSize } from './jsx-dev-runtime--ZXLAgUi.mjs';
2
+ import React__default, { ReactElement, Component, ForwardRefExoticComponent, CSSProperties, FC } from 'react';
3
+ import { M as MouseEvent, a as MoyuEvent, C as Command, b as MaybePromise, J as JSX } from './jsx-dev-runtime-C7RQHtUh.mjs';
4
+ export { A as AudioCommand, c as AudioSettings, B as BubbleEvent, D as DetailedMoyuProps, E as EffectParams, G as GamepadCommand, K as KeyboardEvent, d as KeyboardEventKind, e as MakeNullOptional, f as MouseEventKind, g as MoyuAnimationAttributes, h as MoyuBackdropAttributes, i as MoyuClipAttributes, j as MoyuContainerAttributes, k as MoyuEventHandler, l as MoyuFilterAttributes, m as MoyuListenerAttributes, n as MoyuNodeAttributes, o as MoyuSpriteAttributes, p as MoyuTextAttributes, q as MoyuVideoAttributes, N as Node, S as ScenarioCommand, r as SystemCommand, T as TextCommand, s as TouchEvent, t as TouchEventKind, u as Tuple2, v as Tuple3, w as Tuple4, W as WindowState, x as createBubbleEvent, y as getStageSize } from './jsx-dev-runtime-C7RQHtUh.mjs';
6
5
  import { FluidValue } from '@react-spring/shared';
7
6
  import { ElementType, AssignableKeys, ComponentPropsWithRef } from '@react-spring/types';
8
7
  import { Snapshot } from 'valtio';
@@ -12,22 +11,26 @@ import { ZodType } from 'zod';
12
11
  import 'react/jsx-runtime';
13
12
  import 'react/jsx-dev-runtime';
14
13
 
15
- declare const MoyuRenderer: Reconciler.Reconciler<Node, Node, never, never, never, any>;
14
+ interface RootErrorInfo {
15
+ componentStack?: string;
16
+ }
17
+ interface RootUncaughtErrorInfo extends RootErrorInfo {
18
+ errorBoundary?: Component;
19
+ }
20
+ interface RootHandle {
21
+ render(reactElement: ReactElement, callback?: (() => void) | null): void;
22
+ }
16
23
  interface RootOptions {
17
24
  /**
18
25
  * Prefix for `useId`.
19
26
  */
20
27
  identifierPrefix?: string;
21
- onUncaughtError: (error: Error, info: BaseErrorInfo & {
22
- errorBoundary?: Component;
23
- }) => void;
24
- onCaughtError: (error: Error, info: BaseErrorInfo) => void;
25
- onRecoverableError: (error: Error, info: BaseErrorInfo) => void;
28
+ onUncaughtError: (error: Error, info: RootUncaughtErrorInfo) => void;
29
+ onCaughtError: (error: Error, info: RootErrorInfo) => void;
30
+ onRecoverableError: (error: Error, info: RootErrorInfo) => void;
26
31
  onDefaultTransitionIndicator: () => void;
27
32
  }
28
- declare function createRoot(options?: RootOptions): {
29
- render: (reactElement: ReactElement, callback?: (() => void) | null) => Reconciler.Lane;
30
- };
33
+ declare function createRoot(options?: RootOptions): RootHandle;
31
34
 
32
35
  declare const globalEventListeners: Record<string, ((event: unknown) => void)[]>;
33
36
  declare const globalRequestAnimationFrameListeners: FrameRequestCallback[];
@@ -516,6 +519,7 @@ declare function nextLine(): any;
516
519
  declare function setWaiting(time: number, skippable: boolean): void;
517
520
  /**
518
521
  * Custom hook to manage scenario lifecycle (load, start, terminate).
522
+ * The underlying session is resilient to transient remounts such as Fast Refresh.
519
523
  *
520
524
  * @param {string[]} stories - An array of story names to load.
521
525
  * @param {string} [startName] - The name of the story to start.
@@ -664,4 +668,4 @@ declare module 'zod' {
664
668
  }
665
669
  }
666
670
 
667
- export { type AnimatedComponent, type AnimatedProps, type AnimationFrameCallbackEvent, type AutoTicketHandle, type AutoTicketOptions, type BeforeUnloadEvent, Command, type CommandHandler, type CustomEvent, type DefaultOverlayName, type DefaultPageName, type FocusEvent, type FocusEventKind, type FullScreenEvent, type FullscreenEventKind, type GameControl, type GameEvent, type Gamepad, type GamepadButton, type GamepadEvent, type GamepadHapticActuator, JSX, type KeyboardLocation, type Literal, MaybePromise, MouseEvent, MoyuEvent, MoyuRenderer, type NavigationState, type Navigator, Node, type NodeEvent, type OverlayConfig, type OverlayDescriptor, type OverlayInfo, OverlayParamsContext, type PageConfig, type PageDescriptor, PageParamsContext, type RegisterNavigator, type ResizeEvent, type ResolvedArgument, type ResolvedCommandLine, type ResolvedSystemCallLine, type RootNavigatorList, type RootOptions, type ScenarioCommandBaseType, type ScenarioEvent, type StackNavigatorOptions, StageContextProvider, type StageInstance, type TextEvent, type TextLine, type TextLineHandler, type WheelEvent, type WheelEventDeltaMode, type WithAnimated, animated as a, addEventListener, animated, autoState, createRoot, createStackNavigator, createStage, createStaticNavigation, executeNodeCommand, executePluginCommand, getNavigator, globalEventListeners, globalRequestAnimationFrameListeners, moyu, nextLine, setDefaultAutoTailMs, setWaiting, skipState, useAutoBlocker, useAutoTicket, useBeforeHandleCommandCallback, useFadeIn, useFadeInOut, useFadeOut, useInterruptCallback, useIsAutoing, useIsSkipping, useNavigation, useNavigationParams, useNavigationState, useScenario, useSkipBlocker, useSkipCallback, useSoundEffect, useStageContext };
671
+ export { type AnimatedComponent, type AnimatedProps, type AnimationFrameCallbackEvent, type AutoTicketHandle, type AutoTicketOptions, type BeforeUnloadEvent, Command, type CommandHandler, type CustomEvent, type DefaultOverlayName, type DefaultPageName, type FocusEvent, type FocusEventKind, type FullScreenEvent, type FullscreenEventKind, type GameControl, type GameEvent, type Gamepad, type GamepadButton, type GamepadEvent, type GamepadHapticActuator, JSX, type KeyboardLocation, type Literal, MaybePromise, MouseEvent, MoyuEvent, type NavigationState, type Navigator, type NodeEvent, type OverlayConfig, type OverlayDescriptor, type OverlayInfo, OverlayParamsContext, type PageConfig, type PageDescriptor, PageParamsContext, type RegisterNavigator, type ResizeEvent, type ResolvedArgument, type ResolvedCommandLine, type ResolvedSystemCallLine, type RootErrorInfo, type RootHandle, type RootNavigatorList, type RootOptions, type RootUncaughtErrorInfo, type ScenarioCommandBaseType, type ScenarioEvent, type StackNavigatorOptions, StageContextProvider, type StageInstance, type TextEvent, type TextLine, type TextLineHandler, type WheelEvent, type WheelEventDeltaMode, type WithAnimated, animated as a, addEventListener, animated, autoState, createRoot, createStackNavigator, createStage, createStaticNavigation, executeNodeCommand, executePluginCommand, getNavigator, globalEventListeners, globalRequestAnimationFrameListeners, moyu, nextLine, setDefaultAutoTailMs, setWaiting, skipState, useAutoBlocker, useAutoTicket, useBeforeHandleCommandCallback, useFadeIn, useFadeInOut, useFadeOut, useInterruptCallback, useIsAutoing, useIsSkipping, useNavigation, useNavigationParams, useNavigationState, useScenario, useSkipBlocker, useSkipCallback, useSoundEffect, useStageContext };
package/dist/lib.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import * as React$1 from 'react';
2
- import React__default, { Component, ReactElement, ForwardRefExoticComponent, CSSProperties, FC } from 'react';
3
- import Reconciler, { BaseErrorInfo } from 'react-reconciler';
4
- import { N as Node, M as MouseEvent, a as MoyuEvent, C as Command, b as MaybePromise, J as JSX } from './jsx-dev-runtime--ZXLAgUi.js';
5
- export { A as AudioCommand, c as AudioSettings, B as BubbleEvent, D as DetailedMoyuProps, E as EffectParams, G as GamepadCommand, K as KeyboardEvent, d as KeyboardEventKind, e as MakeNullOptional, f as MouseEventKind, g as MoyuAnimationAttributes, h as MoyuBackdropAttributes, i as MoyuClipAttributes, j as MoyuContainerAttributes, k as MoyuEventHandler, l as MoyuFilterAttributes, m as MoyuListenerAttributes, n as MoyuNodeAttributes, o as MoyuSpriteAttributes, p as MoyuTextAttributes, q as MoyuVideoAttributes, S as ScenarioCommand, r as SystemCommand, T as TextCommand, s as TouchEvent, t as TouchEventKind, u as Tuple2, v as Tuple3, w as Tuple4, W as WindowState, x as createBubbleEvent, y as getStageSize } from './jsx-dev-runtime--ZXLAgUi.js';
2
+ import React__default, { ReactElement, Component, ForwardRefExoticComponent, CSSProperties, FC } from 'react';
3
+ import { M as MouseEvent, a as MoyuEvent, C as Command, b as MaybePromise, J as JSX } from './jsx-dev-runtime-C7RQHtUh.js';
4
+ export { A as AudioCommand, c as AudioSettings, B as BubbleEvent, D as DetailedMoyuProps, E as EffectParams, G as GamepadCommand, K as KeyboardEvent, d as KeyboardEventKind, e as MakeNullOptional, f as MouseEventKind, g as MoyuAnimationAttributes, h as MoyuBackdropAttributes, i as MoyuClipAttributes, j as MoyuContainerAttributes, k as MoyuEventHandler, l as MoyuFilterAttributes, m as MoyuListenerAttributes, n as MoyuNodeAttributes, o as MoyuSpriteAttributes, p as MoyuTextAttributes, q as MoyuVideoAttributes, N as Node, S as ScenarioCommand, r as SystemCommand, T as TextCommand, s as TouchEvent, t as TouchEventKind, u as Tuple2, v as Tuple3, w as Tuple4, W as WindowState, x as createBubbleEvent, y as getStageSize } from './jsx-dev-runtime-C7RQHtUh.js';
6
5
  import { FluidValue } from '@react-spring/shared';
7
6
  import { ElementType, AssignableKeys, ComponentPropsWithRef } from '@react-spring/types';
8
7
  import { Snapshot } from 'valtio';
@@ -12,22 +11,26 @@ import { ZodType } from 'zod';
12
11
  import 'react/jsx-runtime';
13
12
  import 'react/jsx-dev-runtime';
14
13
 
15
- declare const MoyuRenderer: Reconciler.Reconciler<Node, Node, never, never, never, any>;
14
+ interface RootErrorInfo {
15
+ componentStack?: string;
16
+ }
17
+ interface RootUncaughtErrorInfo extends RootErrorInfo {
18
+ errorBoundary?: Component;
19
+ }
20
+ interface RootHandle {
21
+ render(reactElement: ReactElement, callback?: (() => void) | null): void;
22
+ }
16
23
  interface RootOptions {
17
24
  /**
18
25
  * Prefix for `useId`.
19
26
  */
20
27
  identifierPrefix?: string;
21
- onUncaughtError: (error: Error, info: BaseErrorInfo & {
22
- errorBoundary?: Component;
23
- }) => void;
24
- onCaughtError: (error: Error, info: BaseErrorInfo) => void;
25
- onRecoverableError: (error: Error, info: BaseErrorInfo) => void;
28
+ onUncaughtError: (error: Error, info: RootUncaughtErrorInfo) => void;
29
+ onCaughtError: (error: Error, info: RootErrorInfo) => void;
30
+ onRecoverableError: (error: Error, info: RootErrorInfo) => void;
26
31
  onDefaultTransitionIndicator: () => void;
27
32
  }
28
- declare function createRoot(options?: RootOptions): {
29
- render: (reactElement: ReactElement, callback?: (() => void) | null) => Reconciler.Lane;
30
- };
33
+ declare function createRoot(options?: RootOptions): RootHandle;
31
34
 
32
35
  declare const globalEventListeners: Record<string, ((event: unknown) => void)[]>;
33
36
  declare const globalRequestAnimationFrameListeners: FrameRequestCallback[];
@@ -516,6 +519,7 @@ declare function nextLine(): any;
516
519
  declare function setWaiting(time: number, skippable: boolean): void;
517
520
  /**
518
521
  * Custom hook to manage scenario lifecycle (load, start, terminate).
522
+ * The underlying session is resilient to transient remounts such as Fast Refresh.
519
523
  *
520
524
  * @param {string[]} stories - An array of story names to load.
521
525
  * @param {string} [startName] - The name of the story to start.
@@ -664,4 +668,4 @@ declare module 'zod' {
664
668
  }
665
669
  }
666
670
 
667
- export { type AnimatedComponent, type AnimatedProps, type AnimationFrameCallbackEvent, type AutoTicketHandle, type AutoTicketOptions, type BeforeUnloadEvent, Command, type CommandHandler, type CustomEvent, type DefaultOverlayName, type DefaultPageName, type FocusEvent, type FocusEventKind, type FullScreenEvent, type FullscreenEventKind, type GameControl, type GameEvent, type Gamepad, type GamepadButton, type GamepadEvent, type GamepadHapticActuator, JSX, type KeyboardLocation, type Literal, MaybePromise, MouseEvent, MoyuEvent, MoyuRenderer, type NavigationState, type Navigator, Node, type NodeEvent, type OverlayConfig, type OverlayDescriptor, type OverlayInfo, OverlayParamsContext, type PageConfig, type PageDescriptor, PageParamsContext, type RegisterNavigator, type ResizeEvent, type ResolvedArgument, type ResolvedCommandLine, type ResolvedSystemCallLine, type RootNavigatorList, type RootOptions, type ScenarioCommandBaseType, type ScenarioEvent, type StackNavigatorOptions, StageContextProvider, type StageInstance, type TextEvent, type TextLine, type TextLineHandler, type WheelEvent, type WheelEventDeltaMode, type WithAnimated, animated as a, addEventListener, animated, autoState, createRoot, createStackNavigator, createStage, createStaticNavigation, executeNodeCommand, executePluginCommand, getNavigator, globalEventListeners, globalRequestAnimationFrameListeners, moyu, nextLine, setDefaultAutoTailMs, setWaiting, skipState, useAutoBlocker, useAutoTicket, useBeforeHandleCommandCallback, useFadeIn, useFadeInOut, useFadeOut, useInterruptCallback, useIsAutoing, useIsSkipping, useNavigation, useNavigationParams, useNavigationState, useScenario, useSkipBlocker, useSkipCallback, useSoundEffect, useStageContext };
671
+ export { type AnimatedComponent, type AnimatedProps, type AnimationFrameCallbackEvent, type AutoTicketHandle, type AutoTicketOptions, type BeforeUnloadEvent, Command, type CommandHandler, type CustomEvent, type DefaultOverlayName, type DefaultPageName, type FocusEvent, type FocusEventKind, type FullScreenEvent, type FullscreenEventKind, type GameControl, type GameEvent, type Gamepad, type GamepadButton, type GamepadEvent, type GamepadHapticActuator, JSX, type KeyboardLocation, type Literal, MaybePromise, MouseEvent, MoyuEvent, type NavigationState, type Navigator, type NodeEvent, type OverlayConfig, type OverlayDescriptor, type OverlayInfo, OverlayParamsContext, type PageConfig, type PageDescriptor, PageParamsContext, type RegisterNavigator, type ResizeEvent, type ResolvedArgument, type ResolvedCommandLine, type ResolvedSystemCallLine, type RootErrorInfo, type RootHandle, type RootNavigatorList, type RootOptions, type RootUncaughtErrorInfo, type ScenarioCommandBaseType, type ScenarioEvent, type StackNavigatorOptions, StageContextProvider, type StageInstance, type TextEvent, type TextLine, type TextLineHandler, type WheelEvent, type WheelEventDeltaMode, type WithAnimated, animated as a, addEventListener, animated, autoState, createRoot, createStackNavigator, createStage, createStaticNavigation, executeNodeCommand, executePluginCommand, getNavigator, globalEventListeners, globalRequestAnimationFrameListeners, moyu, nextLine, setDefaultAutoTailMs, setWaiting, skipState, useAutoBlocker, useAutoTicket, useBeforeHandleCommandCallback, useFadeIn, useFadeInOut, useFadeOut, useInterruptCallback, useIsAutoing, useIsSkipping, useNavigation, useNavigationParams, useNavigationState, useScenario, useSkipBlocker, useSkipCallback, useSoundEffect, useStageContext };
package/dist/lib.mjs CHANGED
@@ -19,7 +19,6 @@ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "defau
19
19
  // src/lib.ts
20
20
  var lib_exports = {};
21
21
  __export(lib_exports, {
22
- MoyuRenderer: () => MoyuRenderer,
23
22
  OverlayParamsContext: () => OverlayParamsContext,
24
23
  PageParamsContext: () => PageParamsContext,
25
24
  StageContextProvider: () => StageContextProvider,
@@ -377,7 +376,7 @@ function createRoot(options) {
377
376
  });
378
377
  return {
379
378
  render: (reactElement, callback) => {
380
- return MoyuRenderer.updateContainer(reactElement, rootElement, null, callback);
379
+ MoyuRenderer.updateContainer(reactElement, rootElement, null, callback);
381
380
  }
382
381
  };
383
382
  }
@@ -843,6 +842,138 @@ function useFadeOut(outTime, pause = false, onFinished) {
843
842
 
844
843
  // src/hooks/useScenario.ts
845
844
  import { useEffect } from "react";
845
+ var currentScenarioSession = null;
846
+ var scenarioSessionId = 0;
847
+ var scenarioLifecycleQueue = Promise.resolve();
848
+ function normalizeStories(stories) {
849
+ return stories.filter((story) => Boolean(story));
850
+ }
851
+ function createScenarioSessionKey(config) {
852
+ return JSON.stringify({
853
+ stories: config.stories,
854
+ startName: config.startName ?? null,
855
+ entryName: config.entryName ?? null,
856
+ goNextOnLoad: config.goNextOnLoad
857
+ });
858
+ }
859
+ function isCurrentScenarioSession(session) {
860
+ return currentScenarioSession?.id === session.id && !session.disposed;
861
+ }
862
+ function enqueueScenarioLifecycleTask(task) {
863
+ const nextTask = scenarioLifecycleQueue.then(task, task);
864
+ scenarioLifecycleQueue = nextTask.catch(() => {
865
+ });
866
+ return nextTask;
867
+ }
868
+ function clearScenarioDisposeTimer(session) {
869
+ if (session.disposeTimer !== null) {
870
+ clearTimeout(session.disposeTimer);
871
+ session.disposeTimer = null;
872
+ }
873
+ }
874
+ async function terminateCurrentScenario() {
875
+ try {
876
+ await executePluginCommand("scenario", {
877
+ subCommand: "terminateStory"
878
+ });
879
+ } catch (error) {
880
+ console.error("Failed to terminate scenario:", error);
881
+ }
882
+ }
883
+ async function startScenarioSession(session, config) {
884
+ for (const story of config.stories) {
885
+ if (!isCurrentScenarioSession(session)) return;
886
+ try {
887
+ await executePluginCommand("scenario", {
888
+ subCommand: "addStory",
889
+ name: story
890
+ });
891
+ } catch (error) {
892
+ console.error("Failed to load scenario:", error);
893
+ }
894
+ }
895
+ if (!isCurrentScenarioSession(session)) return;
896
+ try {
897
+ if (config.startName) {
898
+ await executePluginCommand("scenario", {
899
+ subCommand: "startStory",
900
+ name: config.startName,
901
+ entry: config.entryName
902
+ });
903
+ }
904
+ if (!isCurrentScenarioSession(session)) return;
905
+ if (config.goNextOnLoad) {
906
+ await nextLine();
907
+ }
908
+ } catch (error) {
909
+ console.error(`Failed to start scenario ${config.startName}:`, error);
910
+ }
911
+ }
912
+ function ensureScenarioSessionStarted(session, config) {
913
+ if (session.started || session.startPromise !== null) {
914
+ return;
915
+ }
916
+ const startPromise = enqueueScenarioLifecycleTask(async () => {
917
+ if (!isCurrentScenarioSession(session)) return;
918
+ await startScenarioSession(session, config);
919
+ if (isCurrentScenarioSession(session)) {
920
+ session.started = true;
921
+ }
922
+ });
923
+ session.startPromise = startPromise;
924
+ void startPromise.finally(() => {
925
+ if (session.startPromise === startPromise) {
926
+ session.startPromise = null;
927
+ }
928
+ });
929
+ }
930
+ function disposeScenarioSession(session, delayMs) {
931
+ clearScenarioDisposeTimer(session);
932
+ session.disposeTimer = setTimeout(() => {
933
+ if (!isCurrentScenarioSession(session) || session.refCount > 0) {
934
+ return;
935
+ }
936
+ session.disposeTimer = null;
937
+ session.disposed = true;
938
+ currentScenarioSession = null;
939
+ void enqueueScenarioLifecycleTask(terminateCurrentScenario);
940
+ }, delayMs);
941
+ }
942
+ function acquireScenarioSession(config) {
943
+ const key = createScenarioSessionKey(config);
944
+ if (currentScenarioSession !== null && currentScenarioSession.key !== key) {
945
+ const previousSession = currentScenarioSession;
946
+ clearScenarioDisposeTimer(previousSession);
947
+ previousSession.disposed = true;
948
+ currentScenarioSession = null;
949
+ void enqueueScenarioLifecycleTask(terminateCurrentScenario);
950
+ }
951
+ if (currentScenarioSession === null) {
952
+ currentScenarioSession = {
953
+ id: ++scenarioSessionId,
954
+ key,
955
+ refCount: 0,
956
+ started: false,
957
+ startPromise: null,
958
+ disposeTimer: null,
959
+ disposed: false
960
+ };
961
+ }
962
+ clearScenarioDisposeTimer(currentScenarioSession);
963
+ currentScenarioSession.refCount += 1;
964
+ ensureScenarioSessionStarted(currentScenarioSession, config);
965
+ return currentScenarioSession;
966
+ }
967
+ function releaseScenarioSession(session) {
968
+ if (!isCurrentScenarioSession(session)) {
969
+ return;
970
+ }
971
+ session.refCount = Math.max(0, session.refCount - 1);
972
+ if (session.refCount > 0) {
973
+ return;
974
+ }
975
+ disposeScenarioSession(session, 0);
976
+ }
846
977
  function nextLine() {
847
978
  return executePluginCommand("scenario", { subCommand: "nextLine" });
848
979
  }
@@ -851,39 +982,14 @@ function setWaiting(time, skippable) {
851
982
  }
852
983
  function useScenario(stories, startName, entryName, goNextOnLoad = false) {
853
984
  useEffect(() => {
854
- const loadScenario = async () => {
855
- for (const story of stories) {
856
- if (!story) continue;
857
- try {
858
- await executePluginCommand("scenario", {
859
- subCommand: "addStory",
860
- name: story
861
- });
862
- } catch (error) {
863
- console.error(`Failed to load scenario:`, error);
864
- }
865
- }
866
- };
867
- void loadScenario().then(async () => {
868
- try {
869
- if (startName) {
870
- await executePluginCommand("scenario", {
871
- subCommand: "startStory",
872
- name: startName,
873
- entry: entryName
874
- });
875
- }
876
- if (goNextOnLoad) {
877
- await nextLine();
878
- }
879
- } catch (error) {
880
- console.error(`Failed to start scenario ${startName}:`, error);
881
- }
985
+ const session = acquireScenarioSession({
986
+ stories: normalizeStories(stories),
987
+ startName,
988
+ entryName,
989
+ goNextOnLoad
882
990
  });
883
991
  return () => {
884
- executePluginCommand("scenario", {
885
- subCommand: "terminateStory"
886
- });
992
+ releaseScenarioSession(session);
887
993
  };
888
994
  }, [stories, startName, entryName, goNextOnLoad]);
889
995
  }
@@ -1490,7 +1596,6 @@ function useBeforeHandleCommandCallback(callback) {
1490
1596
  // src/lib.ts
1491
1597
  export * from "@react-spring/core";
1492
1598
  export {
1493
- MoyuRenderer,
1494
1599
  OverlayParamsContext,
1495
1600
  PageParamsContext,
1496
1601
  StageContextProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momoyu-ink/kit",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Software Development Kit of Moyu Visual Novel Engine",
5
5
  "main": "dist/lib.cjs",
6
6
  "module": "dist/lib.mjs",
@@ -28,7 +28,8 @@
28
28
  },
29
29
  "files": [
30
30
  "dist",
31
- "LICENSE.txt"
31
+ "LICENSE.txt",
32
+ "AGENTS.md"
32
33
  ],
33
34
  "scripts": {
34
35
  "dev": "tsup --watch",
@@ -296,21 +296,6 @@ declare function createBubbleEvent<T extends BubbleEvent>(body: Record<string, a
296
296
  bubbleTargetIds: number[];
297
297
  }, targetId: number): T;
298
298
 
299
- declare class Node {
300
- nodeId: number;
301
- label?: string;
302
- props: Record<string, any>;
303
- listeners: Record<string, (evt?: BubbleEvent | Record<string, any>) => any>;
304
- static create(label: string, type: string, props: Record<string, any>): Node;
305
- static rootNode(): Node;
306
- addChild(child: Node): void;
307
- insertChild(_index: number, child: Node): void;
308
- insertChildBefore(beforeChild: Node, child: Node): void;
309
- removeChild(child: Node): void;
310
- updateProps(props: Record<string, any>): void;
311
- executeCommand(payload: Command): any;
312
- }
313
-
314
299
  type KeyboardEventKind = 'KeyDown' | 'KeyUp' | 'KeyPress';
315
300
 
316
301
  /**
@@ -382,6 +367,21 @@ interface TouchEvent extends BubbleEvent {
382
367
  identifier: number;
383
368
  }
384
369
 
370
+ declare class Node {
371
+ nodeId: number;
372
+ label?: string;
373
+ props: Record<string, any>;
374
+ listeners: Record<string, (evt?: BubbleEvent | Record<string, any>) => any>;
375
+ static create(label: string, type: string, props: Record<string, any>): Node;
376
+ static rootNode(): Node;
377
+ addChild(child: Node): void;
378
+ insertChild(_index: number, child: Node): void;
379
+ insertChildBefore(beforeChild: Node, child: Node): void;
380
+ removeChild(child: Node): void;
381
+ updateProps(props: Record<string, any>): void;
382
+ executeCommand(payload: Command): any;
383
+ }
384
+
385
385
  type MoyuCursor = 'hidden' | 'default' | 'context-menu' | 'help' | 'pointer' | 'progress' | 'wait' | 'cell' | 'crosshair' | 'text' | 'vertical-text' | 'alias' | 'copy' | 'move' | 'no-drop' | 'not-allowed' | 'grab' | 'grabbing' | 'e-resize' | 'n-resize' | 'ne-resize' | 'nw-resize' | 's-resize' | 'se-resize' | 'sw-resize' | 'w-resize' | 'ew-resize' | 'ns-resize' | 'nesw-resize' | 'nwse-resize' | 'col-resize' | 'row-resize' | 'all-scroll' | 'zoom-in' | 'zoom-out' | 'dnd-ask' | 'all-resize';
386
386
 
387
387
  type NodeProps = {
@@ -296,21 +296,6 @@ declare function createBubbleEvent<T extends BubbleEvent>(body: Record<string, a
296
296
  bubbleTargetIds: number[];
297
297
  }, targetId: number): T;
298
298
 
299
- declare class Node {
300
- nodeId: number;
301
- label?: string;
302
- props: Record<string, any>;
303
- listeners: Record<string, (evt?: BubbleEvent | Record<string, any>) => any>;
304
- static create(label: string, type: string, props: Record<string, any>): Node;
305
- static rootNode(): Node;
306
- addChild(child: Node): void;
307
- insertChild(_index: number, child: Node): void;
308
- insertChildBefore(beforeChild: Node, child: Node): void;
309
- removeChild(child: Node): void;
310
- updateProps(props: Record<string, any>): void;
311
- executeCommand(payload: Command): any;
312
- }
313
-
314
299
  type KeyboardEventKind = 'KeyDown' | 'KeyUp' | 'KeyPress';
315
300
 
316
301
  /**
@@ -382,6 +367,21 @@ interface TouchEvent extends BubbleEvent {
382
367
  identifier: number;
383
368
  }
384
369
 
370
+ declare class Node {
371
+ nodeId: number;
372
+ label?: string;
373
+ props: Record<string, any>;
374
+ listeners: Record<string, (evt?: BubbleEvent | Record<string, any>) => any>;
375
+ static create(label: string, type: string, props: Record<string, any>): Node;
376
+ static rootNode(): Node;
377
+ addChild(child: Node): void;
378
+ insertChild(_index: number, child: Node): void;
379
+ insertChildBefore(beforeChild: Node, child: Node): void;
380
+ removeChild(child: Node): void;
381
+ updateProps(props: Record<string, any>): void;
382
+ executeCommand(payload: Command): any;
383
+ }
384
+
385
385
  type MoyuCursor = 'hidden' | 'default' | 'context-menu' | 'help' | 'pointer' | 'progress' | 'wait' | 'cell' | 'crosshair' | 'text' | 'vertical-text' | 'alias' | 'copy' | 'move' | 'no-drop' | 'not-allowed' | 'grab' | 'grabbing' | 'e-resize' | 'n-resize' | 'ne-resize' | 'nw-resize' | 's-resize' | 'se-resize' | 'sw-resize' | 'w-resize' | 'ew-resize' | 'ns-resize' | 'nesw-resize' | 'nwse-resize' | 'col-resize' | 'row-resize' | 'all-scroll' | 'zoom-in' | 'zoom-out' | 'dnd-ask' | 'all-resize';
386
386
 
387
387
  type NodeProps = {