@luna-editor/engine 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/Player.d.ts +1 -1
  2. package/dist/Player.js +676 -95
  3. package/dist/api/conversationBranch.d.ts +4 -0
  4. package/dist/api/conversationBranch.js +83 -0
  5. package/dist/components/BackgroundLayer.d.ts +19 -0
  6. package/dist/components/BackgroundLayer.js +220 -0
  7. package/dist/components/ClickWaitIndicator.d.ts +10 -0
  8. package/dist/components/ClickWaitIndicator.js +31 -0
  9. package/dist/components/ConversationBranchBox.d.ts +2 -0
  10. package/dist/components/ConversationBranchBox.js +29 -0
  11. package/dist/components/DialogueBox.js +16 -1
  12. package/dist/components/FontSettingsPanel.d.ts +10 -0
  13. package/dist/components/FontSettingsPanel.js +30 -0
  14. package/dist/components/FullscreenTextBox.d.ts +6 -0
  15. package/dist/components/FullscreenTextBox.js +70 -0
  16. package/dist/components/GameScreen.d.ts +9 -2
  17. package/dist/components/GameScreen.js +396 -80
  18. package/dist/components/OverlayUI.d.ts +2 -3
  19. package/dist/components/OverlayUI.js +3 -14
  20. package/dist/components/PluginComponentProvider.d.ts +3 -3
  21. package/dist/components/PluginComponentProvider.js +22 -4
  22. package/dist/components/TimeWaitIndicator.d.ts +15 -0
  23. package/dist/components/TimeWaitIndicator.js +17 -0
  24. package/dist/contexts/AudioContext.d.ts +1 -0
  25. package/dist/contexts/AudioContext.js +1 -0
  26. package/dist/contexts/DataContext.d.ts +3 -1
  27. package/dist/contexts/DataContext.js +104 -17
  28. package/dist/contexts/PlaybackTextContext.d.ts +32 -0
  29. package/dist/contexts/PlaybackTextContext.js +29 -0
  30. package/dist/data-api-types.d.ts +251 -0
  31. package/dist/data-api-types.js +6 -0
  32. package/dist/emotion-effect-types.d.ts +86 -0
  33. package/dist/emotion-effect-types.js +6 -0
  34. package/dist/hooks/useBacklog.js +3 -1
  35. package/dist/hooks/useConversationBranch.d.ts +16 -0
  36. package/dist/hooks/useConversationBranch.js +125 -0
  37. package/dist/hooks/useFontLoader.d.ts +30 -0
  38. package/dist/hooks/useFontLoader.js +192 -0
  39. package/dist/hooks/useFullscreenText.d.ts +17 -0
  40. package/dist/hooks/useFullscreenText.js +120 -0
  41. package/dist/hooks/useImagePreloader.d.ts +5 -0
  42. package/dist/hooks/useImagePreloader.js +53 -0
  43. package/dist/hooks/usePlayerLogic.d.ts +10 -3
  44. package/dist/hooks/usePlayerLogic.js +115 -18
  45. package/dist/hooks/usePluginAPI.js +1 -1
  46. package/dist/hooks/usePluginEvents.d.ts +4 -1
  47. package/dist/hooks/usePluginEvents.js +16 -11
  48. package/dist/hooks/usePreloadImages.js +27 -7
  49. package/dist/hooks/useSoundPlayer.d.ts +15 -0
  50. package/dist/hooks/useSoundPlayer.js +209 -0
  51. package/dist/hooks/useTypewriter.d.ts +6 -2
  52. package/dist/hooks/useTypewriter.js +42 -6
  53. package/dist/hooks/useVoice.js +4 -1
  54. package/dist/index.d.ts +5 -3
  55. package/dist/index.js +3 -1
  56. package/dist/plugin/PluginManager.d.ts +86 -5
  57. package/dist/plugin/PluginManager.js +427 -94
  58. package/dist/sdk.d.ts +133 -162
  59. package/dist/sdk.js +39 -4
  60. package/dist/types.d.ts +300 -4
  61. package/dist/utils/branchBlockConverter.d.ts +2 -0
  62. package/dist/utils/branchBlockConverter.js +21 -0
  63. package/dist/utils/branchNavigator.d.ts +14 -0
  64. package/dist/utils/branchNavigator.js +55 -0
  65. package/dist/utils/facePositionCalculator.js +0 -1
  66. package/dist/utils/variableManager.d.ts +18 -0
  67. package/dist/utils/variableManager.js +159 -0
  68. package/package.json +6 -6
  69. package/dist/components/ConversationLogUI.d.ts +0 -2
  70. package/dist/components/ConversationLogUI.js +0 -115
  71. package/dist/hooks/useConversationLog.d.ts +0 -14
  72. package/dist/hooks/useConversationLog.js +0 -82
  73. package/dist/hooks/useUIVisibility.d.ts +0 -9
  74. package/dist/hooks/useUIVisibility.js +0 -19
package/dist/Player.js CHANGED
@@ -10,100 +10,359 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
12
12
  import { clsx } from "clsx";
13
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
13
+ import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react";
14
14
  import { useScreenSizeAtom } from "./atoms/screen-size";
15
+ import { BackgroundLayer } from "./components/BackgroundLayer";
16
+ import { ClickWaitIndicator } from "./components/ClickWaitIndicator";
17
+ import { ConversationBranchBox } from "./components/ConversationBranchBox";
15
18
  import { DialogueBox } from "./components/DialogueBox";
16
19
  import { EndScreen } from "./components/EndScreen";
20
+ import { FullscreenTextBox } from "./components/FullscreenTextBox";
17
21
  import { GameScreen } from "./components/GameScreen";
18
22
  import { OverlayUI } from "./components/OverlayUI";
19
23
  import { PluginComponentProvider } from "./components/PluginComponentProvider";
24
+ import { TimeWaitIndicator } from "./components/TimeWaitIndicator";
20
25
  import { AudioProvider } from "./contexts/AudioContext";
21
26
  import { DataProvider } from "./contexts/DataContext";
27
+ import { PlaybackTextProvider } from "./contexts/PlaybackTextContext";
22
28
  import { useBacklog } from "./hooks/useBacklog";
29
+ import { useConversationBranch } from "./hooks/useConversationBranch";
30
+ import { useFontLoader } from "./hooks/useFontLoader";
23
31
  import { usePlayerLogic } from "./hooks/usePlayerLogic";
24
32
  import { setGlobalUIAPI } from "./hooks/usePluginAPI";
25
33
  import { usePluginEvents } from "./hooks/usePluginEvents";
26
34
  import { usePreloadImages } from "./hooks/usePreloadImages";
35
+ import { useSoundPlayer } from "./hooks/useSoundPlayer";
27
36
  import { useTypewriter } from "./hooks/useTypewriter";
28
37
  import { PluginManager } from "./plugin/PluginManager";
29
38
  import { ComponentType } from "./sdk";
