@luna-editor/engine 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Player.d.ts +1 -1
- package/dist/Player.js +676 -95
- package/dist/api/conversationBranch.d.ts +4 -0
- package/dist/api/conversationBranch.js +83 -0
- package/dist/components/BackgroundLayer.d.ts +19 -0
- package/dist/components/BackgroundLayer.js +220 -0
- package/dist/components/ClickWaitIndicator.d.ts +10 -0
- package/dist/components/ClickWaitIndicator.js +31 -0
- package/dist/components/ConversationBranchBox.d.ts +2 -0
- package/dist/components/ConversationBranchBox.js +29 -0
- package/dist/components/DialogueBox.js +16 -1
- package/dist/components/FontSettingsPanel.d.ts +10 -0
- package/dist/components/FontSettingsPanel.js +30 -0
- package/dist/components/FullscreenTextBox.d.ts +6 -0
- package/dist/components/FullscreenTextBox.js +70 -0
- package/dist/components/GameScreen.d.ts +9 -2
- package/dist/components/GameScreen.js +396 -80
- package/dist/components/OverlayUI.d.ts +2 -3
- package/dist/components/OverlayUI.js +3 -14
- package/dist/components/PluginComponentProvider.d.ts +3 -3
- package/dist/components/PluginComponentProvider.js +22 -4
- package/dist/components/TimeWaitIndicator.d.ts +15 -0
- package/dist/components/TimeWaitIndicator.js +17 -0
- package/dist/contexts/AudioContext.d.ts +1 -0
- package/dist/contexts/AudioContext.js +1 -0
- package/dist/contexts/DataContext.d.ts +3 -1
- package/dist/contexts/DataContext.js +104 -17
- package/dist/contexts/PlaybackTextContext.d.ts +32 -0
- package/dist/contexts/PlaybackTextContext.js +29 -0
- package/dist/data-api-types.d.ts +251 -0
- package/dist/data-api-types.js +6 -0
- package/dist/emotion-effect-types.d.ts +86 -0
- package/dist/emotion-effect-types.js +6 -0
- package/dist/hooks/useBacklog.js +3 -1
- package/dist/hooks/useConversationBranch.d.ts +16 -0
- package/dist/hooks/useConversationBranch.js +125 -0
- package/dist/hooks/useFontLoader.d.ts +30 -0
- package/dist/hooks/useFontLoader.js +192 -0
- package/dist/hooks/useFullscreenText.d.ts +17 -0
- package/dist/hooks/useFullscreenText.js +120 -0
- package/dist/hooks/useImagePreloader.d.ts +5 -0
- package/dist/hooks/useImagePreloader.js +53 -0
- package/dist/hooks/usePlayerLogic.d.ts +10 -3
- package/dist/hooks/usePlayerLogic.js +115 -18
- package/dist/hooks/usePluginAPI.js +1 -1
- package/dist/hooks/usePluginEvents.d.ts +4 -1
- package/dist/hooks/usePluginEvents.js +16 -11
- package/dist/hooks/usePreloadImages.js +27 -7
- package/dist/hooks/useSoundPlayer.d.ts +15 -0
- package/dist/hooks/useSoundPlayer.js +209 -0
- package/dist/hooks/useTypewriter.d.ts +6 -2
- package/dist/hooks/useTypewriter.js +42 -6
- package/dist/hooks/useVoice.js +4 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/plugin/PluginManager.d.ts +86 -5
- package/dist/plugin/PluginManager.js +427 -94
- package/dist/sdk.d.ts +133 -162
- package/dist/sdk.js +39 -4
- package/dist/types.d.ts +300 -4
- package/dist/utils/branchBlockConverter.d.ts +2 -0
- package/dist/utils/branchBlockConverter.js +21 -0
- package/dist/utils/branchNavigator.d.ts +14 -0
- package/dist/utils/branchNavigator.js +55 -0
- package/dist/utils/facePositionCalculator.js +0 -1
- package/dist/utils/variableManager.d.ts +18 -0
- package/dist/utils/variableManager.js +159 -0
- package/package.json +6 -6
- package/dist/components/ConversationLogUI.d.ts +0 -2
- package/dist/components/ConversationLogUI.js +0 -115
- package/dist/hooks/useConversationLog.d.ts +0 -14
- package/dist/hooks/useConversationLog.js +0 -82
- package/dist/hooks/useUIVisibility.d.ts +0 -9
- package/dist/hooks/useUIVisibility.js +0 -19
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ConversationBranch } from "../types";
|
|
2
|
+
export declare function fetchConversationBranch(blockId: string, apiBaseUrl: string): Promise<ConversationBranch>;
|
|
3
|
+
export declare function clearBranchCache(): void;
|
|
4
|
+
export declare function removeBranchFromCache(blockId: string, apiBaseUrl: string): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
11
|
+
const MAX_RETRIES = 3;
|
|
12
|
+
const RETRY_DELAY_BASE = 1000;
|
|
13
|
+
const REQUEST_TIMEOUT = 5000;
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
function isCacheValid(entry) {
|
|
16
|
+
return Date.now() - entry.timestamp < CACHE_TTL;
|
|
17
|
+
}
|
|
18
|
+
function fetchWithTimeout(url, options, timeout) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
22
|
+
try {
|
|
23
|
+
const response = yield fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal }));
|
|
24
|
+
clearTimeout(timeoutId);
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
clearTimeout(timeoutId);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function fetchWithRetry(url_1) {
|
|
34
|
+
return __awaiter(this, arguments, void 0, function* (url, retries = MAX_RETRIES) {
|
|
35
|
+
for (let i = 0; i < retries; i++) {
|
|
36
|
+
try {
|
|
37
|
+
const response = yield fetchWithTimeout(url, {}, REQUEST_TIMEOUT);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
40
|
+
}
|
|
41
|
+
const data = yield response.json();
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (i === retries - 1) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const delay = RETRY_DELAY_BASE * Math.pow(2, i);
|
|
49
|
+
yield new Promise((resolve) => setTimeout(resolve, delay));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error("Max retries exceeded");
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function fetchConversationBranch(blockId, apiBaseUrl) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
const cacheKey = `${apiBaseUrl}:${blockId}`;
|
|
58
|
+
const cached = cache.get(cacheKey);
|
|
59
|
+
if (cached && isCacheValid(cached)) {
|
|
60
|
+
return cached.data;
|
|
61
|
+
}
|
|
62
|
+
const url = `${apiBaseUrl}/api/player/conversation-branch/${blockId}`;
|
|
63
|
+
try {
|
|
64
|
+
const data = yield fetchWithRetry(url);
|
|
65
|
+
cache.set(cacheKey, {
|
|
66
|
+
data,
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
});
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error("[ConversationBranch API] Failed to fetch conversation branch:", error);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export function clearBranchCache() {
|
|
78
|
+
cache.clear();
|
|
79
|
+
}
|
|
80
|
+
export function removeBranchFromCache(blockId, apiBaseUrl) {
|
|
81
|
+
const cacheKey = `${apiBaseUrl}:${blockId}`;
|
|
82
|
+
cache.delete(cacheKey);
|
|
83
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { BackgroundData, BackgroundMediaType } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* メディアタイプを拡張子から判定
|
|
5
|
+
*/
|
|
6
|
+
declare function getMediaType(url: string): BackgroundMediaType;
|
|
7
|
+
interface BackgroundLayerProps {
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 背景表示レイヤー
|
|
12
|
+
* DataContextからbackgroundデータを取得して表示
|
|
13
|
+
* 複数背景の重ね合わせ(backgrounds配列)に対応
|
|
14
|
+
* プラグインによって置き換え可能
|
|
15
|
+
* フェードイン・フェードアウト対応
|
|
16
|
+
*/
|
|
17
|
+
export declare const BackgroundLayer: React.FC<BackgroundLayerProps>;
|
|
18
|
+
export { getMediaType };
|
|
19
|
+
export type { BackgroundData };
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useDataAPI } from "../contexts/DataContext";
|
|
4
|
+
/**
|
|
5
|
+
* iOS/macOSかどうかを判定
|
|
6
|
+
* これらの環境ではmp4を使用し、それ以外ではwebmを使用
|
|
7
|
+
*/
|
|
8
|
+
function isApplePlatform() {
|
|
9
|
+
if (typeof window === "undefined")
|
|
10
|
+
return false;
|
|
11
|
+
const ua = navigator.userAgent;
|
|
12
|
+
const platform = navigator.platform;
|
|
13
|
+
// iOS判定
|
|
14
|
+
const isIOS = /iPad|iPhone|iPod/.test(ua) ||
|
|
15
|
+
(platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
|
16
|
+
// macOS判定
|
|
17
|
+
const isMacOS = /Mac/.test(platform) && !isIOS;
|
|
18
|
+
return isIOS || isMacOS;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* レイヤーに応じたz-indexを計算
|
|
22
|
+
* @param layer 表示レイヤー
|
|
23
|
+
* @param index 同一レイヤー内のインデックス
|
|
24
|
+
* @returns z-index値
|
|
25
|
+
*/
|
|
26
|
+
function getLayerZIndex(layer, index) {
|
|
27
|
+
switch (layer) {
|
|
28
|
+
case "background":
|
|
29
|
+
return 10 + index; // 10-99
|
|
30
|
+
case "above_character":
|
|
31
|
+
return 500 + index; // 500-599
|
|
32
|
+
case "above_dialogue":
|
|
33
|
+
return 2000 + index; // 2000-2099
|
|
34
|
+
case "above_all_ui":
|
|
35
|
+
return 9000 + index; // 9000-9098
|
|
36
|
+
default:
|
|
37
|
+
return 10 + index;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* メディアタイプを拡張子から判定
|
|
42
|
+
*/
|
|
43
|
+
function getMediaType(url) {
|
|
44
|
+
var _a, _b;
|
|
45
|
+
const extension = (_b = (_a = url.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : "";
|
|
46
|
+
// Video formats
|
|
47
|
+
if (["mp4", "webm", "ogg", "mov", "avi", "mkv"].includes(extension)) {
|
|
48
|
+
return "video";
|
|
49
|
+
}
|
|
50
|
+
// GIF format
|
|
51
|
+
if (extension === "gif") {
|
|
52
|
+
return "gif";
|
|
53
|
+
}
|
|
54
|
+
// Image formats (default)
|
|
55
|
+
return "image";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 背景メディアコンポーネント
|
|
59
|
+
* React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
|
|
60
|
+
* これにより displayText の更新による不要な再描画を防止
|
|
61
|
+
*/
|
|
62
|
+
const BackgroundMedia = memo(function BackgroundMedia({ background, opacity, zIndex, useAppleFormat }) {
|
|
63
|
+
var _a, _b;
|
|
64
|
+
// OS判定に基づいて使用するURLを決定
|
|
65
|
+
// iOS/macOS: mp4 (imageUrl)
|
|
66
|
+
// その他: webm (webmUrl) があればそちらを優先、なければ mp4
|
|
67
|
+
const videoUrl = useMemo(() => {
|
|
68
|
+
var _a;
|
|
69
|
+
if (useAppleFormat) {
|
|
70
|
+
// Apple環境ではmp4を使用
|
|
71
|
+
return background.imageUrl;
|
|
72
|
+
}
|
|
73
|
+
// それ以外ではwebmがあればwebm、なければmp4
|
|
74
|
+
return (_a = background.webmUrl) !== null && _a !== void 0 ? _a : background.imageUrl;
|
|
75
|
+
}, [background.imageUrl, background.webmUrl, useAppleFormat]);
|
|
76
|
+
const mediaType = getMediaType(background.imageUrl);
|
|
77
|
+
const objectFitClass = background.objectFit === "contain" ? "object-contain" : "object-cover";
|
|
78
|
+
// background.opacityと引数opacityを掛け合わせる
|
|
79
|
+
const finalOpacity = ((_a = background.opacity) !== null && _a !== void 0 ? _a : 1.0) * opacity;
|
|
80
|
+
// 高レイヤー(UIの上)の背景はposition: fixedにして、ポインターイベントを無効化
|
|
81
|
+
const layer = (_b = background.layer) !== null && _b !== void 0 ? _b : "background";
|
|
82
|
+
const isHighLayer = layer === "above_dialogue" || layer === "above_all_ui";
|
|
83
|
+
return (_jsx("div", { className: `${isHighLayer ? "fixed" : "absolute"} inset-0 overflow-hidden`, style: {
|
|
84
|
+
opacity: finalOpacity,
|
|
85
|
+
zIndex,
|
|
86
|
+
transition: "none", // requestAnimationFrameでアニメーション
|
|
87
|
+
pointerEvents: isHighLayer ? "none" : undefined,
|
|
88
|
+
}, "data-background-object-id": background.objectId, "data-background-state-id": background.stateId, "data-background-layer": layer, children: mediaType === "video" ? (_jsx("video", { src: videoUrl, className: `w-full h-full ${objectFitClass}`, autoPlay: true, muted: true, playsInline: true, loop: background.loop }, videoUrl)) : (_jsx("img", { src: background.imageUrl, alt: background.stateName, className: `w-full h-full ${objectFitClass}` }, background.imageUrl)) }));
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* 背景の配列が同じかどうかを比較
|
|
92
|
+
*/
|
|
93
|
+
function areBackgroundsEqual(a, b) {
|
|
94
|
+
if (a.length !== b.length)
|
|
95
|
+
return false;
|
|
96
|
+
for (let i = 0; i < a.length; i++) {
|
|
97
|
+
if (a[i].objectId !== b[i].objectId ||
|
|
98
|
+
a[i].stateId !== b[i].stateId ||
|
|
99
|
+
a[i].imageUrl !== b[i].imageUrl) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 背景表示レイヤー
|
|
107
|
+
* DataContextからbackgroundデータを取得して表示
|
|
108
|
+
* 複数背景の重ね合わせ(backgrounds配列)に対応
|
|
109
|
+
* プラグインによって置き換え可能
|
|
110
|
+
* フェードイン・フェードアウト対応
|
|
111
|
+
*/
|
|
112
|
+
export const BackgroundLayer = memo(({ className }) => {
|
|
113
|
+
const dataAPI = useDataAPI();
|
|
114
|
+
// Apple環境かどうかを判定(iOS/macOSではmp4を使用)
|
|
115
|
+
const useAppleFormat = useMemo(() => isApplePlatform(), []);
|
|
116
|
+
// 複数背景を優先、なければ単一背景を使用
|
|
117
|
+
const backgrounds = dataAPI.get("backgrounds");
|
|
118
|
+
const singleBackground = dataAPI.get("background");
|
|
119
|
+
// 背景の配列を決定
|
|
120
|
+
const backgroundsToRender = backgrounds && backgrounds.length > 0
|
|
121
|
+
? backgrounds
|
|
122
|
+
: (singleBackground === null || singleBackground === void 0 ? void 0 : singleBackground.imageUrl)
|
|
123
|
+
? [singleBackground]
|
|
124
|
+
: [];
|
|
125
|
+
// フェード状態管理
|
|
126
|
+
const [fadeState, setFadeState] = useState({
|
|
127
|
+
previousBackgrounds: [],
|
|
128
|
+
currentBackgrounds: [],
|
|
129
|
+
fadeProgress: 1,
|
|
130
|
+
isFading: false,
|
|
131
|
+
});
|
|
132
|
+
// 現在の背景を追跡するref(useEffectの依存配列問題を回避)
|
|
133
|
+
const currentBackgroundsRef = useRef([]);
|
|
134
|
+
// アニメーションフレームID
|
|
135
|
+
const animationFrameRef = useRef(null);
|
|
136
|
+
const fadeStartTimeRef = useRef(0);
|
|
137
|
+
const fadeDurationRef = useRef(0);
|
|
138
|
+
// 背景変更を監視
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
var _a, _b;
|
|
141
|
+
const newBackgrounds = backgroundsToRender;
|
|
142
|
+
const currentBgs = currentBackgroundsRef.current;
|
|
143
|
+
// 背景が変更されたかチェック
|
|
144
|
+
const hasChanged = !areBackgroundsEqual(newBackgrounds, currentBgs);
|
|
145
|
+
if (!hasChanged)
|
|
146
|
+
return;
|
|
147
|
+
// 前のアニメーションをキャンセル
|
|
148
|
+
if (animationFrameRef.current) {
|
|
149
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
150
|
+
animationFrameRef.current = null;
|
|
151
|
+
}
|
|
152
|
+
// フェード時間を取得(最初の背景のfadeDurationを使用)
|
|
153
|
+
const fadeDuration = (_b = (_a = newBackgrounds[0]) === null || _a === void 0 ? void 0 : _a.fadeDuration) !== null && _b !== void 0 ? _b : 0;
|
|
154
|
+
if (fadeDuration > 0 && currentBgs.length > 0) {
|
|
155
|
+
// フェードありで背景変更
|
|
156
|
+
fadeStartTimeRef.current = performance.now();
|
|
157
|
+
fadeDurationRef.current = fadeDuration;
|
|
158
|
+
currentBackgroundsRef.current = newBackgrounds;
|
|
159
|
+
setFadeState({
|
|
160
|
+
previousBackgrounds: currentBgs,
|
|
161
|
+
currentBackgrounds: newBackgrounds,
|
|
162
|
+
fadeProgress: 0,
|
|
163
|
+
isFading: true,
|
|
164
|
+
});
|
|
165
|
+
// フェードアニメーション関数
|
|
166
|
+
const animate = (currentTime) => {
|
|
167
|
+
const elapsed = currentTime - fadeStartTimeRef.current;
|
|
168
|
+
const progress = Math.min(elapsed / fadeDurationRef.current, 1);
|
|
169
|
+
if (progress >= 1) {
|
|
170
|
+
// フェード完了
|
|
171
|
+
setFadeState((prev) => (Object.assign(Object.assign({}, prev), { fadeProgress: 1, isFading: false, previousBackgrounds: [] })));
|
|
172
|
+
animationFrameRef.current = null;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
setFadeState((prev) => (Object.assign(Object.assign({}, prev), { fadeProgress: progress })));
|
|
176
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
// アニメーション開始
|
|
180
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// フェードなしで即座に変更
|
|
184
|
+
currentBackgroundsRef.current = newBackgrounds;
|
|
185
|
+
setFadeState({
|
|
186
|
+
previousBackgrounds: [],
|
|
187
|
+
currentBackgrounds: newBackgrounds,
|
|
188
|
+
fadeProgress: 1,
|
|
189
|
+
isFading: false,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}, [backgroundsToRender]);
|
|
193
|
+
// クリーンアップ
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
return () => {
|
|
196
|
+
if (animationFrameRef.current) {
|
|
197
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}, []);
|
|
201
|
+
// 背景データがない場合は黒背景を表示
|
|
202
|
+
if (fadeState.currentBackgrounds.length === 0 &&
|
|
203
|
+
fadeState.previousBackgrounds.length === 0) {
|
|
204
|
+
return (_jsx("div", { className: `absolute inset-0 bg-black ${className !== null && className !== void 0 ? className : ""}`, "data-background-layer": true }));
|
|
205
|
+
}
|
|
206
|
+
return (_jsxs("div", { className: `absolute inset-0 overflow-hidden ${className !== null && className !== void 0 ? className : ""}`, "data-background-layer": true, children: [_jsx("div", { className: "absolute inset-0 bg-black", style: { zIndex: 0 } }), fadeState.previousBackgrounds.length > 0 &&
|
|
207
|
+
fadeState.isFading &&
|
|
208
|
+
fadeState.previousBackgrounds.map((bg, index) => {
|
|
209
|
+
var _a;
|
|
210
|
+
const layer = (_a = bg.layer) !== null && _a !== void 0 ? _a : "background";
|
|
211
|
+
const zIndex = getLayerZIndex(layer, index);
|
|
212
|
+
return (_jsx(BackgroundMedia, { background: bg, opacity: 1 - fadeState.fadeProgress, zIndex: zIndex - 1, useAppleFormat: useAppleFormat }, `prev-${bg.objectId}-${bg.stateId}-${index}`));
|
|
213
|
+
}), fadeState.currentBackgrounds.map((bg, index) => {
|
|
214
|
+
var _a;
|
|
215
|
+
const layer = (_a = bg.layer) !== null && _a !== void 0 ? _a : "background";
|
|
216
|
+
const zIndex = getLayerZIndex(layer, index);
|
|
217
|
+
return (_jsx(BackgroundMedia, { background: bg, opacity: fadeState.isFading ? fadeState.fadeProgress : 1, zIndex: zIndex, useAppleFormat: useAppleFormat }, `curr-${bg.objectId}-${bg.stateId}-${index}`));
|
|
218
|
+
})] }));
|
|
219
|
+
});
|
|
220
|
+
export { getMediaType };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
interface ClickWaitIndicatorProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* クリック待ちインジケーター
|
|
7
|
+
* click_waitブロックで表示され、ユーザーにクリックを促すアニメーションを表示
|
|
8
|
+
*/
|
|
9
|
+
export declare const ClickWaitIndicator: React.FC<ClickWaitIndicatorProps>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* クリック待ちインジケーター
|
|
5
|
+
* click_waitブロックで表示され、ユーザーにクリックを促すアニメーションを表示
|
|
6
|
+
*/
|
|
7
|
+
export const ClickWaitIndicator = ({ className, }) => {
|
|
8
|
+
return (_jsxs("div", { className: className, style: {
|
|
9
|
+
position: "absolute",
|
|
10
|
+
bottom: "32px",
|
|
11
|
+
right: "32px",
|
|
12
|
+
display: "flex",
|
|
13
|
+
alignItems: "center",
|
|
14
|
+
justifyContent: "center",
|
|
15
|
+
pointerEvents: "none",
|
|
16
|
+
}, children: [_jsx("div", { style: {
|
|
17
|
+
width: 0,
|
|
18
|
+
height: 0,
|
|
19
|
+
borderLeft: "8px solid transparent",
|
|
20
|
+
borderRight: "8px solid transparent",
|
|
21
|
+
borderTop: "12px solid white",
|
|
22
|
+
opacity: 0.8,
|
|
23
|
+
animation: "clickWaitBlink 1s ease-in-out infinite",
|
|
24
|
+
filter: "drop-shadow(0 0 4px rgba(0, 0, 0, 0.5))",
|
|
25
|
+
} }), _jsx("style", { children: `
|
|
26
|
+
@keyframes clickWaitBlink {
|
|
27
|
+
0%, 100% { opacity: 0.4; }
|
|
28
|
+
50% { opacity: 1; }
|
|
29
|
+
}
|
|
30
|
+
` })] }));
|
|
31
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { useDataAPI } from "../contexts/DataContext";
|
|
4
|
+
export const ConversationBranchBox = () => {
|
|
5
|
+
const dataAPI = useDataAPI();
|
|
6
|
+
const branchData = dataAPI.get("branchData");
|
|
7
|
+
const handleChoiceClick = useCallback((choiceId) => {
|
|
8
|
+
var _a;
|
|
9
|
+
if ((_a = window.playerAPI) === null || _a === void 0 ? void 0 : _a.selectChoice) {
|
|
10
|
+
window.playerAPI.selectChoice(choiceId);
|
|
11
|
+
}
|
|
12
|
+
}, []);
|
|
13
|
+
if (!branchData) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (branchData.errorState) {
|
|
17
|
+
return (_jsx("div", { className: "absolute bottom-[5%] max-w-[60%] mx-auto min-h-[25%] left-0 right-0 bg-red-50 border-4 rounded-xl border-red-800 z-10 pointer-events-auto p-6", style: { zIndex: 1000 }, "data-branch-element": true, children: _jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "text-center mb-4", children: _jsx("span", { className: "font-bold text-xl text-red-900", children: "\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F" }) }), _jsxs("div", { className: "text-center text-red-800", children: [_jsx("p", { className: "mb-4", children: "\u9078\u629E\u80A2\u30C7\u30FC\u30BF\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F" }), _jsx("p", { className: "text-sm", children: "\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u6B21\u306B\u9032\u3080" })] })] }) }));
|
|
18
|
+
}
|
|
19
|
+
if (!branchData.isActive || branchData.currentChoices.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
// 変数分岐の場合は選択肢UIを表示しない
|
|
23
|
+
if (branchData.branchType === "variable") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return (_jsx("div", { className: "absolute bottom-[5%] max-w-[60%] mx-auto min-h-[25%] left-0 right-0 bg-amber-50 border-4 rounded-xl border-amber-800 z-10 pointer-events-auto p-6", style: { zIndex: 1000 }, "data-branch-element": true, children: _jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "text-center mb-4", children: _jsx("span", { className: "font-bold text-xl text-amber-900", children: "\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044" }) }), _jsx("div", { className: "space-y-3", children: branchData.currentChoices.map((choice) => (_jsx("button", { type: "button", onClick: () => handleChoiceClick(choice.id), disabled: choice.isDisabled, className: `w-full p-4 text-lg text-left rounded-lg border-2 transition-all ${choice.isDisabled
|
|
27
|
+
? "bg-gray-200 text-gray-400 border-gray-300 cursor-not-allowed"
|
|
28
|
+
: "bg-white text-amber-900 border-amber-700 hover:bg-amber-100 hover:border-amber-900 hover:shadow-lg active:scale-98 cursor-pointer"}`, "data-choice-button": true, "data-choice-id": choice.id, children: choice.text }, choice.id))) })] }) }));
|
|
29
|
+
};
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
2
3
|
import { useDataAPI } from "../contexts/DataContext";
|
|
4
|
+
import { getFontFamilyStyle } from "../hooks/useFontLoader";
|
|
3
5
|
export const DialogueBox = () => {
|
|
4
6
|
var _a, _b;
|
|
5
7
|
const dataAPI = useDataAPI();
|
|
6
8
|
const currentBlock = dataAPI.get("playback", "currentBlock");
|
|
7
9
|
const content = dataAPI.get("playback", "displayText") || "";
|
|
8
10
|
const displayedCharacters = dataAPI.get("playback", "displayedCharacters") || [];
|
|
11
|
+
const fontsData = dataAPI.get("fonts");
|
|
12
|
+
// フォントスタイルを計算
|
|
13
|
+
const fontFamilyStyle = useMemo(() => {
|
|
14
|
+
return getFontFamilyStyle(fontsData === null || fontsData === void 0 ? void 0 : fontsData.selectedFontFamily, fontsData === null || fontsData === void 0 ? void 0 : fontsData.fonts);
|
|
15
|
+
}, [fontsData === null || fontsData === void 0 ? void 0 : fontsData.selectedFontFamily, fontsData === null || fontsData === void 0 ? void 0 : fontsData.fonts]);
|
|
9
16
|
if (!currentBlock) {
|
|
10
17
|
return null;
|
|
11
18
|
}
|
|
19
|
+
// fullscreen_text ブロックでは DialogueBox を表示しない
|
|
20
|
+
if (currentBlock.blockType === "fullscreen_text") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
// テキストがない場合は表示しない
|
|
24
|
+
if (!content) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
12
27
|
const speakerName = (_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name;
|
|
13
28
|
const currentSpeakerId = (_b = currentBlock.speaker) === null || _b === void 0 ? void 0 : _b.id;
|
|
14
29
|
// 話者の位置を特定
|
|
@@ -24,5 +39,5 @@ export const DialogueBox = () => {
|
|
|
24
39
|
return "justify-end -mr-[5%]"; // 右側
|
|
25
40
|
return "justify-center"; // 中央
|
|
26
41
|
};
|
|
27
|
-
return (_jsx("div", { className: "absolute bottom-[5%] max-w-[60%] mx-auto h-[25%] left-0 right-0 bg-amber-50 border-4 rounded-xl border-amber-800 z-10 pointer-events-auto", style: { zIndex: 1000 }, "data-dialogue-element": true, children: _jsxs("div", { className: "relative space-y-3", children: [speakerName && (_jsx("div", { className: `flex items-center space-x-2 ${getNamePositionClass()}`, children: _jsx("div", { className: "bg-amber-950 text-white rounded-xl relative -top-5 w-full max-w-30 text-center border-4 border-amber-50 shadow-lg", "data-speaker-name-element": true, children: _jsx("span", { className: "font-bold text-lg", children: speakerName }) }) })), _jsx("div", { className: "absolute top-0 p-4 leading-relaxed text-2xl",
|
|
42
|
+
return (_jsx("div", { className: "absolute bottom-[5%] max-w-[60%] mx-auto h-[25%] left-0 right-0 bg-amber-50 border-4 rounded-xl border-amber-800 z-10 pointer-events-auto", style: { zIndex: 1000 }, "data-dialogue-element": true, children: _jsxs("div", { className: "relative space-y-3", children: [speakerName && (_jsx("div", { className: `flex items-center space-x-2 ${getNamePositionClass()}`, children: _jsx("div", { className: "bg-amber-950 text-white rounded-xl relative -top-5 w-full max-w-30 text-center border-4 border-amber-50 shadow-lg", "data-speaker-name-element": true, children: _jsx("span", { className: "font-bold text-lg", children: speakerName }) }) })), _jsx("div", { className: "absolute top-0 p-4 leading-relaxed text-2xl text-black", style: { fontFamily: fontFamilyStyle }, children: content }), _jsx("div", { className: "flex justify-end", children: _jsx("div", { className: "text-white/60 text-sm animate-pulse", children: "\u25BC" }) })] }) }));
|
|
28
43
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useDataAPI } from "../contexts/DataContext";
|
|
3
|
+
/**
|
|
4
|
+
* フォント設定パネル
|
|
5
|
+
* プレイヤー内でフォントを選択するためのUI
|
|
6
|
+
*/
|
|
7
|
+
export const FontSettingsPanel = ({ onClose, }) => {
|
|
8
|
+
var _a;
|
|
9
|
+
const dataAPI = useDataAPI();
|
|
10
|
+
const fontsData = dataAPI.get("fonts");
|
|
11
|
+
const fonts = (_a = fontsData === null || fontsData === void 0 ? void 0 : fontsData.fonts) !== null && _a !== void 0 ? _a : [];
|
|
12
|
+
const selectedFontFamily = fontsData === null || fontsData === void 0 ? void 0 : fontsData.selectedFontFamily;
|
|
13
|
+
// フォントが設定されていない場合
|
|
14
|
+
if (fonts.length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const handleFontChange = (fontFamily) => {
|
|
18
|
+
dataAPI.updateSettings({ selectedFontFamily: fontFamily });
|
|
19
|
+
};
|
|
20
|
+
// デフォルトフォント(最初のフォント)
|
|
21
|
+
const defaultFont = fonts[0];
|
|
22
|
+
const currentFontFamily = selectedFontFamily || (defaultFont === null || defaultFont === void 0 ? void 0 : defaultFont.fontFamily);
|
|
23
|
+
return (_jsx("div", { className: "absolute inset-0 bg-black/70 flex items-center justify-center z-[1100] pointer-events-auto", onClick: (e) => {
|
|
24
|
+
if (e.target === e.currentTarget) {
|
|
25
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
26
|
+
}
|
|
27
|
+
}, children: _jsxs("div", { className: "bg-white rounded-lg p-6 min-w-[300px] max-w-[400px] max-h-[80%] overflow-y-auto", children: [_jsxs("div", { className: "flex justify-between items-center mb-4", children: [_jsx("h3", { className: "text-lg font-bold text-gray-900", children: "\u30D5\u30A9\u30F3\u30C8\u8A2D\u5B9A" }), onClose && (_jsx("button", { type: "button", onClick: onClose, className: "text-gray-500 hover:text-gray-700 text-2xl leading-none", children: "\u00D7" }))] }), _jsx("div", { className: "space-y-2", children: fonts.map((font, index) => (_jsxs("button", { type: "button", onClick: () => handleFontChange(font.fontFamily), className: `w-full text-left p-3 rounded-lg border transition-colors ${currentFontFamily === font.fontFamily
|
|
28
|
+
? "border-blue-500 bg-blue-50"
|
|
29
|
+
: "border-gray-200 hover:border-gray-300 hover:bg-gray-50"}`, style: { fontFamily: `"${font.fontFamily}", sans-serif` }, children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-gray-900", children: font.name }), _jsxs("div", { className: "flex items-center gap-2", children: [index === 0 && (_jsx("span", { className: "text-xs bg-gray-200 text-gray-600 px-2 py-0.5 rounded", children: "\u30C7\u30D5\u30A9\u30EB\u30C8" })), currentFontFamily === font.fontFamily && (_jsx("span", { className: "text-blue-500", children: "\u2713" }))] })] }), _jsx("p", { className: "text-sm text-gray-500 mt-1", style: { fontFamily: `"${font.fontFamily}", sans-serif` }, children: "\u3042\u3044\u3046\u3048\u304A \u304B\u304D\u304F\u3051\u3053 1234567890" })] }, font.id))) })] }) }));
|
|
30
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
3
|
+
import { useDataAPI } from "../contexts/DataContext";
|
|
4
|
+
import { getFontFamilyStyle } from "../hooks/useFontLoader";
|
|
5
|
+
import { useFullscreenText } from "../hooks/useFullscreenText";
|
|
6
|
+
export const FullscreenTextBox = ({ onComplete, }) => {
|
|
7
|
+
var _a;
|
|
8
|
+
const dataAPI = useDataAPI();
|
|
9
|
+
const currentBlock = dataAPI.get("playback", "currentBlock");
|
|
10
|
+
const settings = dataAPI.get("settings");
|
|
11
|
+
const fontsData = dataAPI.get("fonts");
|
|
12
|
+
// フォントスタイルを計算
|
|
13
|
+
const fontFamilyStyle = useMemo(() => {
|
|
14
|
+
return getFontFamilyStyle(fontsData === null || fontsData === void 0 ? void 0 : fontsData.selectedFontFamily, fontsData === null || fontsData === void 0 ? void 0 : fontsData.fonts);
|
|
15
|
+
}, [fontsData === null || fontsData === void 0 ? void 0 : fontsData.selectedFontFamily, fontsData === null || fontsData === void 0 ? void 0 : fontsData.fonts]);
|
|
16
|
+
const { paragraphs, currentParagraphIndex, displayText, isTyping, isComplete, advanceParagraph, } = useFullscreenText(currentBlock !== null && currentBlock !== void 0 ? currentBlock : null, {
|
|
17
|
+
textSpeed: (_a = settings === null || settings === void 0 ? void 0 : settings.textSpeed) !== null && _a !== void 0 ? _a : 80,
|
|
18
|
+
onAllComplete: onComplete,
|
|
19
|
+
});
|
|
20
|
+
// Get textAlign option from block options (default: center)
|
|
21
|
+
const textAlign = useMemo(() => {
|
|
22
|
+
var _a;
|
|
23
|
+
const options = currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.options;
|
|
24
|
+
return (_a = options === null || options === void 0 ? void 0 : options.textAlign) !== null && _a !== void 0 ? _a : "center";
|
|
25
|
+
}, [currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.options]);
|
|
26
|
+
const handleClick = useCallback((e) => {
|
|
27
|
+
e.stopPropagation();
|
|
28
|
+
const advanced = advanceParagraph();
|
|
29
|
+
if (!advanced && isComplete) {
|
|
30
|
+
onComplete === null || onComplete === void 0 ? void 0 : onComplete();
|
|
31
|
+
}
|
|
32
|
+
}, [advanceParagraph, isComplete, onComplete]);
|
|
33
|
+
if (!currentBlock || currentBlock.blockType !== "fullscreen_text") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (paragraphs.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// Determine alignment classes based on textAlign option
|
|
40
|
+
const alignmentClass = textAlign === "top" ? "items-start pt-[5%]" : "items-center";
|
|
41
|
+
return (_jsx("div", { className: `absolute inset-0 flex ${alignmentClass} justify-center z-[999] pointer-events-auto cursor-pointer`, onClick: handleClick, "data-fullscreen-text-element": true, "data-text-align": textAlign, style: {
|
|
42
|
+
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
43
|
+
// コンテナクエリのコンテキストを設定
|
|
44
|
+
containerType: "size",
|
|
45
|
+
}, children: _jsxs("div", { className: "w-[80%] max-h-[80%] overflow-y-auto p-[3%]", "data-fullscreen-text-container": true, children: [paragraphs.map((paragraph, index) => {
|
|
46
|
+
const state = index < currentParagraphIndex
|
|
47
|
+
? "read"
|
|
48
|
+
: index === currentParagraphIndex
|
|
49
|
+
? "current"
|
|
50
|
+
: "pending";
|
|
51
|
+
return (_jsx("p", { className: "leading-relaxed transition-opacity duration-300", "data-paragraph-index": index, "data-paragraph-state": state, style: {
|
|
52
|
+
fontFamily: fontFamilyStyle,
|
|
53
|
+
// コンテナの高さに対して相対的なフォントサイズ
|
|
54
|
+
// フルスクリーンテキストは目立つように大きめ(約3.5%)
|
|
55
|
+
fontSize: "3.5cqh",
|
|
56
|
+
marginBottom: "3cqh",
|
|
57
|
+
color: state === "read"
|
|
58
|
+
? "rgba(255, 255, 255, 0.4)"
|
|
59
|
+
: state === "current"
|
|
60
|
+
? "rgba(255, 255, 255, 1)"
|
|
61
|
+
: "rgba(255, 255, 255, 0)",
|
|
62
|
+
textShadow: state === "current" ? "0 2px 4px rgba(0, 0, 0, 0.5)" : "none",
|
|
63
|
+
}, children: index === currentParagraphIndex ? displayText : paragraph }, index));
|
|
64
|
+
}), _jsxs("div", { className: "absolute", style: {
|
|
65
|
+
bottom: "2cqh",
|
|
66
|
+
right: "2cqh",
|
|
67
|
+
fontSize: "2cqh",
|
|
68
|
+
color: "rgba(255, 255, 255, 0.6)",
|
|
69
|
+
}, "data-fullscreen-text-progress": true, children: [currentParagraphIndex + 1, " / ", paragraphs.length, isTyping && (_jsx("span", { className: "ml-2 animate-pulse", style: { fontSize: "1.2em" }, children: "|" })), !isTyping && (_jsx("span", { className: "ml-2 animate-bounce inline-block", children: isComplete ? "" : "v" }))] })] }) }));
|
|
70
|
+
};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type React from "react";
|
|
2
1
|
import type { DisplayedCharacter, PublishedScenario, ScenarioBlock } from "../types";
|
|
3
2
|
interface GameScreenProps {
|
|
4
3
|
scenario: PublishedScenario;
|
|
5
4
|
currentBlock: ScenarioBlock;
|
|
5
|
+
previousBlock?: ScenarioBlock | null;
|
|
6
6
|
displayedCharacters: DisplayedCharacter[];
|
|
7
|
+
inactiveCharacterBrightness?: number;
|
|
8
|
+
characterSpacing?: number;
|
|
7
9
|
}
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* ゲームスクリーンコンポーネント
|
|
12
|
+
* React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
|
|
13
|
+
* これにより displayText の更新による不要な再描画を防止
|
|
14
|
+
*/
|
|
15
|
+
export declare const GameScreen: import("react").NamedExoticComponent<GameScreenProps>;
|
|
9
16
|
export {};
|