sa2kit 1.6.0 → 1.6.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.
Files changed (34) hide show
  1. package/README.md +93 -94
  2. package/dist/AliyunOSSProvider-2UZRIGT3.mjs +6 -0
  3. package/dist/{AliyunOSSProvider-GQMSDJGZ.mjs.map → AliyunOSSProvider-2UZRIGT3.mjs.map} +1 -1
  4. package/dist/AliyunOSSProvider-XCTK3QQA.js +15 -0
  5. package/dist/{AliyunOSSProvider-7JLMJDXK.js.map → AliyunOSSProvider-XCTK3QQA.js.map} +1 -1
  6. package/dist/{chunk-3RFBUDRA.js → chunk-EXT3IKQA.js} +39 -6
  7. package/dist/chunk-EXT3IKQA.js.map +1 -0
  8. package/dist/{chunk-YVBU7QDJ.mjs → chunk-OHUE7EL6.mjs} +34 -5
  9. package/dist/chunk-OHUE7EL6.mjs.map +1 -0
  10. package/dist/mmd/admin/index.d.mts +1 -1
  11. package/dist/mmd/admin/index.d.ts +1 -1
  12. package/dist/mmd/admin/index.js +7 -1
  13. package/dist/mmd/admin/index.js.map +1 -1
  14. package/dist/mmd/admin/index.mjs +7 -1
  15. package/dist/mmd/admin/index.mjs.map +1 -1
  16. package/dist/mmd/index.d.mts +52 -6
  17. package/dist/mmd/index.d.ts +52 -6
  18. package/dist/mmd/index.js +276 -74
  19. package/dist/mmd/index.js.map +1 -1
  20. package/dist/mmd/index.mjs +277 -76
  21. package/dist/mmd/index.mjs.map +1 -1
  22. package/dist/mmd/server/index.d.mts +1 -1
  23. package/dist/mmd/server/index.d.ts +1 -1
  24. package/dist/{types-CsTSddwu.d.mts → types-DxYJqqes.d.mts} +34 -2
  25. package/dist/{types-CsTSddwu.d.ts → types-DxYJqqes.d.ts} +34 -2
  26. package/dist/universalFile/server/index.d.mts +15 -0
  27. package/dist/universalFile/server/index.d.ts +15 -0
  28. package/dist/universalFile/server/index.js +3 -3
  29. package/dist/universalFile/server/index.mjs +2 -2
  30. package/package.json +4 -1
  31. package/dist/AliyunOSSProvider-7JLMJDXK.js +0 -15
  32. package/dist/AliyunOSSProvider-GQMSDJGZ.mjs +0 -6
  33. package/dist/chunk-3RFBUDRA.js.map +0 -1
  34. package/dist/chunk-YVBU7QDJ.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { e as MMDPlayerBaseProps, f as MMDPlayerBaseRef, g as MMDPlayerEnhancedProps, h as MMDPlaylistProps, b as MMDPlaylistNode, d as MMDResources, i as MMDStage, j as MobileOptimization } from '../types-CsTSddwu.js';
2
- export { M as MMDPlaylistConfig, a as MMDResourceItem, c as MMDResourceOptions, R as ResourceOption } from '../types-CsTSddwu.js';
1
+ import { e as MMDPlayerBaseProps, f as MMDPlayerBaseRef, g as MMDPlayerEnhancedProps, h as MMDPlaylistProps, b as MMDPlaylistNode, d as MMDResources, i as MMDStage, j as MobileOptimization } from '../types-DxYJqqes.js';
2
+ export { B as BloomOptions, M as MMDPlaylistConfig, a as MMDResourceItem, c as MMDResourceOptions, O as OutlineOptions, R as ResourceOption } from '../types-DxYJqqes.js';
3
3
  import React__default from 'react';
4
4
  import 'three';
5
5
 
