@luna-editor/engine 0.5.12 → 0.5.14
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/dist/Player.js +113 -12
- package/dist/components/DialogueBox.js +8 -6
- package/dist/components/GameScreen.d.ts +2 -0
- package/dist/components/GameScreen.js +177 -49
- package/dist/contexts/DataContext.js +1 -1
- package/dist/hooks/useBacklog.js +1 -1
- package/dist/hooks/useImagePreloader.d.ts +5 -0
- package/dist/hooks/useImagePreloader.js +53 -0
- package/dist/hooks/usePlayerLogic.d.ts +2 -1
- package/dist/hooks/usePlayerLogic.js +136 -98
- package/dist/hooks/useSoundPlayer.js +2 -1
- package/dist/plugin/luna-react.d.ts +41 -0
- package/dist/plugin/luna-react.js +99 -0
- package/dist/types.d.ts +17 -1
- package/dist/utils/branchBlockConverter.js +1 -0
- package/package.json +2 -2
package/dist/Player.js
CHANGED
|
@@ -16,6 +16,7 @@ import { BackgroundLayer } from "./components/BackgroundLayer";
|
|
|
16
16
|
import { ClickWaitIndicator } from "./components/ClickWaitIndicator";
|
|
17
17
|
import { ConversationBranchBox } from "./components/ConversationBranchBox";
|
|
18
18
|
import { DialogueBox } from "./components/DialogueBox";
|
|
19
|
+
import { EndScreen } from "./components/EndScreen";
|
|
19
20
|
import { FullscreenTextBox } from "./components/FullscreenTextBox";
|
|
20
21
|
import { GameScreen } from "./components/GameScreen";
|
|
21
22
|
import { OverlayUI } from "./components/OverlayUI";
|
|
@@ -33,6 +34,7 @@ import { usePluginEvents } from "./hooks/usePluginEvents";
|
|
|
33
34
|
import { usePreloadImages } from "./hooks/usePreloadImages";
|
|
34
35
|
import { useSoundPlayer } from "./hooks/useSoundPlayer";
|
|
35
36
|
import { useTypewriter } from "./hooks/useTypewriter";
|
|
37
|
+
import { useVoice } from "./hooks/useVoice";
|
|
36
38
|
import { PluginManager } from "./plugin/PluginManager";
|
|
37
39
|
import { ComponentType } from "./sdk";
|
|
38
40
|
import { convertBranchBlockToScenarioBlock } from "./utils/branchBlockConverter";
|
|
@@ -40,7 +42,7 @@ import { BranchNavigator } from "./utils/branchNavigator";
|
|
|
40
42
|
import { VariableManager } from "./utils/variableManager";
|
|
41
43
|
const EMPTY_PLUGINS = [];
|
|
42
44
|
const EMPTY_SOUNDS = [];
|
|
43
|
-
export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGINS, sounds = EMPTY_SOUNDS, onEnd, onScenarioEnd, onScenarioStart, onScenarioCancelled, onSettingsChange, className, autoplay = false, preventDefaultScroll = true, screenSize: screenSizeProp, disableKeyboardNavigation = false, }) => {
|
|
45
|
+
export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGINS, sounds = EMPTY_SOUNDS, onEnd, onScenarioEnd, onScenarioStart, onScenarioCancelled, onSettingsChange, className, autoplay = false, preventDefaultScroll = true, screenSize: screenSizeProp, disableKeyboardNavigation = false, hideEndScreen = false, }) => {
|
|
44
46
|
var _a, _b, _c, _d;
|
|
45
47
|
// scenario.blocks が存在しない場合は空の配列を使用
|
|
46
48
|
const scenario = useMemo(() => {
|
|
@@ -409,7 +411,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
409
411
|
}, 100);
|
|
410
412
|
return () => clearTimeout(timeoutId);
|
|
411
413
|
}, [pluginManager, isFirstRenderComplete]);
|
|
412
|
-
const { displayText, isTyping, skipTyping, startTyping, resetAccumulated } = useTypewriter({
|
|
414
|
+
const { displayText, isTyping, skipTyping, startTyping, resetAccumulated, reset: resetTypewriter, } = useTypewriter({
|
|
413
415
|
speed: mergedSettings.textSpeed,
|
|
414
416
|
});
|
|
415
417
|
// 現在の表示可能なブロックを取得(分岐ブロックが優先)
|
|
@@ -456,11 +458,37 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
456
458
|
return blocks;
|
|
457
459
|
}, [scenario.blocks, displayableBlockIndices, state.currentBlockIndex]);
|
|
458
460
|
// サウンド再生フック
|
|
459
|
-
useSoundPlayer({
|
|
461
|
+
const { stopAll: stopAllSounds } = useSoundPlayer({
|
|
460
462
|
soundBlocks: soundBlocksToProcess,
|
|
461
463
|
isFirstRenderComplete,
|
|
462
464
|
muteAudio: mergedSettings.muteAudio,
|
|
463
465
|
});
|
|
466
|
+
// パートボイス再生
|
|
467
|
+
const { playVoice, stopVoice } = useVoice();
|
|
468
|
+
// currentBlockが変更されたらパートボイスを再生
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
var _a;
|
|
471
|
+
if (!isFirstRenderComplete)
|
|
472
|
+
return;
|
|
473
|
+
if (!currentBlock)
|
|
474
|
+
return;
|
|
475
|
+
if (state.isEnded)
|
|
476
|
+
return;
|
|
477
|
+
if ((currentBlock.blockType === "dialogue" ||
|
|
478
|
+
currentBlock.blockType === "narration") &&
|
|
479
|
+
((_a = currentBlock.partVoice) === null || _a === void 0 ? void 0 : _a.url)) {
|
|
480
|
+
playVoice(currentBlock.partVoice.url);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
stopVoice();
|
|
484
|
+
}
|
|
485
|
+
}, [
|
|
486
|
+
currentBlock,
|
|
487
|
+
isFirstRenderComplete,
|
|
488
|
+
state.isEnded,
|
|
489
|
+
playVoice,
|
|
490
|
+
stopVoice,
|
|
491
|
+
]);
|
|
464
492
|
// 会話分岐機能
|
|
465
493
|
const branchNavigatorRef = useRef(new BranchNavigator());
|
|
466
494
|
const conversationBranch = useConversationBranch({
|
|
@@ -514,6 +542,10 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
514
542
|
}
|
|
515
543
|
};
|
|
516
544
|
}, [pluginManager, branchState]);
|
|
545
|
+
// キャラクター登場フェード待ち管理(refはここで宣言、検出ロジックはdisplayedCharacters定義後)
|
|
546
|
+
const isWaitingForEntranceRef = useRef(false);
|
|
547
|
+
const pendingTypingRef = useRef(null);
|
|
548
|
+
const prevCharIdSetRef = useRef(new Set());
|
|
517
549
|
// ダイアログ表示とアクションノード実行のuseEffectを分離
|
|
518
550
|
useEffect(() => {
|
|
519
551
|
if (currentBlock && isFirstRenderComplete && pluginsLoaded) {
|
|
@@ -539,15 +571,33 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
539
571
|
}
|
|
540
572
|
const isContinuableBlock = currentBlock.blockType === "dialogue" ||
|
|
541
573
|
currentBlock.blockType === "narration";
|
|
542
|
-
|
|
543
|
-
|
|
574
|
+
const doTyping = () => {
|
|
575
|
+
if (continueMode && isContinuableBlock) {
|
|
576
|
+
startTyping(content, continueMode);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
resetAccumulated();
|
|
580
|
+
startTyping(content, false);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
// キャラクター登場フェード中はタイピング開始を遅延し、既存テキストを即クリア
|
|
584
|
+
if (isWaitingForEntranceRef.current) {
|
|
585
|
+
resetTypewriter();
|
|
586
|
+
pendingTypingRef.current = doTyping;
|
|
544
587
|
}
|
|
545
588
|
else {
|
|
546
|
-
|
|
547
|
-
startTyping(content, false);
|
|
589
|
+
doTyping();
|
|
548
590
|
}
|
|
549
591
|
}
|
|
550
|
-
}, [
|
|
592
|
+
}, [
|
|
593
|
+
currentBlock,
|
|
594
|
+
previousBlock,
|
|
595
|
+
startTyping,
|
|
596
|
+
resetAccumulated,
|
|
597
|
+
resetTypewriter,
|
|
598
|
+
isFirstRenderComplete,
|
|
599
|
+
pluginsLoaded,
|
|
600
|
+
]);
|
|
551
601
|
// 分岐ブロック自動ロード処理
|
|
552
602
|
useEffect(() => {
|
|
553
603
|
if (currentBlock && currentBlock.blockType === "conversation_branch") {
|
|
@@ -624,6 +674,9 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
624
674
|
scenarioName: scenario.name || "scenario",
|
|
625
675
|
});
|
|
626
676
|
}
|
|
677
|
+
// リスタート時にすべての音声を停止
|
|
678
|
+
stopAllSounds();
|
|
679
|
+
stopVoice();
|
|
627
680
|
setIsFirstRenderComplete(false);
|
|
628
681
|
setHasStarted(false); // これにより次の useEffect で onScenarioStart が呼ばれる
|
|
629
682
|
setCurrentBranchBlock(null);
|
|
@@ -643,6 +696,8 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
643
696
|
pluginManager,
|
|
644
697
|
scenario.id,
|
|
645
698
|
scenario.name,
|
|
699
|
+
stopAllSounds,
|
|
700
|
+
stopVoice,
|
|
646
701
|
]);
|
|
647
702
|
// プラグインからシナリオを中断するためのコールバック
|
|
648
703
|
const cancelScenario = useCallback(() => {
|
|
@@ -653,7 +708,14 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
653
708
|
scenarioName: scenario.name || "scenario",
|
|
654
709
|
});
|
|
655
710
|
}
|
|
656
|
-
}, [
|
|
711
|
+
}, [
|
|
712
|
+
hasStarted,
|
|
713
|
+
state.isEnded,
|
|
714
|
+
onScenarioCancelled,
|
|
715
|
+
pluginManager,
|
|
716
|
+
scenario.id,
|
|
717
|
+
scenario.name,
|
|
718
|
+
]);
|
|
657
719
|
const { handleNext: handleNextInternal, handlePrevious: handlePreviousInternal, togglePlay: _togglePlay, restart: _restartInternal, displayedCharacters, } = usePlayerLogic({
|
|
658
720
|
state: Object.assign(Object.assign({}, state), { currentBlockIndex: actualBlockIndex }),
|
|
659
721
|
setState: (newState) => {
|
|
@@ -685,7 +747,28 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
685
747
|
customRestart: restart,
|
|
686
748
|
variableManager: variableManagerRef.current,
|
|
687
749
|
disableKeyboardNavigation,
|
|
750
|
+
onStopAllSounds: useCallback(() => {
|
|
751
|
+
stopAllSounds();
|
|
752
|
+
stopVoice();
|
|
753
|
+
}, [stopAllSounds, stopVoice]),
|
|
688
754
|
});
|
|
755
|
+
// レンダーフェーズでキャラクター構成の変化を検出(useEffectより前に実行される)
|
|
756
|
+
const currentCharIdSet = useMemo(() => new Set(displayedCharacters.map((c) => c.objectId)), [displayedCharacters]);
|
|
757
|
+
const prevCharIds = prevCharIdSetRef.current;
|
|
758
|
+
const hasCharacterCompositionChange = currentCharIdSet.size !== prevCharIds.size ||
|
|
759
|
+
[...currentCharIdSet].some((id) => !prevCharIds.has(id)) ||
|
|
760
|
+
[...prevCharIds].some((id) => !currentCharIdSet.has(id));
|
|
761
|
+
if (hasCharacterCompositionChange) {
|
|
762
|
+
isWaitingForEntranceRef.current = true;
|
|
763
|
+
prevCharIdSetRef.current = currentCharIdSet;
|
|
764
|
+
}
|
|
765
|
+
const handleEntranceComplete = useCallback(() => {
|
|
766
|
+
isWaitingForEntranceRef.current = false;
|
|
767
|
+
if (pendingTypingRef.current) {
|
|
768
|
+
pendingTypingRef.current();
|
|
769
|
+
pendingTypingRef.current = null;
|
|
770
|
+
}
|
|
771
|
+
}, []);
|
|
689
772
|
// プラグインイベント処理
|
|
690
773
|
const realBlockIndex = currentBlock
|
|
691
774
|
? scenario.blocks.indexOf(currentBlock)
|
|
@@ -760,6 +843,9 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
760
843
|
}
|
|
761
844
|
else {
|
|
762
845
|
setState((prev) => (Object.assign(Object.assign({}, prev), { isEnded: true, isPlaying: false })));
|
|
846
|
+
// シナリオ終了時にすべての音声を停止
|
|
847
|
+
stopAllSounds();
|
|
848
|
+
stopVoice();
|
|
763
849
|
// プラグインのonScenarioEndフックを呼び出し
|
|
764
850
|
pluginManager.callHook("onScenarioEnd", {
|
|
765
851
|
scenarioId: scenario.id,
|
|
@@ -776,9 +862,11 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
776
862
|
handleNextInternal,
|
|
777
863
|
onEnd,
|
|
778
864
|
onScenarioEnd, // プラグインのonScenarioEndフックを呼び出し
|
|
865
|
+
stopVoice,
|
|
779
866
|
pluginManager,
|
|
780
867
|
scenario.id,
|
|
781
868
|
scenario.name,
|
|
869
|
+
stopAllSounds,
|
|
782
870
|
]);
|
|
783
871
|
// プラグインからAUTO再生を制御するためのコールバック
|
|
784
872
|
const toggleAutoPlay = useCallback(() => {
|
|
@@ -798,13 +886,26 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
798
886
|
if (!hasStarted || state.isEnded)
|
|
799
887
|
return;
|
|
800
888
|
setState((prev) => (Object.assign(Object.assign({}, prev), { isEnded: true, isPlaying: false })));
|
|
889
|
+
// スキップ時にすべての音声を停止
|
|
890
|
+
stopAllSounds();
|
|
891
|
+
stopVoice();
|
|
801
892
|
pluginManager.callHook("onScenarioEnd", {
|
|
802
893
|
scenarioId: scenario.id,
|
|
803
894
|
scenarioName: scenario.name || "scenario",
|
|
804
895
|
});
|
|
805
896
|
onScenarioEnd === null || onScenarioEnd === void 0 ? void 0 : onScenarioEnd();
|
|
806
897
|
onEnd === null || onEnd === void 0 ? void 0 : onEnd();
|
|
807
|
-
}, [
|
|
898
|
+
}, [
|
|
899
|
+
hasStarted,
|
|
900
|
+
state.isEnded,
|
|
901
|
+
stopAllSounds,
|
|
902
|
+
stopVoice,
|
|
903
|
+
pluginManager,
|
|
904
|
+
scenario.id,
|
|
905
|
+
scenario.name,
|
|
906
|
+
onScenarioEnd,
|
|
907
|
+
onEnd,
|
|
908
|
+
]);
|
|
808
909
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
809
910
|
const _handlePrevious = useCallback(() => {
|
|
810
911
|
if (state.currentBlockIndex > 0) {
|
|
@@ -967,7 +1068,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
967
1068
|
return width / height;
|
|
968
1069
|
};
|
|
969
1070
|
// 条件付きレンダリングを JSX で処理(フックの後、early return なし)
|
|
970
|
-
return (_jsxs(_Fragment, { children: [!currentBlock && !state.isEnded && (_jsx("div", { className: clsx("flex items-center justify-center p-8", className), children: _jsx("p", { className: "text-gray-500", children: "\u30B7\u30CA\u30EA\u30AA\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" }) })), currentBlock && !state.isEnded && (_jsxs(_Fragment, { children: [(!imagesLoaded ||
|
|
1071
|
+
return (_jsxs(_Fragment, { children: [!currentBlock && !state.isEnded && (_jsx("div", { className: clsx("flex items-center justify-center p-8", className), children: _jsx("p", { className: "text-gray-500", children: "\u30B7\u30CA\u30EA\u30AA\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" }) })), state.isEnded && !hideEndScreen && (_jsx(EndScreen, { scenarioName: scenario.name, onRestart: restart, className: className })), currentBlock && !state.isEnded && (_jsxs(_Fragment, { children: [(!imagesLoaded ||
|
|
971
1072
|
!fontsLoaded ||
|
|
972
1073
|
!isFirstRenderComplete ||
|
|
973
1074
|
!pluginsLoaded) && (_jsx("div", { className: clsx("luna-player fixed inset-0 bg-black overflow-hidden flex items-center justify-center z-50", className), style: {
|
|
@@ -1000,7 +1101,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
1000
1101
|
effectVolume: mergedSettings.effectVolume,
|
|
1001
1102
|
textSoundVolume: mergedSettings.textSoundVolume,
|
|
1002
1103
|
muteAudio: (_b = mergedSettings.muteAudio) !== null && _b !== void 0 ? _b : false,
|
|
1003
|
-
}, children: _jsxs(PlaybackTextProvider, { displayText: displayTextElement, isTyping: isTyping, children: [_jsxs("div", { className: "h-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.Background, pluginManager: pluginManager, fallback: BackgroundLayer }, "background"), _jsx(GameScreen, { scenario: scenario, currentBlock: currentBlock, previousBlock: previousBlock, displayedCharacters: displayedCharacters, inactiveCharacterBrightness: mergedSettings.inactiveCharacterBrightness, characterSpacing: mergedSettings.characterSpacing }, "game-screen")] }), _jsx(OverlayUI, { children: _jsxs("div", { className: "h-full w-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.DialogueBox, pluginManager: pluginManager, fallback: DialogueBox }, "dialogue-box"), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "conversation_branch" && (_jsx(PluginComponentProvider, { type: ComponentType.ConversationBranch, pluginManager: pluginManager, fallback: ConversationBranchBox }, "conversation-branch")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "fullscreen_text" && (_jsx(FullscreenTextBox, { onComplete: () => handleNext("click") }, "fullscreen-text")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "click_wait" && (_jsx(ClickWaitIndicator, {}, "click-wait")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "time_wait" && (_jsx(TimeWaitIndicator, { duration: (_d = (_c = currentBlock.options) === null || _c === void 0 ? void 0 : _c.duration) !== null && _d !== void 0 ? _d : 1, onComplete: () => handleNext("time_wait") }, "time-wait")), pluginManager
|
|
1104
|
+
}, children: _jsxs(PlaybackTextProvider, { displayText: displayTextElement, isTyping: isTyping, children: [_jsxs("div", { className: "h-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.Background, pluginManager: pluginManager, fallback: BackgroundLayer }, "background"), _jsx(GameScreen, { scenario: scenario, currentBlock: currentBlock, previousBlock: previousBlock, displayedCharacters: displayedCharacters, inactiveCharacterBrightness: mergedSettings.inactiveCharacterBrightness, characterSpacing: mergedSettings.characterSpacing, onEntranceComplete: handleEntranceComplete }, "game-screen")] }), _jsx(OverlayUI, { children: _jsxs("div", { className: "h-full w-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.DialogueBox, pluginManager: pluginManager, fallback: DialogueBox }, "dialogue-box"), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "conversation_branch" && (_jsx(PluginComponentProvider, { type: ComponentType.ConversationBranch, pluginManager: pluginManager, fallback: ConversationBranchBox }, "conversation-branch")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "fullscreen_text" && (_jsx(FullscreenTextBox, { onComplete: () => handleNext("click") }, "fullscreen-text")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "click_wait" && (_jsx(ClickWaitIndicator, {}, "click-wait")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "time_wait" && (_jsx(TimeWaitIndicator, { duration: (_d = (_c = currentBlock.options) === null || _c === void 0 ? void 0 : _c.duration) !== null && _d !== void 0 ? _d : 1, onComplete: () => handleNext("time_wait") }, "time-wait")), pluginManager
|
|
1004
1105
|
.getRegisteredComponents()
|
|
1005
1106
|
.filter((type) => type !== ComponentType.DialogueBox &&
|
|
1006
1107
|
type !== ComponentType.ConversationBranch)
|
|
@@ -3,7 +3,7 @@ import { useMemo } from "react";
|
|
|
3
3
|
import { useDataAPI } from "../contexts/DataContext";
|
|
4
4
|
import { getFontFamilyStyle } from "../hooks/useFontLoader";
|
|
5
5
|
export const DialogueBox = () => {
|
|
6
|
-
var _a, _b;
|
|
6
|
+
var _a, _b, _c;
|
|
7
7
|
const dataAPI = useDataAPI();
|
|
8
8
|
const currentBlock = dataAPI.get("playback", "currentBlock");
|
|
9
9
|
const content = dataAPI.get("playback", "displayText") || "";
|
|
@@ -24,13 +24,15 @@ export const DialogueBox = () => {
|
|
|
24
24
|
if (!content) {
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
const
|
|
27
|
+
// 話者不明(speakerId=null)の台詞ブロックでは「???」を表示
|
|
28
|
+
const isUnknownSpeaker = !currentBlock.speaker && currentBlock.blockType === "dialogue";
|
|
29
|
+
const speakerName = (_b = (_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : (isUnknownSpeaker ? "???" : undefined);
|
|
30
|
+
const currentSpeakerId = (_c = currentBlock.speaker) === null || _c === void 0 ? void 0 : _c.id;
|
|
29
31
|
// 話者の位置を特定
|
|
30
32
|
const speakerCharacter = displayedCharacters.find((char) => char.objectId === currentSpeakerId);
|
|
31
|
-
//
|
|
33
|
+
// 位置に基づいて名前表示のクラスを決定(話者不明は中央固定)
|
|
32
34
|
const getNamePositionClass = () => {
|
|
33
|
-
if (!(speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.positionX))
|
|
35
|
+
if (isUnknownSpeaker || !(speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.positionX))
|
|
34
36
|
return "justify-center";
|
|
35
37
|
const positionX = speakerCharacter.positionX;
|
|
36
38
|
if (positionX < 0.33)
|
|
@@ -39,5 +41,5 @@ export const DialogueBox = () => {
|
|
|
39
41
|
return "justify-end -mr-[5%]"; // 右側
|
|
40
42
|
return "justify-center"; // 中央
|
|
41
43
|
};
|
|
42
|
-
return (
|
|
44
|
+
return (_jsxs("div", { className: "absolute bottom-[5%] max-w-[60%] mx-auto left-0 right-0 pointer-events-auto", style: { zIndex: 1000 }, "data-dialogue-element": true, children: [speakerName && (_jsx("div", { className: `flex ${getNamePositionClass()} px-4`, children: _jsx("div", { className: "bg-amber-950 text-white rounded-xl text-center px-4 py-0.5 border-4 border-amber-50 shadow-lg translate-y-3", "data-speaker-name-element": true, children: _jsx("span", { className: "font-bold text-lg", children: speakerName }) }) })), _jsxs("div", { className: `bg-amber-50 border-4 rounded-xl border-amber-800 p-4 ${speakerName ? "pt-5" : ""}`, children: [_jsx("div", { className: "leading-relaxed text-2xl text-black", style: { fontFamily: fontFamilyStyle }, children: content }), _jsx("div", { className: "flex justify-end mt-1", children: _jsx("div", { className: "text-amber-800/60 text-sm animate-pulse", children: "\u25BC" }) })] })] }));
|
|
43
45
|
};
|
|
@@ -5,7 +5,7 @@ import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "re
|
|
|
5
5
|
* React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
|
|
6
6
|
* これにより displayText の更新による不要な再描画を防止
|
|
7
7
|
*/
|
|
8
|
-
export const GameScreen = memo(function GameScreen({ scenario, currentBlock, previousBlock, displayedCharacters, inactiveCharacterBrightness = 0.8, characterSpacing = 0.2, }) {
|
|
8
|
+
export const GameScreen = memo(function GameScreen({ scenario, currentBlock, previousBlock, displayedCharacters, inactiveCharacterBrightness = 0.8, characterSpacing = 0.2, onEntranceComplete, }) {
|
|
9
9
|
var _a;
|
|
10
10
|
// キャラクターごとのフェード状態を管理
|
|
11
11
|
const [fadeStates, setFadeStates] = useState(new Map());
|
|
@@ -24,9 +24,13 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
24
24
|
const pendingEntranceRef = useRef([]);
|
|
25
25
|
// 退場キャラの最終位置を保存(auto-layout再計算に依存しないため)
|
|
26
26
|
const exitPositionsRef = useRef(new Map());
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// スライド方向を保存(登場・退場アニメーション用)
|
|
28
|
+
const slideDirectionsRef = useRef(new Map());
|
|
29
|
+
// 登場・退場の検出(差分ベース: 追加/削除されたキャラだけをアニメーション)
|
|
30
|
+
// useLayoutEffect: ブラウザ描画前に実行し、キャラがopacity=1で一瞬表示されるフラッシュを防ぐ
|
|
31
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: アニメーション検出はdisplayedCharactersの変更のみをトリガーとし、他の依存はrefで管理
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
30
34
|
const currentIds = new Set(displayedCharacters.map((c) => c.objectId));
|
|
31
35
|
const prevIds = prevDisplayedCharIdsRef.current;
|
|
32
36
|
const prevChars = prevDisplayedCharsRef.current;
|
|
@@ -34,29 +38,62 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
34
38
|
if (prevIds.size === 0 && prevChars.length === 0) {
|
|
35
39
|
prevDisplayedCharIdsRef.current = currentIds;
|
|
36
40
|
prevDisplayedCharsRef.current = displayedCharacters;
|
|
41
|
+
onEntranceComplete === null || onEntranceComplete === void 0 ? void 0 : onEntranceComplete();
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
+
// 前のブロックと現在のブロックの間にある character_exit / character_entrance ブロックを検索
|
|
45
|
+
let exitBlockOptions = null;
|
|
46
|
+
let entranceBlockOptions = null;
|
|
47
|
+
let hasNonAdditiveEntrance = false;
|
|
48
|
+
if (previousBlock && currentBlock) {
|
|
49
|
+
const blocks = scenario.blocks;
|
|
50
|
+
const prevIdx = blocks.findIndex((b) => b.id === previousBlock.id);
|
|
51
|
+
const curIdx = blocks.findIndex((b) => b.id === currentBlock.id);
|
|
52
|
+
if (prevIdx >= 0 && curIdx >= 0) {
|
|
53
|
+
for (let i = prevIdx + 1; i <= curIdx; i++) {
|
|
54
|
+
const b = blocks[i];
|
|
55
|
+
if (b.blockType === "character_exit" && b.options) {
|
|
56
|
+
exitBlockOptions = b.options;
|
|
57
|
+
}
|
|
58
|
+
if (b.blockType === "character_entrance") {
|
|
59
|
+
if (b.options) {
|
|
60
|
+
entranceBlockOptions = b.options;
|
|
61
|
+
}
|
|
62
|
+
if (((_a = b.options) === null || _a === void 0 ? void 0 : _a.additive) !== true) {
|
|
63
|
+
hasNonAdditiveEntrance = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 非追加モードのentranceがある場合: 全員退場→全員登場(従来動作)
|
|
70
|
+
// 追加モード/exitのみの場合: 差分のみアニメーション
|
|
71
|
+
let enteringIds;
|
|
72
|
+
let exitingIds;
|
|
73
|
+
if (hasNonAdditiveEntrance) {
|
|
74
|
+
// 全差し替え: 前回の全キャラが退場、現在の全キャラが登場
|
|
75
|
+
exitingIds = [...prevIds];
|
|
76
|
+
enteringIds = [...currentIds];
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// 差分検出: 追加されたキャラ / 削除されたキャラのみ
|
|
80
|
+
enteringIds = [...currentIds].filter((id) => !prevIds.has(id));
|
|
81
|
+
exitingIds = [...prevIds].filter((id) => !currentIds.has(id));
|
|
82
|
+
}
|
|
83
|
+
if (enteringIds.length === 0 && exitingIds.length === 0) {
|
|
44
84
|
prevDisplayedCharsRef.current = displayedCharacters;
|
|
45
85
|
return;
|
|
46
86
|
}
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const allEnterIds = [...currentIds];
|
|
51
|
-
// 退場フェードアウト開始
|
|
52
|
-
if (allExitChars.length > 0) {
|
|
87
|
+
// 退場キャラの処理: フェードアウトを適用
|
|
88
|
+
const exitChars = prevChars.filter((c) => exitingIds.includes(c.objectId));
|
|
89
|
+
if (exitChars.length > 0) {
|
|
53
90
|
const now = performance.now();
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
for (const char of exitChars) {
|
|
92
|
+
// 位置を保存
|
|
56
93
|
if (char.positionX !== null && char.positionY !== null) {
|
|
57
94
|
exitPositionsRef.current.set(char.objectId, {
|
|
58
|
-
x: (
|
|
59
|
-
y: (
|
|
95
|
+
x: (_b = char.positionX) !== null && _b !== void 0 ? _b : 0,
|
|
96
|
+
y: (_c = char.positionY) !== null && _c !== void 0 ? _c : 0,
|
|
60
97
|
});
|
|
61
98
|
}
|
|
62
99
|
else {
|
|
@@ -66,39 +103,96 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
66
103
|
exitPositionsRef.current.set(char.objectId, pos);
|
|
67
104
|
}
|
|
68
105
|
}
|
|
69
|
-
}
|
|
70
|
-
setExitingCharacters((prev) => [...prev, ...allExitChars]);
|
|
71
|
-
const newFades = new Map(entranceFades);
|
|
72
|
-
for (const char of allExitChars) {
|
|
73
|
-
newFades.set(char.objectId, 1);
|
|
74
106
|
entranceFadeStartRef.current.set(char.objectId, now);
|
|
107
|
+
// スライド: character_exit ブロックの anim: フラグがある場合のみ
|
|
108
|
+
const animKey = `anim:${char.objectId}`;
|
|
109
|
+
if ((exitBlockOptions === null || exitBlockOptions === void 0 ? void 0 : exitBlockOptions[animKey]) === true) {
|
|
110
|
+
const dirKey = `dir:${char.objectId}`;
|
|
111
|
+
const dirVal = exitBlockOptions[dirKey];
|
|
112
|
+
const validDirs = ["left", "right", "up", "down"];
|
|
113
|
+
const dir = typeof dirVal === "string" &&
|
|
114
|
+
validDirs.includes(dirVal)
|
|
115
|
+
? dirVal
|
|
116
|
+
: ((_e = (_d = exitPositionsRef.current.get(char.objectId)) === null || _d === void 0 ? void 0 : _d.x) !== null && _e !== void 0 ? _e : 0) < 0
|
|
117
|
+
? "left"
|
|
118
|
+
: "right";
|
|
119
|
+
slideDirectionsRef.current.set(char.objectId, dir);
|
|
120
|
+
}
|
|
75
121
|
}
|
|
76
|
-
|
|
122
|
+
setExitingCharacters((prev) => [...prev, ...exitChars]);
|
|
123
|
+
setEntranceFades((prev) => {
|
|
124
|
+
const newFades = new Map(prev);
|
|
125
|
+
for (const char of exitChars) {
|
|
126
|
+
newFades.set(char.objectId, 1);
|
|
127
|
+
}
|
|
128
|
+
return newFades;
|
|
129
|
+
});
|
|
77
130
|
}
|
|
78
|
-
//
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
131
|
+
// 登場キャラの処理: 全登場キャラにフェードインを適用
|
|
132
|
+
if (enteringIds.length > 0) {
|
|
133
|
+
// スライド: character_entrance ブロックの anim: フラグがある場合のみ
|
|
134
|
+
const enterAutoPositions = calculateAutoLayout(displayedCharacters, characterSpacing !== null && characterSpacing !== void 0 ? characterSpacing : 0.2);
|
|
135
|
+
for (const id of enteringIds) {
|
|
136
|
+
const animKey = `anim:${id}`;
|
|
137
|
+
if ((entranceBlockOptions === null || entranceBlockOptions === void 0 ? void 0 : entranceBlockOptions[animKey]) === true) {
|
|
138
|
+
const dirKey = `dir:${id}`;
|
|
139
|
+
const dirVal = entranceBlockOptions[dirKey];
|
|
140
|
+
const validDirs = ["left", "right", "up", "down"];
|
|
141
|
+
if (typeof dirVal === "string" &&
|
|
142
|
+
validDirs.includes(dirVal)) {
|
|
143
|
+
slideDirectionsRef.current.set(id, dirVal);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const char = displayedCharacters.find((c) => c.objectId === id);
|
|
147
|
+
let x = 0;
|
|
148
|
+
if ((char === null || char === void 0 ? void 0 : char.positionX) !== null && (char === null || char === void 0 ? void 0 : char.positionX) !== undefined) {
|
|
149
|
+
x = char.positionX;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
x = (_g = (_f = enterAutoPositions.get(id)) === null || _f === void 0 ? void 0 : _f.x) !== null && _g !== void 0 ? _g : 0;
|
|
153
|
+
}
|
|
154
|
+
slideDirectionsRef.current.set(id, x < 0 ? "left" : "right");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (exitChars.length > 0) {
|
|
159
|
+
// 退場アニメーション完了を待ってから登場フェードイン開始
|
|
160
|
+
const delay = ENTRANCE_FADE_DURATION + 300;
|
|
161
|
+
pendingEntranceRef.current = enteringIds;
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
const ids = pendingEntranceRef.current;
|
|
164
|
+
if (ids.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
pendingEntranceRef.current = [];
|
|
167
|
+
const now = performance.now();
|
|
168
|
+
setEntranceFades((prev) => {
|
|
169
|
+
const newFades = new Map(prev);
|
|
170
|
+
for (const id of ids) {
|
|
171
|
+
newFades.set(id, 0);
|
|
172
|
+
entranceFadeStartRef.current.set(id, now);
|
|
173
|
+
}
|
|
174
|
+
return newFades;
|
|
175
|
+
});
|
|
176
|
+
}, delay);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// 退場なし: 即座にフェードイン開始(setTimeoutを使わず1フレーム目から opacity=0)
|
|
87
180
|
const now = performance.now();
|
|
88
181
|
setEntranceFades((prev) => {
|
|
89
182
|
const newFades = new Map(prev);
|
|
90
|
-
for (const id of
|
|
183
|
+
for (const id of enteringIds) {
|
|
91
184
|
newFades.set(id, 0);
|
|
92
185
|
entranceFadeStartRef.current.set(id, now);
|
|
93
186
|
}
|
|
94
187
|
return newFades;
|
|
95
188
|
});
|
|
96
|
-
}
|
|
189
|
+
}
|
|
97
190
|
}
|
|
98
191
|
prevDisplayedCharIdsRef.current = currentIds;
|
|
99
192
|
prevDisplayedCharsRef.current = displayedCharacters;
|
|
100
|
-
}, [displayedCharacters]);
|
|
193
|
+
}, [displayedCharacters]);
|
|
101
194
|
// 登場・退場フェードのアニメーションループ
|
|
195
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: アニメーションループはフェード状態と退場キャラの変更のみで制御
|
|
102
196
|
useEffect(() => {
|
|
103
197
|
if (entranceFades.size === 0)
|
|
104
198
|
return;
|
|
@@ -140,6 +234,10 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
140
234
|
else {
|
|
141
235
|
setEntranceFades(new Map());
|
|
142
236
|
entranceFadeStartRef.current.clear();
|
|
237
|
+
// 登場フェード完了を通知(保留中の登場がない場合のみ)
|
|
238
|
+
if (pendingEntranceRef.current.length === 0) {
|
|
239
|
+
onEntranceComplete === null || onEntranceComplete === void 0 ? void 0 : onEntranceComplete();
|
|
240
|
+
}
|
|
143
241
|
}
|
|
144
242
|
};
|
|
145
243
|
entranceAnimFrameRef.current = requestAnimationFrame(animate);
|
|
@@ -148,7 +246,7 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
148
246
|
cancelAnimationFrame(entranceAnimFrameRef.current);
|
|
149
247
|
}
|
|
150
248
|
};
|
|
151
|
-
}, [entranceFades.size > 0, exitingCharacters]);
|
|
249
|
+
}, [entranceFades.size > 0, exitingCharacters]);
|
|
152
250
|
// コンテナサイズを取得(ResizeObserverで確実に取得)
|
|
153
251
|
const containerRef = useRef(null);
|
|
154
252
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
@@ -367,24 +465,28 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
367
465
|
const currentSpeaker = (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.speakerId)
|
|
368
466
|
? displayedCharacters.find((char) => char.objectId === currentBlock.speakerId)
|
|
369
467
|
: null;
|
|
370
|
-
//
|
|
468
|
+
// 簡易モード用の自動配置計算(スロットベース対応)
|
|
371
469
|
const calculateAutoLayout = (characters, spacing) => {
|
|
372
|
-
|
|
470
|
+
var _a, _b;
|
|
373
471
|
const positions = new Map();
|
|
374
|
-
if (
|
|
472
|
+
if (characters.length === 0)
|
|
375
473
|
return positions;
|
|
376
|
-
|
|
377
|
-
|
|
474
|
+
// スロットベース配置: layoutSlotCountがあればそれをスロット数として使用
|
|
475
|
+
const slotCount = (_b = (_a = characters[0]) === null || _a === void 0 ? void 0 : _a.layoutSlotCount) !== null && _b !== void 0 ? _b : characters.length;
|
|
476
|
+
if (slotCount === 1) {
|
|
378
477
|
positions.set(characters[0].objectId, { x: 0, y: 0 });
|
|
379
478
|
return positions;
|
|
380
479
|
}
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
const totalWidth = (
|
|
480
|
+
// 2人の場合は間隔を少し広めに(最低0.3)
|
|
481
|
+
const effectiveSpacing = slotCount === 2 ? Math.max(spacing, 0.3) : spacing;
|
|
482
|
+
// スロット数に基づいた均等配置
|
|
483
|
+
const totalWidth = (slotCount - 1) * effectiveSpacing * 2;
|
|
385
484
|
const startX = -totalWidth / 2;
|
|
386
485
|
characters.forEach((char, index) => {
|
|
387
|
-
|
|
486
|
+
var _a;
|
|
487
|
+
// layoutSlotIndexがあればそのスロット位置を使用、なければ配列順
|
|
488
|
+
const slotIndex = (_a = char.layoutSlotIndex) !== null && _a !== void 0 ? _a : index;
|
|
489
|
+
const x = startX + slotIndex * effectiveSpacing * 2;
|
|
388
490
|
positions.set(char.objectId, { x, y: 0 });
|
|
389
491
|
});
|
|
390
492
|
return positions;
|
|
@@ -443,6 +545,32 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
443
545
|
!isExitingChar) {
|
|
444
546
|
opacity = 0;
|
|
445
547
|
}
|
|
548
|
+
// スライドアニメーション: opacity に連動してスライドオフセットを計算
|
|
549
|
+
let slideOffsetX = 0;
|
|
550
|
+
let slideOffsetY = 0;
|
|
551
|
+
const slideDir = slideDirectionsRef.current.get(image.objectId);
|
|
552
|
+
if (slideDir &&
|
|
553
|
+
(entranceFadeOpacity !== undefined ||
|
|
554
|
+
pendingEntranceRef.current.includes(image.objectId))) {
|
|
555
|
+
const remaining = 1 - (entranceFadeOpacity !== null && entranceFadeOpacity !== void 0 ? entranceFadeOpacity : 0);
|
|
556
|
+
const easedOffset = remaining * remaining;
|
|
557
|
+
const maxHorizontal = containerSize.width * 0.3;
|
|
558
|
+
const maxVertical = containerSize.height * 0.3;
|
|
559
|
+
switch (slideDir) {
|
|
560
|
+
case "left":
|
|
561
|
+
slideOffsetX = -maxHorizontal * easedOffset;
|
|
562
|
+
break;
|
|
563
|
+
case "right":
|
|
564
|
+
slideOffsetX = maxHorizontal * easedOffset;
|
|
565
|
+
break;
|
|
566
|
+
case "up":
|
|
567
|
+
slideOffsetY = -maxVertical * easedOffset;
|
|
568
|
+
break;
|
|
569
|
+
case "down":
|
|
570
|
+
slideOffsetY = maxVertical * easedOffset;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
446
574
|
// 明るさを決定
|
|
447
575
|
let brightness = 1;
|
|
448
576
|
if (displayedCharacters.length > 0 && displayedChar) {
|
|
@@ -540,7 +668,7 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
540
668
|
return (_jsx("div", { style: Object.assign({ position: "absolute", visibility: shouldDisplay ? "visible" : "hidden", opacity: opacity, filter: `brightness(${brightness})`, zIndex: zIndex,
|
|
541
669
|
// CharacterEditDialogと完全に同じ: left/topを0にしてtransformで位置制御
|
|
542
670
|
// translate(%)は画像サイズに対する割合
|
|
543
|
-
left: 0, top: 0, transform: `translate(${leftPx}px, ${topPx}px) translate(${translateXPercent}%, ${translateYPercent}%)`, transition: fadeState ? "none" : undefined }, cropMaskStyle), "data-character-id": image.objectId, "data-character-sprite": true, children: hasLayerFeature && image.baseBodyUrl ? (_jsxs(_Fragment, { children: [_jsx("img", { src: image.baseBodyUrl, alt: "", decoding: "sync", style: {
|
|
671
|
+
left: 0, top: 0, transform: `translate(${leftPx + slideOffsetX}px, ${topPx + slideOffsetY}px) translate(${translateXPercent}%, ${translateYPercent}%)`, transition: fadeState ? "none" : undefined }, cropMaskStyle), "data-character-id": image.objectId, "data-character-sprite": true, children: hasLayerFeature && image.baseBodyUrl ? (_jsxs(_Fragment, { children: [_jsx("img", { src: image.baseBodyUrl, alt: "", decoding: "sync", style: {
|
|
544
672
|
height: `${baseHeight}px`,
|
|
545
673
|
width: "auto",
|
|
546
674
|
objectFit: "contain",
|
|
@@ -24,7 +24,7 @@ const SkipScenarioContext = createContext(null);
|
|
|
24
24
|
* </DataProvider>
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
|
-
export const DataProvider = ({ data, onSettingsUpdate, onEmotionEffectUpdate, onCancelScenario, onToggleAutoPlay, onSetAutoPlay, onSkipToNext, onSkipScenario, children }) => {
|
|
27
|
+
export const DataProvider = ({ data, onSettingsUpdate, onEmotionEffectUpdate, onCancelScenario, onToggleAutoPlay, onSetAutoPlay, onSkipToNext, onSkipScenario, children, }) => {
|
|
28
28
|
const subscribers = useMemo(() => new Map(), []);
|
|
29
29
|
const previousDataRef = useRef(data);
|
|
30
30
|
// 安定したデータ参照ホルダー(Context valueとして提供)
|
package/dist/hooks/useBacklog.js
CHANGED
|
@@ -117,7 +117,7 @@ export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
|
|
|
117
117
|
processedBlocks.current.add(currentBlockIndex);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
}, [currentBlock, currentBlockIndex, addLogEntry]);
|
|
120
|
+
}, [currentBlock, currentBlockIndex, addLogEntry, scenario.blocks]);
|
|
121
121
|
// シナリオ変更時にリセット
|
|
122
122
|
// biome-ignore lint/correctness/useExhaustiveDependencies: scenario.idの変更を検知してログをクリアする意図的な依存
|
|
123
123
|
useEffect(() => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
export var useImagePreloader = function (displayedCharacters, singleImageUrl) {
|
|
3
|
+
var _a = useState(true),
|
|
4
|
+
isLoading = _a[0],
|
|
5
|
+
setIsLoading = _a[1];
|
|
6
|
+
useEffect(
|
|
7
|
+
function () {
|
|
8
|
+
var imageUrls = [];
|
|
9
|
+
// 複数キャラクター表示の場合
|
|
10
|
+
if (displayedCharacters.length > 0) {
|
|
11
|
+
displayedCharacters.forEach(function (char) {
|
|
12
|
+
if (char.entityState.imageUrl) {
|
|
13
|
+
imageUrls.push(char.entityState.imageUrl);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
} else if (singleImageUrl) {
|
|
17
|
+
// 単一キャラクター表示の場合
|
|
18
|
+
imageUrls.push(singleImageUrl);
|
|
19
|
+
}
|
|
20
|
+
// 画像がない場合は即座に完了
|
|
21
|
+
if (imageUrls.length === 0) {
|
|
22
|
+
setIsLoading(false);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setIsLoading(true);
|
|
26
|
+
// すべての画像を並列でプリロード
|
|
27
|
+
var loadPromises = imageUrls.map(function (url) {
|
|
28
|
+
return new Promise(function (resolve, reject) {
|
|
29
|
+
var img = new Image();
|
|
30
|
+
img.onload = function () {
|
|
31
|
+
return resolve();
|
|
32
|
+
};
|
|
33
|
+
img.onerror = function () {
|
|
34
|
+
return reject(new Error("Failed to load image: ".concat(url)));
|
|
35
|
+
};
|
|
36
|
+
img.src = url;
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
// すべての画像の読み込みを待つ
|
|
40
|
+
Promise.all(loadPromises)
|
|
41
|
+
.then(function () {
|
|
42
|
+
setIsLoading(false);
|
|
43
|
+
})
|
|
44
|
+
.catch(function (error) {
|
|
45
|
+
console.error("Image preload error:", error);
|
|
46
|
+
// エラーが発生しても続行
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
[displayedCharacters, singleImageUrl]
|
|
51
|
+
);
|
|
52
|
+
return isLoading;
|
|
53
|
+
};
|
|
@@ -16,8 +16,9 @@ interface UsePlayerLogicProps {
|
|
|
16
16
|
customRestart?: () => void;
|
|
17
17
|
variableManager?: VariableManager | null;
|
|
18
18
|
disableKeyboardNavigation?: boolean;
|
|
19
|
+
onStopAllSounds?: () => void;
|
|
19
20
|
}
|
|
20
|
-
export declare const usePlayerLogic: ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, branchState, branchNavigator: _branchNavigator, customRestart, variableManager, disableKeyboardNavigation, }: UsePlayerLogicProps) => {
|
|
21
|
+
export declare const usePlayerLogic: ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, branchState, branchNavigator: _branchNavigator, customRestart, variableManager, disableKeyboardNavigation, onStopAllSounds, }: UsePlayerLogicProps) => {
|
|
21
22
|
handleNext: () => void;
|
|
22
23
|
handlePrevious: () => void;
|
|
23
24
|
togglePlay: () => void;
|
|
@@ -1,114 +1,150 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
// character_entranceブロックからキャラクターデータを生成するヘルパー
|
|
3
|
+
const mapEntranceCharacters = (block) => {
|
|
4
|
+
if (!block.characters)
|
|
5
|
+
return [];
|
|
6
|
+
const opts = block.options;
|
|
7
|
+
const layoutSlotCount = opts && typeof opts.layoutSlotCount === "number"
|
|
8
|
+
? opts.layoutSlotCount
|
|
9
|
+
: undefined;
|
|
10
|
+
return block.characters.map((char) => {
|
|
11
|
+
const slotKey = `slot:${char.objectId}`;
|
|
12
|
+
const slotVal = opts === null || opts === void 0 ? void 0 : opts[slotKey];
|
|
13
|
+
const layoutSlotIndex = typeof slotVal === "number" ? slotVal : undefined;
|
|
14
|
+
return {
|
|
15
|
+
objectId: char.objectId,
|
|
16
|
+
entityStateId: char.entityStateId,
|
|
17
|
+
positionX: char.positionX,
|
|
18
|
+
positionY: char.positionY,
|
|
19
|
+
zIndex: char.zIndex,
|
|
20
|
+
scale: char.scale,
|
|
21
|
+
cropLeft: char.cropLeft,
|
|
22
|
+
cropRight: char.cropRight,
|
|
23
|
+
cropFade: char.cropFade,
|
|
24
|
+
object: char.object,
|
|
25
|
+
entityState: char.entityState,
|
|
26
|
+
baseBodyState: char.baseBodyState,
|
|
27
|
+
layoutSlotCount,
|
|
28
|
+
layoutSlotIndex,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
};
|
|
2
32
|
// ブロックインデックスから表示すべきキャラクター状態を計算する純粋関数
|
|
3
33
|
const calculateDisplayedCharacters = (blocks, currentIndex) => {
|
|
34
|
+
var _a, _b, _c, _d;
|
|
4
35
|
let characters = [];
|
|
5
|
-
//
|
|
36
|
+
// 後方探索: 最初の非追加モードのcharacter_entranceを見つける(リセットポイント)
|
|
37
|
+
let startIndex = 0;
|
|
6
38
|
for (let i = currentIndex; i >= 0; i--) {
|
|
7
39
|
const block = blocks[i];
|
|
8
40
|
if (block.blockType === "character_entrance") {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// speakerStateにレイヤー情報があればそれを使用、なければ既存を保持
|
|
52
|
-
layers: (_k = (_j = processBlock.speakerState) === null || _j === void 0 ? void 0 : _j.layers) !== null && _k !== void 0 ? _k : char.entityState.layers,
|
|
53
|
-
},
|
|
54
|
-
// baseBodyStateを保持
|
|
55
|
-
baseBodyState: char.baseBodyState }) : char;
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
break;
|
|
59
|
-
case "character_state_change":
|
|
60
|
-
// 状態変更ブロック: 登場済みキャラクターの状態のみを変更(位置は維持)
|
|
61
|
-
// characters配列がある場合はそれを使用(複数キャラクター対応)
|
|
62
|
-
if (processBlock.characters && processBlock.characters.length > 0) {
|
|
63
|
-
for (const stateChange of processBlock.characters) {
|
|
64
|
-
characters = characters.map((char) => {
|
|
65
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
66
|
-
return char.objectId === stateChange.objectId
|
|
67
|
-
? Object.assign(Object.assign({}, char), { entityStateId: stateChange.entityStateId, entityState: {
|
|
68
|
-
id: stateChange.entityStateId,
|
|
69
|
-
name: ((_a = stateChange.entityState) === null || _a === void 0 ? void 0 : _a.name) || "",
|
|
70
|
-
imageUrl: ((_b = stateChange.entityState) === null || _b === void 0 ? void 0 : _b.imageUrl) || null,
|
|
71
|
-
cropArea: ((_c = stateChange.entityState) === null || _c === void 0 ? void 0 : _c.cropArea) || null,
|
|
72
|
-
scale: (_d = stateChange.entityState) === null || _d === void 0 ? void 0 : _d.scale,
|
|
73
|
-
translateX: (_e = stateChange.entityState) === null || _e === void 0 ? void 0 : _e.translateX,
|
|
74
|
-
translateY: (_f = stateChange.entityState) === null || _f === void 0 ? void 0 : _f.translateY,
|
|
75
|
-
isDefault: (_g = stateChange.entityState) === null || _g === void 0 ? void 0 : _g.isDefault,
|
|
76
|
-
layers: (_j = (_h = stateChange.entityState) === null || _h === void 0 ? void 0 : _h.layers) !== null && _j !== void 0 ? _j : char.entityState.layers,
|
|
77
|
-
},
|
|
78
|
-
// baseBodyStateと位置情報を保持
|
|
79
|
-
baseBodyState: char.baseBodyState }) : char;
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
else if (processBlock.speakerId && processBlock.speakerStateId) {
|
|
84
|
-
// speakerId/speakerStateIdを使用した単一キャラクター状態変更(インラインエディタ形式)
|
|
85
|
-
characters = characters.map((char) => {
|
|
86
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
87
|
-
return char.objectId === processBlock.speakerId
|
|
88
|
-
? Object.assign(Object.assign({}, char), { entityStateId: processBlock.speakerStateId || "", entityState: {
|
|
89
|
-
id: (_a = processBlock.speakerStateId) !== null && _a !== void 0 ? _a : "",
|
|
90
|
-
name: ((_b = processBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name) || "",
|
|
91
|
-
imageUrl: ((_c = processBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl) || null,
|
|
92
|
-
cropArea: ((_d = processBlock.speakerState) === null || _d === void 0 ? void 0 : _d.cropArea) || null,
|
|
93
|
-
scale: (_e = processBlock.speakerState) === null || _e === void 0 ? void 0 : _e.scale,
|
|
94
|
-
translateX: (_f = processBlock.speakerState) === null || _f === void 0 ? void 0 : _f.translateX,
|
|
95
|
-
translateY: (_g = processBlock.speakerState) === null || _g === void 0 ? void 0 : _g.translateY,
|
|
96
|
-
isDefault: (_h = processBlock.speakerState) === null || _h === void 0 ? void 0 : _h.isDefault,
|
|
97
|
-
layers: (_k = (_j = processBlock.speakerState) === null || _j === void 0 ? void 0 : _j.layers) !== null && _k !== void 0 ? _k : char.entityState.layers,
|
|
98
|
-
},
|
|
99
|
-
// baseBodyStateと位置情報を保持
|
|
100
|
-
baseBodyState: char.baseBodyState }) : char;
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
break;
|
|
41
|
+
const isAdditive = ((_a = block.options) === null || _a === void 0 ? void 0 : _a.additive) === true;
|
|
42
|
+
if (!isAdditive) {
|
|
43
|
+
startIndex = i;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// startIndexから現在位置まで前方処理
|
|
49
|
+
for (let j = startIndex; j <= currentIndex; j++) {
|
|
50
|
+
const processBlock = blocks[j];
|
|
51
|
+
switch (processBlock.blockType) {
|
|
52
|
+
case "character_entrance": {
|
|
53
|
+
const isAdditive = ((_b = processBlock.options) === null || _b === void 0 ? void 0 : _b.additive) === true;
|
|
54
|
+
const newCharacters = mapEntranceCharacters(processBlock);
|
|
55
|
+
if (newCharacters.length === 0)
|
|
56
|
+
break;
|
|
57
|
+
if (isAdditive) {
|
|
58
|
+
// 追加モード: 既存キャラに追加(同一objectIdは上書き)
|
|
59
|
+
characters = [
|
|
60
|
+
...characters.filter((existing) => !newCharacters.some((nc) => nc.objectId === existing.objectId)),
|
|
61
|
+
...newCharacters,
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// 全差し替え(従来動作)
|
|
66
|
+
characters = newCharacters;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "character_exit": {
|
|
71
|
+
// 指定されたキャラクターを表示リストから削除
|
|
72
|
+
if (processBlock.characters && processBlock.characters.length > 0) {
|
|
73
|
+
const exitIds = new Set(processBlock.characters.map((c) => c.objectId));
|
|
74
|
+
// 退場前のスロット総数を保存(残りキャラの位置を維持するため)
|
|
75
|
+
const prevSlotCount = (_d = (_c = characters[0]) === null || _c === void 0 ? void 0 : _c.layoutSlotCount) !== null && _d !== void 0 ? _d : characters.length;
|
|
76
|
+
// 各キャラにスロット情報を付与してからフィルタ
|
|
77
|
+
characters = characters
|
|
78
|
+
.map((c, idx) => {
|
|
79
|
+
var _a;
|
|
80
|
+
return (Object.assign(Object.assign({}, c), { layoutSlotCount: prevSlotCount, layoutSlotIndex: (_a = c.layoutSlotIndex) !== null && _a !== void 0 ? _a : idx }));
|
|
81
|
+
})
|
|
82
|
+
.filter((c) => !exitIds.has(c.objectId));
|
|
104
83
|
}
|
|
84
|
+
break;
|
|
105
85
|
}
|
|
106
|
-
|
|
86
|
+
case "dialogue":
|
|
87
|
+
if (processBlock.speakerId && processBlock.speakerStateId) {
|
|
88
|
+
characters = characters.map((char) => {
|
|
89
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
90
|
+
return char.objectId === processBlock.speakerId
|
|
91
|
+
? Object.assign(Object.assign({}, char), { entityStateId: processBlock.speakerStateId || "", entityState: {
|
|
92
|
+
id: (_a = processBlock.speakerStateId) !== null && _a !== void 0 ? _a : "",
|
|
93
|
+
name: ((_b = processBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name) || "",
|
|
94
|
+
imageUrl: ((_c = processBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl) || null,
|
|
95
|
+
cropArea: ((_d = processBlock.speakerState) === null || _d === void 0 ? void 0 : _d.cropArea) || null,
|
|
96
|
+
scale: (_e = processBlock.speakerState) === null || _e === void 0 ? void 0 : _e.scale,
|
|
97
|
+
translateX: (_f = processBlock.speakerState) === null || _f === void 0 ? void 0 : _f.translateX,
|
|
98
|
+
translateY: (_g = processBlock.speakerState) === null || _g === void 0 ? void 0 : _g.translateY,
|
|
99
|
+
isDefault: (_h = processBlock.speakerState) === null || _h === void 0 ? void 0 : _h.isDefault,
|
|
100
|
+
layers: (_k = (_j = processBlock.speakerState) === null || _j === void 0 ? void 0 : _j.layers) !== null && _k !== void 0 ? _k : char.entityState.layers,
|
|
101
|
+
}, baseBodyState: char.baseBodyState }) : char;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case "character_state_change":
|
|
106
|
+
if (processBlock.characters && processBlock.characters.length > 0) {
|
|
107
|
+
for (const stateChange of processBlock.characters) {
|
|
108
|
+
characters = characters.map((char) => {
|
|
109
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
110
|
+
return char.objectId === stateChange.objectId
|
|
111
|
+
? Object.assign(Object.assign({}, char), { entityStateId: stateChange.entityStateId, entityState: {
|
|
112
|
+
id: stateChange.entityStateId,
|
|
113
|
+
name: ((_a = stateChange.entityState) === null || _a === void 0 ? void 0 : _a.name) || "",
|
|
114
|
+
imageUrl: ((_b = stateChange.entityState) === null || _b === void 0 ? void 0 : _b.imageUrl) || null,
|
|
115
|
+
cropArea: ((_c = stateChange.entityState) === null || _c === void 0 ? void 0 : _c.cropArea) || null,
|
|
116
|
+
scale: (_d = stateChange.entityState) === null || _d === void 0 ? void 0 : _d.scale,
|
|
117
|
+
translateX: (_e = stateChange.entityState) === null || _e === void 0 ? void 0 : _e.translateX,
|
|
118
|
+
translateY: (_f = stateChange.entityState) === null || _f === void 0 ? void 0 : _f.translateY,
|
|
119
|
+
isDefault: (_g = stateChange.entityState) === null || _g === void 0 ? void 0 : _g.isDefault,
|
|
120
|
+
layers: (_j = (_h = stateChange.entityState) === null || _h === void 0 ? void 0 : _h.layers) !== null && _j !== void 0 ? _j : char.entityState.layers,
|
|
121
|
+
}, baseBodyState: char.baseBodyState }) : char;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (processBlock.speakerId && processBlock.speakerStateId) {
|
|
126
|
+
characters = characters.map((char) => {
|
|
127
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
128
|
+
return char.objectId === processBlock.speakerId
|
|
129
|
+
? Object.assign(Object.assign({}, char), { entityStateId: processBlock.speakerStateId || "", entityState: {
|
|
130
|
+
id: (_a = processBlock.speakerStateId) !== null && _a !== void 0 ? _a : "",
|
|
131
|
+
name: ((_b = processBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name) || "",
|
|
132
|
+
imageUrl: ((_c = processBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl) || null,
|
|
133
|
+
cropArea: ((_d = processBlock.speakerState) === null || _d === void 0 ? void 0 : _d.cropArea) || null,
|
|
134
|
+
scale: (_e = processBlock.speakerState) === null || _e === void 0 ? void 0 : _e.scale,
|
|
135
|
+
translateX: (_f = processBlock.speakerState) === null || _f === void 0 ? void 0 : _f.translateX,
|
|
136
|
+
translateY: (_g = processBlock.speakerState) === null || _g === void 0 ? void 0 : _g.translateY,
|
|
137
|
+
isDefault: (_h = processBlock.speakerState) === null || _h === void 0 ? void 0 : _h.isDefault,
|
|
138
|
+
layers: (_k = (_j = processBlock.speakerState) === null || _j === void 0 ? void 0 : _j.layers) !== null && _k !== void 0 ? _k : char.entityState.layers,
|
|
139
|
+
}, baseBodyState: char.baseBodyState }) : char;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
107
143
|
}
|
|
108
144
|
}
|
|
109
145
|
return characters;
|
|
110
146
|
};
|
|
111
|
-
export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, branchState, branchNavigator: _branchNavigator, customRestart, variableManager, disableKeyboardNavigation = false, }) => {
|
|
147
|
+
export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, branchState, branchNavigator: _branchNavigator, customRestart, variableManager, disableKeyboardNavigation = false, onStopAllSounds, }) => {
|
|
112
148
|
const isLastBlock = state.currentBlockIndex === scenario.blocks.length - 1;
|
|
113
149
|
// 現在のインデックスに基づいて表示すべきキャラクターを計算
|
|
114
150
|
const displayedCharacters = useMemo(() => calculateDisplayedCharacters(scenario.blocks, state.currentBlockIndex), [scenario.blocks, state.currentBlockIndex]);
|
|
@@ -132,6 +168,7 @@ export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlo
|
|
|
132
168
|
}
|
|
133
169
|
if (isLastBlock) {
|
|
134
170
|
setState((prev) => (Object.assign(Object.assign({}, prev), { isEnded: true, isPlaying: false })));
|
|
171
|
+
onStopAllSounds === null || onStopAllSounds === void 0 ? void 0 : onStopAllSounds();
|
|
135
172
|
onEnd === null || onEnd === void 0 ? void 0 : onEnd();
|
|
136
173
|
onScenarioEnd === null || onScenarioEnd === void 0 ? void 0 : onScenarioEnd();
|
|
137
174
|
if (typeof window !== "undefined") {
|
|
@@ -161,6 +198,7 @@ export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlo
|
|
|
161
198
|
isLastBlock,
|
|
162
199
|
onEnd,
|
|
163
200
|
onScenarioEnd,
|
|
201
|
+
onStopAllSounds,
|
|
164
202
|
isTyping,
|
|
165
203
|
currentBlock,
|
|
166
204
|
skipTyping,
|
|
@@ -107,7 +107,8 @@ export const useSoundPlayer = ({ soundBlocks, isFirstRenderComplete, muteAudio =
|
|
|
107
107
|
stopBGM();
|
|
108
108
|
}
|
|
109
109
|
const audio = new Audio(sound.url);
|
|
110
|
-
|
|
110
|
+
// BGMは常にループ再生
|
|
111
|
+
audio.loop = isBGMSound ? true : sound.loop;
|
|
111
112
|
applyVolume(audio, block);
|
|
112
113
|
const category = block.sound.soundType.category;
|
|
113
114
|
// エラーハンドリング(イベントリスナーを保持してクリーンアップ可能にする)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* プラグインAPIからReactインスタンスを設定
|
|
3
|
+
*/
|
|
4
|
+
export declare function setReactRuntime(react: any): void;
|
|
5
|
+
/**
|
|
6
|
+
* JSX Transform用のjsx関数
|
|
7
|
+
*/
|
|
8
|
+
export declare function jsx(type: any, props: any, key?: any): any;
|
|
9
|
+
/**
|
|
10
|
+
* JSX Transform用のjsxs関数(複数子要素用)
|
|
11
|
+
*/
|
|
12
|
+
export declare function jsxs(type: any, props: any, key?: any): any;
|
|
13
|
+
/**
|
|
14
|
+
* Fragment用
|
|
15
|
+
*/
|
|
16
|
+
export declare function Fragment(props: {
|
|
17
|
+
children?: any;
|
|
18
|
+
}): any;
|
|
19
|
+
/**
|
|
20
|
+
* Reactフックと関数のプロキシ
|
|
21
|
+
*/
|
|
22
|
+
export declare const useState: (...args: any[]) => any;
|
|
23
|
+
export declare const useEffect: (...args: any[]) => any;
|
|
24
|
+
export declare const useCallback: (...args: any[]) => any;
|
|
25
|
+
export declare const useMemo: (...args: any[]) => any;
|
|
26
|
+
export declare const useRef: (...args: any[]) => any;
|
|
27
|
+
export declare const useContext: (...args: any[]) => any;
|
|
28
|
+
export declare const useReducer: (...args: any[]) => any;
|
|
29
|
+
export declare const createElement: (...args: any[]) => any;
|
|
30
|
+
declare const _default: {
|
|
31
|
+
createElement: (...args: any[]) => any;
|
|
32
|
+
Fragment: typeof Fragment;
|
|
33
|
+
useState: (...args: any[]) => any;
|
|
34
|
+
useEffect: (...args: any[]) => any;
|
|
35
|
+
useCallback: (...args: any[]) => any;
|
|
36
|
+
useMemo: (...args: any[]) => any;
|
|
37
|
+
useRef: (...args: any[]) => any;
|
|
38
|
+
useContext: (...args: any[]) => any;
|
|
39
|
+
useReducer: (...args: any[]) => any;
|
|
40
|
+
};
|
|
41
|
+
export default _default;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable import/no-anonymous-default-export */
|
|
3
|
+
let runtimeReact = null;
|
|
4
|
+
/**
|
|
5
|
+
* プラグインAPIからReactインスタンスを設定
|
|
6
|
+
*/
|
|
7
|
+
export function setReactRuntime(react) {
|
|
8
|
+
runtimeReact = react;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* JSX Transform用のjsx関数
|
|
12
|
+
*/
|
|
13
|
+
export function jsx(type, props, key) {
|
|
14
|
+
if (!runtimeReact) {
|
|
15
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
16
|
+
}
|
|
17
|
+
return runtimeReact.createElement(type, key ? Object.assign(Object.assign({}, props), { key }) : props);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* JSX Transform用のjsxs関数(複数子要素用)
|
|
21
|
+
*/
|
|
22
|
+
export function jsxs(type, props, key) {
|
|
23
|
+
if (!runtimeReact) {
|
|
24
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
25
|
+
}
|
|
26
|
+
return runtimeReact.createElement(type, key ? Object.assign(Object.assign({}, props), { key }) : props);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fragment用
|
|
30
|
+
*/
|
|
31
|
+
export function Fragment(props) {
|
|
32
|
+
if (!runtimeReact) {
|
|
33
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
34
|
+
}
|
|
35
|
+
return runtimeReact.createElement(runtimeReact.Fragment, null, props.children);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Reactフックと関数のプロキシ
|
|
39
|
+
*/
|
|
40
|
+
export const useState = (...args) => {
|
|
41
|
+
if (!runtimeReact) {
|
|
42
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
43
|
+
}
|
|
44
|
+
return runtimeReact.useState(...args);
|
|
45
|
+
};
|
|
46
|
+
export const useEffect = (...args) => {
|
|
47
|
+
if (!runtimeReact) {
|
|
48
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
49
|
+
}
|
|
50
|
+
return runtimeReact.useEffect(...args);
|
|
51
|
+
};
|
|
52
|
+
export const useCallback = (...args) => {
|
|
53
|
+
if (!runtimeReact) {
|
|
54
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
55
|
+
}
|
|
56
|
+
return runtimeReact.useCallback(...args);
|
|
57
|
+
};
|
|
58
|
+
export const useMemo = (...args) => {
|
|
59
|
+
if (!runtimeReact) {
|
|
60
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
61
|
+
}
|
|
62
|
+
return runtimeReact.useMemo(...args);
|
|
63
|
+
};
|
|
64
|
+
export const useRef = (...args) => {
|
|
65
|
+
if (!runtimeReact) {
|
|
66
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
67
|
+
}
|
|
68
|
+
return runtimeReact.useRef(...args);
|
|
69
|
+
};
|
|
70
|
+
export const useContext = (...args) => {
|
|
71
|
+
if (!runtimeReact) {
|
|
72
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
73
|
+
}
|
|
74
|
+
return runtimeReact.useContext(...args);
|
|
75
|
+
};
|
|
76
|
+
export const useReducer = (...args) => {
|
|
77
|
+
if (!runtimeReact) {
|
|
78
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
79
|
+
}
|
|
80
|
+
return runtimeReact.useReducer(...args);
|
|
81
|
+
};
|
|
82
|
+
export const createElement = (...args) => {
|
|
83
|
+
if (!runtimeReact) {
|
|
84
|
+
throw new Error("React runtime not initialized. Make sure plugin is loaded properly.");
|
|
85
|
+
}
|
|
86
|
+
return runtimeReact.createElement(...args);
|
|
87
|
+
};
|
|
88
|
+
// デフォルトエクスポート(互換性用)
|
|
89
|
+
export default {
|
|
90
|
+
createElement,
|
|
91
|
+
Fragment,
|
|
92
|
+
useState,
|
|
93
|
+
useEffect,
|
|
94
|
+
useCallback,
|
|
95
|
+
useMemo,
|
|
96
|
+
useRef,
|
|
97
|
+
useContext,
|
|
98
|
+
useReducer,
|
|
99
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type SoundCategory = "bgm" | "se" | "voice";
|
|
2
2
|
/** Block options stored as key-value pairs */
|
|
3
3
|
export type BlockOptions = Record<string, string | number | boolean>;
|
|
4
|
-
export type ScenarioBlockType = "dialogue" | "narration" | "action_node" | "character_entrance" | "character_state_change" | "bgm_play" | "se_play" | "bgm_stop" | "conversation_branch" | "background_change" | "background_group" | "variable_operation" | "fullscreen_text" | "click_wait" | "time_wait";
|
|
4
|
+
export type ScenarioBlockType = "dialogue" | "narration" | "action_node" | "character_entrance" | "character_exit" | "character_state_change" | "bgm_play" | "se_play" | "bgm_stop" | "conversation_branch" | "background_change" | "background_group" | "variable_operation" | "fullscreen_text" | "click_wait" | "time_wait";
|
|
5
5
|
export interface ScenarioBlock {
|
|
6
6
|
id: string;
|
|
7
7
|
scenarioId: string;
|
|
@@ -11,6 +11,11 @@ export interface ScenarioBlock {
|
|
|
11
11
|
speakerId: string | null;
|
|
12
12
|
speakerStateId: string | null;
|
|
13
13
|
partVoiceId: string | null;
|
|
14
|
+
partVoice?: {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
url: string;
|
|
18
|
+
} | null;
|
|
14
19
|
actionNodeId: string | null;
|
|
15
20
|
createdAt: Date;
|
|
16
21
|
updatedAt: Date;
|
|
@@ -205,6 +210,10 @@ export interface DisplayedCharacter {
|
|
|
205
210
|
entityStateId: string;
|
|
206
211
|
positionX?: number | null;
|
|
207
212
|
positionY?: number | null;
|
|
213
|
+
/** スロットベース配置: スロット総数 */
|
|
214
|
+
layoutSlotCount?: number;
|
|
215
|
+
/** スロットベース配置: このキャラのスロット位置(0始まり) */
|
|
216
|
+
layoutSlotIndex?: number;
|
|
208
217
|
zIndex?: number | null;
|
|
209
218
|
scale?: number | null;
|
|
210
219
|
cropLeft?: number | null;
|
|
@@ -374,6 +383,8 @@ export interface PlayerProps {
|
|
|
374
383
|
};
|
|
375
384
|
/** キーボードナビゲーションを無効化するかどうか(デフォルト: false) */
|
|
376
385
|
disableKeyboardNavigation?: boolean;
|
|
386
|
+
/** 終了画面(リスタートボタン等)を非表示にするかどうか(デフォルト: false) */
|
|
387
|
+
hideEndScreen?: boolean;
|
|
377
388
|
}
|
|
378
389
|
export type VariableValue = string | number | boolean;
|
|
379
390
|
export interface PlayerState {
|
|
@@ -426,6 +437,11 @@ export interface ConversationBranchBlock {
|
|
|
426
437
|
speakerStateId: string | null;
|
|
427
438
|
actionNodeId: string | null;
|
|
428
439
|
partVoiceId: string | null;
|
|
440
|
+
partVoice?: {
|
|
441
|
+
id: string;
|
|
442
|
+
name: string;
|
|
443
|
+
url: string;
|
|
444
|
+
} | null;
|
|
429
445
|
order: number;
|
|
430
446
|
createdAt: Date;
|
|
431
447
|
updatedAt: Date;
|
|
@@ -8,6 +8,7 @@ export function convertBranchBlockToScenarioBlock(branchBlock) {
|
|
|
8
8
|
speakerId: branchBlock.speakerId,
|
|
9
9
|
speakerStateId: branchBlock.speakerStateId,
|
|
10
10
|
partVoiceId: branchBlock.partVoiceId,
|
|
11
|
+
partVoice: branchBlock.partVoice,
|
|
11
12
|
actionNodeId: branchBlock.actionNodeId,
|
|
12
13
|
createdAt: branchBlock.createdAt,
|
|
13
14
|
updatedAt: branchBlock.updatedAt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luna-editor/engine",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"description": "Luna Editor scenario playback engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -52,4 +52,4 @@
|
|
|
52
52
|
"game-engine"
|
|
53
53
|
],
|
|
54
54
|
"license": "MIT"
|
|
55
|
-
}
|
|
55
|
+
}
|