sa2kit 1.6.0 → 1.6.1

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.
@@ -87,15 +87,31 @@ interface DialogueLine {
87
87
  expression?: string;
88
88
  /** 对话时播放的音效(可选) */
89
89
  voicePath?: string;
90
+ /** 对话中插入的分支选项(可选,若有则显示选项) */
91
+ choices?: DialogueChoice[];
92
+ }
93
+ /** 分支判定逻辑 */
94
+ interface BranchCondition {
95
+ /** 变量名 */
96
+ key: string;
97
+ /** 变量值与节点索引的映射 */
98
+ map: Record<string | number, number>;
99
+ /** 默认跳转的节点索引 */
100
+ defaultIndex: number;
90
101
  }
91
102
  /** 对话分支选项 */
92
103
  interface DialogueChoice {
93
104
  /** 选项文字 */
94
105
  text: string;
95
- /** 跳转到的节点索引 */
96
- nextNodeIndex: number;
106
+ /** 跳转到的节点索引(可选,若不填则继续当前剧情) */
107
+ nextNodeIndex?: number;
97
108
  /** 跳转到的对话索引(可选,默认 0) */
98
109
  nextDialogueIndex?: number;
110
+ /** 设置变量(可选,用于后续剧情判定) */
111
+ setVariable?: {
112
+ key: string;
113
+ value: string | number | boolean;
114
+ };
99
115
  /** 选项点击后的回调(可选) */
100
116
  onSelect?: () => void;
101
117
  }
