@luna-editor/engine 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Player.d.ts +1 -1
- package/dist/Player.js +527 -85
- 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 +218 -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 +1 -0
- package/dist/components/GameScreen.js +363 -81
- package/dist/components/PluginComponentProvider.d.ts +2 -2
- package/dist/components/PluginComponentProvider.js +3 -3
- package/dist/components/TimeWaitIndicator.d.ts +15 -0
- package/dist/components/TimeWaitIndicator.js +17 -0
- package/dist/contexts/AudioContext.d.ts +14 -0
- package/dist/contexts/AudioContext.js +14 -0
- package/dist/contexts/DataContext.d.ts +4 -1
- package/dist/contexts/DataContext.js +82 -13
- package/dist/hooks/useBacklog.js +3 -0
- package/dist/hooks/useConversationBranch.d.ts +16 -0
- package/dist/hooks/useConversationBranch.js +125 -0
- package/dist/hooks/useFontLoader.d.ts +23 -0
- package/dist/hooks/useFontLoader.js +153 -0
- package/dist/hooks/useFullscreenText.d.ts +17 -0
- package/dist/hooks/useFullscreenText.js +120 -0
- package/dist/hooks/usePlayerLogic.d.ts +10 -3
- package/dist/hooks/usePlayerLogic.js +115 -18
- 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 +7 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/plugin/PluginManager.d.ts +66 -2
- package/dist/plugin/PluginManager.js +352 -79
- package/dist/sdk.d.ts +184 -22
- package/dist/sdk.js +27 -2
- package/dist/types.d.ts +303 -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 +1 -1
- 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/plugin/luna-react.d.ts +0 -41
- package/dist/plugin/luna-react.js +0 -99
|
@@ -1,111 +1,393 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
|
-
export const GameScreen = ({ scenario, currentBlock, displayedCharacters, }) => {
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
export const GameScreen = ({ scenario, currentBlock, previousBlock, displayedCharacters, }) => {
|
|
4
|
+
var _a;
|
|
5
|
+
// キャラクターごとのフェード状態を管理
|
|
6
|
+
const [fadeStates, setFadeStates] = useState(new Map());
|
|
7
|
+
const animationFrameRef = useRef(null);
|
|
8
|
+
const fadeStartTimeRef = useRef(new Map());
|
|
9
|
+
// 完了したフェードを追跡(同じpendingFadeで再度フェードしないため)
|
|
10
|
+
const completedFadesRef = useRef(new Set());
|
|
11
|
+
// コンテナサイズを取得(ResizeObserverで確実に取得)
|
|
12
|
+
const containerRef = useRef(null);
|
|
13
|
+
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const container = containerRef.current;
|
|
16
|
+
if (!container)
|
|
17
|
+
return;
|
|
18
|
+
const updateSize = () => {
|
|
19
|
+
const clientWidth = container.clientWidth;
|
|
20
|
+
const clientHeight = container.clientHeight;
|
|
21
|
+
setContainerSize({ width: clientWidth, height: clientHeight });
|
|
22
|
+
};
|
|
23
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
24
|
+
updateSize();
|
|
25
|
+
});
|
|
26
|
+
resizeObserver.observe(container);
|
|
27
|
+
updateSize();
|
|
28
|
+
return () => resizeObserver.disconnect();
|
|
29
|
+
}, []);
|
|
4
30
|
// シナリオ全体で使用される全ての画像を収集
|
|
5
31
|
const allImages = useMemo(() => {
|
|
6
32
|
const images = new Map();
|
|
33
|
+
// オブジェクトごとのbaseBodyStateを記憶(レイヤー機能用)
|
|
34
|
+
const baseBodyStateByObjectId = new Map();
|
|
35
|
+
// 1st pass: character_entranceブロックからbaseBodyStateを収集
|
|
7
36
|
scenario.blocks.forEach((block) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
translateY: block.speakerState.translateY,
|
|
20
|
-
translateX: block.speakerState.translateX,
|
|
37
|
+
if (block.characters) {
|
|
38
|
+
block.characters.forEach((char) => {
|
|
39
|
+
var _a;
|
|
40
|
+
if ((_a = char.baseBodyState) === null || _a === void 0 ? void 0 : _a.imageUrl) {
|
|
41
|
+
baseBodyStateByObjectId.set(char.objectId, {
|
|
42
|
+
imageUrl: char.baseBodyState.imageUrl,
|
|
43
|
+
scale: char.baseBodyState.scale,
|
|
44
|
+
translateX: char.baseBodyState.translateX,
|
|
45
|
+
translateY: char.baseBodyState.translateY,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
21
48
|
});
|
|
22
49
|
}
|
|
23
|
-
|
|
50
|
+
});
|
|
51
|
+
// 2nd pass: 全ブロックから画像を収集
|
|
52
|
+
scenario.blocks.forEach((block) => {
|
|
53
|
+
var _a;
|
|
54
|
+
// character_entranceブロックのキャラクター画像(レイヤー情報含む)
|
|
24
55
|
if (block.characters) {
|
|
25
56
|
block.characters.forEach((char) => {
|
|
26
|
-
|
|
57
|
+
var _a, _b, _c, _d, _e, _f;
|
|
58
|
+
if (char.entityState.imageUrl || ((_a = char.baseBodyState) === null || _a === void 0 ? void 0 : _a.imageUrl)) {
|
|
27
59
|
const key = `${char.objectId}-${char.entityStateId}`;
|
|
28
60
|
images.set(key, {
|
|
29
|
-
url: char.entityState.imageUrl,
|
|
61
|
+
url: (_b = char.entityState.imageUrl) !== null && _b !== void 0 ? _b : "",
|
|
30
62
|
objectId: char.objectId,
|
|
31
63
|
entityStateId: char.entityStateId,
|
|
32
64
|
scale: char.entityState.scale,
|
|
33
65
|
translateY: char.entityState.translateY,
|
|
34
66
|
translateX: char.entityState.translateX,
|
|
67
|
+
// レイヤー機能用
|
|
68
|
+
baseBodyUrl: (_c = char.baseBodyState) === null || _c === void 0 ? void 0 : _c.imageUrl,
|
|
69
|
+
baseBodyScale: (_d = char.baseBodyState) === null || _d === void 0 ? void 0 : _d.scale,
|
|
70
|
+
baseBodyTranslateX: (_e = char.baseBodyState) === null || _e === void 0 ? void 0 : _e.translateX,
|
|
71
|
+
baseBodyTranslateY: (_f = char.baseBodyState) === null || _f === void 0 ? void 0 : _f.translateY,
|
|
72
|
+
layers: char.entityState.layers,
|
|
35
73
|
});
|
|
36
74
|
}
|
|
37
75
|
});
|
|
38
76
|
}
|
|
77
|
+
// dialogue/narrationブロックのspeaker画像
|
|
78
|
+
// 既にレイヤー情報付きで登録されている場合は上書きしない
|
|
79
|
+
if (((_a = block.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl) &&
|
|
80
|
+
block.speakerId &&
|
|
81
|
+
block.speakerStateId) {
|
|
82
|
+
const key = `${block.speakerId}-${block.speakerStateId}`;
|
|
83
|
+
if (!images.has(key)) {
|
|
84
|
+
// レイヤー機能用:同じobjectIdのbaseBodyStateを継承
|
|
85
|
+
const baseBodyState = baseBodyStateByObjectId.get(block.speakerId);
|
|
86
|
+
images.set(key, {
|
|
87
|
+
url: block.speakerState.imageUrl,
|
|
88
|
+
objectId: block.speakerId,
|
|
89
|
+
entityStateId: block.speakerStateId,
|
|
90
|
+
scale: block.speakerState.scale,
|
|
91
|
+
translateY: block.speakerState.translateY,
|
|
92
|
+
translateX: block.speakerState.translateX,
|
|
93
|
+
// レイヤー機能用(baseBodyStateを継承、speakerStateからlayersを取得)
|
|
94
|
+
baseBodyUrl: baseBodyState === null || baseBodyState === void 0 ? void 0 : baseBodyState.imageUrl,
|
|
95
|
+
baseBodyScale: baseBodyState === null || baseBodyState === void 0 ? void 0 : baseBodyState.scale,
|
|
96
|
+
baseBodyTranslateX: baseBodyState === null || baseBodyState === void 0 ? void 0 : baseBodyState.translateX,
|
|
97
|
+
baseBodyTranslateY: baseBodyState === null || baseBodyState === void 0 ? void 0 : baseBodyState.translateY,
|
|
98
|
+
layers: block.speakerState.layers,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
39
102
|
});
|
|
40
103
|
return Array.from(images.values());
|
|
41
104
|
}, [scenario.blocks]);
|
|
105
|
+
// objectId-entityStateIdからImageDataを引くマップ
|
|
106
|
+
const imageDataMap = useMemo(() => {
|
|
107
|
+
const map = new Map();
|
|
108
|
+
for (const image of allImages) {
|
|
109
|
+
const key = `${image.objectId}-${image.entityStateId}`;
|
|
110
|
+
map.set(key, image);
|
|
111
|
+
}
|
|
112
|
+
return map;
|
|
113
|
+
}, [allImages]);
|
|
114
|
+
// フェードが必要かどうかを事前に計算(レンダリング時に使用)
|
|
115
|
+
const pendingFade = useMemo(() => {
|
|
116
|
+
var _a, _b, _c, _d, _e;
|
|
117
|
+
if (!previousBlock || !currentBlock)
|
|
118
|
+
return null;
|
|
119
|
+
const fadeTime = (_b = (_a = previousBlock.options) === null || _a === void 0 ? void 0 : _a.fadeToNextState) !== null && _b !== void 0 ? _b : 0;
|
|
120
|
+
if (fadeTime === 0)
|
|
121
|
+
return null;
|
|
122
|
+
if (previousBlock.speakerId &&
|
|
123
|
+
previousBlock.speakerId === currentBlock.speakerId &&
|
|
124
|
+
previousBlock.speakerStateId !== currentBlock.speakerStateId &&
|
|
125
|
+
((_c = previousBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl)) {
|
|
126
|
+
// ユニークなキーを生成(同じフェードを再度実行しないため)
|
|
127
|
+
const fadeKey = `${currentBlock.speakerId}:${previousBlock.speakerStateId}:${currentBlock.speakerStateId}`;
|
|
128
|
+
return {
|
|
129
|
+
objectId: currentBlock.speakerId,
|
|
130
|
+
previousEntityStateId: (_d = previousBlock.speakerStateId) !== null && _d !== void 0 ? _d : "",
|
|
131
|
+
currentEntityStateId: (_e = currentBlock.speakerStateId) !== null && _e !== void 0 ? _e : "",
|
|
132
|
+
fadeTime,
|
|
133
|
+
fadeKey,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}, [previousBlock, currentBlock]);
|
|
138
|
+
// pendingFadeが変わったら完了フェードをクリア
|
|
139
|
+
const fadeKey = pendingFade === null || pendingFade === void 0 ? void 0 : pendingFade.fadeKey;
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (fadeKey) {
|
|
142
|
+
// 新しいpendingFadeが来たら、完了フェードをクリア
|
|
143
|
+
completedFadesRef.current.clear();
|
|
144
|
+
}
|
|
145
|
+
}, [fadeKey]);
|
|
146
|
+
// フェード状態の初期化(描画前に同期的に実行)
|
|
147
|
+
useLayoutEffect(() => {
|
|
148
|
+
var _a;
|
|
149
|
+
if (!pendingFade)
|
|
150
|
+
return;
|
|
151
|
+
if (!((_a = previousBlock === null || previousBlock === void 0 ? void 0 : previousBlock.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl))
|
|
152
|
+
return;
|
|
153
|
+
const { objectId, previousEntityStateId, fadeTime, fadeKey } = pendingFade;
|
|
154
|
+
// 既に完了したフェードなら何もしない
|
|
155
|
+
if (completedFadesRef.current.has(fadeKey))
|
|
156
|
+
return;
|
|
157
|
+
// 既にこのオブジェクトのフェードが進行中なら何もしない
|
|
158
|
+
if (fadeStates.has(objectId))
|
|
159
|
+
return;
|
|
160
|
+
// フェード状態を初期化
|
|
161
|
+
setFadeStates((prev) => {
|
|
162
|
+
var _a, _b;
|
|
163
|
+
const newMap = new Map(prev);
|
|
164
|
+
newMap.set(objectId, {
|
|
165
|
+
previousEntityStateId,
|
|
166
|
+
previousImageUrl: (_b = (_a = previousBlock.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl) !== null && _b !== void 0 ? _b : "",
|
|
167
|
+
fadeProgress: 0,
|
|
168
|
+
fadeDuration: fadeTime,
|
|
169
|
+
});
|
|
170
|
+
return newMap;
|
|
171
|
+
});
|
|
172
|
+
}, [pendingFade, (_a = previousBlock === null || previousBlock === void 0 ? void 0 : previousBlock.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl, fadeStates]);
|
|
173
|
+
// アニメーション開始(レンダリング後に実行)
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (!pendingFade)
|
|
176
|
+
return;
|
|
177
|
+
const { objectId, fadeTime, fadeKey } = pendingFade;
|
|
178
|
+
// 既に完了したフェードなら何もしない
|
|
179
|
+
if (completedFadesRef.current.has(fadeKey))
|
|
180
|
+
return;
|
|
181
|
+
// 既にアニメーション開始済みなら何もしない
|
|
182
|
+
if (fadeStartTimeRef.current.has(objectId))
|
|
183
|
+
return;
|
|
184
|
+
fadeStartTimeRef.current.set(objectId, performance.now());
|
|
185
|
+
// アニメーションループ
|
|
186
|
+
const animate = (currentTime) => {
|
|
187
|
+
const startTime = fadeStartTimeRef.current.get(objectId);
|
|
188
|
+
if (startTime === undefined)
|
|
189
|
+
return;
|
|
190
|
+
const elapsed = currentTime - startTime;
|
|
191
|
+
const progress = Math.min(elapsed / fadeTime, 1);
|
|
192
|
+
setFadeStates((prev) => {
|
|
193
|
+
const newMap = new Map(prev);
|
|
194
|
+
const state = newMap.get(objectId);
|
|
195
|
+
if (state) {
|
|
196
|
+
newMap.set(objectId, Object.assign(Object.assign({}, state), { fadeProgress: progress }));
|
|
197
|
+
}
|
|
198
|
+
return newMap;
|
|
199
|
+
});
|
|
200
|
+
if (progress < 1) {
|
|
201
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// フェード完了、状態クリア
|
|
205
|
+
completedFadesRef.current.add(fadeKey);
|
|
206
|
+
setFadeStates((prev) => {
|
|
207
|
+
const newMap = new Map(prev);
|
|
208
|
+
newMap.delete(objectId);
|
|
209
|
+
return newMap;
|
|
210
|
+
});
|
|
211
|
+
fadeStartTimeRef.current.delete(objectId);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
215
|
+
return () => {
|
|
216
|
+
if (animationFrameRef.current) {
|
|
217
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}, [pendingFade]);
|
|
42
221
|
// 現在の話者
|
|
43
222
|
const currentSpeaker = (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.speakerId)
|
|
44
223
|
? displayedCharacters.find((char) => char.objectId === currentBlock.speakerId)
|
|
45
224
|
: null;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
225
|
+
// キャラクター描画用のヘルパー関数
|
|
226
|
+
const renderCharacter = (image, displayedChar, isCurrentSpeaker, keyPrefix) => {
|
|
227
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
228
|
+
// フェード状態を確認
|
|
229
|
+
const fadeState = fadeStates.get(image.objectId);
|
|
230
|
+
const isFadingIn = fadeState && fadeState.previousEntityStateId !== image.entityStateId;
|
|
231
|
+
const isFadingOut = fadeState && fadeState.previousEntityStateId === image.entityStateId;
|
|
232
|
+
// pendingFadeがあるがfadeStateがまだない場合(初回レンダリング)
|
|
233
|
+
// ただし、既に完了したフェードは除外
|
|
234
|
+
const isPendingFadeIn = !fadeState &&
|
|
235
|
+
(pendingFade === null || pendingFade === void 0 ? void 0 : pendingFade.objectId) === image.objectId &&
|
|
236
|
+
pendingFade.currentEntityStateId === image.entityStateId &&
|
|
237
|
+
!completedFadesRef.current.has(pendingFade.fadeKey);
|
|
238
|
+
const isPendingFadeOut = !fadeState &&
|
|
239
|
+
(pendingFade === null || pendingFade === void 0 ? void 0 : pendingFade.objectId) === image.objectId &&
|
|
240
|
+
pendingFade.previousEntityStateId === image.entityStateId &&
|
|
241
|
+
!completedFadesRef.current.has(pendingFade.fadeKey);
|
|
242
|
+
// 表示すべきかどうか
|
|
243
|
+
const shouldDisplay = displayedChar || isCurrentSpeaker || isFadingOut || isPendingFadeOut;
|
|
244
|
+
// opacityを決定
|
|
245
|
+
let opacity = 1;
|
|
246
|
+
if (isFadingIn || isPendingFadeIn) {
|
|
247
|
+
// フェードイン中、またはフェード開始直後(まだstateがない)
|
|
248
|
+
opacity = (_a = fadeState === null || fadeState === void 0 ? void 0 : fadeState.fadeProgress) !== null && _a !== void 0 ? _a : 0;
|
|
249
|
+
}
|
|
250
|
+
else if (isFadingOut || isPendingFadeOut) {
|
|
251
|
+
// フェードアウト中
|
|
252
|
+
opacity = fadeState ? 1 - fadeState.fadeProgress : 1;
|
|
253
|
+
}
|
|
254
|
+
// 明るさを決定
|
|
255
|
+
let brightness = 1;
|
|
256
|
+
if (displayedCharacters.length > 0 && displayedChar) {
|
|
257
|
+
// 複数キャラクター表示で、現在の話者でない場合
|
|
258
|
+
if (currentSpeaker &&
|
|
259
|
+
currentSpeaker.objectId !== displayedChar.objectId) {
|
|
260
|
+
brightness = 0.8;
|
|
65
261
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (displayedChar
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
displayedChar.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
y: (_e = displayedChar.positionY) !== null && _e !== void 0 ? _e : 1.0,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
// カスタム位置が設定されていない場合はデフォルト位置のまま
|
|
262
|
+
}
|
|
263
|
+
// z-indexを決定
|
|
264
|
+
const zIndex = (_b = displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.zIndex) !== null && _b !== void 0 ? _b : (displayedChar ? 200 : 0);
|
|
265
|
+
// スケールを決定
|
|
266
|
+
// displayedCharのscaleが設定されている場合はそれを使用し、EntityStateのscaleと組み合わせる
|
|
267
|
+
// 設定されていない場合はEntityStateのscaleのみ使用
|
|
268
|
+
const characterScale = (displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.scale)
|
|
269
|
+
? displayedChar.scale * ((_c = image.scale) !== null && _c !== void 0 ? _c : 1)
|
|
270
|
+
: ((_d = image.scale) !== null && _d !== void 0 ? _d : 1);
|
|
271
|
+
// 位置を決定(カスタム位置またはデフォルト位置)
|
|
272
|
+
// 新座標系: x: -1=左見切れ, 0=中央, 1=右見切れ / y: -1=上見切れ, 0=中央, 1=下見切れ
|
|
273
|
+
let finalPosition = { x: 0, y: 1.0 }; // デフォルトは中央、下端
|
|
274
|
+
if (displayedChar) {
|
|
275
|
+
// カスタム位置が設定されている場合
|
|
276
|
+
if (displayedChar.positionX !== null &&
|
|
277
|
+
displayedChar.positionY !== null) {
|
|
278
|
+
finalPosition = {
|
|
279
|
+
x: (_e = displayedChar.positionX) !== null && _e !== void 0 ? _e : 0,
|
|
280
|
+
y: (_f = displayedChar.positionY) !== null && _f !== void 0 ? _f : 0,
|
|
281
|
+
};
|
|
90
282
|
}
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
283
|
+
// カスタム位置が設定されていない場合はデフォルト位置のまま
|
|
284
|
+
}
|
|
285
|
+
// コンテナ相対の位置計算(パーセンテージ使用)
|
|
286
|
+
// positionX: -1.0 = 完全に左に見切れ, 0.0 = 中央, 1.0 = 完全に右に見切れ
|
|
287
|
+
// positionY: -1.0 = 完全に上に見切れ, 0.0 = 中央, 1.0 = 完全に下に見切れ
|
|
288
|
+
// -1〜1の範囲を0〜100%に変換: (value + 1) / 2 * 100
|
|
289
|
+
const leftPercent = ((finalPosition.x + 1) / 2) * 100;
|
|
290
|
+
const topPercent = ((finalPosition.y + 1) / 2) * 100;
|
|
291
|
+
// 完全に見切れるようにtranslateを動的に計算
|
|
292
|
+
// X: -1→0%, 0→-50%, 1→-100% (画像が完全に見切れる)
|
|
293
|
+
// Y: -1→0%, 0→-50%, 1→-100% (画像が完全に見切れる)
|
|
294
|
+
const translateXPercent = ((finalPosition.x - 1) / 2) * 100; // -100%〜0%
|
|
295
|
+
const translateYPercent = ((finalPosition.y - 1) / 2) * 100; // -100%〜0%
|
|
296
|
+
// レイヤー機能を使用するか判定(素体画像がある場合はレイヤーモード)
|
|
297
|
+
const hasLayerFeature = !!image.baseBodyUrl;
|
|
298
|
+
// 有効な画像がない場合は描画しない
|
|
299
|
+
const hasValidImage = hasLayerFeature ? !!image.baseBodyUrl : !!image.url;
|
|
300
|
+
if (!hasValidImage) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
// 現在の状態のlayersを取得(displayedCharがあればそれを優先、なければimageのlayers)
|
|
304
|
+
const currentLayers = (_h = (_g = displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.entityState) === null || _g === void 0 ? void 0 : _g.layers) !== null && _h !== void 0 ? _h : image.layers;
|
|
305
|
+
// コンテナサイズが取得できていない場合はスキップ
|
|
306
|
+
if (containerSize.width === 0 || containerSize.height === 0) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
// CharacterEditDialogと完全に同じ方式
|
|
310
|
+
// 画像の高さはコンテナの高さ × スケール
|
|
311
|
+
const baseHeight = containerSize.height * characterScale;
|
|
312
|
+
// 位置をピクセルで計算(CharacterEditDialogと同じ)
|
|
313
|
+
const leftPx = (leftPercent / 100) * containerSize.width;
|
|
314
|
+
const topPx = (topPercent / 100) * containerSize.height;
|
|
315
|
+
return (_jsx("div", { style: {
|
|
316
|
+
position: "absolute",
|
|
317
|
+
visibility: shouldDisplay ? "visible" : "hidden",
|
|
318
|
+
opacity: opacity,
|
|
319
|
+
filter: `brightness(${brightness})`,
|
|
320
|
+
zIndex: zIndex,
|
|
321
|
+
// CharacterEditDialogと完全に同じ: left/topを0にしてtransformで位置制御
|
|
322
|
+
// translate(%)は画像サイズに対する割合
|
|
323
|
+
left: 0,
|
|
324
|
+
top: 0,
|
|
325
|
+
transform: `translate(${leftPx}px, ${topPx}px) translate(${translateXPercent}%, ${translateYPercent}%)`,
|
|
326
|
+
transition: fadeState ? "none" : undefined,
|
|
327
|
+
}, "data-character-id": image.objectId, "data-character-sprite": true, children: hasLayerFeature && image.baseBodyUrl ? (_jsxs(_Fragment, { children: [_jsx("img", { src: image.baseBodyUrl, alt: "", style: {
|
|
328
|
+
height: `${baseHeight}px`,
|
|
329
|
+
width: "auto",
|
|
106
330
|
objectFit: "contain",
|
|
107
|
-
transform: `
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
331
|
+
transform: `translateX(${image.baseBodyTranslateX || 0}%) translateY(${image.baseBodyTranslateY || 0}%)`,
|
|
332
|
+
} }), currentLayers === null || currentLayers === void 0 ? void 0 : currentLayers.filter((layer) => layer.imageUrl).sort((a, b) => a.sortOrder - b.sortOrder).map((layer) => (_jsx("img", { src: layer.imageUrl, alt: "", style: {
|
|
333
|
+
position: "absolute",
|
|
334
|
+
left: 0,
|
|
335
|
+
top: 0,
|
|
336
|
+
height: `${baseHeight}px`,
|
|
337
|
+
width: "auto",
|
|
338
|
+
objectFit: "contain",
|
|
339
|
+
transform: `translateX(${image.translateX || 0}%) translateY(${image.translateY || 0}%)`,
|
|
340
|
+
} }, layer.id)))] })) : (
|
|
341
|
+
/* 従来の単一画像表示 */
|
|
342
|
+
_jsx("img", { src: image.url, alt: "", style: {
|
|
343
|
+
height: `${baseHeight}px`,
|
|
344
|
+
width: "auto",
|
|
345
|
+
objectFit: "contain",
|
|
346
|
+
transform: `translateX(${image.translateX || 0}%) translateY(${image.translateY || 0}%)`,
|
|
347
|
+
} })) }, `${keyPrefix}-${image.objectId}-${image.entityStateId}`));
|
|
348
|
+
};
|
|
349
|
+
return (_jsx("div", { ref: containerRef, className: "w-full h-full relative overflow-hidden", children: _jsxs("div", { className: "absolute inset-0", children: [allImages.map((image) => {
|
|
350
|
+
var _a;
|
|
351
|
+
return (_jsxs("div", { style: { display: "none" }, children: [image.baseBodyUrl && _jsx("img", { src: image.baseBodyUrl, alt: "" }), image.url && _jsx("img", { src: image.url, alt: "" }), (_a = image.layers) === null || _a === void 0 ? void 0 : _a.map((layer) => (_jsx("img", { src: layer.imageUrl, alt: "" }, layer.id)))] }, `preload-${image.objectId}-${image.entityStateId}`));
|
|
352
|
+
}), displayedCharacters.map((char) => {
|
|
353
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
354
|
+
// このキャラクターの現在の状態に対応するImageDataを取得
|
|
355
|
+
const imageKey = `${char.objectId}-${char.entityStateId}`;
|
|
356
|
+
const image = imageDataMap.get(imageKey);
|
|
357
|
+
if (!image) {
|
|
358
|
+
// ImageDataがない場合はdisplayedCharacterの情報から直接描画
|
|
359
|
+
// (シナリオに含まれていない状態への変更など)
|
|
360
|
+
const fallbackImage = {
|
|
361
|
+
url: (_b = (_a = char.entityState) === null || _a === void 0 ? void 0 : _a.imageUrl) !== null && _b !== void 0 ? _b : "",
|
|
362
|
+
objectId: char.objectId,
|
|
363
|
+
entityStateId: char.entityStateId,
|
|
364
|
+
scale: (_c = char.entityState) === null || _c === void 0 ? void 0 : _c.scale,
|
|
365
|
+
translateX: (_d = char.entityState) === null || _d === void 0 ? void 0 : _d.translateX,
|
|
366
|
+
translateY: (_e = char.entityState) === null || _e === void 0 ? void 0 : _e.translateY,
|
|
367
|
+
baseBodyUrl: (_f = char.baseBodyState) === null || _f === void 0 ? void 0 : _f.imageUrl,
|
|
368
|
+
baseBodyScale: (_g = char.baseBodyState) === null || _g === void 0 ? void 0 : _g.scale,
|
|
369
|
+
baseBodyTranslateX: (_h = char.baseBodyState) === null || _h === void 0 ? void 0 : _h.translateX,
|
|
370
|
+
baseBodyTranslateY: (_j = char.baseBodyState) === null || _j === void 0 ? void 0 : _j.translateY,
|
|
371
|
+
layers: (_k = char.entityState) === null || _k === void 0 ? void 0 : _k.layers,
|
|
372
|
+
};
|
|
373
|
+
return renderCharacter(fallbackImage, char, false, "char");
|
|
374
|
+
}
|
|
375
|
+
return renderCharacter(image, char, false, "char");
|
|
376
|
+
}), displayedCharacters.length === 0 &&
|
|
377
|
+
currentBlock.speakerId &&
|
|
378
|
+
currentBlock.speakerStateId && (_jsxs(_Fragment, { children: [(() => {
|
|
379
|
+
const imageKey = `${currentBlock.speakerId}-${currentBlock.speakerStateId}`;
|
|
380
|
+
const image = imageDataMap.get(imageKey);
|
|
381
|
+
if (!image)
|
|
382
|
+
return null;
|
|
383
|
+
return renderCharacter(image, null, true, "speaker");
|
|
384
|
+
})(), pendingFade &&
|
|
385
|
+
!completedFadesRef.current.has(pendingFade.fadeKey) &&
|
|
386
|
+
(() => {
|
|
387
|
+
const imageKey = `${pendingFade.objectId}-${pendingFade.previousEntityStateId}`;
|
|
388
|
+
const image = imageDataMap.get(imageKey);
|
|
389
|
+
if (!image)
|
|
390
|
+
return null;
|
|
391
|
+
return renderCharacter(image, null, false, "fadeout");
|
|
392
|
+
})()] }))] }) }));
|
|
111
393
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import type { PluginManager } from "../plugin/PluginManager";
|
|
3
|
-
import { ComponentType } from "../sdk";
|
|
3
|
+
import type { ComponentType } from "../sdk";
|
|
4
4
|
interface PluginComponentProviderProps {
|
|
5
5
|
type: ComponentType;
|
|
6
6
|
pluginManager: PluginManager;
|
|
7
|
-
fallback?: React.ComponentType
|
|
7
|
+
fallback?: React.ComponentType;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Wrapper component that renders a plugin-registered component or a fallback
|
|
@@ -6,10 +6,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
export function PluginComponentProvider({ type, pluginManager, fallback: FallbackComponent, }) {
|
|
7
7
|
const Component = pluginManager.getComponent(type);
|
|
8
8
|
if (Component) {
|
|
9
|
-
return _jsx(Component, {}
|
|
9
|
+
return _jsx(Component, {});
|
|
10
10
|
}
|
|
11
11
|
if (FallbackComponent) {
|
|
12
|
-
return _jsx(FallbackComponent, {}
|
|
12
|
+
return _jsx(FallbackComponent, {});
|
|
13
13
|
}
|
|
14
14
|
// If no component is registered and no fallback provided, show a warning
|
|
15
15
|
console.warn(`No component registered for type: ${type}`);
|
|
@@ -20,5 +20,5 @@ export function PluginComponentProvider({ type, pluginManager, fallback: Fallbac
|
|
|
20
20
|
borderRadius: "4px",
|
|
21
21
|
color: "#666",
|
|
22
22
|
textAlign: "center",
|
|
23
|
-
}, children: [_jsxs("p", { children: ["Component not found: ", type] }), _jsx("p", { style: { fontSize: "12px", marginTop: "10px" }, children: "Please install a plugin that provides this component" })] }
|
|
23
|
+
}, children: [_jsxs("p", { children: ["Component not found: ", type] }), _jsx("p", { style: { fontSize: "12px", marginTop: "10px" }, children: "Please install a plugin that provides this component" })] }));
|
|
24
24
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
interface TimeWaitIndicatorProps {
|
|
3
|
+
/** 待機時間(秒) */
|
|
4
|
+
duration: number;
|
|
5
|
+
/** 待機完了時のコールバック */
|
|
6
|
+
onComplete: () => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 時間待ちコンポーネント
|
|
11
|
+
* time_waitブロックで使用され、指定秒数経過後に自動で次に進む
|
|
12
|
+
* UIは表示せず、タイマー機能のみを提供
|
|
13
|
+
*/
|
|
14
|
+
export declare const TimeWaitIndicator: React.FC<TimeWaitIndicatorProps>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* 時間待ちコンポーネント
|
|
5
|
+
* time_waitブロックで使用され、指定秒数経過後に自動で次に進む
|
|
6
|
+
* UIは表示せず、タイマー機能のみを提供
|
|
7
|
+
*/
|
|
8
|
+
export const TimeWaitIndicator = ({ duration, onComplete, }) => {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const timeoutId = setTimeout(() => {
|
|
11
|
+
onComplete();
|
|
12
|
+
}, duration * 1000);
|
|
13
|
+
return () => clearTimeout(timeoutId);
|
|
14
|
+
}, [duration, onComplete]);
|
|
15
|
+
// UIは表示しない
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface AudioSettings {
|
|
3
|
+
bgmVolume: number;
|
|
4
|
+
seVolume: number;
|
|
5
|
+
voiceVolume: number;
|
|
6
|
+
effectVolume: number;
|
|
7
|
+
textSoundVolume: number;
|
|
8
|
+
muteAudio: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const AudioProvider: ({ children, settings, }: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
settings: AudioSettings;
|
|
13
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare const useAudioSettings: () => AudioSettings;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const AudioContext = createContext({
|
|
4
|
+
bgmVolume: 1.0,
|
|
5
|
+
seVolume: 1.0,
|
|
6
|
+
voiceVolume: 1.0,
|
|
7
|
+
effectVolume: 1.0,
|
|
8
|
+
textSoundVolume: 1.0,
|
|
9
|
+
muteAudio: false,
|
|
10
|
+
});
|
|
11
|
+
export const AudioProvider = ({ children, settings, }) => {
|
|
12
|
+
return (_jsx(AudioContext.Provider, { value: settings, children: children }));
|
|
13
|
+
};
|
|
14
|
+
export const useAudioSettings = () => useContext(AudioContext);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type { DataAPI, DataContext } from "../sdk";
|
|
2
|
+
import type { DataAPI, DataContext, PlayerSettingsData } from "../sdk";
|
|
3
|
+
type SettingsUpdater = (settings: Partial<PlayerSettingsData>) => void;
|
|
3
4
|
/**
|
|
4
5
|
* データプロバイダー
|
|
5
6
|
* プラグインがシナリオ情報にリアクティブにアクセスするためのProvider
|
|
@@ -15,6 +16,7 @@ import type { DataAPI, DataContext } from "../sdk";
|
|
|
15
16
|
*/
|
|
16
17
|
export declare const DataProvider: React.FC<{
|
|
17
18
|
data: DataContext;
|
|
19
|
+
onSettingsUpdate?: SettingsUpdater;
|
|
18
20
|
children: ReactNode;
|
|
19
21
|
}>;
|
|
20
22
|
/**
|
|
@@ -22,3 +24,4 @@ export declare const DataProvider: React.FC<{
|
|
|
22
24
|
* プラグインから呼び出される
|
|
23
25
|
*/
|
|
24
26
|
export declare function useDataAPI(): DataAPI;
|
|
27
|
+
export {};
|