@@ -87,17 +87,50 @@ interface DialogueLine {
87
87
  expression?: string;
88
88
  /** 对话时播放的音效(可选) */
89
89
  voicePath?: string;
90
+ /** 对话中插入的分支选项(可选,若有则显示选项) */
91
+ choices?: DialogueChoice[];
92
+ /** 视觉特效(可选) */
93
+ effect?: VisualEffect;
94
+ }
95
+ /** 视觉特效配置 */
96
+ interface VisualEffect {
97
+ /** 特效类型 */
98
+ type: 'flash' | 'gif' | 'shake';
99
+ /** 闪屏颜色,如 'white', 'red' */
100
+ color?: string;
101
+ /** 持续时间(毫秒) */
102
+ duration?: number;
103
+ /** GIF 动画地址 */
104
+ url?: string;
105
+ /** GIF 显示位置 */
106
+ position?: 'center' | 'full';
107
+ }
108
+ /** 分支判定逻辑 */
109
+ interface BranchCondition {
110
+ /** 变量名 */
111
+ key: string;
112
+ /** 变量值与节点索引的映射 */
113
+ map: Record<string | number, number>;
114
+ /** 默认跳转的节点索引 */
115
+ defaultIndex: number;
90
116
  }
91
117
  /** 对话分支选项 */
92
118
  interface DialogueChoice {
93
119
  /** 选项文字 */
94
120
  text: string;
95
- /** 跳转到的节点索引 */
96
- nextNodeIndex: number;
121
+ /** 跳转到的节点索引(可选,若不填则继续当前剧情) */
122
+ nextNodeIndex?: number;
97
123
  /** 跳转到的对话索引(可选,默认 0) */
98
124
  nextDialogueIndex?: number;
125
+ /** 设置变量(可选,用于后续剧情判定) */
126
+ setVariable?: {
127
+ key: string;
128
+ value: string | number | boolean;
129
+ };
99
130
  /** 选项点击后的回调(可选) */
100
131
  onSelect?: () => void;
132
+ /** 点击选项后触发的特效(可选) */
133
+ effect?: VisualEffect;
101
134
  }
102
135
  /** 视觉小说播放节点 */