@@ -111,8 +127,10 @@ interface VisualNovelNode {
111
127
  dialogues: DialogueLine[];
112
128
  /** 节点特定的舞台配置(可选,覆盖全局配置) */
113
129
  stage?: MMDStage;
114
- /** 节点结束时的分支选项(可选,若有则显示选项,不自动跳转) */
130
+ /** 节点结束时的分支选项(可选,已废弃,建议使用 DialogueLine.choices) */
115
131
  choices?: DialogueChoice[];
132
+ /** 节点结束时的分支判定逻辑(可选,根据变量跳转不同节点) */
133
+ nextCondition?: BranchCondition;
116
134
  /** 节点开始时播放的背景音乐(可选) */
117
135
  bgmPath?: string;
118
136
  /** 背景音乐音量 0-1(默认 0.5) */
@@ -240,6 +258,10 @@ interface MMDVisualNovelRef {
240
258
  getCurrentDialogueIndex: () => number;
241
259
  /** 获取对话历史 */
242
260
  getHistory: () => DialogueHistoryItem[];
261
+ /** 获取当前剧情变量 */
262
+ getVariables: () => Record<string, string | number | boolean>;
263
+ /** 设置剧情变量 */
264
+ setVariable: (key: string, value: string | number | boolean) => void;
243
265
  /** 设置自动播放模式 */
244
266
  setAutoMode: (enabled: boolean) => void;
245
267
  /** 跳过当前打字动画 */
@@ -344,6 +366,13 @@ interface StartScreenProps {
344
366
  */
345
367
  declare const StartScreen: React__default.FC<StartScreenProps>;
346
368
 
369
+ interface ChoiceMenuProps {
370
+ choices: DialogueChoice[];
371
+ onSelect: (choice: DialogueChoice) => void;
372
+ theme?: DialogueBoxTheme;
373
+ }
374
+ declare const ChoiceMenu: React__default.FC<ChoiceMenuProps>;
375
+
347
376
  /** 音乐曲目配置 */
348
377
  interface MusicTrack {
349
378
  /** 唯一标识 */
@@ -459,4 +488,4 @@ interface TrackInfoProps {
459
488
  }
460
489
  declare const TrackInfo: React__default.FC<TrackInfoProps>;
461
490
 
462
- export { DialogueBox, type DialogueBoxProps, type DialogueBoxTheme, type DialogueChoice, type DialogueHistoryItem, type DialogueLine, HistoryPanel, LoadingOverlay, type LoadingOverlayProps, LoadingScreen, type LoadingScreenProps, MMDMusicPlayer, type MMDMusicPlayerConfig, type MMDMusicPlayerProps, type MMDMusicPlayerRef, MMDPlayerBase, MMDPlayerBaseProps, MMDPlayerBaseRef, MMDPlayerEnhanced, MMDPlayerEnhancedDebugInfo, MMDPlayerEnhancedProps, MMDPlaylist, MMDPlaylistDebugInfo, MMDPlaylistNode, MMDPlaylistProps, MMDResources, MMDStage, MMDVisualNovel, type MMDVisualNovelProps, type MMDVisualNovelRef, MobileOptimization, MusicControls, type MusicControlsProps, type MusicTrack, PlaylistPanel, type PlaylistPanelProps, StartScreen, type StartScreenProps, TrackInfo, type TrackInfoProps, type VisualNovelNode, type VisualNovelScript, loadAmmo };
491
+ export { type BranchCondition, ChoiceMenu, type ChoiceMenuProps, DialogueBox, type DialogueBoxProps, type DialogueBoxTheme, type DialogueChoice, type DialogueHistoryItem, type DialogueLine, HistoryPanel, LoadingOverlay, type LoadingOverlayProps, LoadingScreen, type LoadingScreenProps, MMDMusicPlayer, type MMDMusicPlayerConfig, type MMDMusicPlayerProps, type MMDMusicPlayerRef, MMDPlayerBase, MMDPlayerBaseProps, MMDPlayerBaseRef, MMDPlayerEnhanced, MMDPlayerEnhancedDebugInfo, MMDPlayerEnhancedProps, MMDPlaylist, MMDPlaylistDebugInfo, MMDPlaylistNode, MMDPlaylistProps, MMDResources, MMDStage, MMDVisualNovel, type MMDVisualNovelProps, type MMDVisualNovelRef, MobileOptimization, MusicControls, type MusicControlsProps, type MusicTrack, PlaylistPanel, type PlaylistPanelProps, StartScreen, type StartScreenProps, TrackInfo, type TrackInfoProps, type VisualNovelNode, type VisualNovelScript, loadAmmo };
@@ -87,15 +87,31 @@ interface DialogueLine {
87
87
  expression?: string;
88
88
  /** 对话时播放的音效(可选) */
89
89
  voicePath?: string;
90
+ /** 对话中插入的分支选项(可选,若有则显示选项) */
91
+ choices?: DialogueChoice[];
92
+ }
93
+ /** 分支判定逻辑 */
94
+ interface BranchCondition {
95
+ /** 变量名 */
96
+ key: string;
97
+ /** 变量值与节点索引的映射 */
98
+ map: Record<string | number, number>;
99
+ /** 默认跳转的节点索引 */
100
+ defaultIndex: number;
90
101
  }
91
102
  /** 对话分支选项 */
92
103
  interface DialogueChoice {
93
104
  /** 选项文字 */
94
105
  text: string;
95
- /** 跳转到的节点索引 */
96
- nextNodeIndex: number;
106
+ /** 跳转到的节点索引(可选,若不填则继续当前剧情) */
107
+ nextNodeIndex?: number;
97
108
  /** 跳转到的对话索引(可选,默认 0) */
98
109
  nextDialogueIndex?: number;
110
+ /** 设置变量(可选,用于后续剧情判定) */
111
+ setVariable?: {
112
+ key: string;
113
+ value: string | number | boolean;
114
+ };
99
115
  /** 选项点击后的回调(可选) */
100
116
  onSelect?: () => void;
101
117
  }
@@ -111,8 +127,10 @@ interface VisualNovelNode {
111
127
  dialogues: DialogueLine[];
112
128
  /** 节点特定的舞台配置(可选,覆盖全局配置) */
113
129
  stage?: MMDStage;
114
- /** 节点结束时的分支选项(可选,若有则显示选项,不自动跳转) */
130
+ /** 节点结束时的分支选项(可选,已废弃,建议使用 DialogueLine.choices) */
115
131
  choices?: DialogueChoice[];
132
+ /** 节点结束时的分支判定逻辑(可选,根据变量跳转不同节点) */
133
+ nextCondition?: BranchCondition;
116
134
  /** 节点开始时播放的背景音乐(可选) */
117
135
  bgmPath?: string;
118
136
  /** 背景音乐音量 0-1(默认 0.5) */
@@ -240,6 +258,10 @@ interface MMDVisualNovelRef {
240
258
  getCurrentDialogueIndex: () => number;
241
259
  /** 获取对话历史 */
242
260
  getHistory: () => DialogueHistoryItem[];
261
+ /** 获取当前剧情变量 */
262
+ getVariables: () => Record<string, string | number | boolean>;
263
+ /** 设置剧情变量 */
264
+ setVariable: (key: string, value: string | number | boolean) => void;
243
265
  /** 设置自动播放模式 */
244
266
  setAutoMode: (enabled: boolean) => void;
245
267
  /** 跳过当前打字动画 */
@@ -344,6 +366,13 @@ interface StartScreenProps {
344
366
  */
345
367
  declare const StartScreen: React__default.FC<StartScreenProps>;
346
368
 
369
+ interface ChoiceMenuProps {
370
+ choices: DialogueChoice[];
371
+ onSelect: (choice: DialogueChoice) => void;
372
+ theme?: DialogueBoxTheme;
373
+ }
374
+ declare const ChoiceMenu: React__default.FC<ChoiceMenuProps>;
375
+
347
376
  /** 音乐曲目配置 */
348
377
  interface MusicTrack {
349
378
  /** 唯一标识 */
@@ -459,4 +488,4 @@ interface TrackInfoProps {
459
488
  }
460
489
  declare const TrackInfo: React__default.FC<TrackInfoProps>;
461
490
 
462
- export { DialogueBox, type DialogueBoxProps, type DialogueBoxTheme, type DialogueChoice, type DialogueHistoryItem, type DialogueLine, HistoryPanel, LoadingOverlay, type LoadingOverlayProps, LoadingScreen, type LoadingScreenProps, MMDMusicPlayer, type MMDMusicPlayerConfig, type MMDMusicPlayerProps, type MMDMusicPlayerRef, MMDPlayerBase, MMDPlayerBaseProps, MMDPlayerBaseRef, MMDPlayerEnhanced, MMDPlayerEnhancedDebugInfo, MMDPlayerEnhancedProps, MMDPlaylist, MMDPlaylistDebugInfo, MMDPlaylistNode, MMDPlaylistProps, MMDResources, MMDStage, MMDVisualNovel, type MMDVisualNovelProps, type MMDVisualNovelRef, MobileOptimization, MusicControls, type MusicControlsProps, type MusicTrack, PlaylistPanel, type PlaylistPanelProps, StartScreen, type StartScreenProps, TrackInfo, type TrackInfoProps, type VisualNovelNode, type VisualNovelScript, loadAmmo };
491
+ export { type BranchCondition, ChoiceMenu, type ChoiceMenuProps, DialogueBox, type DialogueBoxProps, type DialogueBoxTheme, type DialogueChoice, type DialogueHistoryItem, type DialogueLine, HistoryPanel, LoadingOverlay, type LoadingOverlayProps, LoadingScreen, type LoadingScreenProps, MMDMusicPlayer, type MMDMusicPlayerConfig, type MMDMusicPlayerProps, type MMDMusicPlayerRef, MMDPlayerBase, MMDPlayerBaseProps, MMDPlayerBaseRef, MMDPlayerEnhanced, MMDPlayerEnhancedDebugInfo, MMDPlayerEnhancedProps, MMDPlaylist, MMDPlaylistDebugInfo, MMDPlaylistNode, MMDPlaylistProps, MMDResources, MMDStage, MMDVisualNovel, type MMDVisualNovelProps, type MMDVisualNovelRef, MobileOptimization, MusicControls, type MusicControlsProps, type MusicTrack, PlaylistPanel, type PlaylistPanelProps, StartScreen, type StartScreenProps, TrackInfo, type TrackInfoProps, type VisualNovelNode, type VisualNovelScript, loadAmmo };
package/dist/mmd/index.js CHANGED
@@ -2852,6 +2852,7 @@ var MMDVisualNovel = React6.forwardRef(
2852
2852
  const [pendingNodeIndex, setPendingNodeIndex] = React6.useState(null);
2853
2853
  const [showChoices, setShowChoices] = React6.useState(false);
2854
2854
  const [isCameraManual, setIsCameraManual] = React6.useState(false);
2855
+ const [variables, setVariables] = React6.useState({});
2855
2856
  const playerRef = React6.useRef(null);
2856
2857
  const containerRef = React6.useRef(null);
2857
2858
  const autoTimerRef = React6.useRef(null);
@@ -2873,32 +2874,6 @@ var MMDVisualNovel = React6.forwardRef(
2873
2874
  }
2874
2875
  ]);
2875
2876
  }, []);
2876
- const goToNextDialogue = React6.useCallback(() => {
2877
- if (!currentNode) return;
2878
- if (autoTimerRef.current) {
2879
- clearTimeout(autoTimerRef.current);
2880
- autoTimerRef.current = null;
2881
- }
2882
- const nextDialogueIndex = currentDialogueIndex + 1;
2883
- if (nextDialogueIndex < currentNode.dialogues.length && currentNode?.dialogues[nextDialogueIndex] !== void 0) {
2884
- const nextDialogue = currentNode.dialogues[nextDialogueIndex];
2885
- setCurrentDialogueIndex(nextDialogueIndex);
2886
- addToHistory(nextDialogue, currentNodeIndex, nextDialogueIndex);
2887
- onDialogueChange?.(nextDialogue, nextDialogueIndex, currentNodeIndex);
2888
- typingCompleteRef.current = false;
2889
- } else if (currentNode.choices && currentNode.choices.length > 0) {
2890
- setShowChoices(true);
2891
- } else {
2892
- const nextNodeIndex = currentNodeIndex + 1;
2893
- if (nextNodeIndex < nodes.length) {
2894
- goToNode(nextNodeIndex);
2895
- } else if (loop) {
2896
- goToNode(0);
2897
- } else {
2898
- onScriptComplete?.();
2899
- }
2900
- }
2901
- }, [currentNode, currentDialogueIndex, currentNodeIndex, nodes.length, loop, addToHistory, onDialogueChange, onScriptComplete]);
2902
2877
  const goToNode = React6.useCallback(
2903
2878
  (nodeIndex, force = false) => {
2904
2879
  if (nodeIndex < 0 || nodeIndex >= nodes.length) return;
@@ -2937,8 +2912,52 @@ var MMDVisualNovel = React6.forwardRef(
2937
2912
  }, 100);
2938
2913
  }, 300);
2939
2914
  },
2940
- [nodes, isTransitioning, addToHistory, onNodeChange, onDialogueChange, currentNodeIndex, isVmdFinished]
2915
+ [nodes, isTransitioning, addToHistory, onNodeChange, onDialogueChange, currentNodeIndex]
2941
2916
  );
2917
+ const triggerNodeTransition = React6.useCallback(() => {
2918
+ if (!currentNode) return;
2919
+ let nextNodeIndex = currentNodeIndex + 1;
2920
+ if (currentNode.nextCondition) {
2921
+ const { key, map, defaultIndex } = currentNode.nextCondition;
2922
+ const val = variables[key];
2923
+ if (val !== void 0 && map[val] !== void 0) {
2924
+ nextNodeIndex = map[val];
2925
+ console.log(`[MMDVisualNovel] Branching: ${key}=${val} -> node ${nextNodeIndex}`);
2926
+ } else {
2927
+ nextNodeIndex = defaultIndex;
2928
+ }
2929
+ }
2930
+ if (nextNodeIndex < nodes.length && nextNodeIndex >= 0) {
2931
+ goToNode(nextNodeIndex);
2932
+ } else if (loop) {
2933
+ goToNode(0);
2934
+ } else {
2935
+ onScriptComplete?.();
2936
+ }
2937
+ }, [currentNode, currentNodeIndex, nodes.length, loop, variables, goToNode, onScriptComplete]);
2938
+ const goToNextDialogue = React6.useCallback(() => {
2939
+ if (!currentNode) return;
2940
+ if (currentDialogue?.choices && currentDialogue.choices.length > 0 && !showChoices) {
2941
+ setShowChoices(true);
2942
+ return;
2943
+ }
2944
+ if (autoTimerRef.current) {
2945
+ clearTimeout(autoTimerRef.current);
2946
+ autoTimerRef.current = null;
2947
+ }
2948
+ const nextDialogueIndex = currentDialogueIndex + 1;
2949
+ if (nextDialogueIndex < currentNode.dialogues.length && currentNode?.dialogues[nextDialogueIndex] !== void 0) {
2950
+ const nextDialogue = currentNode.dialogues[nextDialogueIndex];
2951
+ setCurrentDialogueIndex(nextDialogueIndex);
2952
+ addToHistory(nextDialogue, currentNodeIndex, nextDialogueIndex);
2953
+ onDialogueChange?.(nextDialogue, nextDialogueIndex, currentNodeIndex);
2954
+ typingCompleteRef.current = false;
2955
+ } else if (currentNode.choices && currentNode.choices.length > 0) {
2956
+ setShowChoices(true);
2957
+ } else {
2958
+ triggerNodeTransition();
2959
+ }
2960
+ }, [currentNode, currentDialogue, currentDialogueIndex, currentNodeIndex, nodes.length, loop, addToHistory, onDialogueChange, onScriptComplete, showChoices, variables, goToNode, triggerNodeTransition]);
2942
2961
  const goToDialogue = React6.useCallback(
2943
2962
  (dialogueIndex) => {
2944
2963
  if (!currentNode) return;
@@ -3017,6 +3036,10 @@ var MMDVisualNovel = React6.forwardRef(
3017
3036
  getCurrentNodeIndex: () => currentNodeIndex,
3018
3037
  getCurrentDialogueIndex: () => currentDialogueIndex,
3019
3038
  getHistory: () => history,
3039
+ getVariables: () => variables,
3040
+ setVariable: (key, value) => {
3041
+ setVariables((prev) => ({ ...prev, [key]: value }));
3042
+ },
3020
3043
  setAutoMode: setIsAutoMode,
3021
3044
  skipTyping: () => {
3022
3045
  typingCompleteRef.current = true;
@@ -3177,18 +3200,38 @@ var MMDVisualNovel = React6.forwardRef(
3177
3200
  }
3178
3201
  }
3179
3202
  ),
3180
- showChoices && currentNode.choices && /* @__PURE__ */ React6__default.default.createElement(
3203
+ showChoices && (currentDialogue?.choices || currentNode.choices) && /* @__PURE__ */ React6__default.default.createElement(
3181
3204
  ChoiceMenu,
3182
3205
  {
3183
- choices: currentNode.choices,
3206
+ choices: currentDialogue?.choices || currentNode.choices,
3184
3207
  theme: dialogueTheme,
3185
3208
  onSelect: (choice) => {
3209
+ if (choice.setVariable) {
3210
+ const { key, value } = choice.setVariable;
3211
+ setVariables((prev) => ({ ...prev, [key]: value }));
3212
+ console.log(`[MMDVisualNovel] Variable set: ${key} = ${value}`);
3213
+ }
3186
3214
  choice.onSelect?.();
3187
- if (choice.nextNodeIndex === currentNodeIndex) {
3188
- goToDialogue(choice.nextDialogueIndex || 0);
3189
- setShowChoices(false);
3190
- } else {
3191
- goToNode(choice.nextNodeIndex, true);
3215
+ setShowChoices(false);
3216
+ if (choice.nextNodeIndex !== void 0) {
3217
+ if (choice.nextNodeIndex === currentNodeIndex) {
3218
+ goToDialogue(choice.nextDialogueIndex || 0);
3219
+ } else {
3220
+ goToNode(choice.nextNodeIndex, true);
3221
+ }
3222
+ } else if (currentDialogue?.choices) {
3223
+ const nextIdx = currentDialogueIndex + 1;
3224
+ if (currentNode && nextIdx < currentNode.dialogues.length) {
3225
+ const nextDialogue = currentNode.dialogues[nextIdx];
3226
+ if (nextDialogue) {
3227
+ setCurrentDialogueIndex(nextIdx);
3228
+ addToHistory(nextDialogue, currentNodeIndex, nextIdx);
3229
+ onDialogueChange?.(nextDialogue, nextIdx, currentNodeIndex);
3230
+ typingCompleteRef.current = false;
3231
+ }
3232
+ } else {
3233
+ triggerNodeTransition();
3234
+ }
3192
3235
  }
3193
3236
  }
3194
3237
  }
@@ -3607,6 +3650,7 @@ var MMDMusicPlayer = React6.forwardRef(
3607
3650
  );
3608
3651
  MMDMusicPlayer.displayName = "MMDMusicPlayer";
3609
3652
 
3653
+ exports.ChoiceMenu = ChoiceMenu;
3610
3654
  exports.DialogueBox = DialogueBox;
3611
3655
  exports.HistoryPanel = HistoryPanel;
3612
3656
  exports.LoadingOverlay = LoadingOverlay;