@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.
- package/dist/Player.d.ts +1 -1
- package/dist/Player.js +676 -95
- package/dist/api/conversationBranch.d.ts +4 -0
- package/dist/api/conversationBranch.js +83 -0
- package/dist/components/BackgroundLayer.d.ts +19 -0
- package/dist/components/BackgroundLayer.js +220 -0
- package/dist/components/ClickWaitIndicator.d.ts +10 -0
- package/dist/components/ClickWaitIndicator.js +31 -0
- package/dist/components/ConversationBranchBox.d.ts +2 -0
- package/dist/components/ConversationBranchBox.js +29 -0
- package/dist/components/DialogueBox.js +16 -1
- package/dist/components/FontSettingsPanel.d.ts +10 -0
- package/dist/components/FontSettingsPanel.js +30 -0
- package/dist/components/FullscreenTextBox.d.ts +6 -0
- package/dist/components/FullscreenTextBox.js +70 -0
- package/dist/components/GameScreen.d.ts +9 -2
- package/dist/components/GameScreen.js +396 -80
- package/dist/components/OverlayUI.d.ts +2 -3
- package/dist/components/OverlayUI.js +3 -14
- package/dist/components/PluginComponentProvider.d.ts +3 -3
- package/dist/components/PluginComponentProvider.js +22 -4
- package/dist/components/TimeWaitIndicator.d.ts +15 -0
- package/dist/components/TimeWaitIndicator.js +17 -0
- package/dist/contexts/AudioContext.d.ts +1 -0
- package/dist/contexts/AudioContext.js +1 -0
- package/dist/contexts/DataContext.d.ts +3 -1
- package/dist/contexts/DataContext.js +104 -17
- package/dist/contexts/PlaybackTextContext.d.ts +32 -0
- package/dist/contexts/PlaybackTextContext.js +29 -0
- package/dist/data-api-types.d.ts +251 -0
- package/dist/data-api-types.js +6 -0
- package/dist/emotion-effect-types.d.ts +86 -0
- package/dist/emotion-effect-types.js +6 -0
- package/dist/hooks/useBacklog.js +3 -1
- package/dist/hooks/useConversationBranch.d.ts +16 -0
- package/dist/hooks/useConversationBranch.js +125 -0
- package/dist/hooks/useFontLoader.d.ts +30 -0
- package/dist/hooks/useFontLoader.js +192 -0
- package/dist/hooks/useFullscreenText.d.ts +17 -0
- package/dist/hooks/useFullscreenText.js +120 -0
- package/dist/hooks/useImagePreloader.d.ts +5 -0
- package/dist/hooks/useImagePreloader.js +53 -0
- package/dist/hooks/usePlayerLogic.d.ts +10 -3
- package/dist/hooks/usePlayerLogic.js +115 -18
- package/dist/hooks/usePluginAPI.js +1 -1
- package/dist/hooks/usePluginEvents.d.ts +4 -1
- package/dist/hooks/usePluginEvents.js +16 -11
- package/dist/hooks/usePreloadImages.js +27 -7
- package/dist/hooks/useSoundPlayer.d.ts +15 -0
- package/dist/hooks/useSoundPlayer.js +209 -0
- package/dist/hooks/useTypewriter.d.ts +6 -2
- package/dist/hooks/useTypewriter.js +42 -6
- package/dist/hooks/useVoice.js +4 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/plugin/PluginManager.d.ts +86 -5
- package/dist/plugin/PluginManager.js +427 -94
- package/dist/sdk.d.ts +133 -162
- package/dist/sdk.js +39 -4
- package/dist/types.d.ts +300 -4
- package/dist/utils/branchBlockConverter.d.ts +2 -0
- package/dist/utils/branchBlockConverter.js +21 -0
- package/dist/utils/branchNavigator.d.ts +14 -0
- package/dist/utils/branchNavigator.js +55 -0
- package/dist/utils/facePositionCalculator.js +0 -1
- package/dist/utils/variableManager.d.ts +18 -0
- package/dist/utils/variableManager.js +159 -0
- package/package.json +6 -6
- package/dist/components/ConversationLogUI.d.ts +0 -2
- package/dist/components/ConversationLogUI.js +0 -115
- package/dist/hooks/useConversationLog.d.ts +0 -14
- package/dist/hooks/useConversationLog.js +0 -82
- package/dist/hooks/useUIVisibility.d.ts +0 -9
- 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
|
-
|
|
31
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
44
|
-
setGlobalUIAPI(uiAPI,
|
|
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
|
-
|
|
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:
|
|
57
|
-
height:
|
|
121
|
+
width: rect.width,
|
|
122
|
+
height: rect.height,
|
|
58
123
|
});
|
|
59
124
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 = [
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
+
pluginManager.registerStyleElement("speakerName", speakerNameElement);
|
|
124
377
|
}
|
|
125
378
|
if (dialogueElement) {
|
|
126
|
-
|
|
127
|
-
|
|
379
|
+
pluginManager.registerStyleElement("dialogueBox", dialogueElement);
|
|
380
|
+
pluginManager.registerStyleElement("scenarioBlockContent", dialogueElement);
|
|
128
381
|
}
|
|
129
382
|
if (characterSpriteElement) {
|
|
130
|
-
|
|
383
|
+
pluginManager.registerStyleElement("characterSprite", characterSpriteElement);
|
|
131
384
|
}
|
|
132
385
|
if (gameScreenElement) {
|
|
133
|
-
|
|
134
|
-
|
|
386
|
+
pluginManager.registerStyleElement("gameScreen", gameScreenElement);
|
|
387
|
+
pluginManager.registerStyleElement("background", gameScreenElement);
|
|
135
388
|
}
|
|
136
389
|
}
|
|
137
|
-
}, [isFirstRenderComplete]);
|
|
138
|
-
|
|
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 =
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
770
|
+
const _handlePrevious = useCallback(() => {
|
|
242
771
|
if (state.currentBlockIndex > 0) {
|
|
243
772
|
handlePreviousInternal();
|
|
244
773
|
}
|
|
245
774
|
}, [state.currentBlockIndex, handlePreviousInternal]);
|
|
246
|
-
//
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
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
|
|
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
|
-
|
|
840
|
+
displayTextElement,
|
|
297
841
|
isTyping,
|
|
298
842
|
displayedCharacters,
|
|
299
843
|
backlog,
|
|
300
|
-
|
|
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 ||
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
};
|