@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 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 EMPTY_PLUGINS = [];
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, inactiveCharacterBrightness: 0.8 }, settings);
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
- const container = aspectRatioContainerRef.current;
108
- if (!container)
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: rect.width,
124
- height: rect.height,
100
+ width: window.innerWidth,
101
+ height: window.innerHeight,
125
102
  });
126
103
  };
127
- // 初期サイズを設定(次のフレームで実行してレンダリング完了を待つ)
128
- requestAnimationFrame(() => {
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, plugin.assets, plugin.plugin);
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, // プラグインの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
- // displayText JSX に変換(PlaybackTextProvider 用)
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: displayTextElement,
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
- displayTextElement,
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", { ref: aspectRatioContainerRef, className: "relative bg-white flex flex-col overflow-hidden", style: {
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: _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 }, "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
933
- .getRegisteredComponents()
934
- .filter((type) => type !== ComponentType.DialogueBox &&
935
- type !== ComponentType.ConversationBranch)
936
- .map((componentType) => (_jsx(PluginComponentProvider, { type: componentType, pluginManager: pluginManager }, componentType)))] }) })] }) }) }) }) })] }))] }));
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 { memo, useEffect, useMemo, useRef, useState } from "react";
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 = memo(function BackgroundMedia({ background, opacity, zIndex, useAppleFormat }) {
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 = memo(({ className }) => {
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 { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
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 = inactiveCharacterBrightness;
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
- else {
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
- * アスペクト比コンテナ全体に配置されるUIオーバーレイ
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
- * アスペクト比コンテナ全体に配置されるUIオーバーレイ
6
- * 親コンテナ(アスペクト比コンテナ)のサイズに合わせて自動調整される
6
+ * 16:9のアスペクト比を維持しながら、様々な画面サイズに対応するコンテナ
7
+ * - スマートフォン(幅 < 1000px): フルサイズ表示
8
+ * - タブレット(幅 ≥ 1000px): 70%に縮小して中央配置
7
9
  */
8
10
  export const OverlayUI = ({ children }) => {
9
- return (_jsx("div", { className: "absolute inset-0 pointer-events-none z-[9999]", children: _jsx("div", { className: "w-full h-full", children: children }) }));
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(DataRefContext.Provider, { value: dataRefHolder, children: _jsx(DataContextInstance.Provider, { value: data, children: children }) }) }) }) }));
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
- var _a;
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
- // 注意: usePlaybackTextOptional()をここで呼ぶと、PlaybackTextContextの更新で
79
- // useDataAPIを使う全コンポーネントが再レンダリングされてしまう
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 = currentData[key];
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 currentData[key];
94
- }, [dataRefHolder.current] // 依存配列を空にして、get関数を安定化
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 = dataRefHolder.current.playback) === null || _a === void 0 ? void 0 : _a.currentBlock;
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
- }, [(_a = dataRefHolder.current.playback) === null || _a === void 0 ? void 0 : _a.currentBlock] // 依存配列を空にして安定化
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
  }