@luna-editor/engine 0.2.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.
Files changed (64) hide show
  1. package/dist/Player.d.ts +1 -1
  2. package/dist/Player.js +504 -77
  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 +218 -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 +1 -0
  17. package/dist/components/GameScreen.js +363 -81
  18. package/dist/components/PluginComponentProvider.d.ts +2 -2
  19. package/dist/components/PluginComponentProvider.js +3 -3
  20. package/dist/components/TimeWaitIndicator.d.ts +15 -0
  21. package/dist/components/TimeWaitIndicator.js +17 -0
  22. package/dist/contexts/AudioContext.d.ts +1 -0
  23. package/dist/contexts/AudioContext.js +1 -0
  24. package/dist/contexts/DataContext.js +69 -11
  25. package/dist/hooks/useBacklog.js +3 -0
  26. package/dist/hooks/useConversationBranch.d.ts +16 -0
  27. package/dist/hooks/useConversationBranch.js +125 -0
  28. package/dist/hooks/useFontLoader.d.ts +23 -0
  29. package/dist/hooks/useFontLoader.js +153 -0
  30. package/dist/hooks/useFullscreenText.d.ts +17 -0
  31. package/dist/hooks/useFullscreenText.js +120 -0
  32. package/dist/hooks/usePlayerLogic.d.ts +10 -3
  33. package/dist/hooks/usePlayerLogic.js +115 -18
  34. package/dist/hooks/usePluginEvents.d.ts +4 -1
  35. package/dist/hooks/usePluginEvents.js +16 -11
  36. package/dist/hooks/usePreloadImages.js +27 -7
  37. package/dist/hooks/useSoundPlayer.d.ts +15 -0
  38. package/dist/hooks/useSoundPlayer.js +209 -0
  39. package/dist/hooks/useTypewriter.d.ts +6 -2
  40. package/dist/hooks/useTypewriter.js +42 -6
  41. package/dist/hooks/useVoice.js +4 -1
  42. package/dist/index.d.ts +4 -3
  43. package/dist/index.js +2 -1
  44. package/dist/plugin/PluginManager.d.ts +66 -2
  45. package/dist/plugin/PluginManager.js +349 -79
  46. package/dist/sdk.d.ts +178 -21
  47. package/dist/sdk.js +27 -2
  48. package/dist/types.d.ts +288 -4
  49. package/dist/utils/branchBlockConverter.d.ts +2 -0
  50. package/dist/utils/branchBlockConverter.js +21 -0
  51. package/dist/utils/branchNavigator.d.ts +14 -0
  52. package/dist/utils/branchNavigator.js +55 -0
  53. package/dist/utils/facePositionCalculator.js +0 -1
  54. package/dist/utils/variableManager.d.ts +18 -0
  55. package/dist/utils/variableManager.js +159 -0
  56. package/package.json +1 -1
  57. package/dist/components/ConversationLogUI.d.ts +0 -2
  58. package/dist/components/ConversationLogUI.js +0 -115
  59. package/dist/hooks/useConversationLog.d.ts +0 -14
  60. package/dist/hooks/useConversationLog.js +0 -82
  61. package/dist/hooks/useUIVisibility.d.ts +0 -9
  62. package/dist/hooks/useUIVisibility.js +0 -19
  63. package/dist/plugin/luna-react.d.ts +0 -41
  64. 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
- 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,
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
- // character_entranceブロックのキャラクター画像
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
- if (char.entityState.imageUrl) {
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
- 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
- }
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
- // 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}`);
77
- }
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
- };
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
- // 画面の中央(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: {
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: `scale(${characterScale}) translateX(${image.translateX || 0}%) translateY(${image.translateY || 0}%)`,
108
- transformOrigin: "bottom center",
109
- } }) }) }, `${image.objectId}-${image.entityStateId}`));
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<any>;
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, {}, `plugin-component-${type}`);
9
+ return _jsx(Component, {});
10
10
  }
11
11
  if (FallbackComponent) {
12
- return _jsx(FallbackComponent, {}, `fallback-component-${type}`);
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" })] }, `warning-component-${type}`));
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
+ };
@@ -5,6 +5,7 @@ export interface AudioSettings {
5
5
  voiceVolume: number;
6
6
  effectVolume: number;
7
7
  textSoundVolume: number;
8
+ muteAudio: boolean;
8
9
  }
9
10
  export declare const AudioProvider: ({ children, settings, }: {
10
11
  children: ReactNode;
@@ -6,6 +6,7 @@ const AudioContext = createContext({
6
6
  voiceVolume: 1.0,
7
7
  effectVolume: 1.0,
8
8
  textSoundVolume: 1.0,
9
+ muteAudio: false,
9
10
  });
10
11
  export const AudioProvider = ({ children, settings, }) => {
11
12
  return (_jsx(AudioContext.Provider, { value: settings, children: children }));