30
- export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd, onScenarioStart, onScenarioCancelled, onSettingsChange, className, autoplay = false, }) => {
31
- var _a;
39
+ import { convertBranchBlockToScenarioBlock } from "./utils/branchBlockConverter";
40
+ import { BranchNavigator } from "./utils/branchNavigator";
41
+ import { VariableManager } from "./utils/variableManager";
42
+ export const Player = ({ scenario: scenarioProp, settings, plugins = [], sounds = [], onEnd, onScenarioEnd, onScenarioStart, onScenarioCancelled, onSettingsChange, className, autoplay = false, preventDefaultScroll = true, screenSize: screenSizeProp, disableKeyboardNavigation = false, }) => {
43
+ var _a, _b, _c, _d;
44
+ // scenario.blocks が存在しない場合は空の配列を使用
45
+ const scenario = useMemo(() => {
46
+ var _a;
47
+ return (Object.assign(Object.assign({}, scenarioProp), { blocks: (_a = scenarioProp.blocks) !== null && _a !== void 0 ? _a : [] }));
48
+ }, [scenarioProp]);
32
49
  // デフォルト値とマージ
33
- const mergedSettings = Object.assign({ textSpeed: 80, autoPlaySpeed: 3, bgmVolume: 1.0, seVolume: 1.0, voiceVolume: 1.0, effectVolume: 1.0, textSoundVolume: 1.0 }, settings);
50
+ 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);
34
51
  // プラグインからの設定更新ハンドラ
35
52
  const handleSettingsUpdate = useCallback((updatedSettings) => {
36
53
  var _a, _b;
37
54
  const newSettings = Object.assign(Object.assign({ aspectRatio: (_a = settings === null || settings === void 0 ? void 0 : settings.aspectRatio) !== null && _a !== void 0 ? _a : "16:9", bgObjectFit: (_b = settings === null || settings === void 0 ? void 0 : settings.bgObjectFit) !== null && _b !== void 0 ? _b : "cover" }, settings), updatedSettings);
38
55
  onSettingsChange === null || onSettingsChange === void 0 ? void 0 : onSettingsChange(newSettings);
39
56
  }, [settings, onSettingsChange]);
40
- const pluginManagerRef = useRef(new PluginManager());
57
+ // 感情エフェクト状態管理
58
+ const [emotionEffectState, setEmotionEffectState] = useState(null);
59
+ // プラグインからの感情エフェクト更新ハンドラ
60
+ const handleEmotionEffectUpdate = useCallback((state) => {
61
+ setEmotionEffectState(state);
62
+ }, []);
63
+ // 遅延初期化でPluginManagerを作成(毎レンダリングでnew PluginManager()が評価されるのを防ぐ)
64
+ const pluginManagerRef = useRef(undefined);
65
+ if (!pluginManagerRef.current) {
66
+ pluginManagerRef.current = new PluginManager();
67
+ }
68
+ // 以降の参照用(undefinedにならないことを保証)
69
+ const pluginManager = pluginManagerRef.current;
70
+ const handleChoiceSelectionRef = useRef(null);
41
71
  // グローバルUIAPIを初期化(プラグインコンポーネントから使用可能にする)
42
72
  useEffect(() => {
43
- const uiAPI = pluginManagerRef.current.getUIAPI();
44
- setGlobalUIAPI(uiAPI, pluginManagerRef.current);
45
- }, []);
73
+ const uiAPI = pluginManager.getUIAPI();
74
+ setGlobalUIAPI(uiAPI, pluginManager);
75
+ }, [pluginManager]);
76
+ // 感情エフェクト更新コールバックを登録
77
+ useEffect(() => {
78
+ pluginManager.setEmotionEffectUpdaterCallback(handleEmotionEffectUpdate);
79
+ }, [pluginManager, handleEmotionEffectUpdate]);
80
+ // DataContextへの参照を保持(プラグインがdata.get()を呼び出せるようにする)
81
+ const dataContextRef = useRef(null);
82
+ // DataContext getterを登録
83
+ useEffect(() => {
84
+ pluginManager.setDataContextGetter(() => dataContextRef.current);
85
+ }, [pluginManager]);
86
+ // プラグインのミュート状態を設定
87
+ useEffect(() => {
88
+ var _a;
89
+ pluginManager.setMuteAudio((_a = mergedSettings.muteAudio) !== null && _a !== void 0 ? _a : false);
90
+ }, [pluginManager, mergedSettings.muteAudio]);
46
91
  // 画面サイズの初期化
47
92
  const [, setScreenSize] = useScreenSizeAtom();
93
+ const aspectRatioContainerRef = useRef(null);
48
94
  useEffect(() => {
95
+ // screenSizeが明示的に指定されている場合はそれを使用(プレビュー用)
96
+ if (screenSizeProp) {
97
+ setScreenSize(screenSizeProp);
98
+ return;
99
+ }
49
100
  // クライアントサイドでのみ実行
50
101
  if (typeof window === "undefined")
51
102
  return;
103
+ // 初期値として一時的にウィンドウサイズを設定
52
104
  setScreenSize({ width: window.innerWidth, height: window.innerHeight });
53
- // リサイズ監視
54
- const handleResize = () => {
105
+ const container = aspectRatioContainerRef.current;
106
+ if (!container)
107
+ return;
108
+ // アスペクト比コンテナのサイズを監視
109
+ const updateScreenSize = () => {
110
+ const rect = container.getBoundingClientRect();
111
+ // サイズが0の場合はスキップ(まだレンダリングされていない)
112
+ if (rect.width === 0 || rect.height === 0)
113
+ return;
114
+ console.log("[Player] screenSize更新:", {
115
+ width: rect.width,
116
+ height: rect.height,
117
+ windowWidth: window.innerWidth,
118
+ windowHeight: window.innerHeight,
119
+ });
55
120
  setScreenSize({
56
- width: window.innerWidth,
57
- height: window.innerHeight,
121
+ width: rect.width,
122
+ height: rect.height,
58
123
  });
59
124
  };
60
- window.addEventListener("resize", handleResize);
61
- return () => window.removeEventListener("resize", handleResize);
62
- }, [setScreenSize]);
125
+ // 初期サイズを設定(次のフレームで実行してレンダリング完了を待つ)
126
+ requestAnimationFrame(() => {
127
+ updateScreenSize();
128
+ });
129
+ // ResizeObserverでコンテナのサイズ変更を監視
130
+ const resizeObserver = new ResizeObserver(() => {
131
+ updateScreenSize();
132
+ });
133
+ resizeObserver.observe(container);
134
+ return () => {
135
+ resizeObserver.disconnect();
136
+ };
137
+ }, [setScreenSize, screenSizeProp]);
63
138
  // 表示可能なブロックのインデックスを事前計算
64
139
  const displayableBlockIndices = useMemo(() => {
65
- const supportedBlockTypes = ["dialogue", "narration"];
66
- return scenario.blocks
140
+ const supportedBlockTypes = [
141
+ "dialogue",
142
+ "narration",
143
+ "conversation_branch",
144
+ "fullscreen_text",
145
+ "click_wait",
146
+ "time_wait",
147
+ ];
148
+ const indices = scenario.blocks
67
149
  .map((block, index) => ({ block, index }))
68
150
  .filter(({ block }) => supportedBlockTypes.includes(block.blockType))
69
151
  .map(({ index }) => index);
152
+ return indices;
70
153
  }, [scenario.blocks]);
