@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
@@ -1,111 +1,427 @@
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 { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
3
+ /**
4
+ * ゲームスクリーンコンポーネント
5
+ * React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
6
+ * これにより displayText の更新による不要な再描画を防止
7
+ */
8
+ export const GameScreen = memo(function GameScreen({ scenario, currentBlock, previousBlock, displayedCharacters, inactiveCharacterBrightness = 0.8, characterSpacing = 0.1, }) {
9
+ var _a;
10
+ // キャラクターごとのフェード状態を管理
11
+ const [fadeStates, setFadeStates] = useState(new Map());
12
+ const animationFrameRef = useRef(null);
13
+ const fadeStartTimeRef = useRef(new Map());
14
+ // 完了したフェードを追跡(同じpendingFadeで再度フェードしないため)
15
+ const completedFadesRef = useRef(new Set());
16
+ // コンテナサイズを取得(ResizeObserverで確実に取得)
17
+ const containerRef = useRef(null);
18
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
19
+ useEffect(() => {
20
+ const container = containerRef.current;
21
+ if (!container)
22
+ return;
23
+ const updateSize = () => {
24
+ const clientWidth = container.clientWidth;
25
+ const clientHeight = container.clientHeight;
26
+ setContainerSize({ width: clientWidth, height: clientHeight });
27
+ };
28
+ const resizeObserver = new ResizeObserver(() => {
29
+ updateSize();
30
+ });
31
+ resizeObserver.observe(container);
32
+ updateSize();
33
+ return () => resizeObserver.disconnect();
34
+ }, []);
4
35
  // シナリオ全体で使用される全ての画像を収集
5
36
  const allImages = useMemo(() => {
6
37
  const images = new Map();
38
+ // オブジェクトごとのbaseBodyStateを記憶(レイヤー機能用)
39
+ const baseBodyStateByObjectId = new Map();
40
+ // 1st pass: character_entranceブロックからbaseBodyStateを収集
7
41
  scenario.blocks.forEach((block) => {
8
- var _a;
9
- // dialogue/narrationブロックのspeaker画像
10
- if (((_a = block.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl) &&
11
- block.speakerId &&
12
- block.speakerStateId) {
13
- const key = `${block.speakerId}-${block.speakerStateId}`;
14
- images.set(key, {
15
- url: block.speakerState.imageUrl,
16
- objectId: block.speakerId,
17
- entityStateId: block.speakerStateId,
18
- scale: block.speakerState.scale,
19
- translateY: block.speakerState.translateY,
20
- translateX: block.speakerState.translateX,
42
+ if (block.characters) {
43
+ block.characters.forEach((char) => {
44
+ var _a;
45
+ if ((_a = char.baseBodyState) === null || _a === void 0 ? void 0 : _a.imageUrl) {
46
+ baseBodyStateByObjectId.set(char.objectId, {
47
+ imageUrl: char.baseBodyState.imageUrl,
48
+ scale: char.baseBodyState.scale,
49
+ translateX: char.baseBodyState.translateX,
50
+ translateY: char.baseBodyState.translateY,
51
+ });
52
+ }
21
53
  });
22
54
  }
23
- // character_entranceブロックのキャラクター画像
55
+ });
56
+ // 2nd pass: 全ブロックから画像を収集
57
+ scenario.blocks.forEach((block) => {
58
+ var _a;
59
+ // character_entranceブロックのキャラクター画像(レイヤー情報含む)
24
60
  if (block.characters) {
25
61
  block.characters.forEach((char) => {
26
- if (char.entityState.imageUrl) {
62
+ var _a, _b, _c, _d, _e, _f;
63
+ if (char.entityState.imageUrl || ((_a = char.baseBodyState) === null || _a === void 0 ? void 0 : _a.imageUrl)) {
27
64
  const key = `${char.objectId}-${char.entityStateId}`;
28
65
  images.set(key, {
29
- url: char.entityState.imageUrl,
66
+ url: (_b = char.entityState.imageUrl) !== null && _b !== void 0 ? _b : "",
30
67
  objectId: char.objectId,
31
68
  entityStateId: char.entityStateId,
32
69
  scale: char.entityState.scale,
33
70
  translateY: char.entityState.translateY,
34
71
  translateX: char.entityState.translateX,
72
+ // レイヤー機能用
73
+ baseBodyUrl: (_c = char.baseBodyState) === null || _c === void 0 ? void 0 : _c.imageUrl,
74
+ baseBodyScale: (_d = char.baseBodyState) === null || _d === void 0 ? void 0 : _d.scale,
75
+ baseBodyTranslateX: (_e = char.baseBodyState) === null || _e === void 0 ? void 0 : _e.translateX,
76
+ baseBodyTranslateY: (_f = char.baseBodyState) === null || _f === void 0 ? void 0 : _f.translateY,
77
+ layers: char.entityState.layers,
35
78
  });
36
79
  }
37
80
  });
38
81
  }
82
+ // dialogue/narrationブロックのspeaker画像
83
+ // 既にレイヤー情報付きで登録されている場合は上書きしない
84
+ if (((_a = block.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl) &&
85
+ block.speakerId &&
86
+ block.speakerStateId) {
87
+ const key = `${block.speakerId}-${block.speakerStateId}`;
88
+ if (!images.has(key)) {
89
+ // レイヤー機能用:同じobjectIdのbaseBodyStateを継承
90
+ const baseBodyState = baseBodyStateByObjectId.get(block.speakerId);
91
+ images.set(key, {
92
+ url: block.speakerState.imageUrl,
93
+ objectId: block.speakerId,
94
+ entityStateId: block.speakerStateId,
95
+ scale: block.speakerState.scale,
96
+ translateY: block.speakerState.translateY,
97
+ translateX: block.speakerState.translateX,
98
+ // レイヤー機能用(baseBodyStateを継承、speakerStateからlayersを取得)
99
+ baseBodyUrl: baseBodyState === null || baseBodyState === void 0 ? void 0 : baseBodyState.imageUrl,
100
+ baseBodyScale: baseBodyState === null || baseBodyState === void 0 ? void 0 : baseBodyState.scale,
101
+ baseBodyTranslateX: baseBodyState === null || baseBodyState === void 0 ? void 0 : baseBodyState.translateX,
102
+ baseBodyTranslateY: baseBodyState === null || baseBodyState === void 0 ? void 0 : baseBodyState.translateY,
103
+ layers: block.speakerState.layers,
104
+ });
105
+ }
106
+ }
39
107
  });
40
108
  return Array.from(images.values());
41
109
  }, [scenario.blocks]);
110
+ // objectId-entityStateIdからImageDataを引くマップ
111
+ const imageDataMap = useMemo(() => {
112
+ const map = new Map();
113
+ for (const image of allImages) {
114
+ const key = `${image.objectId}-${image.entityStateId}`;
115
+ map.set(key, image);
116
+ }
117
+ return map;
118
+ }, [allImages]);
119
+ // フェードが必要かどうかを事前に計算(レンダリング時に使用)
120
+ const pendingFade = useMemo(() => {
121
+ var _a, _b, _c, _d, _e;
122
+ if (!previousBlock || !currentBlock)
123
+ return null;
124
+ const fadeTime = (_b = (_a = previousBlock.options) === null || _a === void 0 ? void 0 : _a.fadeToNextState) !== null && _b !== void 0 ? _b : 0;
125
+ if (fadeTime === 0)
126
+ return null;
127
+ if (previousBlock.speakerId &&
128
+ previousBlock.speakerId === currentBlock.speakerId &&
129
+ previousBlock.speakerStateId !== currentBlock.speakerStateId &&
130
+ ((_c = previousBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl)) {
131
+ // ユニークなキーを生成(同じフェードを再度実行しないため)
132
+ const fadeKey = `${currentBlock.speakerId}:${previousBlock.speakerStateId}:${currentBlock.speakerStateId}`;
133
+ return {
134
+ objectId: currentBlock.speakerId,
135
+ previousEntityStateId: (_d = previousBlock.speakerStateId) !== null && _d !== void 0 ? _d : "",
136
+ currentEntityStateId: (_e = currentBlock.speakerStateId) !== null && _e !== void 0 ? _e : "",
137
+ fadeTime,
138
+ fadeKey,
139
+ };
140
+ }
141
+ return null;
142
+ }, [previousBlock, currentBlock]);
143
+ // pendingFadeが変わったら完了フェードをクリア
144
+ const fadeKey = pendingFade === null || pendingFade === void 0 ? void 0 : pendingFade.fadeKey;
145
+ useEffect(() => {
146
+ if (fadeKey) {
147
+ // 新しいpendingFadeが来たら、完了フェードをクリア
148
+ completedFadesRef.current.clear();
149
+ }
150
+ }, [fadeKey]);
151
+ // フェード状態の初期化(描画前に同期的に実行)
152
+ useLayoutEffect(() => {
153
+ var _a;
154
+ if (!pendingFade)
155
+ return;
156
+ if (!((_a = previousBlock === null || previousBlock === void 0 ? void 0 : previousBlock.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl))
157
+ return;
158
+ const { objectId, previousEntityStateId, fadeTime, fadeKey } = pendingFade;
159
+ // 既に完了したフェードなら何もしない
160
+ if (completedFadesRef.current.has(fadeKey))
161
+ return;
162
+ // 既にこのオブジェクトのフェードが進行中なら何もしない
163
+ if (fadeStates.has(objectId))
164
+ return;
165
+ // フェード状態を初期化
166
+ setFadeStates((prev) => {
167
+ var _a, _b;
168
+ const newMap = new Map(prev);
169
+ newMap.set(objectId, {
170
+ previousEntityStateId,
171
+ previousImageUrl: (_b = (_a = previousBlock.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl) !== null && _b !== void 0 ? _b : "",
172
+ fadeProgress: 0,
173
+ fadeDuration: fadeTime,
174
+ });
175
+ return newMap;
176
+ });
177
+ }, [pendingFade, (_a = previousBlock === null || previousBlock === void 0 ? void 0 : previousBlock.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl, fadeStates]);
178
+ // アニメーション開始(レンダリング後に実行)
179
+ useEffect(() => {
180
+ if (!pendingFade)
181
+ return;
182
+ const { objectId, fadeTime, fadeKey } = pendingFade;
183
+ // 既に完了したフェードなら何もしない
184
+ if (completedFadesRef.current.has(fadeKey))
185
+ return;
186
+ // 既にアニメーション開始済みなら何もしない
187
+ if (fadeStartTimeRef.current.has(objectId))
188
+ return;
189
+ fadeStartTimeRef.current.set(objectId, performance.now());
190
+ // アニメーションループ
191
+ const animate = (currentTime) => {
192
+ const startTime = fadeStartTimeRef.current.get(objectId);
193
+ if (startTime === undefined)
194
+ return;
195
+ const elapsed = currentTime - startTime;
196
+ const progress = Math.min(elapsed / fadeTime, 1);
197
+ setFadeStates((prev) => {
198
+ const newMap = new Map(prev);
199
+ const state = newMap.get(objectId);
200
+ if (state) {
201
+ newMap.set(objectId, Object.assign(Object.assign({}, state), { fadeProgress: progress }));
202
+ }
203
+ return newMap;
204
+ });
205
+ if (progress < 1) {
206
+ animationFrameRef.current = requestAnimationFrame(animate);
207
+ }
208
+ else {
209
+ // フェード完了、状態クリア
210
+ completedFadesRef.current.add(fadeKey);
211
+ setFadeStates((prev) => {
212
+ const newMap = new Map(prev);
213
+ newMap.delete(objectId);
214
+ return newMap;
215
+ });
216
+ fadeStartTimeRef.current.delete(objectId);
217
+ }
218
+ };
219
+ animationFrameRef.current = requestAnimationFrame(animate);
220
+ return () => {
221
+ if (animationFrameRef.current) {
222
+ cancelAnimationFrame(animationFrameRef.current);
223
+ }
224
+ };
225
+ }, [pendingFade]);
42
226
  // 現在の話者
43
227
  const currentSpeaker = (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.speakerId)
44
228
  ? displayedCharacters.find((char) => char.objectId === currentBlock.speakerId)
45
229
  : null;
46
- return (_jsx("div", { className: "w-full h-full relative", children: allImages.map((image) => {
47
- var _a, _b, _c, _d, _e;
48
- // 複数キャラクター表示の場合、表示されているキャラクターを探す
49
- const displayedChar = displayedCharacters.find((char) => char.objectId === image.objectId &&
50
- char.entityStateId === image.entityStateId);
51
- // 単一キャラクター表示の場合、現在の話者かチェック
52
- const isCurrentSpeaker = displayedCharacters.length === 0 &&
53
- currentBlock.speakerId === image.objectId &&
54
- currentBlock.speakerStateId === image.entityStateId;
55
- // 表示すべきかどうか
56
- const shouldDisplay = displayedChar || isCurrentSpeaker;
57
- // 明るさを決定
58
- let brightness = 1;
59
- if (displayedCharacters.length > 0 && displayedChar) {
60
- // 複数キャラクター表示で、現在の話者でない場合
61
- if (currentSpeaker &&
62
- currentSpeaker.objectId !== displayedChar.objectId) {
63
- brightness = 0.8;
64
- }
230
+ // 簡易モード用の自動配置計算
231
+ const calculateAutoLayout = (characters, spacing) => {
232
+ const total = characters.length;
233
+ const positions = new Map();
234
+ if (total === 0)
235
+ return positions;
236
+ if (total === 1) {
237
+ // 1人の場合は中央
238
+ positions.set(characters[0].objectId, { x: 0, y: 0 });
239
+ return positions;
240
+ }
241
+ // 複数の場合は均等配置
242
+ // spacing: 画面幅に対する割合(例: 0.1 = 10%)
243
+ // 座標系: -1(左端)〜 1(右端)
244
+ const totalWidth = (total - 1) * spacing * 2;
245
+ const startX = -totalWidth / 2;
246
+ characters.forEach((char, index) => {
247
+ const x = startX + index * spacing * 2;
248
+ positions.set(char.objectId, { x, y: 0 });
249
+ });
250
+ return positions;
251
+ };
252
+ // キャラクター描画用のヘルパー関数
253
+ const renderCharacter = (image, displayedChar, isCurrentSpeaker, keyPrefix) => {
254
+ var _a, _b, _c, _d, _e, _f, _g, _h;
255
+ // フェード状態を確認
256
+ const fadeState = fadeStates.get(image.objectId);
257
+ const isFadingIn = fadeState && fadeState.previousEntityStateId !== image.entityStateId;
258
+ const isFadingOut = fadeState && fadeState.previousEntityStateId === image.entityStateId;
259
+ // pendingFadeがあるがfadeStateがまだない場合(初回レンダリング)
260
+ // ただし、既に完了したフェードは除外
261
+ const isPendingFadeIn = !fadeState &&
262
+ (pendingFade === null || pendingFade === void 0 ? void 0 : pendingFade.objectId) === image.objectId &&
263
+ pendingFade.currentEntityStateId === image.entityStateId &&
264
+ !completedFadesRef.current.has(pendingFade.fadeKey);
265
+ const isPendingFadeOut = !fadeState &&
266
+ (pendingFade === null || pendingFade === void 0 ? void 0 : pendingFade.objectId) === image.objectId &&
267
+ pendingFade.previousEntityStateId === image.entityStateId &&
268
+ !completedFadesRef.current.has(pendingFade.fadeKey);
269
+ // 表示すべきかどうか
270
+ const shouldDisplay = displayedChar || isCurrentSpeaker || isFadingOut || isPendingFadeOut;
271
+ // opacityを決定
272
+ let opacity = 1;
273
+ if (isFadingIn || isPendingFadeIn) {
274
+ // フェードイン中、またはフェード開始直後(まだstateがない)
275
+ opacity = (_a = fadeState === null || fadeState === void 0 ? void 0 : fadeState.fadeProgress) !== null && _a !== void 0 ? _a : 0;
276
+ }
277
+ else if (isFadingOut || isPendingFadeOut) {
278
+ // フェードアウト中
279
+ opacity = fadeState ? 1 - fadeState.fadeProgress : 1;
280
+ }
281
+ // 明るさを決定
282
+ let brightness = 1;
283
+ if (displayedCharacters.length > 0 && displayedChar) {
284
+ // 複数キャラクター表示で、現在の話者でない場合
285
+ if (currentSpeaker &&
286
+ currentSpeaker.objectId !== displayedChar.objectId) {
287
+ brightness = inactiveCharacterBrightness;
65
288
  }
66
- // z-indexを決定
67
- const zIndex = (_a = displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.zIndex) !== null && _a !== void 0 ? _a : (displayedChar ? 200 : 0);
68
- // スケールを決定
69
- // displayedCharのscaleが設定されている場合はそれを使用し、EntityStateのscaleと組み合わせる
70
- // 設定されていない場合はEntityStateのscaleのみ使用
71
- const characterScale = (displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.scale)
72
- ? displayedChar.scale * ((_b = image.scale) !== null && _b !== void 0 ? _b : 1)
73
- : ((_c = image.scale) !== null && _c !== void 0 ? _c : 1);
74
- // デバッグ: スケール値をログ出力
75
- if ((displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.scale) && displayedChar.scale !== 1) {
76
- console.log(`Character ${image.objectId} - displayedChar.scale: ${displayedChar.scale}, image.scale: ${image.scale}, final: ${characterScale}`);
289
+ }
290
+ // z-indexを決定
291
+ const zIndex = (_b = displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.zIndex) !== null && _b !== void 0 ? _b : (displayedChar ? 200 : 0);
292
+ // スケールを決定
293
+ // displayedCharのscaleが設定されている場合はそれを使用し、EntityStateのscaleと組み合わせる
294
+ // 設定されていない場合はEntityStateのscaleのみ使用
295
+ const characterScale = (displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.scale)
296
+ ? displayedChar.scale * ((_c = image.scale) !== null && _c !== void 0 ? _c : 1)
297
+ : ((_d = image.scale) !== null && _d !== void 0 ? _d : 1);
298
+ // 位置を決定(カスタム位置またはデフォルト位置)
299
+ // 新座標系: x: -1=左見切れ, 0=中央, 1=右見切れ / y: -1=上見切れ, 0=中央, 1=下見切れ
300
+ let finalPosition = { x: 0, y: 0 };
301
+ if (displayedChar) {
302
+ // カスタム位置が設定されている場合(詳細モード)
303
+ if (displayedChar.positionX !== null &&
304
+ displayedChar.positionY !== null) {
305
+ finalPosition = {
306
+ x: (_e = displayedChar.positionX) !== null && _e !== void 0 ? _e : 0,
307
+ y: (_f = displayedChar.positionY) !== null && _f !== void 0 ? _f : 0,
308
+ };
77
309
  }
78
- // 位置を決定(カスタム位置またはデフォルト位置)
79
- let finalPosition = { x: 0.5, y: 1.0 }; // デフォルトは中央下
80
- if (displayedChar) {
81
- // カスタム位置が設定されている場合
82
- if (displayedChar.positionX !== null &&
83
- displayedChar.positionY !== null) {
84
- finalPosition = {
85
- x: (_d = displayedChar.positionX) !== null && _d !== void 0 ? _d : 0.5,
86
- y: (_e = displayedChar.positionY) !== null && _e !== void 0 ? _e : 1.0,
87
- };
310
+ else {
311
+ // 簡易モード: 自動配置
312
+ const autoPositions = calculateAutoLayout(displayedCharacters, characterSpacing !== null && characterSpacing !== void 0 ? characterSpacing : 0.25);
313
+ const autoPos = autoPositions.get(displayedChar.objectId);
314
+ if (autoPos) {
315
+ finalPosition = autoPos;
88
316
  }
89
- // カスタム位置が設定されていない場合はデフォルト位置のまま
90
317
  }
91
- // 高さ基準の位置計算(中央基準)
92
- // 画面の中央(50vw)を基準として、vh単位で位置を指定
93
- // positionX: 0.0 = 中央から -56.25vh(左端), 0.5 = 中央, 1.0 = 中央から +56.25vh(右端)
94
- const aspectRatio = 16 / 9;
95
- const maxOffsetVh = 100 / aspectRatio / 2; // 56.25vh (16:9比率での半幅)
96
- const offsetFromCenterVh = (finalPosition.x - 0.5) * 2 * maxOffsetVh;
97
- const topVh = finalPosition.y * 100; // vh単位での上端からの距離
98
- return (_jsx("div", { className: "absolute w-auto h-4/5 flex items-end", style: {
99
- visibility: shouldDisplay ? "visible" : "hidden",
100
- filter: `brightness(${brightness})`,
101
- zIndex: zIndex,
102
- left: "50vw",
103
- top: `${topVh}vh`,
104
- transform: `translate(calc(-50% + ${offsetFromCenterVh}vh), -100%)`, // 中央から相対位置 + 下揃え
105
- }, "data-character-id": image.objectId, "data-character-sprite": true, children: _jsx("div", { className: "w-full h-full flex items-end justify-center", children: _jsx("img", { src: image.url, alt: "", className: "max-w-full max-h-full", style: {
318
+ }
319
+ // コンテナ相対の位置計算(パーセンテージ使用)
320
+ // positionX: -1.0 = 完全に左に見切れ, 0.0 = 中央, 1.0 = 完全に右に見切れ
321
+ // positionY: -1.0 = 完全に上に見切れ, 0.0 = 中央, 1.0 = 完全に下に見切れ
322
+ // -1〜1の範囲を0〜100%に変換: (value + 1) / 2 * 100
323
+ const leftPercent = ((finalPosition.x + 1) / 2) * 100;
324
+ const topPercent = ((finalPosition.y + 1) / 2) * 100;
325
+ // 完全に見切れるようにtranslateを動的に計算
326
+ // X: -1→0%, 0→-50%, 1→-100% (画像が完全に見切れる)
327
+ // Y: -1→0%, 0→-50%, 1→-100% (画像が完全に見切れる)
328
+ const translateXPercent = ((finalPosition.x - 1) / 2) * 100; // -100%〜0%
329
+ const translateYPercent = ((finalPosition.y - 1) / 2) * 100; // -100%〜0%
330
+ // レイヤー機能を使用するか判定(素体画像がある場合はレイヤーモード)
331
+ const hasLayerFeature = !!image.baseBodyUrl;
332
+ // 有効な画像がない場合は描画しない
333
+ const hasValidImage = hasLayerFeature ? !!image.baseBodyUrl : !!image.url;
334
+ if (!hasValidImage) {
335
+ return null;
336
+ }
337
+ // 現在の状態のlayersを取得(displayedCharがあればそれを優先、なければimageのlayers)
338
+ 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;
339
+ // コンテナサイズが取得できていない場合はスキップ
340
+ if (containerSize.width === 0 || containerSize.height === 0) {
341
+ return null;
342
+ }
343
+ // CharacterEditDialogと完全に同じ方式
344
+ // 画像の高さはコンテナの高さ × スケール
345
+ const baseHeight = containerSize.height * characterScale;
346
+ // 位置をピクセルで計算(CharacterEditDialogと同じ)
347
+ const leftPx = (leftPercent / 100) * containerSize.width;
348
+ const topPx = (topPercent / 100) * containerSize.height;
349
+ return (_jsx("div", { style: {
350
+ position: "absolute",
351
+ visibility: shouldDisplay ? "visible" : "hidden",
352
+ opacity: opacity,
353
+ filter: `brightness(${brightness})`,
354
+ zIndex: zIndex,
355
+ // CharacterEditDialogと完全に同じ: left/topを0にしてtransformで位置制御
356
+ // translate(%)は画像サイズに対する割合
357
+ left: 0,
358
+ top: 0,
359
+ transform: `translate(${leftPx}px, ${topPx}px) translate(${translateXPercent}%, ${translateYPercent}%)`,
360
+ transition: fadeState ? "none" : undefined,
361
+ }, "data-character-id": image.objectId, "data-character-sprite": true, children: hasLayerFeature && image.baseBodyUrl ? (_jsxs(_Fragment, { children: [_jsx("img", { src: image.baseBodyUrl, alt: "", style: {
362
+ height: `${baseHeight}px`,
363
+ width: "auto",
106
364
  objectFit: "contain",
107
- transform: `scale(${characterScale}) translateX(${image.translateX || 0}%) translateY(${image.translateY || 0}%)`,
108
- transformOrigin: "bottom center",
109
- } }) }) }, `${image.objectId}-${image.entityStateId}`));
110
- }) }));
111
- };
365
+ transform: `translateX(${image.baseBodyTranslateX || 0}%) translateY(${image.baseBodyTranslateY || 0}%)`,
366
+ } }), 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: {
367
+ position: "absolute",
368
+ left: 0,
369
+ top: 0,
370
+ height: `${baseHeight}px`,
371
+ width: "auto",
372
+ objectFit: "contain",
373
+ transform: `translateX(${image.translateX || 0}%) translateY(${image.translateY || 0}%)`,
374
+ } }, layer.id)))] })) : (
375
+ /* 従来の単一画像表示 */
376
+ _jsx("img", { src: image.url, alt: "", style: {
377
+ height: `${baseHeight}px`,
378
+ width: "auto",
379
+ objectFit: "contain",
380
+ transform: `translateX(${image.translateX || 0}%) translateY(${image.translateY || 0}%)`,
381
+ } })) }, `${keyPrefix}-${image.objectId}-${image.entityStateId}`));
382
+ };
383
+ return (_jsx("div", { ref: containerRef, className: "w-full h-full relative overflow-hidden", children: _jsxs("div", { className: "absolute inset-0", children: [allImages.map((image) => {
384
+ var _a;
385
+ 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}`));
386
+ }), displayedCharacters.map((char) => {
387
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
388
+ // このキャラクターの現在の状態に対応するImageDataを取得
389
+ const imageKey = `${char.objectId}-${char.entityStateId}`;
390
+ const image = imageDataMap.get(imageKey);
391
+ if (!image) {
392
+ // ImageDataがない場合はdisplayedCharacterの情報から直接描画
393
+ // (シナリオに含まれていない状態への変更など)
394
+ const fallbackImage = {
395
+ url: (_b = (_a = char.entityState) === null || _a === void 0 ? void 0 : _a.imageUrl) !== null && _b !== void 0 ? _b : "",
396
+ objectId: char.objectId,
397
+ entityStateId: char.entityStateId,
398
+ scale: (_c = char.entityState) === null || _c === void 0 ? void 0 : _c.scale,
399
+ translateX: (_d = char.entityState) === null || _d === void 0 ? void 0 : _d.translateX,
400
+ translateY: (_e = char.entityState) === null || _e === void 0 ? void 0 : _e.translateY,
401
+ baseBodyUrl: (_f = char.baseBodyState) === null || _f === void 0 ? void 0 : _f.imageUrl,
402
+ baseBodyScale: (_g = char.baseBodyState) === null || _g === void 0 ? void 0 : _g.scale,
403
+ baseBodyTranslateX: (_h = char.baseBodyState) === null || _h === void 0 ? void 0 : _h.translateX,
404
+ baseBodyTranslateY: (_j = char.baseBodyState) === null || _j === void 0 ? void 0 : _j.translateY,
405
+ layers: (_k = char.entityState) === null || _k === void 0 ? void 0 : _k.layers,
406
+ };
407
+ return renderCharacter(fallbackImage, char, false, "char");
408
+ }
409
+ return renderCharacter(image, char, false, "char");
410
+ }), displayedCharacters.length === 0 &&
411
+ currentBlock.speakerId &&
412
+ currentBlock.speakerStateId && (_jsxs(_Fragment, { children: [(() => {
413
+ const imageKey = `${currentBlock.speakerId}-${currentBlock.speakerStateId}`;
414
+ const image = imageDataMap.get(imageKey);
415
+ if (!image)
416
+ return null;
417
+ return renderCharacter(image, null, true, "speaker");
418
+ })(), pendingFade &&
419
+ !completedFadesRef.current.has(pendingFade.fadeKey) &&
420
+ (() => {
421
+ const imageKey = `${pendingFade.objectId}-${pendingFade.previousEntityStateId}`;
422
+ const image = imageDataMap.get(imageKey);
423
+ if (!image)
424
+ return null;
425
+ return renderCharacter(image, null, false, "fadeout");
426
+ })()] }))] }) }));
427
+ });
@@ -5,9 +5,8 @@ interface OverlayUIProps {
5
5
  /**
6
6
  * OverlayUI コンポーネント
7
7
  *
8
- * 16:9のアスペクト比を維持しながら、様々な画面サイズに対応するコンテナ
9
- * - スマートフォン(幅 < 1000px): フルサイズ表示
10
- * - タブレット(幅 ≥ 1000px): 70%に縮小して中央配置
8
+ * アスペクト比コンテナ全体に配置されるUIオーバーレイ
9
+ * 親コンテナ(アスペクト比コンテナ)のサイズに合わせて自動調整される
11
10
  */
12
11
  export declare const OverlayUI: React.FC<OverlayUIProps>;
13
12
  export {};
@@ -1,21 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useScreenSizeAtom } from "../atoms/screen-size";
3
2
  /**
4
3
  * OverlayUI コンポーネント
5
4
  *
6
- * 16:9のアスペクト比を維持しながら、様々な画面サイズに対応するコンテナ
7
- * - スマートフォン(幅 < 1000px): フルサイズ表示
8
- * - タブレット(幅 ≥ 1000px): 70%に縮小して中央配置
5
+ * アスペクト比コンテナ全体に配置されるUIオーバーレイ
6
+ * 親コンテナ(アスペクト比コンテナ)のサイズに合わせて自動調整される
9
7
  */
10
8
  export const OverlayUI = ({ children }) => {
11
- // 16:9のアスペクト比を基準
12
- let [{ height: screenHeight }] = useScreenSizeAtom();
13
- let screenWidth = (screenHeight * 16) / 9;
14
- // タブレット(幅1000px以上)では70%に縮小
15
- const [{ width: checkWidth }] = useScreenSizeAtom();
16
- if (checkWidth >= 1000) {
17
- screenHeight = (screenHeight * 7) / 10;
18
- screenWidth = (screenHeight * 16) / 9;
19
- }
20
- return (_jsx("div", { className: "absolute inset-0 pointer-events-none z-[9999] flex justify-center items-center", children: _jsx("div", { style: { width: screenWidth, height: screenHeight }, children: children }) }));
9
+ return (_jsx("div", { className: "absolute inset-0 pointer-events-none z-[9999]", children: _jsx("div", { className: "w-full h-full", children: children }) }));
21
10
  };
@@ -1,14 +1,14 @@
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<any>;
7
+ fallback?: React.ComponentType;
8
8
  }
9
9
  /**
10
10
  * Wrapper component that renders a plugin-registered component or a fallback
11
- * All data is provided via DataContext, so no props are passed
11
+ * DataAPI is provided via props to avoid React context issues across different React instances
12
12
  */
13
13
  export declare function PluginComponentProvider({ type, pluginManager, fallback: FallbackComponent, }: PluginComponentProviderProps): import("react/jsx-runtime").JSX.Element;
14
14
  export {};
@@ -1,15 +1,33 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { useDataAPI } from "../contexts/DataContext";
2
4
  /**
3
5
  * Wrapper component that renders a plugin-registered component or a fallback
4
- * All data is provided via DataContext, so no props are passed
6
+ * DataAPI is provided via props to avoid React context issues across different React instances
5
7
  */
6
8
  export function PluginComponentProvider({ type, pluginManager, fallback: FallbackComponent, }) {
9
+ // Get DataAPI in Player's React context
10
+ const dataAPI = useDataAPI();
11
+ // 【emotionEffect監視】: emotionEffectの変更を監視して再レンダリング
12
+ // 【実装方針】: プラグインコンポーネントがdataAPI.get()を使用して状態を取得する場合、
13
+ // Reactフックを使えないため、親側で変更を監視して再レンダリングを強制する
14
+ const [, forceUpdate] = useState({});
15
+ useEffect(() => {
16
+ // 全てのプラグインコンポーネントでemotionEffectの変更を監視
17
+ const unsubscribe = dataAPI.subscribe("emotionEffect", () => {
18
+ forceUpdate({});
19
+ });
20
+ return () => {
21
+ unsubscribe();
22
+ };
23
+ }, [dataAPI]);
7
24
  const Component = pluginManager.getComponent(type);
8
25
  if (Component) {
9
- return _jsx(Component, {}, `plugin-component-${type}`);
26
+ // Pass DataAPI as prop to avoid context issues
27
+ return _jsx(Component, { dataAPI: dataAPI });
10
28
  }
11
29
  if (FallbackComponent) {
12
- return _jsx(FallbackComponent, {}, `fallback-component-${type}`);
30
+ return _jsx(FallbackComponent, {});
13
31
  }
14
32
  // If no component is registered and no fallback provided, show a warning
15
33
  console.warn(`No component registered for type: ${type}`);
@@ -20,5 +38,5 @@ export function PluginComponentProvider({ type, pluginManager, fallback: Fallbac
20
38
  borderRadius: "4px",
21
39
  color: "#666",
22
40
  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" })] }, `warning-component-${type}`));
41
+ }, 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
42
  }
@@ -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 {};