@luna-editor/engine 0.4.5 → 0.5.0
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 +35 -113
- package/dist/components/BackgroundLayer.js +5 -7
- package/dist/components/GameScreen.d.ts +2 -8
- package/dist/components/GameScreen.js +7 -41
- package/dist/components/OverlayUI.d.ts +3 -2
- package/dist/components/OverlayUI.js +14 -3
- package/dist/components/PluginComponentProvider.js +1 -1
- package/dist/contexts/DataContext.d.ts +2 -0
- package/dist/contexts/DataContext.js +21 -26
- package/dist/data-api-types.d.ts +8 -6
- package/dist/hooks/useFontLoader.d.ts +2 -9
- package/dist/hooks/useFontLoader.js +87 -126
- package/dist/hooks/usePluginAPI.js +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/plugin/PluginManager.d.ts +1 -4
- package/dist/plugin/PluginManager.js +22 -51
- package/dist/sdk.d.ts +1 -1
- package/dist/types.d.ts +2 -17
- package/package.json +1 -1
- package/dist/contexts/PlaybackTextContext.d.ts +0 -32
- package/dist/contexts/PlaybackTextContext.js +0 -29
- package/dist/hooks/useImagePreloader.d.ts +0 -5
- package/dist/hooks/useImagePreloader.js +0 -53
- package/dist/plugin/luna-react.d.ts +0 -41
- package/dist/plugin/luna-react.js +0 -99
package/dist/Player.js
CHANGED
|
@@ -24,7 +24,6 @@ import { PluginComponentProvider } from "./components/PluginComponentProvider";
|
|
|
24
24
|
import { TimeWaitIndicator } from "./components/TimeWaitIndicator";
|
|
25
25
|
import { AudioProvider } from "./contexts/AudioContext";
|
|
26
26
|
import { DataProvider } from "./contexts/DataContext";
|
|
27
|
-
import { PlaybackTextProvider } from "./contexts/PlaybackTextContext";
|
|
28
27
|
import { useBacklog } from "./hooks/useBacklog";
|
|
29
28
|
import { useConversationBranch } from "./hooks/useConversationBranch";
|
|
30
29
|
import { useFontLoader } from "./hooks/useFontLoader";
|
|
@@ -39,17 +38,10 @@ import { ComponentType } from "./sdk";
|
|
|
39
38
|
import { convertBranchBlockToScenarioBlock } from "./utils/branchBlockConverter";
|
|
40
39
|
import { BranchNavigator } from "./utils/branchNavigator";
|
|
41
40
|
import { VariableManager } from "./utils/variableManager";
|
|
42
|
-
const
|
|
43
|
-
const EMPTY_SOUNDS = [];
|
|
44
|
-
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, }) => {
|
|
41
|
+
export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, onScenarioEnd, onScenarioStart, onScenarioCancelled, onSettingsChange, className, autoplay = false, preventDefaultScroll = true, screenSize: screenSizeProp, disableKeyboardNavigation = false, }) => {
|
|
45
42
|
var _a, _b, _c, _d;
|
|
46
|
-
// scenario.blocks が存在しない場合は空の配列を使用
|
|
47
|
-
const scenario = useMemo(() => {
|
|
48
|
-
var _a;
|
|
49
|
-
return (Object.assign(Object.assign({}, scenarioProp), { blocks: (_a = scenarioProp.blocks) !== null && _a !== void 0 ? _a : [] }));
|
|
50
|
-
}, [scenarioProp]);
|
|
51
43
|
// デフォルト値とマージ
|
|
52
|
-
const mergedSettings = Object.assign({ textSpeed: 80, autoPlaySpeed: 3, bgmVolume: 0.5, seVolume: 1.0, voiceVolume: 1.0, effectVolume: 1.0, textSoundVolume: 1.0, defaultBackgroundFadeDuration: 0
|
|
44
|
+
const mergedSettings = Object.assign({ textSpeed: 80, autoPlaySpeed: 3, bgmVolume: 0.5, seVolume: 1.0, voiceVolume: 1.0, effectVolume: 1.0, textSoundVolume: 1.0, defaultBackgroundFadeDuration: 0 }, settings);
|
|
53
45
|
// プラグインからの設定更新ハンドラ
|
|
54
46
|
const handleSettingsUpdate = useCallback((updatedSettings) => {
|
|
55
47
|
var _a, _b;
|
|
@@ -92,7 +84,6 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
92
84
|
}, [pluginManager, mergedSettings.muteAudio]);
|
|
93
85
|
// 画面サイズの初期化
|
|
94
86
|
const [, setScreenSize] = useScreenSizeAtom();
|
|
95
|
-
const aspectRatioContainerRef = useRef(null);
|
|
96
87
|
useEffect(() => {
|
|
97
88
|
// screenSizeが明示的に指定されている場合はそれを使用(プレビュー用)
|
|
98
89
|
if (screenSizeProp) {
|
|
@@ -102,40 +93,16 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
102
93
|
// クライアントサイドでのみ実行
|
|
103
94
|
if (typeof window === "undefined")
|
|
104
95
|
return;
|
|
105
|
-
// 初期値として一時的にウィンドウサイズを設定
|
|
106
96
|
setScreenSize({ width: window.innerWidth, height: window.innerHeight });
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return;
|
|
110
|
-
// アスペクト比コンテナのサイズを監視
|
|
111
|
-
const updateScreenSize = () => {
|
|
112
|
-
const rect = container.getBoundingClientRect();
|
|
113
|
-
// サイズが0の場合はスキップ(まだレンダリングされていない)
|
|
114
|
-
if (rect.width === 0 || rect.height === 0)
|
|
115
|
-
return;
|
|
116
|
-
console.log("[Player] screenSize更新:", {
|
|
117
|
-
width: rect.width,
|
|
118
|
-
height: rect.height,
|
|
119
|
-
windowWidth: window.innerWidth,
|
|
120
|
-
windowHeight: window.innerHeight,
|
|
121
|
-
});
|
|
97
|
+
// リサイズ監視
|
|
98
|
+
const handleResize = () => {
|
|
122
99
|
setScreenSize({
|
|
123
|
-
width:
|
|
124
|
-
height:
|
|
100
|
+
width: window.innerWidth,
|
|
101
|
+
height: window.innerHeight,
|
|
125
102
|
});
|
|
126
103
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
updateScreenSize();
|
|
130
|
-
});
|
|
131
|
-
// ResizeObserverでコンテナのサイズ変更を監視
|
|
132
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
133
|
-
updateScreenSize();
|
|
134
|
-
});
|
|
135
|
-
resizeObserver.observe(container);
|
|
136
|
-
return () => {
|
|
137
|
-
resizeObserver.disconnect();
|
|
138
|
-
};
|
|
104
|
+
window.addEventListener("resize", handleResize);
|
|
105
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
139
106
|
}, [setScreenSize, screenSizeProp]);
|
|
140
107
|
// 表示可能なブロックのインデックスを事前計算
|
|
141
108
|
const displayableBlockIndices = useMemo(() => {
|
|
@@ -146,6 +113,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
146
113
|
"fullscreen_text",
|
|
147
114
|
"click_wait",
|
|
148
115
|
"time_wait",
|
|
116
|
+
"action_node",
|
|
149
117
|
];
|
|
150
118
|
const indices = scenario.blocks
|
|
151
119
|
.map((block, index) => ({ block, index }))
|
|
@@ -301,28 +269,8 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
301
269
|
}, [scenario.id, autoplay, hasStarted, pluginManager]);
|
|
302
270
|
// 画像を事前読み込み
|
|
303
271
|
const imagesLoaded = usePreloadImages(scenario);
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
// 文字送り中のフォント切り替わりを防止
|
|
307
|
-
const allScenarioText = useMemo(() => {
|
|
308
|
-
var _a;
|
|
309
|
-
const texts = [];
|
|
310
|
-
for (const block of scenario.blocks) {
|
|
311
|
-
// dialogue, narration, fullscreen_textのcontent
|
|
312
|
-
if (block.content) {
|
|
313
|
-
texts.push(block.content);
|
|
314
|
-
}
|
|
315
|
-
// speaker name
|
|
316
|
-
if ((_a = block.speaker) === null || _a === void 0 ? void 0 : _a.name) {
|
|
317
|
-
texts.push(block.speaker.name);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
// 重複を除去してユニークな文字のみを残す
|
|
321
|
-
const uniqueChars = [...new Set(texts.join(""))].join("");
|
|
322
|
-
return uniqueChars;
|
|
323
|
-
}, [scenario.blocks]);
|
|
324
|
-
// フォントを読み込み(シナリオ内の全テキストでプリロード)
|
|
325
|
-
const { isLoaded: fontsLoaded } = useFontLoader(scenario.fonts, allScenarioText);
|
|
272
|
+
// フォントを読み込み
|
|
273
|
+
const { isLoaded: fontsLoaded } = useFontLoader(scenario.fonts);
|
|
326
274
|
// プラグインの読み込み状態
|
|
327
275
|
const [pluginsLoaded, setPluginsLoaded] = useState(false);
|
|
328
276
|
// 読み込み済みプラグインのパッケージ名を追跡
|
|
@@ -344,7 +292,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
344
292
|
for (const plugin of newPlugins) {
|
|
345
293
|
if (isCancelled)
|
|
346
294
|
return;
|
|
347
|
-
yield pluginManager.loadPlugin(plugin.packageName, plugin.bundleUrl, plugin.config
|
|
295
|
+
yield pluginManager.loadPlugin(plugin.packageName, plugin.bundleUrl, plugin.config);
|
|
348
296
|
loadedPluginNamesRef.current.add(plugin.packageName);
|
|
349
297
|
}
|
|
350
298
|
if (isCancelled)
|
|
@@ -361,9 +309,6 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
361
309
|
loadPlugins();
|
|
362
310
|
return () => {
|
|
363
311
|
isCancelled = true;
|
|
364
|
-
// Strict Mode再マウント時にPluginManagerインスタンスが変わる場合に備えて
|
|
365
|
-
// ロード済みリストをクリアし、新しいインスタンスで再ロードされるようにする
|
|
366
|
-
loadedPluginNamesRef.current.clear();
|
|
367
312
|
};
|
|
368
313
|
}, [plugins, sounds]);
|
|
369
314
|
// 初回レンダリング完了フラグ
|
|
@@ -604,16 +549,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
604
549
|
scenarioName: scenario.name || "scenario",
|
|
605
550
|
});
|
|
606
551
|
}
|
|
607
|
-
}, [
|
|
608
|
-
isFirstRenderComplete,
|
|
609
|
-
hasStarted,
|
|
610
|
-
currentBlock,
|
|
611
|
-
pluginsLoaded,
|
|
612
|
-
onScenarioStart,
|
|
613
|
-
pluginManager,
|
|
614
|
-
scenario.id,
|
|
615
|
-
scenario.name,
|
|
616
|
-
]);
|
|
552
|
+
}, [isFirstRenderComplete, hasStarted, currentBlock, pluginsLoaded, onScenarioStart, pluginManager, scenario.id, scenario.name]);
|
|
617
553
|
// restartをusePlayerLogicの前に定義(分岐状態もリセット + キャンセルコールバック)
|
|
618
554
|
const restart = useCallback(() => {
|
|
619
555
|
// リスタート時はシナリオがキャンセルされたとみなす
|
|
@@ -645,6 +581,16 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
645
581
|
scenario.id,
|
|
646
582
|
scenario.name,
|
|
647
583
|
]);
|
|
584
|
+
// プラグインからシナリオを中断するためのコールバック
|
|
585
|
+
const cancelScenario = useCallback(() => {
|
|
586
|
+
if (hasStarted && !state.isEnded) {
|
|
587
|
+
onScenarioCancelled === null || onScenarioCancelled === void 0 ? void 0 : onScenarioCancelled();
|
|
588
|
+
pluginManager.callHook("onScenarioEnd", {
|
|
589
|
+
scenarioId: scenario.id,
|
|
590
|
+
scenarioName: scenario.name || "scenario",
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}, [hasStarted, state.isEnded, onScenarioCancelled, pluginManager, scenario.id, scenario.name]);
|
|
648
594
|
const { handleNext: handleNextInternal, handlePrevious: handlePreviousInternal, togglePlay: _togglePlay, restart: _restartInternal, displayedCharacters, } = usePlayerLogic({
|
|
649
595
|
state: Object.assign(Object.assign({}, state), { currentBlockIndex: actualBlockIndex }),
|
|
650
596
|
setState: (newState) => {
|
|
@@ -766,10 +712,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
766
712
|
displayableBlockIndices.length,
|
|
767
713
|
handleNextInternal,
|
|
768
714
|
onEnd,
|
|
769
|
-
onScenarioEnd,
|
|
770
|
-
pluginManager,
|
|
771
|
-
scenario.id,
|
|
772
|
-
scenario.name,
|
|
715
|
+
onScenarioEnd,
|
|
773
716
|
]);
|
|
774
717
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
775
718
|
const _handlePrevious = useCallback(() => {
|
|
@@ -779,16 +722,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
779
722
|
}, [state.currentBlockIndex, handlePreviousInternal]);
|
|
780
723
|
// 現在の背景を計算
|
|
781
724
|
const currentBackground = useMemo(() => calculateCurrentBackground(actualBlockIndex), [calculateCurrentBackground, actualBlockIndex]);
|
|
782
|
-
//
|
|
783
|
-
const displayTextElement = useMemo(() => {
|
|
784
|
-
if (displayText.includes("\n")) {
|
|
785
|
-
return displayText.split("\n").map((line, index, array) => (_jsxs(React.Fragment, { children: [line, index < array.length - 1 && _jsx("br", {})] }, index)));
|
|
786
|
-
}
|
|
787
|
-
return displayText;
|
|
788
|
-
}, [displayText]);
|
|
789
|
-
// DataContext の構築
|
|
790
|
-
// DataRefContext で安定した参照を提供することで、
|
|
791
|
-
// useDataAPI() を使うコンポーネントの再レンダリングを防ぐ
|
|
725
|
+
// DataContext の構築 - displayedCharactersが必要なため usePlayerLogic の後に配置
|
|
792
726
|
const dataContext = useMemo(() => {
|
|
793
727
|
var _a, _b, _c;
|
|
794
728
|
return ({
|
|
@@ -798,7 +732,9 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
798
732
|
scenarioId: scenario.id,
|
|
799
733
|
scenarioName: scenario.name,
|
|
800
734
|
currentBlock: currentBlock || null,
|
|
801
|
-
displayText:
|
|
735
|
+
displayText: displayText.includes("\n")
|
|
736
|
+
? displayText.split("\n").map((line, index, array) => (_jsxs(React.Fragment, { children: [line, index < array.length - 1 && _jsx("br", {})] }, index)))
|
|
737
|
+
: displayText,
|
|
802
738
|
isTyping,
|
|
803
739
|
displayedCharacters,
|
|
804
740
|
},
|
|
@@ -817,8 +753,6 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
817
753
|
seVolume: mergedSettings.seVolume,
|
|
818
754
|
voiceVolume: mergedSettings.voiceVolume,
|
|
819
755
|
skipMode: "unread",
|
|
820
|
-
selectedFontFamily: mergedSettings.selectedFontFamily,
|
|
821
|
-
selectedUIFontFamily: mergedSettings.selectedUIFontFamily,
|
|
822
756
|
},
|
|
823
757
|
pluginAssets: {
|
|
824
758
|
getAssetUrl: (pluginName, filename) => {
|
|
@@ -832,7 +766,6 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
832
766
|
fonts: {
|
|
833
767
|
fonts: (_c = scenario.fonts) !== null && _c !== void 0 ? _c : [],
|
|
834
768
|
selectedFontFamily: mergedSettings.selectedFontFamily,
|
|
835
|
-
selectedUIFontFamily: mergedSettings.selectedUIFontFamily,
|
|
836
769
|
isLoaded: fontsLoaded,
|
|
837
770
|
},
|
|
838
771
|
emotionEffect: emotionEffectState,
|
|
@@ -842,7 +775,7 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
842
775
|
actualBlockIndex,
|
|
843
776
|
scenario,
|
|
844
777
|
currentBlock,
|
|
845
|
-
|
|
778
|
+
displayText,
|
|
846
779
|
isTyping,
|
|
847
780
|
displayedCharacters,
|
|
848
781
|
backlog,
|
|
@@ -856,7 +789,6 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
856
789
|
mergedSettings.textSpeed,
|
|
857
790
|
mergedSettings.voiceVolume,
|
|
858
791
|
mergedSettings.selectedFontFamily,
|
|
859
|
-
mergedSettings.selectedUIFontFamily,
|
|
860
792
|
currentBackground,
|
|
861
793
|
fontsLoaded,
|
|
862
794
|
]);
|
|
@@ -896,12 +828,6 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
896
828
|
const [width, height] = settings.aspectRatio.split(":").map(Number);
|
|
897
829
|
return `${width}/${height}`;
|
|
898
830
|
};
|
|
899
|
-
const getAspectRatioValue = () => {
|
|
900
|
-
if (!(settings === null || settings === void 0 ? void 0 : settings.aspectRatio))
|
|
901
|
-
return 16 / 9;
|
|
902
|
-
const [width, height] = settings.aspectRatio.split(":").map(Number);
|
|
903
|
-
return width / height;
|
|
904
|
-
};
|
|
905
831
|
// 条件付きレンダリングを JSX で処理(フックの後、early return なし)
|
|
906
832
|
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 && (_jsx(EndScreen, { scenarioName: scenario.name, onRestart: restart, className: className })), currentBlock && !state.isEnded && (_jsxs(_Fragment, { children: [(!imagesLoaded ||
|
|
907
833
|
!fontsLoaded ||
|
|
@@ -918,20 +844,16 @@ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGI
|
|
|
918
844
|
touchAction: "none",
|
|
919
845
|
userSelect: "none",
|
|
920
846
|
WebkitUserSelect: "none",
|
|
921
|
-
}, children: _jsx("div", {
|
|
922
|
-
aspectRatio: getAspectRatio(),
|
|
923
|
-
width: `min(100vw, calc(100vh * ${getAspectRatioValue()}))`,
|
|
924
|
-
height: `min(100vh, calc(100vw / ${getAspectRatioValue()}))`,
|
|
925
|
-
}, children: _jsx(DataProvider, { data: dataContext, onSettingsUpdate: handleSettingsUpdate, onEmotionEffectUpdate: handleEmotionEffectUpdate, children: _jsx(AudioProvider, { settings: {
|
|
847
|
+
}, children: _jsx("div", { className: "relative bg-white flex flex-col w-full overflow-hidden h-full", style: { aspectRatio: getAspectRatio() }, children: _jsx(DataProvider, { data: dataContext, onSettingsUpdate: handleSettingsUpdate, onEmotionEffectUpdate: handleEmotionEffectUpdate, onCancelScenario: cancelScenario, children: _jsxs(AudioProvider, { settings: {
|
|
926
848
|
bgmVolume: mergedSettings.bgmVolume,
|
|
927
849
|
seVolume: mergedSettings.seVolume,
|
|
928
850
|
voiceVolume: mergedSettings.voiceVolume,
|
|
929
851
|
effectVolume: mergedSettings.effectVolume,
|
|
930
852
|
textSoundVolume: mergedSettings.textSoundVolume,
|
|
931
853
|
muteAudio: (_b = mergedSettings.muteAudio) !== null && _b !== void 0 ? _b : false,
|
|
932
|
-
}, children:
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
854
|
+
}, 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 }, "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
|
|
855
|
+
.getRegisteredComponents()
|
|
856
|
+
.filter((type) => type !== ComponentType.DialogueBox &&
|
|
857
|
+
type !== ComponentType.ConversationBranch)
|
|
858
|
+
.map((componentType) => (_jsx(PluginComponentProvider, { type: componentType, pluginManager: pluginManager }, componentType)))] }) })] }) }) }) })] }))] }));
|
|
937
859
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useDataAPI } from "../contexts/DataContext";
|
|
4
4
|
/**
|
|
5
5
|
* iOS/macOSかどうかを判定
|
|
@@ -56,10 +56,8 @@ function getMediaType(url) {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* 背景メディアコンポーネント
|
|
59
|
-
* React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
|
|
60
|
-
* これにより displayText の更新による不要な再描画を防止
|
|
61
59
|
*/
|
|
62
|
-
const BackgroundMedia =
|
|
60
|
+
const BackgroundMedia = ({ background, opacity, zIndex, useAppleFormat }) => {
|
|
63
61
|
var _a, _b;
|
|
64
62
|
// OS判定に基づいて使用するURLを決定
|
|
65
63
|
// iOS/macOS: mp4 (imageUrl)
|
|
@@ -86,7 +84,7 @@ const BackgroundMedia = memo(function BackgroundMedia({ background, opacity, zIn
|
|
|
86
84
|
transition: "none", // requestAnimationFrameでアニメーション
|
|
87
85
|
pointerEvents: isHighLayer ? "none" : undefined,
|
|
88
86
|
}, "data-background-object-id": background.objectId, "data-background-state-id": background.stateId, "data-background-layer": layer, children: mediaType === "video" ? (_jsx("video", { src: videoUrl, className: `w-full h-full ${objectFitClass}`, autoPlay: true, muted: true, playsInline: true, loop: background.loop }, videoUrl)) : (_jsx("img", { src: background.imageUrl, alt: background.stateName, className: `w-full h-full ${objectFitClass}` }, background.imageUrl)) }));
|
|
89
|
-
}
|
|
87
|
+
};
|
|
90
88
|
/**
|
|
91
89
|
* 背景の配列が同じかどうかを比較
|
|
92
90
|
*/
|
|
@@ -109,7 +107,7 @@ function areBackgroundsEqual(a, b) {
|
|
|
109
107
|
* プラグインによって置き換え可能
|
|
110
108
|
* フェードイン・フェードアウト対応
|
|
111
109
|
*/
|
|
112
|
-
export const BackgroundLayer =
|
|
110
|
+
export const BackgroundLayer = ({ className, }) => {
|
|
113
111
|
const dataAPI = useDataAPI();
|
|
114
112
|
// Apple環境かどうかを判定(iOS/macOSではmp4を使用)
|
|
115
113
|
const useAppleFormat = useMemo(() => isApplePlatform(), []);
|
|
@@ -216,5 +214,5 @@ export const BackgroundLayer = memo(({ className }) => {
|
|
|
216
214
|
const zIndex = getLayerZIndex(layer, index);
|
|
217
215
|
return (_jsx(BackgroundMedia, { background: bg, opacity: fadeState.isFading ? fadeState.fadeProgress : 1, zIndex: zIndex, useAppleFormat: useAppleFormat }, `curr-${bg.objectId}-${bg.stateId}-${index}`));
|
|
218
216
|
})] }));
|
|
219
|
-
}
|
|
217
|
+
};
|
|
220
218
|
export { getMediaType };
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
+
import type React from "react";
|
|
1
2
|
import type { DisplayedCharacter, PublishedScenario, ScenarioBlock } from "../types";
|
|
2
3
|
interface GameScreenProps {
|
|
3
4
|
scenario: PublishedScenario;
|
|
4
5
|
currentBlock: ScenarioBlock;
|
|
5
6
|
previousBlock?: ScenarioBlock | null;
|
|
6
7
|
displayedCharacters: DisplayedCharacter[];
|
|
7
|
-
inactiveCharacterBrightness?: number;
|
|
8
|
-
characterSpacing?: number;
|
|
9
8
|
}
|
|
10
|
-
|
|
11
|
-
* ゲームスクリーンコンポーネント
|
|
12
|
-
* React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
|
|
13
|
-
* これにより displayText の更新による不要な再描画を防止
|
|
14
|
-
*/
|
|
15
|
-
export declare const GameScreen: import("react").NamedExoticComponent<GameScreenProps>;
|
|
9
|
+
export declare const GameScreen: React.FC<GameScreenProps>;
|
|
16
10
|
export {};
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
* ゲームスクリーンコンポーネント
|
|
5
|
-
* React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
|
|
6
|
-
* これにより displayText の更新による不要な再描画を防止
|
|
7
|
-
*/
|
|
8
|
-
export const GameScreen = memo(function GameScreen({ scenario, currentBlock, previousBlock, displayedCharacters, inactiveCharacterBrightness = 0.8, characterSpacing = 0.1, }) {
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
export const GameScreen = ({ scenario, currentBlock, previousBlock, displayedCharacters, }) => {
|
|
9
4
|
var _a;
|
|
10
5
|
// キャラクターごとのフェード状態を管理
|
|
11
6
|
const [fadeStates, setFadeStates] = useState(new Map());
|
|
@@ -227,28 +222,6 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
227
222
|
const currentSpeaker = (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.speakerId)
|
|
228
223
|
? displayedCharacters.find((char) => char.objectId === currentBlock.speakerId)
|
|
229
224
|
: null;
|
|
230
|
-
// 簡易モード用の自動配置計算
|
|
231
|
-
const calculateAutoLayout = (characters, spacing) => {
|
|
232
|
-
const total = characters.length;
|
|
233
|
-
const positions = new Map();
|
|
234
|
-
if (total === 0)
|
|
235
|
-
return positions;
|
|
236
|
-
if (total === 1) {
|
|
237
|
-
// 1人の場合は中央
|
|
238
|
-
positions.set(characters[0].objectId, { x: 0, y: 0 });
|
|
239
|
-
return positions;
|
|
240
|
-
}
|
|
241
|
-
// 複数の場合は均等配置
|
|
242
|
-
// spacing: 画面幅に対する割合(例: 0.1 = 10%)
|
|
243
|
-
// 座標系: -1(左端)〜 1(右端)
|
|
244
|
-
const totalWidth = (total - 1) * spacing * 2;
|
|
245
|
-
const startX = -totalWidth / 2;
|
|
246
|
-
characters.forEach((char, index) => {
|
|
247
|
-
const x = startX + index * spacing * 2;
|
|
248
|
-
positions.set(char.objectId, { x, y: 0 });
|
|
249
|
-
});
|
|
250
|
-
return positions;
|
|
251
|
-
};
|
|
252
225
|
// キャラクター描画用のヘルパー関数
|
|
253
226
|
const renderCharacter = (image, displayedChar, isCurrentSpeaker, keyPrefix) => {
|
|
254
227
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
@@ -284,7 +257,7 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
284
257
|
// 複数キャラクター表示で、現在の話者でない場合
|
|
285
258
|
if (currentSpeaker &&
|
|
286
259
|
currentSpeaker.objectId !== displayedChar.objectId) {
|
|
287
|
-
brightness =
|
|
260
|
+
brightness = 0.8;
|
|
288
261
|
}
|
|
289
262
|
}
|
|
290
263
|
// z-indexを決定
|
|
@@ -297,9 +270,9 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
297
270
|
: ((_d = image.scale) !== null && _d !== void 0 ? _d : 1);
|
|
298
271
|
// 位置を決定(カスタム位置またはデフォルト位置)
|
|
299
272
|
// 新座標系: x: -1=左見切れ, 0=中央, 1=右見切れ / y: -1=上見切れ, 0=中央, 1=下見切れ
|
|
300
|
-
let finalPosition = { x: 0, y: 0 };
|
|
273
|
+
let finalPosition = { x: 0, y: 1.0 }; // デフォルトは中央、下端
|
|
301
274
|
if (displayedChar) {
|
|
302
|
-
//
|
|
275
|
+
// カスタム位置が設定されている場合
|
|
303
276
|
if (displayedChar.positionX !== null &&
|
|
304
277
|
displayedChar.positionY !== null) {
|
|
305
278
|
finalPosition = {
|
|
@@ -307,14 +280,7 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
307
280
|
y: (_f = displayedChar.positionY) !== null && _f !== void 0 ? _f : 0,
|
|
308
281
|
};
|
|
309
282
|
}
|
|
310
|
-
|
|
311
|
-
// 簡易モード: 自動配置
|
|
312
|
-
const autoPositions = calculateAutoLayout(displayedCharacters, characterSpacing !== null && characterSpacing !== void 0 ? characterSpacing : 0.25);
|
|
313
|
-
const autoPos = autoPositions.get(displayedChar.objectId);
|
|
314
|
-
if (autoPos) {
|
|
315
|
-
finalPosition = autoPos;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
283
|
+
// カスタム位置が設定されていない場合はデフォルト位置のまま
|
|
318
284
|
}
|
|
319
285
|
// コンテナ相対の位置計算(パーセンテージ使用)
|
|
320
286
|
// positionX: -1.0 = 完全に左に見切れ, 0.0 = 中央, 1.0 = 完全に右に見切れ
|
|
@@ -424,4 +390,4 @@ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, pre
|
|
|
424
390
|
return null;
|
|
425
391
|
return renderCharacter(image, null, false, "fadeout");
|
|
426
392
|
})()] }))] }) }));
|
|
427
|
-
}
|
|
393
|
+
};
|
|
@@ -5,8 +5,9 @@ interface OverlayUIProps {
|
|
|
5
5
|
/**
|
|
6
6
|
* OverlayUI コンポーネント
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* 16:9のアスペクト比を維持しながら、様々な画面サイズに対応するコンテナ
|
|
9
|
+
* - スマートフォン(幅 < 1000px): フルサイズ表示
|
|
10
|
+
* - タブレット(幅 ≥ 1000px): 70%に縮小して中央配置
|
|
10
11
|
*/
|
|
11
12
|
export declare const OverlayUI: React.FC<OverlayUIProps>;
|
|
12
13
|
export {};
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useScreenSizeAtom } from "../atoms/screen-size";
|
|
2
3
|
/**
|
|
3
4
|
* OverlayUI コンポーネント
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* 16:9のアスペクト比を維持しながら、様々な画面サイズに対応するコンテナ
|
|
7
|
+
* - スマートフォン(幅 < 1000px): フルサイズ表示
|
|
8
|
+
* - タブレット(幅 ≥ 1000px): 70%に縮小して中央配置
|
|
7
9
|
*/
|
|
8
10
|
export const OverlayUI = ({ children }) => {
|
|
9
|
-
|
|
11
|
+
// 16:9のアスペクト比を基準
|
|
12
|
+
let [{ height: screenHeight }] = useScreenSizeAtom();
|
|
13
|
+
let screenWidth = (screenHeight * 16) / 9;
|
|
14
|
+
// タブレット(幅1000px以上)では70%に縮小
|
|
15
|
+
const [{ width: checkWidth }] = useScreenSizeAtom();
|
|
16
|
+
if (checkWidth >= 1000) {
|
|
17
|
+
screenHeight = (screenHeight * 7) / 10;
|
|
18
|
+
screenWidth = (screenHeight * 16) / 9;
|
|
19
|
+
}
|
|
20
|
+
return (_jsx("div", { className: "absolute inset-0 pointer-events-none z-[9999] flex justify-center items-center", children: _jsx("div", { "data-overlay-container": true, className: "relative", style: { width: screenWidth, height: screenHeight }, children: children }) }));
|
|
10
21
|
};
|
|
@@ -20,7 +20,7 @@ export function PluginComponentProvider({ type, pluginManager, fallback: Fallbac
|
|
|
20
20
|
return () => {
|
|
21
21
|
unsubscribe();
|
|
22
22
|
};
|
|
23
|
-
}, [dataAPI]);
|
|
23
|
+
}, [dataAPI, type]);
|
|
24
24
|
const Component = pluginManager.getComponent(type);
|
|
25
25
|
if (Component) {
|
|
26
26
|
// Pass DataAPI as prop to avoid context issues
|
|
@@ -2,6 +2,7 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
import type { DataAPI, DataContext, EmotionEffectState, PlayerSettingsData } from "../sdk";
|
|
3
3
|
type SettingsUpdater = (settings: Partial<PlayerSettingsData>) => void;
|
|
4
4
|
type EmotionEffectUpdater = (state: EmotionEffectState | null) => void;
|
|
5
|
+
type ScenarioCanceller = () => void;
|
|
5
6
|
/**
|
|
6
7
|
* データプロバイダー
|
|
7
8
|
* プラグインがシナリオ情報にリアクティブにアクセスするためのProvider
|
|
@@ -19,6 +20,7 @@ export declare const DataProvider: React.FC<{
|
|
|
19
20
|
data: DataContext;
|
|
20
21
|
onSettingsUpdate?: SettingsUpdater;
|
|
21
22
|
onEmotionEffectUpdate?: EmotionEffectUpdater;
|
|
23
|
+
onCancelScenario?: ScenarioCanceller;
|
|
22
24
|
children: ReactNode;
|
|
23
25
|
}>;
|
|
24
26
|
/**
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from "react";
|
|
3
3
|
const DataContextInstance = createContext(null);
|
|
4
|
-
// データへの参照を共有するContext(Context value自体は変わらない)
|
|
5
|
-
const DataRefContext = createContext(null);
|
|
6
4
|
const SubscribersContext = createContext(null);
|
|
7
5
|
const SettingsUpdaterContext = createContext(null);
|
|
8
6
|
const EmotionEffectUpdaterContext = createContext(null);
|
|
7
|
+
const ScenarioCancellerContext = createContext(null);
|
|
9
8
|
/**
|
|
10
9
|
* データプロバイダー
|
|
11
10
|
* プラグインがシナリオ情報にリアクティブにアクセスするためのProvider
|
|
@@ -19,15 +18,9 @@ const EmotionEffectUpdaterContext = createContext(null);
|
|
|
19
18
|
* </DataProvider>
|
|
20
19
|
* ```
|
|
21
20
|
*/
|
|
22
|
-
export const DataProvider = ({ data, onSettingsUpdate, onEmotionEffectUpdate, children }) => {
|
|
21
|
+
export const DataProvider = ({ data, onSettingsUpdate, onEmotionEffectUpdate, onCancelScenario, children }) => {
|
|
23
22
|
const subscribers = useMemo(() => new Map(), []);
|
|
24
23
|
const previousDataRef = useRef(data);
|
|
25
|
-
// 安定したデータ参照ホルダー(Context valueとして提供)
|
|
26
|
-
// オブジェクト自体の参照は変わらず、currentプロパティを更新
|
|
27
|
-
// これにより、useContextを使うコンポーネントは再レンダリングされない
|
|
28
|
-
const dataRefHolder = useMemo(() => ({ current: data }), [data]);
|
|
29
|
-
// dataが変わるたびにcurrentを更新
|
|
30
|
-
dataRefHolder.current = data;
|
|
31
24
|
// データ変更時に購読者に通知
|
|
32
25
|
useEffect(() => {
|
|
33
26
|
const prevData = previousDataRef.current;
|
|
@@ -61,38 +54,31 @@ export const DataProvider = ({ data, onSettingsUpdate, onEmotionEffectUpdate, ch
|
|
|
61
54
|
}
|
|
62
55
|
previousDataRef.current = data;
|
|
63
56
|
}, [data, subscribers]);
|
|
64
|
-
return (_jsx(SubscribersContext.Provider, { value: subscribers, children: _jsx(SettingsUpdaterContext.Provider, { value: onSettingsUpdate !== null && onSettingsUpdate !== void 0 ? onSettingsUpdate : null, children: _jsx(EmotionEffectUpdaterContext.Provider, { value: onEmotionEffectUpdate !== null && onEmotionEffectUpdate !== void 0 ? onEmotionEffectUpdate : null, children: _jsx(
|
|
57
|
+
return (_jsx(SubscribersContext.Provider, { value: subscribers, children: _jsx(SettingsUpdaterContext.Provider, { value: onSettingsUpdate !== null && onSettingsUpdate !== void 0 ? onSettingsUpdate : null, children: _jsx(EmotionEffectUpdaterContext.Provider, { value: onEmotionEffectUpdate !== null && onEmotionEffectUpdate !== void 0 ? onEmotionEffectUpdate : null, children: _jsx(ScenarioCancellerContext.Provider, { value: onCancelScenario !== null && onCancelScenario !== void 0 ? onCancelScenario : null, children: _jsx(DataContextInstance.Provider, { value: data, children: children }) }) }) }) }));
|
|
65
58
|
};
|
|
66
59
|
/**
|
|
67
60
|
* DataAPI実装を提供するフック
|
|
68
61
|
* プラグインから呼び出される
|
|
69
62
|
*/
|
|
70
63
|
export function useDataAPI() {
|
|
71
|
-
|
|
72
|
-
// DataRefContextから安定したデータ参照を取得
|
|
73
|
-
// これはContext valueが変わらないので、再レンダリングを引き起こさない
|
|
74
|
-
const dataRefHolder = useContext(DataRefContext);
|
|
64
|
+
const data = useContext(DataContextInstance);
|
|
75
65
|
const subscribers = useContext(SubscribersContext);
|
|
76
66
|
const settingsUpdater = useContext(SettingsUpdaterContext);
|
|
77
67
|
const emotionEffectUpdater = useContext(EmotionEffectUpdaterContext);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// 代わりに、displayTextが必要なコンポーネントはusePlaybackText()を直接使う
|
|
81
|
-
if (!dataRefHolder || !subscribers) {
|
|
68
|
+
const scenarioCanceller = useContext(ScenarioCancellerContext);
|
|
69
|
+
if (!data || !subscribers) {
|
|
82
70
|
throw new Error("useDataAPI must be used within DataProvider");
|
|
83
71
|
}
|
|
84
72
|
const get = useCallback((key, property) => {
|
|
85
|
-
const currentData = dataRefHolder.current;
|
|
86
73
|
if (property !== undefined) {
|
|
87
|
-
const value =
|
|
74
|
+
const value = data[key];
|
|
88
75
|
if (value && typeof value === "object" && property in value) {
|
|
89
76
|
return value[property];
|
|
90
77
|
}
|
|
91
78
|
return undefined;
|
|
92
79
|
}
|
|
93
|
-
return
|
|
94
|
-
}, [
|
|
95
|
-
);
|
|
80
|
+
return data[key];
|
|
81
|
+
}, [data]);
|
|
96
82
|
const subscribe = useCallback((key, callback) => {
|
|
97
83
|
var _a;
|
|
98
84
|
const subscriberKey = key;
|
|
@@ -158,13 +144,12 @@ export function useDataAPI() {
|
|
|
158
144
|
}, [updateSettings]);
|
|
159
145
|
const getBlockOption = useCallback((key) => {
|
|
160
146
|
var _a;
|
|
161
|
-
const currentBlock = (_a =
|
|
147
|
+
const currentBlock = (_a = data.playback) === null || _a === void 0 ? void 0 : _a.currentBlock;
|
|
162
148
|
if (!(currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.options)) {
|
|
163
149
|
return undefined;
|
|
164
150
|
}
|
|
165
151
|
return currentBlock.options[key];
|
|
166
|
-
}, [
|
|
167
|
-
);
|
|
152
|
+
}, [data]);
|
|
168
153
|
const updateEmotionEffect = useCallback((state) => {
|
|
169
154
|
if (emotionEffectUpdater) {
|
|
170
155
|
emotionEffectUpdater(state);
|
|
@@ -173,6 +158,14 @@ export function useDataAPI() {
|
|
|
173
158
|
console.warn("updateEmotionEffect called but no emotion effect updater provided");
|
|
174
159
|
}
|
|
175
160
|
}, [emotionEffectUpdater]);
|
|
161
|
+
const cancelScenario = useCallback(() => {
|
|
162
|
+
if (scenarioCanceller) {
|
|
163
|
+
scenarioCanceller();
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.warn("cancelScenario called but no scenario canceller provided");
|
|
167
|
+
}
|
|
168
|
+
}, [scenarioCanceller]);
|
|
176
169
|
return useMemo(() => ({
|
|
177
170
|
get,
|
|
178
171
|
subscribe,
|
|
@@ -184,6 +177,7 @@ export function useDataAPI() {
|
|
|
184
177
|
setVolumes,
|
|
185
178
|
getBlockOption,
|
|
186
179
|
updateEmotionEffect,
|
|
180
|
+
cancelScenario,
|
|
187
181
|
}), [
|
|
188
182
|
get,
|
|
189
183
|
subscribe,
|
|
@@ -195,5 +189,6 @@ export function useDataAPI() {
|
|
|
195
189
|
setVolumes,
|
|
196
190
|
getBlockOption,
|
|
197
191
|
updateEmotionEffect,
|
|
192
|
+
cancelScenario,
|
|
198
193
|
]);
|
|
199
194
|
}
|