71
- const [state, setState] = useState({
72
- currentBlockIndex: 0,
73
- isPlaying: autoplay,
74
- isEnded: false,
154
+ // 変数マネージャーの初期化
155
+ const variableManagerRef = useRef(scenario.variables && scenario.variables.length > 0
156
+ ? new VariableManager(scenario.variables)
157
+ : null);
158
+ // メディアタイプを判定するヘルパー関数
159
+ const getMediaType = useCallback((url) => {
160
+ var _a, _b;
161
+ const extension = (_b = (_a = url.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : "";
162
+ if (["mp4", "webm", "ogg", "mov", "avi", "mkv"].includes(extension)) {
163
+ return "video";
164
+ }
165
+ if (extension === "gif") {
166
+ return "gif";
167
+ }
168
+ return "image";
169
+ }, []);
170
+ // 現在の背景状態を計算(character_entranceパターンに倣う)
171
+ // background_groupの場合は複数の背景を配列で返す
172
+ const calculateCurrentBackground = useCallback((upToBlockIndex) => {
173
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
174
+ let currentBackgroundBlock = null;
175
+ let previousBackgroundBlock = null;
176
+ let backgroundGroupBlock = null;
177
+ // 現在のブロックまでを逆順で走査して、最後のbackground_changeまたはbackground_groupを見つける
178
+ // background_changeが先に見つかった場合、background_groupは無視する(背景がリセットされる)
179
+ for (let i = upToBlockIndex; i >= 0; i--) {
180
+ const block = scenario.blocks[i];
181
+ // background_groupの場合
182
+ if (block.blockType === "background_group" &&
183
+ block.backgroundGroupItems &&
184
+ block.backgroundGroupItems.length > 0) {
185
+ // すでにbackground_changeが見つかっている場合は無視
186
+ // (background_changeがbackground_groupより後にあるので、背景はリセットされている)
187
+ if (currentBackgroundBlock) {
188
+ break;
189
+ }
190
+ backgroundGroupBlock = block;
191
+ break;
192
+ }
193
+ // imageUrlが存在し、かつ空文字列でないことを確認
194
+ const imageUrl = (_a = block.backgroundState) === null || _a === void 0 ? void 0 : _a.imageUrl;
195
+ if (block.blockType === "background_change" &&
196
+ imageUrl &&
197
+ imageUrl.trim() !== "") {
198
+ if (!currentBackgroundBlock) {
199
+ currentBackgroundBlock = block;
200
+ }
201
+ else if (!previousBackgroundBlock) {
202
+ previousBackgroundBlock = block;
203
+ break; // 2つ見つかったら終了
204
+ }
205
+ }
206
+ }
207
+ // background_groupが見つかった場合(background_changeより後にない場合のみ)
208
+ if (backgroundGroupBlock) {
209
+ return ((_c = (_b = backgroundGroupBlock.backgroundGroupItems) === null || _b === void 0 ? void 0 : _b.filter((item) => !!item.state.imageUrl && item.state.imageUrl.trim() !== "").map((item) => {
210
+ var _a, _b;
211
+ return ({
212
+ objectId: item.objectId,
213
+ stateId: item.stateId,
214
+ objectName: item.object.name,
215
+ stateName: item.state.name,
216
+ imageUrl: item.state.imageUrl,
217
+ webmUrl: (_a = item.state.webmUrl) !== null && _a !== void 0 ? _a : null,
218
+ objectFit: item.objectFit,
219
+ loop: item.loop,
220
+ mediaType: getMediaType(item.state.imageUrl),
221
+ opacity: item.opacity,
222
+ layer: (_b = item.layer) !== null && _b !== void 0 ? _b : "background",
223
+ });
224
+ })) !== null && _c !== void 0 ? _c : []);
225
+ }
226
+ if (!currentBackgroundBlock) {
227
+ return [];
228
+ }
229
+ const imageUrl = (_e = (_d = currentBackgroundBlock.backgroundState) === null || _d === void 0 ? void 0 : _d.imageUrl) !== null && _e !== void 0 ? _e : "";
230
+ // 前の背景ブロックのfadeDurationを使用(前の背景→現在の背景へのフェード時間)
231
+ // ブロックに設定がない場合はデフォルト値を使用
232
+ const previousOptions = previousBackgroundBlock === null || previousBackgroundBlock === void 0 ? void 0 : previousBackgroundBlock.options;
233
+ const blockFadeDuration = typeof (previousOptions === null || previousOptions === void 0 ? void 0 : previousOptions.fadeDuration) === "number"
234
+ ? previousOptions.fadeDuration
235
+ : 0;
236
+ const fadeDuration = blockFadeDuration > 0
237
+ ? blockFadeDuration
238
+ : mergedSettings.defaultBackgroundFadeDuration;
239
+ return [
240
+ {
241
+ objectId: (_f = currentBackgroundBlock.backgroundObjectId) !== null && _f !== void 0 ? _f : "",
242
+ stateId: (_g = currentBackgroundBlock.backgroundStateId) !== null && _g !== void 0 ? _g : "",
243
+ objectName: (_j = (_h = currentBackgroundBlock.backgroundObject) === null || _h === void 0 ? void 0 : _h.name) !== null && _j !== void 0 ? _j : "",
244
+ stateName: (_l = (_k = currentBackgroundBlock.backgroundState) === null || _k === void 0 ? void 0 : _k.name) !== null && _l !== void 0 ? _l : "",
245
+ imageUrl: imageUrl,
246
+ webmUrl: (_o = (_m = currentBackgroundBlock.backgroundState) === null || _m === void 0 ? void 0 : _m.webmUrl) !== null && _o !== void 0 ? _o : null,
247
+ objectFit: (_p = currentBackgroundBlock.backgroundObjectFit) !== null && _p !== void 0 ? _p : "cover",
248
+ loop: (_q = currentBackgroundBlock.backgroundLoop) !== null && _q !== void 0 ? _q : true,
249
+ mediaType: getMediaType(imageUrl),
250
+ fadeDuration,
251
+ opacity: 1.0,
252
+ layer: "background",
253
+ },
254
+ ];
255
+ }, [
256
+ scenario.blocks,
257
+ getMediaType,
258
+ mergedSettings.defaultBackgroundFadeDuration,
259
+ ]);
260
+ const [state, setState] = useState(() => {
261
+ var _a;
262
+ return {
263
+ currentBlockIndex: 0,
264
+ isPlaying: autoplay,
265
+ isEnded: false,
266
+ variables: (_a = variableManagerRef.current) === null || _a === void 0 ? void 0 : _a.getVariablesMap(),
267
+ };
75
268
  });
269
+ const [currentBranchBlock, setCurrentBranchBlock] = useState(null);
270
+ // 遷移の種類を追跡(クリック、自動、時間待ちなど)
271
+ const [transitionSource, setTransitionSource] = useState("click");
272
+ // シナリオ開始コールバック(後でcurrentBlockが定義された後に実行)
273
+ const [hasStarted, setHasStarted] = useState(false);
274
+ // シナリオIDを追跡して、変更時に状態をリセット(プレビュー用)
275
+ const previousScenarioIdRef = useRef(scenario.id);
276
+ // シナリオが変更されたときに状態をリセット(keyを変更せずに対応)
277
+ useEffect(() => {
278
+ var _a;
279
+ if (previousScenarioIdRef.current !== scenario.id) {
280
+ const previousId = previousScenarioIdRef.current;
281
+ // 前のシナリオが開始されていた場合、onScenarioEndを呼び出す
282
+ if (hasStarted) {
283
+ pluginManager.callHook("onScenarioEnd", {
284
+ scenarioId: previousId,
285
+ scenarioName: "scenario",
286
+ });
287
+ }
288
+ previousScenarioIdRef.current = scenario.id;
289
+ // 状態をリセット
290
+ setState({
291
+ currentBlockIndex: 0,
292
+ isPlaying: autoplay,
293
+ isEnded: false,
294
+ variables: (_a = variableManagerRef.current) === null || _a === void 0 ? void 0 : _a.getVariablesMap(),
295
+ });
296
+ setCurrentBranchBlock(null);
297
+ setHasStarted(false); // これにより次の useEffect で onScenarioStart が呼ばれる
298
+ }
299
+ }, [scenario.id, autoplay, hasStarted, pluginManager]);
76
300
  // 画像を事前読み込み
77
301
  const imagesLoaded = usePreloadImages(scenario);
302
+ // シナリオ内の全テキストを抽出(フォントのプリロード用)
303
+ // サブセットフォントの分割ファイルを事前に読み込むことで、
304
+ // 文字送り中のフォント切り替わりを防止
305
+ const allScenarioText = useMemo(() => {
306
+ var _a;
307
+ const texts = [];
308
+ for (const block of scenario.blocks) {
309
+ // dialogue, narration, fullscreen_textのcontent
310
+ if (block.content) {
311
+ texts.push(block.content);
312
+ }
313
+ // speaker name
314
+ if ((_a = block.speaker) === null || _a === void 0 ? void 0 : _a.name) {
315
+ texts.push(block.speaker.name);
316
+ }
317
+ }
318
+ // 重複を除去してユニークな文字のみを残す
319
+ const uniqueChars = [...new Set(texts.join(""))].join("");
320
+ return uniqueChars;
321
+ }, [scenario.blocks]);
322
+ // フォントを読み込み(シナリオ内の全テキストでプリロード)
323
+ const { isLoaded: fontsLoaded } = useFontLoader(scenario.fonts, allScenarioText);
78
324
  // プラグインの読み込み状態
79
325
  const [pluginsLoaded, setPluginsLoaded] = useState(false);
326
+ // 読み込み済みプラグインのパッケージ名を追跡
327
+ const loadedPluginNamesRef = useRef(new Set());
80
328
  // プラグインの読み込み
329
+ // biome-ignore lint/correctness/useExhaustiveDependencies: pluginManagerは安定した参照なので依存配列から除外しても安全
81
330
  useEffect(() => {
331
+ let isCancelled = false;
82
332
  const loadPlugins = () => __awaiter(void 0, void 0, void 0, function* () {
83
- console.log("Loading plugins:", plugins);
84
- setPluginsLoaded(false);
85
- for (const plugin of plugins) {
86
- console.log("Loading plugin:", plugin.packageName);
87
- yield pluginManagerRef.current.loadPlugin(plugin.packageName, plugin.bundleUrl, plugin.config);
333
+ // 作品のサウンドデータをPluginManagerに設定
334
+ if (sounds.length > 0) {
335
+ pluginManager.setWorkSounds(sounds);
336
+ }
337
+ // 未読み込みのプラグインのみを抽出
338
+ const newPlugins = plugins.filter((plugin) => !loadedPluginNamesRef.current.has(plugin.packageName));
339
+ // 新しいプラグインがある場合のみローディング状態にする
340
+ if (newPlugins.length > 0) {
341
+ setPluginsLoaded(false);
342
+ for (const plugin of newPlugins) {
343
+ if (isCancelled)
344
+ return;
345
+ yield pluginManager.loadPlugin(plugin.packageName, plugin.bundleUrl, plugin.config, plugin.assets);
346
+ loadedPluginNamesRef.current.add(plugin.packageName);
347
+ }
348
+ if (isCancelled)
349
+ return;
350
+ pluginManager.callHook("onInit");
351
+ pluginManager.showUI(ComponentType.DialogueBox);
352
+ // 全てのアセットプリロードが完了するまで待機
353
+ yield pluginManager.waitForPreloads();
354
+ }
355
+ if (!isCancelled) {
356
+ setPluginsLoaded(true);
88
357
  }
89
- // プラグイン初期化フック - すべてのプラグインの読み込み完了後に実行
90
- console.log("Calling onInit hook");
91
- pluginManagerRef.current.callHook("onInit");
92
- // DialogueBoxの初期状態を表示に設定
93
- pluginManagerRef.current.showUI(ComponentType.DialogueBox);
94
- setPluginsLoaded(true);
95
- console.log("All plugins loaded successfully");
96
358
  });
97
359
  loadPlugins();
98
360
  return () => {
99
- const manager = pluginManagerRef.current;
100
- manager.cleanup();
361
+ isCancelled = true;
101
362
  };
102
- }, [plugins]);
363
+ }, [plugins, sounds]);
103
364
  // 初回レンダリング完了フラグ
104
365
  const [isFirstRenderComplete, setIsFirstRenderComplete] = useState(false);
105
- // シナリオ開始コールバック(後でcurrentBlockが定義された後に実行)
106
- const [hasStarted, setHasStarted] = useState(false);
107
366
  // スタイル要素の登録
108
367
  useEffect(() => {
109
368
  if (isFirstRenderComplete) {
@@ -113,35 +372,54 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
113
372
  // data-character-id属性を持つ最初のキャラクター要素を取得
114
373
  const characterSpriteElement = document.querySelector("[data-character-id]");
115
374
  const gameScreenElement = document.querySelector(".h-screen.w-full");
116
- console.log("🔍 [StyleAPI] Element registration:", {
117
- speakerNameElement: !!speakerNameElement,
118
- dialogueElement: !!dialogueElement,
119
- characterSpriteElement: !!characterSpriteElement,
120
- gameScreenElement: !!gameScreenElement,
121
- });
122
375
  if (speakerNameElement) {
123
- pluginManagerRef.current.registerStyleElement("speakerName", speakerNameElement);
376
+ pluginManager.registerStyleElement("speakerName", speakerNameElement);
124
377
  }
125
378
  if (dialogueElement) {
126
- pluginManagerRef.current.registerStyleElement("dialogueBox", dialogueElement);
127
- pluginManagerRef.current.registerStyleElement("scenarioBlockContent", dialogueElement);
379
+ pluginManager.registerStyleElement("dialogueBox", dialogueElement);
380
+ pluginManager.registerStyleElement("scenarioBlockContent", dialogueElement);
128
381
  }
129
382
  if (characterSpriteElement) {
130
- pluginManagerRef.current.registerStyleElement("characterSprite", characterSpriteElement);
383
+ pluginManager.registerStyleElement("characterSprite", characterSpriteElement);
131
384
  }
132
385
  if (gameScreenElement) {
133
- pluginManagerRef.current.registerStyleElement("gameScreen", gameScreenElement);
134
- pluginManagerRef.current.registerStyleElement("background", gameScreenElement);
386
+ pluginManager.registerStyleElement("gameScreen", gameScreenElement);
387
+ pluginManager.registerStyleElement("background", gameScreenElement);
135
388
  }
136
389
  }
137
- }, [isFirstRenderComplete]);
138
- const { displayText, isTyping, skipTyping, startTyping } = useTypewriter({
390
+ }, [pluginManager, isFirstRenderComplete]);
391
+ // Fullscreen text要素の登録(ブロックタイプがfullscreen_textの時のみ)
392
+ useEffect(() => {
393
+ if (!isFirstRenderComplete)
394
+ return;
395
+ // 少し遅延させてDOMが更新されるのを待つ
396
+ const timeoutId = setTimeout(() => {
397
+ const fullscreenOverlay = document.querySelector("[data-fullscreen-text-element]");
398
+ const fullscreenContainer = document.querySelector("[data-fullscreen-text-container]");
399
+ if (fullscreenOverlay) {
400
+ pluginManager.registerStyleElement("fullscreenTextOverlay", fullscreenOverlay);
401
+ }
402
+ if (fullscreenContainer) {
403
+ pluginManager.registerStyleElement("fullscreenTextContainer", fullscreenContainer);
404
+ }
405
+ }, 100);
406
+ return () => clearTimeout(timeoutId);
407
+ }, [pluginManager, isFirstRenderComplete]);
408
+ const { displayText, isTyping, skipTyping, startTyping, resetAccumulated } = useTypewriter({
139
409
  speed: mergedSettings.textSpeed,
140
410
  });
141
- // 現在の表示可能なブロックを取得
142
- const currentBlock = displayableBlockIndices[state.currentBlockIndex] !== undefined
143
- ? scenario.blocks[displayableBlockIndices[state.currentBlockIndex]]
144
- : undefined;
411
+ // 現在の表示可能なブロックを取得(分岐ブロックが優先)
412
+ const currentBlock = currentBranchBlock ||
413
+ (displayableBlockIndices[state.currentBlockIndex] !== undefined
414
+ ? scenario.blocks[displayableBlockIndices[state.currentBlockIndex]]
415
+ : undefined);
416
+ // 前のブロックを取得(フェード処理用)
417
+ const previousBlock = useMemo(() => {
418
+ if (state.currentBlockIndex <= 0)
419
+ return null;
420
+ const prevIdx = displayableBlockIndices[state.currentBlockIndex - 1];
421
+ return prevIdx !== undefined ? scenario.blocks[prevIdx] : null;
422
+ }, [scenario.blocks, displayableBlockIndices, state.currentBlockIndex]);
145
423
  // usePlayerLogicに渡す実際のブロックインデックス
146
424
  const actualBlockIndex = (_a = displayableBlockIndices[state.currentBlockIndex]) !== null && _a !== void 0 ? _a : 0;
147
425
  // バックログ機能
@@ -150,30 +428,219 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
150
428
  currentBlockIndex: actualBlockIndex,
151
429
  currentBlock: currentBlock || null,
152
430
  });
431
+ // サウンド再生機能
432
+ // actualBlockIndexの前後にあるサウンドブロックを処理するため、
433
+ // 表示ブロックに到達する直前のサウンドブロックを取得
434
+ const soundBlocksToProcess = useMemo(() => {
435
+ const blocks = [];
436
+ const currentDisplayableIdx = displayableBlockIndices[state.currentBlockIndex];
437
+ if (currentDisplayableIdx === undefined)
438
+ return blocks;
439
+ // 前の表示可能ブロックのインデックスを取得
440
+ const prevDisplayableIdx = state.currentBlockIndex > 0
441
+ ? displayableBlockIndices[state.currentBlockIndex - 1]
442
+ : -1;
443
+ // 前の表示ブロックから現在の表示ブロックまでの間にあるサウンドブロックを収集
444
+ for (let i = prevDisplayableIdx + 1; i < currentDisplayableIdx; i++) {
445
+ const block = scenario.blocks[i];
446
+ if (block.blockType === "bgm_play" ||
447
+ block.blockType === "se_play" ||
448
+ block.blockType === "bgm_stop") {
449
+ blocks.push(block);
450
+ }
451
+ }
452
+ return blocks;
453
+ }, [scenario.blocks, displayableBlockIndices, state.currentBlockIndex]);
454
+ // サウンド再生フック
455
+ useSoundPlayer({
456
+ soundBlocks: soundBlocksToProcess,
457
+ isFirstRenderComplete,
458
+ muteAudio: mergedSettings.muteAudio,
459
+ });
460
+ // 会話分岐機能
461
+ const branchNavigatorRef = useRef(new BranchNavigator());
462
+ const conversationBranch = useConversationBranch({
463
+ apiBaseUrl: typeof window !== "undefined" ? window.location.origin : "",
464
+ variableManager: variableManagerRef.current,
465
+ scenario,
466
+ });
467
+ // conversationBranchから安定した参照を抽出
468
+ const { loadBranch, selectChoice, getSelectedChoiceFullData, branchState } = conversationBranch;
469
+ // 選択肢選択処理
470
+ const handleChoiceSelection = useCallback((choiceId) => {
471
+ selectChoice(choiceId);
472
+ const selectedChoice = getSelectedChoiceFullData(choiceId);
473
+ if (selectedChoice) {
474
+ branchNavigatorRef.current.startBranchNavigation(selectedChoice);
475
+ pluginManager.callHook("onChoiceSelected", {
476
+ choiceId,
477
+ choiceText: selectedChoice.choiceText,
478
+ });
479
+ const firstBranchBlock = branchNavigatorRef.current.nextBranchBlock();
480
+ if (firstBranchBlock) {
481
+ setCurrentBranchBlock(convertBranchBlockToScenarioBlock(firstBranchBlock));
482
+ }
483
+ }
484
+ else {
485
+ console.error("[ConversationBranch] No choice data found for:", choiceId);
486
+ }
487
+ }, [pluginManager, selectChoice, getSelectedChoiceFullData]);
488
+ // handleChoiceSelectionRefを更新
489
+ handleChoiceSelectionRef.current = handleChoiceSelection;
490
+ // window.playerAPIとPluginManagerのコールバックを設定
491
+ useEffect(() => {
492
+ if (typeof window !== "undefined") {
493
+ window.playerAPI = {
494
+ selectChoice: (choiceId) => {
495
+ var _a;
496
+ (_a = handleChoiceSelectionRef.current) === null || _a === void 0 ? void 0 : _a.call(handleChoiceSelectionRef, choiceId);
497
+ },
498
+ };
499
+ }
500
+ pluginManager.setSelectChoiceCallback((choiceId) => {
501
+ var _a;
502
+ (_a = handleChoiceSelectionRef.current) === null || _a === void 0 ? void 0 : _a.call(handleChoiceSelectionRef, choiceId);
503
+ });
504
+ pluginManager.setGetBranchStateCallback(() => {
505
+ return branchState;
506
+ });
507
+ return () => {
508
+ if (typeof window !== "undefined") {
509
+ delete window.playerAPI;
510
+ }
511
+ };
512
+ }, [pluginManager, branchState]);
153
513
  // ダイアログ表示とアクションノード実行のuseEffectを分離
154
514
  useEffect(() => {
155
515
  if (currentBlock) {
156
- startTyping(currentBlock.content || "");
516
+ // fullscreen_text は独自のタイプライターを使うので、displayTextをクリアしてスキップ
517
+ if (currentBlock.blockType === "fullscreen_text") {
518
+ startTyping("");
519
+ return;
520
+ }
521
+ let content = currentBlock.content || "";
522
+ if (variableManagerRef.current &&
523
+ (currentBlock.blockType === "dialogue" ||
524
+ currentBlock.blockType === "narration")) {
525
+ content = variableManagerRef.current.interpolateText(content);
526
+ }
527
+ // 前のブロックのcontinueModeをチェック
528
+ const previousOptions = previousBlock === null || previousBlock === void 0 ? void 0 : previousBlock.options;
529
+ let continueMode = false;
530
+ if (previousOptions === null || previousOptions === void 0 ? void 0 : previousOptions.continueMode) {
531
+ const mode = previousOptions.continueMode;
532
+ if (mode === "continue" || mode === "continueWithNewline") {
533
+ continueMode = mode;
534
+ }
535
+ }
536
+ const isContinuableBlock = currentBlock.blockType === "dialogue" ||
537
+ currentBlock.blockType === "narration";
538
+ if (continueMode && isContinuableBlock) {
539
+ startTyping(content, continueMode);
540
+ }
541
+ else {
542
+ resetAccumulated();
543
+ startTyping(content, false);
544
+ }
157
545
  }
158
- }, [currentBlock, startTyping]);
159
- // 画像読み込み完了後、GameScreenがマウントされてから表示する
546
+ }, [currentBlock, previousBlock, startTyping, resetAccumulated]);
547
+ // 分岐ブロック自動ロード処理
548
+ useEffect(() => {
549
+ if (currentBlock && currentBlock.blockType === "conversation_branch") {
550
+ loadBranch(currentBlock.id);
551
+ }
552
+ }, [currentBlock, loadBranch]);
553
+ // プラグインフックを別のuseEffectで実行(branchStateが更新されてから)
554
+ useEffect(() => {
555
+ if (currentBlock &&
556
+ currentBlock.blockType === "conversation_branch" &&
557
+ branchState.isActive) {
558
+ pluginManager.callHook("onBranchStart", {
559
+ branchBlockId: currentBlock.id,
560
+ choices: branchState.currentChoices,
561
+ });
562
+ // 変数分岐の場合は自動で選択された分岐に進む
563
+ if (branchState.branchType === "variable") {
564
+ if (branchState.selectedChoiceId) {
565
+ handleChoiceSelection(branchState.selectedChoiceId);
566
+ }
567
+ else if (branchState.errorState === "NO_MATCHING_CONDITION") {
568
+ console.error("変数分岐: いずれの条件にも一致しませんでした。シナリオが停止します。");
569
+ }
570
+ }
571
+ }
572
+ }, [
573
+ pluginManager,
574
+ currentBlock,
575
+ branchState.isActive,
576
+ branchState.currentChoices,
577
+ branchState.branchType,
578
+ branchState.selectedChoiceId,
579
+ branchState.errorState,
580
+ handleChoiceSelection,
581
+ ]);
582
+ // 画像とフォント読み込み完了後、GameScreenがマウントされてから表示する
160
583
  useEffect(() => {
161
- if (imagesLoaded && currentBlock && !isFirstRenderComplete) {
584
+ if (imagesLoaded && fontsLoaded && currentBlock && !isFirstRenderComplete) {
162
585
  // requestAnimationFrameを使用してレンダリング完了を待つ
163
586
  requestAnimationFrame(() => {
164
587
  setIsFirstRenderComplete(true);
165
588
  });
166
589
  }
167
- }, [imagesLoaded, currentBlock, isFirstRenderComplete]);
168
- // シナリオ開始コールバック(currentBlock定義後)
590
+ }, [imagesLoaded, fontsLoaded, currentBlock, isFirstRenderComplete]);
591
+ // シナリオ開始コールバック(currentBlock定義後、プラグインロード完了後)
169
592
  useEffect(() => {
170
- if (isFirstRenderComplete && !hasStarted && currentBlock) {
593
+ if (isFirstRenderComplete && !hasStarted && currentBlock && pluginsLoaded) {
171
594
  setHasStarted(true);
172
595
  onScenarioStart === null || onScenarioStart === void 0 ? void 0 : onScenarioStart();
596
+ // プラグインのonScenarioStartフックを呼び出し
597
+ pluginManager.callHook("onScenarioStart", {
598
+ scenarioId: scenario.id,
599
+ scenarioName: scenario.name || "scenario",
600
+ });
601
+ }
602
+ }, [
603
+ isFirstRenderComplete,
604
+ hasStarted,
605
+ currentBlock,
606
+ pluginsLoaded,
607
+ onScenarioStart,
608
+ pluginManager,
609
+ scenario.id,
610
+ scenario.name,
611
+ ]);
612
+ // restartをusePlayerLogicの前に定義(分岐状態もリセット + キャンセルコールバック)
613
+ const restart = useCallback(() => {
614
+ // リスタート時はシナリオがキャンセルされたとみなす
615
+ if (hasStarted && !state.isEnded) {
616
+ onScenarioCancelled === null || onScenarioCancelled === void 0 ? void 0 : onScenarioCancelled();
617
+ // プラグインのonScenarioEndフックを呼び出し(シナリオ中断時)
618
+ pluginManager.callHook("onScenarioEnd", {
619
+ scenarioId: scenario.id,
620
+ scenarioName: scenario.name || "scenario",
621
+ });
173
622
  }
174
- }, [isFirstRenderComplete, hasStarted, currentBlock, onScenarioStart]);
175
- const { handleNext: handleNextInternal, handlePrevious: handlePreviousInternal, togglePlay, // eslint-disable-line @typescript-eslint/no-unused-vars
176
- restart: restartInternal, displayedCharacters, } = usePlayerLogic({
623
+ setIsFirstRenderComplete(false);
624
+ setHasStarted(false); // これにより次の useEffect onScenarioStart が呼ばれる
625
+ setCurrentBranchBlock(null);
626
+ branchNavigatorRef.current.reset();
627
+ resetAccumulated(); // 蓄積テキストをクリア
628
+ setState({
629
+ currentBlockIndex: 0,
630
+ isPlaying: autoplay,
631
+ isEnded: false,
632
+ });
633
+ }, [
634
+ autoplay,
635
+ hasStarted,
636
+ state.isEnded,
637
+ onScenarioCancelled,
638
+ resetAccumulated,
639
+ pluginManager,
640
+ scenario.id,
641
+ scenario.name,
642
+ ]);
643
+ const { handleNext: handleNextInternal, handlePrevious: handlePreviousInternal, togglePlay: _togglePlay, restart: _restartInternal, displayedCharacters, } = usePlayerLogic({
177
644
  state: Object.assign(Object.assign({}, state), { currentBlockIndex: actualBlockIndex }),
178
645
  setState: (newState) => {
179
646
  if (typeof newState === "function") {
@@ -199,13 +666,18 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
199
666
  onEnd,
200
667
  onScenarioEnd,
201
668
  autoplay,
669
+ branchState: branchState,
670
+ branchNavigator: branchNavigatorRef.current,
671
+ customRestart: restart,
672
+ variableManager: variableManagerRef.current,
673
+ disableKeyboardNavigation,
202
674
  });
203
675
  // プラグインイベント処理
204
676
  const realBlockIndex = currentBlock
205
677
  ? scenario.blocks.indexOf(currentBlock)
206
678
  : 0;
207
679
  usePluginEvents({
208
- pluginManager: pluginManagerRef.current,
680
+ pluginManager: pluginManager,
209
681
  currentBlock,
210
682
  displayedCharacters,
211
683
  blockIndex: state.currentBlockIndex,
@@ -213,7 +685,31 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
213
685
  allBlocks: scenario.blocks,
214
686
  realBlockIndex,
215
687
  pluginsLoaded,
688
+ transitionSource,
216
689
  });
690
+ // ActionNode実行処理
691
+ useEffect(() => {
692
+ var _a;
693
+ if (currentBlock && currentBlock.blockType === "action_node") {
694
+ const attrs = {};
695
+ // attributeValuesから属性値を収集
696
+ if (currentBlock.attributeValues) {
697
+ for (const attrValue of currentBlock.attributeValues) {
698
+ attrs[attrValue.attribute.name] = attrValue.value;
699
+ }
700
+ }
701
+ // ActionNode実行(actionNode.typeがundefinedでないことを確認)
702
+ if ((_a = currentBlock.actionNode) === null || _a === void 0 ? void 0 : _a.type) {
703
+ pluginManager.executeActionNode(currentBlock.actionNode.type, {
704
+ attributes: attrs,
705
+ currentBlock,
706
+ currentSpeaker: undefined, // TODO: 現在の発話キャラクターを取得
707
+ displayedCharacters: displayedCharacters,
708
+ api: pluginManager,
709
+ });
710
+ }
711
+ }
712
+ }, [currentBlock, displayedCharacters, pluginManager]);
217
713
  // 初期履歴構築(シナリオ開始時)
218
714
  useEffect(() => {
219
715
  if (isFirstRenderComplete && actualBlockIndex >= 0) {
@@ -221,41 +717,75 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
221
717
  }
222
718
  }, [isFirstRenderComplete, actualBlockIndex, backlog]);
223
719
  // ハンドラーをラップして表示可能インデックスで動作するように
224
- const handleNext = useCallback(() => {
225
- if (state.currentBlockIndex < displayableBlockIndices.length - 1) {
720
+ const handleNext = useCallback((source = "click") => {
721
+ // time_waitブロックはクリックで遷移できないようにする
722
+ if (source === "click" && (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "time_wait") {
723
+ return;
724
+ }
725
+ // 遷移元を設定
726
+ setTransitionSource(source);
727
+ if (currentBranchBlock &&
728
+ branchNavigatorRef.current.hasMoreBranchBlocks()) {
729
+ const nextBranchBlock = branchNavigatorRef.current.nextBranchBlock();
730
+ if (nextBranchBlock) {
731
+ setCurrentBranchBlock(convertBranchBlockToScenarioBlock(nextBranchBlock));
732
+ }
733
+ else {
734
+ setCurrentBranchBlock(null);
735
+ branchNavigatorRef.current.reset();
736
+ handleNextInternal();
737
+ }
738
+ }
739
+ else if (currentBranchBlock) {
740
+ setCurrentBranchBlock(null);
741
+ branchNavigatorRef.current.reset();
742
+ handleNextInternal();
743
+ }
744
+ else if (state.currentBlockIndex < displayableBlockIndices.length - 1) {
226
745
  handleNextInternal();
227
746
  }
228
747
  else {
229
748
  setState((prev) => (Object.assign(Object.assign({}, prev), { isEnded: true, isPlaying: false })));
749
+ // プラグインのonScenarioEndフックを呼び出し
750
+ pluginManager.callHook("onScenarioEnd", {
751
+ scenarioId: scenario.id,
752
+ scenarioName: scenario.name || "scenario",
753
+ });
230
754
  onEnd === null || onEnd === void 0 ? void 0 : onEnd();
231
755
  onScenarioEnd === null || onScenarioEnd === void 0 ? void 0 : onScenarioEnd();
232
756
  }
233
757
  }, [
758
+ currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType,
759
+ currentBranchBlock,
234
760
  state.currentBlockIndex,
235
761
  displayableBlockIndices.length,
236
762
  handleNextInternal,
237
763
  onEnd,
238
- onScenarioEnd,
764
+ onScenarioEnd, // プラグインのonScenarioEndフックを呼び出し
765
+ pluginManager,
766
+ scenario.id,
767
+ scenario.name,
239
768
  ]);
240
769
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
241
- const handlePrevious = useCallback(() => {
770
+ const _handlePrevious = useCallback(() => {
242
771
  if (state.currentBlockIndex > 0) {
243
772
  handlePreviousInternal();
244
773
  }
245
774
  }, [state.currentBlockIndex, handlePreviousInternal]);
246
- // restartをラップして、firstRenderCompleteもリセット
247
- const restart = useCallback(() => {
248
- // リスタート時はシナリオがキャンセルされたとみなす
249
- if (hasStarted && !state.isEnded) {
250
- onScenarioCancelled === null || onScenarioCancelled === void 0 ? void 0 : onScenarioCancelled();
775
+ // 現在の背景を計算
776
+ const currentBackground = useMemo(() => calculateCurrentBackground(actualBlockIndex), [calculateCurrentBackground, actualBlockIndex]);
777
+ // displayText を JSX に変換(PlaybackTextProvider 用)
778
+ const displayTextElement = useMemo(() => {
779
+ if (displayText.includes("\n")) {
780
+ return displayText.split("\n").map((line, index, array) => (_jsxs(React.Fragment, { children: [line, index < array.length - 1 && _jsx("br", {})] }, index)));
251
781
  }
252
- setIsFirstRenderComplete(false);
253
- setHasStarted(false);
254
- restartInternal();
255
- }, [hasStarted, state.isEnded, onScenarioCancelled, restartInternal]);
256
- // DataContext の構築 - displayedCharactersが必要なため usePlayerLogic の後に配置
782
+ return displayText;
783
+ }, [displayText]);
784
+ // DataContext の構築
785
+ // DataRefContext で安定した参照を提供することで、
786
+ // useDataAPI() を使うコンポーネントの再レンダリングを防ぐ
257
787
  const dataContext = useMemo(() => {
258
- var _a, _b;
788
+ var _a, _b, _c;
259
789
  return ({
260
790
  playback: {
261
791
  currentBlockIndex: actualBlockIndex,
@@ -263,7 +793,7 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
263
793
  scenarioId: scenario.id,
264
794
  scenarioName: scenario.name,
265
795
  currentBlock: currentBlock || null,
266
- displayText,
796
+ displayText: displayTextElement,
267
797
  isTyping,
268
798
  displayedCharacters,
269
799
  },
@@ -282,25 +812,57 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
282
812
  seVolume: mergedSettings.seVolume,
283
813
  voiceVolume: mergedSettings.voiceVolume,
284
814
  skipMode: "unread",
815
+ selectedFontFamily: mergedSettings.selectedFontFamily,
816
+ selectedUIFontFamily: mergedSettings.selectedUIFontFamily,
285
817
  },
286
818
  pluginAssets: {
287
819
  getAssetUrl: (pluginName, filename) => {
288
- return pluginManagerRef.current.getPluginAssetUrl(pluginName, filename);
820
+ return pluginManager.getPluginAssetUrl(pluginName, filename);
289
821
  },
290
822
  },
823
+ branchData: conversationBranch.branchState,
824
+ branchHistory: [],
825
+ background: currentBackground.length > 0 ? currentBackground[0] : null,
826
+ backgrounds: currentBackground,
827
+ fonts: {
828
+ fonts: (_c = scenario.fonts) !== null && _c !== void 0 ? _c : [],
829
+ selectedFontFamily: mergedSettings.selectedFontFamily,
830
+ selectedUIFontFamily: mergedSettings.selectedUIFontFamily,
831
+ isLoaded: fontsLoaded,
832
+ },
833
+ emotionEffect: emotionEffectState,
291
834
  });
292
835
  }, [
836
+ pluginManager,
293
837
  actualBlockIndex,
294
838
  scenario,
295
839
  currentBlock,
296
- displayText,
840
+ displayTextElement,
297
841
  isTyping,
298
842
  displayedCharacters,
299
843
  backlog,
300
- settings,
844
+ conversationBranch.branchState,
845
+ mergedSettings.aspectRatio,
846
+ mergedSettings.autoPlaySpeed,
847
+ emotionEffectState,
848
+ mergedSettings.bgObjectFit,
849
+ mergedSettings.bgmVolume,
850
+ mergedSettings.seVolume,
851
+ mergedSettings.textSpeed,
852
+ mergedSettings.voiceVolume,
853
+ mergedSettings.selectedFontFamily,
854
+ mergedSettings.selectedUIFontFamily,
855
+ currentBackground,
856
+ fontsLoaded,
301
857
  ]);
302
- // マウスホイールとタッチジェスチャーを無効化
858
+ // DataContextの参照を更新(プラグインがapi.data.get()を呼び出せるようにする)
303
859
  useEffect(() => {
860
+ dataContextRef.current = dataContext;
861
+ }, [dataContext]);
862
+ // マウスホイールとタッチジェスチャーを無効化(preventDefaultScrollがtrueの場合のみ)
863
+ useEffect(() => {
864
+ if (!preventDefaultScroll)
865
+ return;
304
866
  const handleWheel = (e) => {
305
867
  e.preventDefault();
306
868
  };
@@ -321,7 +883,7 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
321
883
  document.removeEventListener("touchmove", handleTouchMove);
322
884
  document.removeEventListener("gesturestart", handleGestureStart);
323
885
  };
324
- }, []);
886
+ }, [preventDefaultScroll]);
325
887
  // アスペクト比を計算
326
888
  const getAspectRatio = () => {
327
889
  if (!(settings === null || settings === void 0 ? void 0 : settings.aspectRatio))
@@ -329,23 +891,42 @@ export const Player = ({ scenario, settings, plugins = [], onEnd, onScenarioEnd,
329
891
  const [width, height] = settings.aspectRatio.split(":").map(Number);
330
892
  return `${width}/${height}`;
331
893
  };
894
+ const getAspectRatioValue = () => {
895
+ if (!(settings === null || settings === void 0 ? void 0 : settings.aspectRatio))
896
+ return 16 / 9;
897
+ const [width, height] = settings.aspectRatio.split(":").map(Number);
898
+ return width / height;
899
+ };
332
900
  // 条件付きレンダリングを JSX で処理(フックの後、early return なし)
333
- 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 || !isFirstRenderComplete) && (_jsx("div", { className: clsx("luna-player fixed inset-0 bg-black overflow-hidden flex items-center justify-center z-50", className), style: {
901
+ 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 ||
902
+ !fontsLoaded ||
903
+ !isFirstRenderComplete ||
904
+ !pluginsLoaded) && (_jsx("div", { className: clsx("luna-player fixed inset-0 bg-black overflow-hidden flex items-center justify-center z-50", className), style: {
334
905
  touchAction: "none",
335
906
  userSelect: "none",
336
907
  WebkitUserSelect: "none",
337
- } })), _jsx("div", { className: clsx("luna-player fixed inset-0 bg-black overflow-hidden flex items-center justify-center", className, (!imagesLoaded || !isFirstRenderComplete) && "opacity-0"), onClick: handleNext, style: {
908
+ } })), _jsx("div", { className: clsx("luna-player fixed inset-0 bg-black overflow-hidden flex items-center justify-center", className, (!imagesLoaded ||
909
+ !fontsLoaded ||
910
+ !isFirstRenderComplete ||
911
+ !pluginsLoaded) &&
912
+ "opacity-0"), onClick: () => handleNext("click"), style: {
338
913
  touchAction: "none",
339
914
  userSelect: "none",
340
915
  WebkitUserSelect: "none",
341
- }, 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, children: _jsxs(AudioProvider, { settings: {
916
+ }, children: _jsx("div", { ref: aspectRatioContainerRef, className: "relative bg-white flex flex-col overflow-hidden", style: {
917
+ aspectRatio: getAspectRatio(),
918
+ width: `min(100vw, calc(100vh * ${getAspectRatioValue()}))`,
919
+ height: `min(100vh, calc(100vw / ${getAspectRatioValue()}))`,
920
+ }, children: _jsx(DataProvider, { data: dataContext, onSettingsUpdate: handleSettingsUpdate, onEmotionEffectUpdate: handleEmotionEffectUpdate, children: _jsx(AudioProvider, { settings: {
342
921
  bgmVolume: mergedSettings.bgmVolume,
343
922
  seVolume: mergedSettings.seVolume,
344
923
  voiceVolume: mergedSettings.voiceVolume,
345
924
  effectVolume: mergedSettings.effectVolume,
346
925
  textSoundVolume: mergedSettings.textSoundVolume,
347
- }, children: [_jsx("div", { className: "h-full", children: _jsx(GameScreen, { scenario: scenario, currentBlock: currentBlock, displayedCharacters: displayedCharacters }) }), _jsx(OverlayUI, { children: _jsxs("div", { className: "h-full w-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.DialogueBox, pluginManager: pluginManagerRef.current, fallback: DialogueBox }), pluginManagerRef.current
348
- .getRegisteredComponents()
349
- .filter((type) => type !== ComponentType.DialogueBox) // DialogueBoxは既に上でレンダリング済み
350
- .map((componentType) => (_jsx(PluginComponentProvider, { type: componentType, pluginManager: pluginManagerRef.current }, componentType)))] }) })] }) }) }) })] }))] }));
926
+ muteAudio: (_b = mergedSettings.muteAudio) !== null && _b !== void 0 ? _b : false,
927
+ }, 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
928
+ .getRegisteredComponents()
929
+ .filter((type) => type !== ComponentType.DialogueBox &&
930
+ type !== ComponentType.ConversationBranch)
931
+ .map((componentType) => (_jsx(PluginComponentProvider, { type: componentType, pluginManager: pluginManager }, componentType)))] }) })] }) }) }) }) })] }))] }));
351
932
  };