103
136
  interface VisualNovelNode {
@@ -111,8 +144,10 @@ interface VisualNovelNode {
111
144
  dialogues: DialogueLine[];
112
145
  /** 节点特定的舞台配置(可选,覆盖全局配置) */
113
146
  stage?: MMDStage;
114
- /** 节点结束时的分支选项(可选,若有则显示选项,不自动跳转) */
147
+ /** 节点结束时的分支选项(可选,已废弃,建议使用 DialogueLine.choices) */
115
148
  choices?: DialogueChoice[];
149
+ /** 节点结束时的分支判定逻辑(可选,根据变量跳转不同节点) */
150
+ nextCondition?: BranchCondition;
116
151
  /** 节点开始时播放的背景音乐(可选) */
117
152
  bgmPath?: string;
118
153
  /** 背景音乐音量 0-1(默认 0.5) */
@@ -240,6 +275,10 @@ interface MMDVisualNovelRef {
240
275
  getCurrentDialogueIndex: () => number;
241
276
  /** 获取对话历史 */
242
277
  getHistory: () => DialogueHistoryItem[];
278
+ /** 获取当前剧情变量 */
279
+ getVariables: () => Record<string, string | number | boolean>;
280
+ /** 设置剧情变量 */
281
+ setVariable: (key: string, value: string | number | boolean) => void;
243
282
  /** 设置自动播放模式 */
244
283
  setAutoMode: (enabled: boolean) => void;
245
284
  /** 跳过当前打字动画 */
@@ -344,6 +383,13 @@ interface StartScreenProps {
344
383
  */
345
384
  declare const StartScreen: React__default.FC<StartScreenProps>;
346
385
 
386
+ interface ChoiceMenuProps {
387
+ choices: DialogueChoice[];
388
+ onSelect: (choice: DialogueChoice) => void;
389
+ theme?: DialogueBoxTheme;
390
+ }
391
+ declare const ChoiceMenu: React__default.FC<ChoiceMenuProps>;
392
+
347
393
  /** 音乐曲目配置 */
348
394
  interface MusicTrack {
349
395
  /** 唯一标识 */
@@ -459,4 +505,4 @@ interface TrackInfoProps {
459
505
  }
460
506
  declare const TrackInfo: React__default.FC<TrackInfoProps>;
461
507
 
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 };
508
+ 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 VisualEffect, type VisualNovelNode, type VisualNovelScript, loadAmmo };
package/dist/mmd/index.js CHANGED
@@ -163,8 +163,37 @@ async function waitForMaterialsReady(object, renderer, scene, camera) {
163
163
  for (let i = 0; i < 3; i++) {
164
164
  await new Promise((resolve) => {
165
165
  requestAnimationFrame(() => {
166
- renderer.render(scene, camera);
167
- console.log(`[MMDPlayerBase] Warmup render ${i + 1}/3`);
166
+ try {
167
+ object.traverse((obj) => {
168
+ if (obj.isMesh) {
169
+ const mesh = obj;
170
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
171
+ const hasMorphAttributes = mesh.geometry.morphAttributes && Object.keys(mesh.geometry.morphAttributes).length > 0;
172
+ materials.forEach((m) => {
173
+ if (!hasMorphAttributes) {
174
+ m.morphTargets = false;
175
+ if (mesh.geometry.morphAttributes) {
176
+ mesh.geometry.morphAttributes = {};
177
+ }
178
+ if (mesh.morphTargetInfluences) {
179
+ mesh.morphTargetInfluences = [];
180
+ }
181
+ if (mesh.morphTargetDictionary) {
182
+ mesh.morphTargetDictionary = {};
183
+ }
184
+ m.needsUpdate = true;
185
+ }
186
+ });
187
+ if (mesh.updateMorphTargets) {
188
+ mesh.updateMorphTargets();
189
+ }
190
+ }
191
+ });
192
+ renderer.render(scene, camera);
193
+ console.log(`[MMDPlayerBase] Warmup render ${i + 1}/3`);
194
+ } catch (renderError) {
195
+ console.warn("[MMDPlayerBase] Warmup render failed (shader error?), skipping...", renderError);
196
+ }
168
197
  resolve();
169
198
  });
170
199
  });
@@ -192,10 +221,15 @@ var MMDPlayerBase = React6.forwardRef((props, ref) => {
192
221
  className,
193
222
  style
194
223
  } = props;
224
+ const renderEffect = props.renderEffect || stage.renderEffect || "default";
225
+ const outlineOptions = { ...stage.outlineOptions, ...props.outlineOptions };
226
+ const bloomOptions = { ...stage.bloomOptions, ...props.bloomOptions };
195
227
  const containerRef = React6.useRef(null);
196
228
  const sceneRef = React6.useRef(null);
197
229
  const cameraRef = React6.useRef(null);
198
230
  const rendererRef = React6.useRef(null);
231
+ const outlineEffectRef = React6.useRef(null);
232
+ const composerRef = React6.useRef(null);
199
233
  const controlsRef = React6.useRef(null);
200
234
  const helperRef = React6.useRef(null);
201
235
  const axesHelperRef = React6.useRef(null);
@@ -403,10 +437,28 @@ var MMDPlayerBase = React6.forwardRef((props, ref) => {
403
437
  }
404
438
  container.appendChild(renderer.domElement);
405
439
  rendererRef.current = renderer;
440
+ const effect = new threeStdlib.OutlineEffect(renderer, {
441
+ defaultThickness: outlineOptions.thickness ?? 3e-3,
442
+ defaultColor: new THREE__namespace.Color(outlineOptions.color ?? "#000000").toArray(),
443
+ defaultAlpha: 1,
444
+ defaultKeepAlive: true
445
+ });
446
+ outlineEffectRef.current = effect;
447
+ const composer = new threeStdlib.EffectComposer(renderer);
448
+ const renderPass = new threeStdlib.RenderPass(scene, camera);
449
+ composer.addPass(renderPass);
450
+ const bloomPass = new threeStdlib.UnrealBloomPass(
451
+ new THREE__namespace.Vector2(width, height),
452
+ bloomOptions.strength ?? 1,
453
+ bloomOptions.radius ?? 0.4,
454
+ bloomOptions.threshold ?? 0.8
455
+ );
456
+ composer.addPass(bloomPass);
457
+ composerRef.current = composer;
406
458
  const ambientLight = new THREE__namespace.AmbientLight(16777215, stage.ambientLightIntensity ?? 0.5);
407
459
  scene.add(ambientLight);
408
460
  const dirLight = new THREE__namespace.DirectionalLight(16777215, stage.directionalLightIntensity ?? 0.8);
409
- dirLight.position.set(10, 20, 10);
461
+ dirLight.position.set(0, 10, 0);
410
462
  if (stage.enableShadow !== false) {
411
463
  dirLight.castShadow = true;
412
464
  dirLight.shadow.mapSize.width = mobileOptimization.enabled ? 1024 : 2048;
@@ -441,6 +493,9 @@ var MMDPlayerBase = React6.forwardRef((props, ref) => {
441
493
  cameraRef.current.aspect = w / h;
442
494
  cameraRef.current.updateProjectionMatrix();
443
495
  rendererRef.current.setSize(w, h);
496
+ if (composerRef.current) {
497
+ composerRef.current.setSize(w, h);
498
+ }
444
499
  };
445
500
  const resizeObserver = new ResizeObserver(onResize);
446
501
  resizeObserver.observe(container);
@@ -448,7 +503,11 @@ var MMDPlayerBase = React6.forwardRef((props, ref) => {
448
503
  onResize();
449
504
  console.log("[MMDPlayerBase] Starting render loop (animation paused)");
450
505
  animate();
451
- console.log("[MMDPlayerBase] Start loading resources...", resources);
506
+ console.log("[MMDPlayerBase] Start loading resources...", {
507
+ model: resources.modelPath,
508
+ stage: resources.stageModelPath,
509
+ motion: resources.motionPath
510
+ });
452
511
  const loader = new threeStdlib.MMDLoader();
453
512
  const helper = new threeStdlib.MMDAnimationHelper({
454
513
  afterglow: 2
@@ -621,51 +680,67 @@ var MMDPlayerBase = React6.forwardRef((props, ref) => {
621
680
  (err) => console.error("Failed to load audio:", err)
622
681
  );
623
682
  }
624
- let stageMesh = null;
625
- if (resources.stageModelPath) {
683
+ const stagePaths = Array.isArray(resources.stageModelPath) ? resources.stageModelPath : resources.stageModelPath ? [resources.stageModelPath] : [];
684
+ for (const stagePath of stagePaths) {
626
685
  try {
627
- stageMesh = await new Promise((resolve, reject) => {
686
+ console.log(`[MMDPlayerBase] Loading stage from: ${stagePath}`);
687
+ const stageMesh = await new Promise((resolve, reject) => {
628
688
  loader.load(
629
- resources.stageModelPath,
689
+ stagePath,
630
690
  (mesh2) => resolve(mesh2),
631
- void 0,
691
+ (xhr) => {
692
+ if (xhr.lengthComputable) {
693
+ const percent = xhr.loaded / xhr.total * 100;
694
+ if (Math.round(percent) % 20 === 0) console.log(`[MMDPlayerBase] Stage loading: ${percent.toFixed(1)}%`);
695
+ }
696
+ },
632
697
  (err) => reject(err)
633
698
  );
634
699
  });
635
700
  if (checkCancelled()) return;
636
- console.log("[MMDPlayerBase] Stage model loaded:", stageMesh);
637
- stageMesh.castShadow = true;
638
- stageMesh.receiveShadow = true;
639
- console.log("[MMDPlayerBase] Waiting for stage materials and textures...");
640
- const tempStageScene = new THREE__namespace.Scene();
641
- tempStageScene.add(stageMesh);
642
- await waitForMaterialsReady(stageMesh, renderer, tempStageScene, camera);
643
- tempStageScene.remove(stageMesh);
701
+ console.log(`[MMDPlayerBase] Stage model loaded: ${stagePath}`, stageMesh);
702
+ stageMesh.traverse((child) => {
703
+ if (child instanceof THREE__namespace.Mesh) {
704
+ child.castShadow = true;
705
+ child.receiveShadow = true;
706
+ const mesh2 = child;
707
+ const materials = Array.isArray(mesh2.material) ? mesh2.material : [mesh2.material];
708
+ materials.forEach((m, idx) => {
709
+ if (m.morphTargets) {
710
+ m.morphTargets = false;
711
+ m.needsUpdate = true;
712
+ }
713
+ });
714
+ if (mesh2.geometry.morphAttributes) {
715
+ mesh2.geometry.morphAttributes = {};
716
+ }
717
+ if (mesh2.morphTargetInfluences) {
718
+ mesh2.morphTargetInfluences = [];
719
+ }
720
+ }
721
+ });
722
+ try {
723
+ await waitForMaterialsReady(stageMesh, renderer, scene, camera);
724
+ } catch (e) {
725
+ console.warn(`[MMDPlayerBase] Warmup error for stage ${stagePath}:`, e);
726
+ }
644
727
  if (checkCancelled()) return;
645
- console.log("[MMDPlayerBase] \u2705 Stage materials and textures loaded");
646
728
  scene.add(stageMesh);
647
- console.log("[MMDPlayerBase] \u2705 Stage added to scene (fully loaded)");
648
- if (resources.stageMotionPath && stageMesh) {
649
- console.log("[MMDPlayerBase] Loading stage motion:", resources.stageMotionPath);
650
- const anyLoader = loader;
651
- const anyHelper = helper;
652
- const anyStage = stageMesh;
653
- anyLoader.loadAnimation(
654
- resources.stageMotionPath,
655
- anyStage,
656
- (stageAnimation) => {
657
- if (checkCancelled()) return;
658
- anyHelper.add(anyStage, {
659
- animation: stageAnimation
660
- });
661
- console.log("[MMDPlayerBase] \u2705 Stage motion bound successfully");
662
- },
663
- void 0,
664
- (err) => console.error("Failed to load stage motion:", err)
665
- );
729
+ const stageBox = new THREE__namespace.Box3().setFromObject(stageMesh);
730
+ const stageSize = stageBox.getSize(new THREE__namespace.Vector3());
731
+ if (stageSize.length() < 1) {
732
+ stageMesh.scale.multiplyScalar(100);
733
+ } else if (stageSize.y < 5) {
734
+ stageMesh.scale.multiplyScalar(10);
735
+ }
736
+ console.log(`[MMDPlayerBase] \u2705 Stage added: ${stagePath}`);
737
+ if (resources.stageMotionPath) {
738
+ loader.loadAnimation(resources.stageMotionPath, stageMesh, (anim) => {
739
+ if (!checkCancelled()) helper.add(stageMesh, { animation: anim });
740
+ });
666
741
  }
667
742
  } catch (err) {
668
- console.error("Failed to load stage:", err);
743
+ console.error(`Failed to load stage ${stagePath}:`, err);
669
744
  }
670
745
  }
671
746
  if (checkCancelled()) return;
@@ -1043,6 +1118,13 @@ ${errorMessage}
1043
1118
  }
1044
1119
  if (rendererRef.current) {
1045
1120
  try {
1121
+ if (composerRef.current) {
1122
+ composerRef.current.passes.forEach((pass) => {
1123
+ if (pass.dispose) pass.dispose();
1124
+ });
1125
+ composerRef.current = null;
1126
+ }
1127
+ outlineEffectRef.current = null;
1046
1128
  const renderer = rendererRef.current;
1047
1129
  if (renderer.renderLists) {
1048
1130
  renderer.renderLists.dispose();
@@ -1109,6 +1191,17 @@ ${errorMessage}
1109
1191
  audioRef.current.setLoop(loop);
1110
1192
  }
1111
1193
  }, [loop]);
1194
+ React6.useEffect(() => {
1195
+ if (outlineEffectRef.current) ;
1196
+ if (composerRef.current) {
1197
+ const bloomPass = composerRef.current.passes.find((p) => p instanceof threeStdlib.UnrealBloomPass);
1198
+ if (bloomPass) {
1199
+ bloomPass.strength = bloomOptions.strength ?? 1;
1200
+ bloomPass.radius = bloomOptions.radius ?? 0.4;
1201
+ bloomPass.threshold = bloomOptions.threshold ?? 0.8;
1202
+ }
1203
+ }
1204
+ }, [bloomOptions.strength, bloomOptions.radius, bloomOptions.threshold]);
1112
1205
  React6.useEffect(() => {
1113
1206
  if (!isReadyRef.current) return;
1114
1207
  if (sceneRef.current) {
@@ -1157,7 +1250,15 @@ ${errorMessage}
1157
1250
  latestCallbacks.current.onEnded?.();
1158
1251
  }
1159
1252
  }
1160
- rendererRef.current.render(sceneRef.current, cameraRef.current);
1253
+ const useOutline = renderEffect === "outline" || renderEffect === "outline+bloom";
1254
+ const useBloom = renderEffect === "bloom" || renderEffect === "outline+bloom";
1255
+ if (useBloom && composerRef.current) {
1256
+ composerRef.current.render();
1257
+ } else if (useOutline && outlineEffectRef.current) {
1258
+ outlineEffectRef.current.render(sceneRef.current, cameraRef.current);
1259
+ } else {
1260
+ rendererRef.current.render(sceneRef.current, cameraRef.current);
1261
+ }
1161
1262
  }
1162
1263
  };
1163
1264
  return /* @__PURE__ */ React6__default.default.createElement(
@@ -2852,6 +2953,8 @@ var MMDVisualNovel = React6.forwardRef(
2852
2953
  const [pendingNodeIndex, setPendingNodeIndex] = React6.useState(null);
2853
2954
  const [showChoices, setShowChoices] = React6.useState(false);
2854
2955
  const [isCameraManual, setIsCameraManual] = React6.useState(false);
2956
+ const [variables, setVariables] = React6.useState({});
2957
+ const [activeEffect, setActiveEffect] = React6.useState(null);
2855
2958
  const playerRef = React6.useRef(null);
2856
2959
  const containerRef = React6.useRef(null);
2857
2960
  const autoTimerRef = React6.useRef(null);
@@ -2859,6 +2962,7 @@ var MMDVisualNovel = React6.forwardRef(
2859
2962
  const isStartedRef = React6.useRef(autoStart);
2860
2963
  const lastAnimationTimeRef = React6.useRef(0);
2861
2964
  const isVmdFinishedRef = React6.useRef(false);
2965
+ const effectTimerRef = React6.useRef(null);
2862
2966
  const currentNode = nodes[currentNodeIndex];
2863
2967
  const currentDialogue = currentNode?.dialogues[currentDialogueIndex] || null;
2864
2968
  const addToHistory = React6.useCallback((dialogue, nodeIndex, dialogueIndex) => {
@@ -2873,32 +2977,17 @@ var MMDVisualNovel = React6.forwardRef(
2873
2977
  }
2874
2978
  ]);
2875
2979
  }, []);
2876
- const goToNextDialogue = React6.useCallback(() => {
2877
- if (!currentNode) return;
2878
- if (autoTimerRef.current) {
2879
- clearTimeout(autoTimerRef.current);
2880
- autoTimerRef.current = null;
2980
+ const triggerEffect = React6.useCallback((effect) => {
2981
+ if (!effect) return;
2982
+ if (effectTimerRef.current) {
2983
+ clearTimeout(effectTimerRef.current);
2881
2984
  }
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]);
2985
+ setActiveEffect(effect);
2986
+ effectTimerRef.current = setTimeout(() => {
2987
+ setActiveEffect(null);
2988
+ effectTimerRef.current = null;
2989
+ }, effect.duration || 1e3);
2990
+ }, []);
2902
2991
  const goToNode = React6.useCallback(
2903
2992
  (nodeIndex, force = false) => {
2904
2993
  if (nodeIndex < 0 || nodeIndex >= nodes.length) return;
@@ -2937,8 +3026,52 @@ var MMDVisualNovel = React6.forwardRef(
2937
3026
  }, 100);
2938
3027
  }, 300);
2939
3028
  },
2940
- [nodes, isTransitioning, addToHistory, onNodeChange, onDialogueChange, currentNodeIndex, isVmdFinished]
3029
+ [nodes, isTransitioning, addToHistory, onNodeChange, onDialogueChange, currentNodeIndex]
2941
3030
  );
3031
+ const triggerNodeTransition = React6.useCallback(() => {
3032
+ if (!currentNode) return;
3033
+ let nextNodeIndex = currentNodeIndex + 1;
3034
+ if (currentNode.nextCondition) {
3035
+ const { key, map, defaultIndex } = currentNode.nextCondition;
3036
+ const val = variables[key];
3037
+ if (val !== void 0 && map[val] !== void 0) {
3038
+ nextNodeIndex = map[val];
3039
+ console.log(`[MMDVisualNovel] Branching: ${key}=${val} -> node ${nextNodeIndex}`);
3040
+ } else {
3041
+ nextNodeIndex = defaultIndex;
3042
+ }
3043
+ }
3044
+ if (nextNodeIndex < nodes.length && nextNodeIndex >= 0) {
3045
+ goToNode(nextNodeIndex);
3046
+ } else if (loop) {
3047
+ goToNode(0);
3048
+ } else {
3049
+ onScriptComplete?.();
3050
+ }
3051
+ }, [currentNode, currentNodeIndex, nodes.length, loop, variables, goToNode, onScriptComplete]);
3052
+ const goToNextDialogue = React6.useCallback(() => {
3053
+ if (!currentNode) return;
3054
+ if (currentDialogue?.choices && currentDialogue.choices.length > 0 && !showChoices) {
3055
+ setShowChoices(true);
3056
+ return;
3057
+ }
3058
+ if (autoTimerRef.current) {
3059
+ clearTimeout(autoTimerRef.current);
3060
+ autoTimerRef.current = null;
3061
+ }
3062
+ const nextDialogueIndex = currentDialogueIndex + 1;
3063
+ if (nextDialogueIndex < currentNode.dialogues.length && currentNode?.dialogues[nextDialogueIndex] !== void 0) {
3064
+ const nextDialogue = currentNode.dialogues[nextDialogueIndex];
3065
+ setCurrentDialogueIndex(nextDialogueIndex);
3066
+ addToHistory(nextDialogue, currentNodeIndex, nextDialogueIndex);
3067
+ onDialogueChange?.(nextDialogue, nextDialogueIndex, currentNodeIndex);
3068
+ typingCompleteRef.current = false;
3069
+ } else if (currentNode.choices && currentNode.choices.length > 0) {
3070
+ setShowChoices(true);
3071
+ } else {
3072
+ triggerNodeTransition();
3073
+ }
3074
+ }, [currentNode, currentDialogue, currentDialogueIndex, currentNodeIndex, nodes.length, loop, addToHistory, onDialogueChange, onScriptComplete, showChoices, variables, goToNode, triggerNodeTransition]);
2942
3075
  const goToDialogue = React6.useCallback(
2943
3076
  (dialogueIndex) => {
2944
3077
  if (!currentNode) return;
@@ -2982,6 +3115,11 @@ var MMDVisualNovel = React6.forwardRef(
2982
3115
  }
2983
3116
  return void 0;
2984
3117
  }, [currentDialogue, handleTypingComplete]);
3118
+ React6.useEffect(() => {
3119
+ if (currentDialogue?.effect) {
3120
+ triggerEffect(currentDialogue.effect);
3121
+ }
3122
+ }, [currentNodeIndex, currentDialogueIndex, triggerEffect]);
2985
3123
  const toggleAutoMode = React6.useCallback(() => {
2986
3124
  setIsAutoMode((prev) => !prev);
2987
3125
  }, []);
@@ -3017,12 +3155,17 @@ var MMDVisualNovel = React6.forwardRef(
3017
3155
  getCurrentNodeIndex: () => currentNodeIndex,
3018
3156
  getCurrentDialogueIndex: () => currentDialogueIndex,
3019
3157
  getHistory: () => history,
3158
+ getVariables: () => variables,
3159
+ setVariable: (key, value) => {
3160
+ setVariables((prev) => ({ ...prev, [key]: value }));
3161
+ },
3020
3162
  setAutoMode: setIsAutoMode,
3021
3163
  skipTyping: () => {
3022
3164
  typingCompleteRef.current = true;
3023
- }
3165
+ },
3166
+ triggerEffect
3024
3167
  }),
3025
- [goToNode, goToDialogue, currentNodeIndex, currentDialogueIndex, history]
3168
+ [goToNode, goToDialogue, currentNodeIndex, currentDialogueIndex, history, triggerEffect]
3026
3169
  );
3027
3170
  React6.useEffect(() => {
3028
3171
  if (autoStart && currentNode && currentNode.dialogues.length > 0 && history.length === 0 && currentNode?.dialogues[0] !== void 0) {
@@ -3034,6 +3177,9 @@ var MMDVisualNovel = React6.forwardRef(
3034
3177
  if (autoTimerRef.current) {
3035
3178
  clearTimeout(autoTimerRef.current);
3036
3179
  }
3180
+ if (effectTimerRef.current) {
3181
+ clearTimeout(effectTimerRef.current);
3182
+ }
3037
3183
  };
3038
3184
  }, []);
3039
3185
  if (!currentNode) {
@@ -3106,6 +3252,38 @@ var MMDVisualNovel = React6.forwardRef(
3106
3252
  }
3107
3253
  )
3108
3254
  ),
3255
+ activeEffect && /* @__PURE__ */ React6__default.default.createElement(
3256
+ "div",
3257
+ {
3258
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center",
3259
+ style: { zIndex: 999 }
3260
+ },
3261
+ activeEffect.type === "flash" && /* @__PURE__ */ React6__default.default.createElement(
3262
+ "div",
3263
+ {
3264
+ className: "h-full w-full",
3265
+ style: {
3266
+ backgroundColor: activeEffect.color || "white",
3267
+ animation: `flash-anim ${activeEffect.duration || 500}ms ease-out forwards`
3268
+ }
3269
+ }
3270
+ ),
3271
+ activeEffect.type === "gif" && activeEffect.url && /* @__PURE__ */ React6__default.default.createElement(
3272
+ "img",
3273
+ {
3274
+ src: activeEffect.url,
3275
+ alt: "effect",
3276
+ className: activeEffect.position === "full" ? "h-full w-full object-cover" : "max-h-full max-w-full"
3277
+ }
3278
+ ),
3279
+ /* @__PURE__ */ React6__default.default.createElement("style", null, `
3280
+ @keyframes flash-anim {
3281
+ 0% { opacity: 0; }
3282
+ 25% { opacity: 1; }
3283
+ 100% { opacity: 0; }
3284
+ }
3285
+ `)
3286
+ ),
3109
3287
  /* @__PURE__ */ React6__default.default.createElement(
3110
3288
  LoadingOverlay,
3111
3289
  {
@@ -3177,18 +3355,41 @@ var MMDVisualNovel = React6.forwardRef(
3177
3355
  }
3178
3356
  }
3179
3357
  ),
3180
- showChoices && currentNode.choices && /* @__PURE__ */ React6__default.default.createElement(
3358
+ showChoices && (currentDialogue?.choices || currentNode.choices) && /* @__PURE__ */ React6__default.default.createElement(
3181
3359
  ChoiceMenu,
3182
3360
  {
3183
- choices: currentNode.choices,
3361
+ choices: currentDialogue?.choices || currentNode.choices,
3184
3362
  theme: dialogueTheme,
3185
3363
  onSelect: (choice) => {
3364
+ if (choice.setVariable) {
3365
+ const { key, value } = choice.setVariable;
3366
+ setVariables((prev) => ({ ...prev, [key]: value }));
3367
+ console.log(`[MMDVisualNovel] Variable set: ${key} = ${value}`);
3368
+ }
3186
3369
  choice.onSelect?.();
3187
- if (choice.nextNodeIndex === currentNodeIndex) {
3188
- goToDialogue(choice.nextDialogueIndex || 0);
3189
- setShowChoices(false);
3190
- } else {
3191
- goToNode(choice.nextNodeIndex, true);
3370
+ if (choice.effect) {
3371
+ triggerEffect(choice.effect);
3372
+ }
3373
+ setShowChoices(false);
3374
+ if (choice.nextNodeIndex !== void 0) {
3375
+ if (choice.nextNodeIndex === currentNodeIndex) {
3376
+ goToDialogue(choice.nextDialogueIndex || 0);
3377
+ } else {
3378
+ goToNode(choice.nextNodeIndex, true);
3379
+ }
3380
+ } else if (currentDialogue?.choices) {
3381
+ const nextIdx = currentDialogueIndex + 1;
3382
+ if (currentNode && nextIdx < currentNode.dialogues.length) {
3383
+ const nextDialogue = currentNode.dialogues[nextIdx];
3384
+ if (nextDialogue) {
3385
+ setCurrentDialogueIndex(nextIdx);
3386
+ addToHistory(nextDialogue, currentNodeIndex, nextIdx);
3387
+ onDialogueChange?.(nextDialogue, nextIdx, currentNodeIndex);
3388
+ typingCompleteRef.current = false;
3389
+ }
3390
+ } else {
3391
+ triggerNodeTransition();
3392
+ }
3192
3393
  }
3193
3394
  }
3194
3395
  }
@@ -3607,6 +3808,7 @@ var MMDMusicPlayer = React6.forwardRef(
3607
3808
  );
3608
3809
  MMDMusicPlayer.displayName = "MMDMusicPlayer";
3609
3810
 
3811
+ exports.ChoiceMenu = ChoiceMenu;
3610
3812
  exports.DialogueBox = DialogueBox;
3611
3813
  exports.HistoryPanel = HistoryPanel;
3612
3814
  exports.LoadingOverlay = LoadingOverlay;