@luna-editor/engine 0.5.0 → 0.5.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.js +120 -25
- package/dist/components/BackgroundLayer.js +7 -5
- package/dist/components/GameScreen.d.ts +8 -2
- package/dist/components/GameScreen.js +253 -26
- package/dist/components/OverlayUI.d.ts +2 -3
- package/dist/components/OverlayUI.js +3 -14
- package/dist/components/PluginComponentProvider.js +1 -1
- package/dist/contexts/DataContext.js +25 -8
- package/dist/contexts/PlaybackTextContext.d.ts +32 -0
- package/dist/contexts/PlaybackTextContext.js +29 -0
- package/dist/data-api-types.d.ts +6 -2
- package/dist/hooks/useFontLoader.d.ts +9 -2
- package/dist/hooks/useFontLoader.js +126 -87
- package/dist/hooks/usePlayerLogic.js +3 -0
- package/dist/hooks/usePluginAPI.js +1 -1
- package/dist/hooks/useSoundPlayer.js +33 -5
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/plugin/PluginManager.d.ts +4 -1
- package/dist/plugin/PluginManager.js +51 -19
- package/dist/sdk.d.ts +1 -1
- package/dist/types.d.ts +29 -2
- package/package.json +1 -1
|
@@ -10,115 +10,137 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { useEffect, useRef, useState } from "react";
|
|
11
11
|
// グローバルなフォントキャッシュ(ページ内で共有)
|
|
12
12
|
const fontCache = new Set();
|
|
13
|
+
/**
|
|
14
|
+
* Googleフォントの<link>タグを挿入し、CSSの読み込み完了を待つ
|
|
15
|
+
*/
|
|
16
|
+
function loadGoogleFontCSS(fontFamily) {
|
|
17
|
+
const existingLink = document.querySelector(`link[data-font-family="${fontFamily}"]`);
|
|
18
|
+
if (existingLink) {
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
const link = document.createElement("link");
|
|
22
|
+
link.rel = "stylesheet";
|
|
23
|
+
// display=blockでフォント読み込みまでテキストを非表示にする(FOUTを防止)
|
|
24
|
+
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@400;700&display=block`;
|
|
25
|
+
link.setAttribute("data-font-family", fontFamily);
|
|
26
|
+
document.head.appendChild(link);
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
link.onload = () => resolve();
|
|
29
|
+
link.onerror = () => {
|
|
30
|
+
console.warn(`Failed to load Google font CSS: ${fontFamily}`);
|
|
31
|
+
resolve();
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Googleフォントのフォントデータを事前にダウンロード
|
|
37
|
+
* 400/700両方のウェイトをプリロードする
|
|
38
|
+
*/
|
|
39
|
+
function preloadGoogleFont(fontFamily, textToPreload) {
|
|
40
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
try {
|
|
42
|
+
// 400と700両方のウェイトをプリロード
|
|
43
|
+
yield Promise.all([
|
|
44
|
+
document.fonts.load(`400 16px "${fontFamily}"`, textToPreload),
|
|
45
|
+
document.fonts.load(`700 16px "${fontFamily}"`, textToPreload),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.warn(`Failed to preload Google font: ${fontFamily}`, error);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* カスタムフォントをFontFace APIで読み込み
|
|
55
|
+
*/
|
|
56
|
+
function loadCustomFont(fontFamily, url, fontName) {
|
|
57
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
try {
|
|
59
|
+
// 既にdocument.fontsに登録されているかチェック
|
|
60
|
+
let fontExists = false;
|
|
61
|
+
for (const existingFont of document.fonts) {
|
|
62
|
+
if (existingFont.family === fontFamily) {
|
|
63
|
+
fontExists = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!fontExists) {
|
|
68
|
+
const fontFace = new FontFace(fontFamily, `url(${url})`);
|
|
69
|
+
yield fontFace.load();
|
|
70
|
+
document.fonts.add(fontFace);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.warn(`Failed to load custom font: ${fontName}`, error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
13
78
|
/**
|
|
14
79
|
* フォントを読み込むカスタムフック
|
|
15
80
|
* - Googleフォント: <link>タグで読み込み + document.fonts.load()でプリロード
|
|
16
81
|
* - カスタムフォント: FontFace APIで読み込み
|
|
17
|
-
* -
|
|
82
|
+
* - すべてのフォントが完全にダウンロード&キャッシュされてからisLoaded=trueになる
|
|
83
|
+
* - 再生開始前に全フォントを一括ダウンロードし、再生中のフォントスワップを防止する
|
|
84
|
+
* - preloadText: シナリオ内の全テキストを渡すと、使用される全ての文字でフォントをプリロード
|
|
18
85
|
*/
|
|
19
|
-
export function useFontLoader(fonts) {
|
|
86
|
+
export function useFontLoader(fonts, preloadText) {
|
|
20
87
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
21
88
|
const [loadedFonts, setLoadedFonts] = useState([]);
|
|
22
|
-
|
|
89
|
+
// 現在のロード処理をキャンセルするための世代カウンター
|
|
90
|
+
const generationRef = useRef(0);
|
|
23
91
|
useEffect(() => {
|
|
24
92
|
if (!fonts || fonts.length === 0) {
|
|
25
93
|
setIsLoaded(true);
|
|
26
94
|
return;
|
|
27
95
|
}
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
loadingRef.current = true;
|
|
96
|
+
// 新しいロード処理を開始する際、前回の結果を無効化
|
|
97
|
+
const generation = ++generationRef.current;
|
|
98
|
+
setIsLoaded(false);
|
|
32
99
|
const loadFonts = () => __awaiter(this, void 0, void 0, function* () {
|
|
100
|
+
var _a;
|
|
33
101
|
const loadedFontFamilies = [];
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
102
|
+
const textToPreload = preloadText || "あいうえおABCabc123";
|
|
103
|
+
// すべてのGoogle FontsのCSS読み込みを並列で開始
|
|
104
|
+
const googleFonts = fonts.filter((f) => f.type === "google" && !fontCache.has(f.fontFamily));
|
|
105
|
+
const customFonts = fonts.filter((f) => f.type === "custom" && f.url && !fontCache.has(f.fontFamily));
|
|
106
|
+
const cachedFonts = fonts.filter((f) => fontCache.has(f.fontFamily));
|
|
107
|
+
// キャッシュ済みフォントは即座に追加
|
|
108
|
+
for (const font of cachedFonts) {
|
|
109
|
+
loadedFontFamilies.push(font.fontFamily);
|
|
110
|
+
}
|
|
111
|
+
// Google FontsのCSS読み込みを並列実行
|
|
112
|
+
yield Promise.all(googleFonts.map((font) => loadGoogleFontCSS(font.fontFamily)));
|
|
113
|
+
// キャンセルチェック
|
|
114
|
+
if (generation !== generationRef.current)
|
|
115
|
+
return;
|
|
116
|
+
// Google Fontsのフォントデータプリロード + カスタムフォント読み込みを並列実行
|
|
117
|
+
const allFontPromises = [];
|
|
118
|
+
for (const font of googleFonts) {
|
|
119
|
+
allFontPromises.push(preloadGoogleFont(font.fontFamily, textToPreload).then(() => {
|
|
120
|
+
fontCache.add(font.fontFamily);
|
|
38
121
|
loadedFontFamilies.push(font.fontFamily);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const link = document.createElement("link");
|
|
47
|
-
link.rel = "stylesheet";
|
|
48
|
-
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(font.fontFamily)}:wght@400;700&display=swap`;
|
|
49
|
-
link.setAttribute("data-font-family", font.fontFamily);
|
|
50
|
-
document.head.appendChild(link);
|
|
51
|
-
// CSSファイルの読み込み完了を待つ
|
|
52
|
-
yield new Promise((resolve) => {
|
|
53
|
-
link.onload = () => resolve();
|
|
54
|
-
link.onerror = () => {
|
|
55
|
-
console.warn(`Failed to load Google font CSS: ${font.fontFamily}`);
|
|
56
|
-
resolve();
|
|
57
|
-
};
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
// フォントの実際の読み込み(プリロード)を開始
|
|
61
|
-
// document.fonts.load()でフォントデータを事前にダウンロード
|
|
62
|
-
const preloadPromise = document.fonts
|
|
63
|
-
.load(`16px "${font.fontFamily}"`)
|
|
64
|
-
.then(() => {
|
|
65
|
-
fontCache.add(font.fontFamily);
|
|
66
|
-
loadedFontFamilies.push(font.fontFamily);
|
|
67
|
-
})
|
|
68
|
-
.catch((error) => {
|
|
69
|
-
console.warn(`Failed to preload Google font: ${font.fontFamily}`, error);
|
|
70
|
-
// エラーでもキャッシュに追加(再試行防止)
|
|
71
|
-
fontCache.add(font.fontFamily);
|
|
72
|
-
loadedFontFamilies.push(font.fontFamily);
|
|
73
|
-
});
|
|
74
|
-
fontLoadPromises.push(preloadPromise);
|
|
75
|
-
}
|
|
76
|
-
else if (font.type === "custom" && font.url) {
|
|
77
|
-
// カスタムフォントをFontFace APIで読み込み
|
|
78
|
-
const loadCustomFont = () => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
-
try {
|
|
80
|
-
// 既にdocument.fontsに登録されているかチェック
|
|
81
|
-
let fontExists = false;
|
|
82
|
-
for (const existingFont of document.fonts) {
|
|
83
|
-
if (existingFont.family === font.fontFamily) {
|
|
84
|
-
fontExists = true;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (!fontExists) {
|
|
89
|
-
const fontFace = new FontFace(font.fontFamily, `url(${font.url})`);
|
|
90
|
-
// フォントデータをダウンロード&パース
|
|
91
|
-
yield fontFace.load();
|
|
92
|
-
// document.fontsに追加(キャッシュとして機能)
|
|
93
|
-
document.fonts.add(fontFace);
|
|
94
|
-
}
|
|
95
|
-
fontCache.add(font.fontFamily);
|
|
96
|
-
loadedFontFamilies.push(font.fontFamily);
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
console.warn(`Failed to load custom font: ${font.name}`, error);
|
|
100
|
-
// エラーでもキャッシュに追加(再試行防止)
|
|
101
|
-
fontCache.add(font.fontFamily);
|
|
102
|
-
loadedFontFamilies.push(font.fontFamily);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
fontLoadPromises.push(loadCustomFont());
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
console.warn(`Failed to load font: ${font.name}`, error);
|
|
110
|
-
}
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
for (const font of customFonts) {
|
|
125
|
+
allFontPromises.push(loadCustomFont(font.fontFamily, (_a = font.url) !== null && _a !== void 0 ? _a : "", font.name).then(() => {
|
|
126
|
+
fontCache.add(font.fontFamily);
|
|
127
|
+
loadedFontFamilies.push(font.fontFamily);
|
|
128
|
+
}));
|
|
111
129
|
}
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
yield Promise.all(allFontPromises);
|
|
131
|
+
// キャンセルチェック
|
|
132
|
+
if (generation !== generationRef.current)
|
|
133
|
+
return;
|
|
114
134
|
// document.fonts.readyで全フォントの準備完了を確認
|
|
115
135
|
yield document.fonts.ready;
|
|
136
|
+
// キャンセルチェック
|
|
137
|
+
if (generation !== generationRef.current)
|
|
138
|
+
return;
|
|
116
139
|
setLoadedFonts(loadedFontFamilies);
|
|
117
140
|
setIsLoaded(true);
|
|
118
|
-
loadingRef.current = false;
|
|
119
141
|
});
|
|
120
142
|
loadFonts();
|
|
121
|
-
}, [fonts]);
|
|
143
|
+
}, [fonts, preloadText]);
|
|
122
144
|
return { isLoaded, loadedFonts };
|
|
123
145
|
}
|
|
124
146
|
/**
|
|
@@ -139,6 +161,23 @@ export function getFontFamilyStyle(selectedFontFamily, fonts) {
|
|
|
139
161
|
// 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
|
|
140
162
|
return `"${fonts[0].fontFamily}", sans-serif`;
|
|
141
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* UIフォントファミリーに基づいてCSSのfont-family値を生成
|
|
166
|
+
* メニュー、ボタン、ラベルなどのUI要素に使用
|
|
167
|
+
*/
|
|
168
|
+
export function getUIFontFamilyStyle(selectedUIFontFamily, fonts) {
|
|
169
|
+
if (!fonts || fonts.length === 0) {
|
|
170
|
+
return "sans-serif";
|
|
171
|
+
}
|
|
172
|
+
if (selectedUIFontFamily) {
|
|
173
|
+
const selectedFont = fonts.find((f) => f.fontFamily === selectedUIFontFamily);
|
|
174
|
+
if (selectedFont) {
|
|
175
|
+
return `"${selectedUIFontFamily}", sans-serif`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
|
|
179
|
+
return `"${fonts[0].fontFamily}", sans-serif`;
|
|
180
|
+
}
|
|
142
181
|
/**
|
|
143
182
|
* フォントがキャッシュに存在するかチェック
|
|
144
183
|
*/
|
|
@@ -20,6 +20,9 @@ const calculateDisplayedCharacters = (blocks, currentIndex) => {
|
|
|
20
20
|
positionY: char.positionY,
|
|
21
21
|
zIndex: char.zIndex,
|
|
22
22
|
scale: char.scale,
|
|
23
|
+
cropLeft: char.cropLeft,
|
|
24
|
+
cropRight: char.cropRight,
|
|
25
|
+
cropFade: char.cropFade,
|
|
23
26
|
object: char.object,
|
|
24
27
|
entityState: char.entityState,
|
|
25
28
|
// レイヤー機能用
|
|
@@ -40,13 +40,36 @@ export const useSoundPlayer = ({ soundBlocks, isFirstRenderComplete, muteAudio =
|
|
|
40
40
|
isBGM,
|
|
41
41
|
isVoice,
|
|
42
42
|
]);
|
|
43
|
-
|
|
43
|
+
const bgmFadeIntervalRef = useRef(null);
|
|
44
|
+
// BGMをフェードアウトして停止
|
|
44
45
|
const stopBGM = useCallback(() => {
|
|
45
|
-
if (bgmRef.current)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
if (!bgmRef.current)
|
|
47
|
+
return;
|
|
48
|
+
// 既にフェード中なら前回のをクリア
|
|
49
|
+
if (bgmFadeIntervalRef.current) {
|
|
50
|
+
clearInterval(bgmFadeIntervalRef.current);
|
|
49
51
|
}
|
|
52
|
+
const audio = bgmRef.current.audio;
|
|
53
|
+
const fadeDuration = 1000; // 1秒フェードアウト
|
|
54
|
+
const fadeSteps = 20;
|
|
55
|
+
const stepTime = fadeDuration / fadeSteps;
|
|
56
|
+
const volumeStep = audio.volume / fadeSteps;
|
|
57
|
+
let stepsRemaining = fadeSteps;
|
|
58
|
+
bgmFadeIntervalRef.current = setInterval(() => {
|
|
59
|
+
stepsRemaining--;
|
|
60
|
+
if (stepsRemaining <= 0) {
|
|
61
|
+
if (bgmFadeIntervalRef.current) {
|
|
62
|
+
clearInterval(bgmFadeIntervalRef.current);
|
|
63
|
+
bgmFadeIntervalRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
audio.pause();
|
|
66
|
+
audio.currentTime = 0;
|
|
67
|
+
bgmRef.current = null;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
audio.volume = Math.max(0, volumeStep * stepsRemaining);
|
|
71
|
+
}
|
|
72
|
+
}, stepTime);
|
|
50
73
|
}, []);
|
|
51
74
|
// 特定のSEを停止
|
|
52
75
|
const stopSE = useCallback((soundId) => {
|
|
@@ -185,6 +208,11 @@ export const useSoundPlayer = ({ soundBlocks, isFirstRenderComplete, muteAudio =
|
|
|
185
208
|
// クリーンアップ(直接refにアクセスして依存配列の問題を回避)
|
|
186
209
|
useEffect(() => {
|
|
187
210
|
return () => {
|
|
211
|
+
// フェードintervalをクリア
|
|
212
|
+
if (bgmFadeIntervalRef.current) {
|
|
213
|
+
clearInterval(bgmFadeIntervalRef.current);
|
|
214
|
+
bgmFadeIntervalRef.current = null;
|
|
215
|
+
}
|
|
188
216
|
// BGMを停止
|
|
189
217
|
if (bgmRef.current) {
|
|
190
218
|
bgmRef.current.audio.pause();
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,10 @@ export { aspectRatio, BasisHeight, BasisWidth } from "./constants/screen-size";
|
|
|
5
5
|
export type { AudioSettings } from "./contexts/AudioContext";
|
|
6
6
|
export { useAudioSettings } from "./contexts/AudioContext";
|
|
7
7
|
export { useDataAPI } from "./contexts/DataContext";
|
|
8
|
+
export { getFontFamilyStyle, getUIFontFamilyStyle, } from "./hooks/useFontLoader";
|
|
8
9
|
export { setGlobalUIAPI, usePluginAPI, useUIVisibility, } from "./hooks/usePluginAPI";
|
|
9
10
|
export { useScreenScale, useScreenSize, useToPixel, } from "./hooks/useScreenSize";
|
|
10
11
|
export { Player } from "./Player";
|
|
11
|
-
export type { BacklogData, BlockChangeContext, DataAPI, DataContext, FontsData, PlayerSettingsData, ScenarioPlaybackData, TransitionSource, } from "./sdk";
|
|
12
|
-
export { ComponentType, renderTextWithLineBreaks } from "./sdk";
|
|
12
|
+
export type { BacklogData, BlockChangeContext, DataAPI, DataContext, FontsData, LunaPlugin, PlayerSettingsData, PluginAPI, ScenarioPlaybackData, TransitionSource, } from "./sdk";
|
|
13
|
+
export { ComponentType, definePlugin, renderTextWithLineBreaks } from "./sdk";
|
|
13
14
|
export type { ActionNode, BacklogEntry, Character, DisplayedCharacter, EntityState, FontType, PlayerProps, PlayerSettings, PlayerState, PluginConfig, PublishedScenario, ScenarioBlock, ScenarioBlockAttributeValue, ScenarioBlockCharacter, ScenarioBlockEntityAttributeValue, ScenarioBlockType, WorkFont, WorkSound, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ export { OverlayUI } from "./components/OverlayUI";
|
|
|
5
5
|
export { aspectRatio, BasisHeight, BasisWidth } from "./constants/screen-size";
|
|
6
6
|
export { useAudioSettings } from "./contexts/AudioContext";
|
|
7
7
|
export { useDataAPI } from "./contexts/DataContext";
|
|
8
|
+
export { getFontFamilyStyle, getUIFontFamilyStyle, } from "./hooks/useFontLoader";
|
|
8
9
|
export { setGlobalUIAPI, usePluginAPI, useUIVisibility, } from "./hooks/usePluginAPI";
|
|
9
10
|
export { useScreenScale, useScreenSize, useToPixel, } from "./hooks/useScreenSize";
|
|
10
11
|
export { Player } from "./Player";
|
|
11
|
-
export { ComponentType, renderTextWithLineBreaks } from "./sdk";
|
|
12
|
+
export { ComponentType, definePlugin, renderTextWithLineBreaks } from "./sdk";
|
|
@@ -62,7 +62,10 @@ export declare class PluginManager {
|
|
|
62
62
|
* 既に初期化済みの場合はスキップ
|
|
63
63
|
*/
|
|
64
64
|
private initializeReactRuntime;
|
|
65
|
-
loadPlugin(packageName: string, bundleUrl: string, config?: unknown
|
|
65
|
+
loadPlugin(packageName: string, bundleUrl: string | undefined, config?: unknown, assets?: {
|
|
66
|
+
filename: string;
|
|
67
|
+
url: string;
|
|
68
|
+
}[], plugin?: LunaPlugin): Promise<void>;
|
|
66
69
|
private doLoadPlugin;
|
|
67
70
|
private loadPluginScript;
|
|
68
71
|
private createPluginAPI;
|
|
@@ -21,6 +21,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
21
21
|
import React from "react";
|
|
22
22
|
import { useScreenSizeAtom } from "../atoms/screen-size";
|
|
23
23
|
import { useDataAPI } from "../contexts/DataContext";
|
|
24
|
+
import { getFontFamilyStyle, getUIFontFamilyStyle, } from "../hooks/useFontLoader";
|
|
24
25
|
import { usePluginAPI, useUIVisibility } from "../hooks/usePluginAPI";
|
|
25
26
|
import { useScreenScale, useScreenSize, useToPixel, } from "../hooks/useScreenSize";
|
|
26
27
|
import { ComponentType, definePlugin, } from "../sdk";
|
|
@@ -107,18 +108,32 @@ export class PluginManager {
|
|
|
107
108
|
useScreenScale,
|
|
108
109
|
useScreenSizeAtom,
|
|
109
110
|
ComponentType,
|
|
110
|
-
definePlugin
|
|
111
|
+
definePlugin,
|
|
112
|
+
getFontFamilyStyle,
|
|
113
|
+
getUIFontFamilyStyle });
|
|
111
114
|
}
|
|
112
|
-
loadPlugin(packageName, bundleUrl, config) {
|
|
115
|
+
loadPlugin(packageName, bundleUrl, config, assets, plugin) {
|
|
113
116
|
return __awaiter(this, void 0, void 0, function* () {
|
|
114
117
|
// 開発環境ではキャッシュをバイパスして常に最新コードをロード
|
|
115
118
|
const isDevelopment = this.isDevelopment();
|
|
116
119
|
if (isDevelopment) {
|
|
117
|
-
//
|
|
120
|
+
// ロード中の場合はそのPromiseを待って完了させる(二重ロード防止)
|
|
121
|
+
const existingLoadPromise = globalLoadingPlugins.get(packageName);
|
|
122
|
+
if (existingLoadPromise) {
|
|
123
|
+
yield existingLoadPromise;
|
|
124
|
+
// ロード完了後、結果をローカルにコピー
|
|
125
|
+
const loaded = globalLoadedPlugins.get(packageName);
|
|
126
|
+
if (loaded) {
|
|
127
|
+
this.plugins.set(packageName, Object.assign(Object.assign({}, loaded), { config: config !== null && config !== void 0 ? config : loaded.config }));
|
|
128
|
+
for (const [type, component] of loaded.instance.components) {
|
|
129
|
+
this.componentRegistry.set(type, component);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// ロード中でなければキャッシュをクリアして再ロード
|
|
118
135
|
globalLoadedPlugins.delete(packageName);
|
|
119
136
|
this.plugins.delete(packageName);
|
|
120
|
-
// ロード中のPromiseもクリア(再ロードを確実にするため)
|
|
121
|
-
globalLoadingPlugins.delete(packageName);
|
|
122
137
|
}
|
|
123
138
|
else {
|
|
124
139
|
// 本番環境のみキャッシュを使用
|
|
@@ -160,7 +175,7 @@ export class PluginManager {
|
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
177
|
// ロード処理をPromiseとして保存
|
|
163
|
-
const loadPromise = this.doLoadPlugin(packageName, bundleUrl, config);
|
|
178
|
+
const loadPromise = this.doLoadPlugin(packageName, bundleUrl, config, assets, plugin);
|
|
164
179
|
globalLoadingPlugins.set(packageName, loadPromise);
|
|
165
180
|
try {
|
|
166
181
|
yield loadPromise;
|
|
@@ -171,11 +186,13 @@ export class PluginManager {
|
|
|
171
186
|
}
|
|
172
187
|
});
|
|
173
188
|
}
|
|
174
|
-
doLoadPlugin(packageName, bundleUrl, config) {
|
|
189
|
+
doLoadPlugin(packageName, bundleUrl, config, assets, localPlugin) {
|
|
175
190
|
return __awaiter(this, void 0, void 0, function* () {
|
|
176
191
|
try {
|
|
177
|
-
//
|
|
178
|
-
const plugin =
|
|
192
|
+
// ローカルプラグインが渡された場合はスクリプトロードをスキップ
|
|
193
|
+
const plugin = localPlugin
|
|
194
|
+
? localPlugin
|
|
195
|
+
: yield this.loadPluginScript(bundleUrl, packageName);
|
|
179
196
|
const instance = {
|
|
180
197
|
actionNodes: new Map(),
|
|
181
198
|
hooks: {},
|
|
@@ -185,6 +202,12 @@ export class PluginManager {
|
|
|
185
202
|
components: new Map(),
|
|
186
203
|
assetUrls: new Map(),
|
|
187
204
|
};
|
|
205
|
+
// 外部アプリから渡されたアセットURLを事前にキャッシュ
|
|
206
|
+
if (assets) {
|
|
207
|
+
for (const asset of assets) {
|
|
208
|
+
instance.assetUrls.set(asset.filename, asset.url);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
188
211
|
// プラグインAPIを作成
|
|
189
212
|
const api = this.createPluginAPI(packageName, instance);
|
|
190
213
|
// プラグインをセットアップ
|
|
@@ -610,16 +633,26 @@ export class PluginManager {
|
|
|
610
633
|
preload: (filenames) => {
|
|
611
634
|
const preloadPromise = (() => __awaiter(this, void 0, void 0, function* () {
|
|
612
635
|
const promises = filenames.map((filename) => __awaiter(this, void 0, void 0, function* () {
|
|
613
|
-
// API
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
636
|
+
// 既にキャッシュされたURLがあればAPIフェッチをスキップ
|
|
637
|
+
let finalUrl = instance.assetUrls.get(filename);
|
|
638
|
+
if (!finalUrl) {
|
|
639
|
+
// APIエンドポイントURL
|
|
640
|
+
const encodedPackageName = encodeURIComponent(packageName);
|
|
641
|
+
const encodedFilename = encodeURIComponent(filename);
|
|
642
|
+
const apiUrl = `/api/plugin/${encodedPackageName}/assets/${encodedFilename}`;
|
|
643
|
+
// fetchでリダイレクト後の最終URLを取得
|
|
644
|
+
try {
|
|
645
|
+
const response = yield fetch(apiUrl, { method: "HEAD" });
|
|
646
|
+
finalUrl = response.url; // リダイレクト後の最終URL
|
|
647
|
+
// 最終URLをキャッシュに保存
|
|
648
|
+
instance.assetUrls.set(filename, finalUrl);
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
console.error(`Failed to preload asset: ${filename}`, error);
|
|
652
|
+
return Promise.resolve();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
618
655
|
try {
|
|
619
|
-
const response = yield fetch(apiUrl, { method: "HEAD" });
|
|
620
|
-
const finalUrl = response.url; // リダイレクト後の最終URL
|
|
621
|
-
// 最終URLをキャッシュに保存
|
|
622
|
-
instance.assetUrls.set(filename, finalUrl);
|
|
623
656
|
// 画像をプリロード
|
|
624
657
|
if (filename.match(/\.(png|jpg|jpeg|gif|webp)$/i)) {
|
|
625
658
|
return new Promise((resolve, reject) => {
|
|
@@ -642,7 +675,6 @@ export class PluginManager {
|
|
|
642
675
|
catch (error) {
|
|
643
676
|
console.error(`Failed to preload asset: ${filename}`, error);
|
|
644
677
|
}
|
|
645
|
-
return Promise.resolve();
|
|
646
678
|
}));
|
|
647
679
|
yield Promise.all(promises);
|
|
648
680
|
}))();
|
package/dist/sdk.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { useDataAPI } from "./contexts/DataContext";
|
|
2
3
|
import type { DataAPI, DataContext, DataSubscriber } from "./data-api-types";
|
|
3
4
|
import type { FacePosition } from "./emotion-effect-types";
|
|
4
5
|
import type { Character, ConversationBranchState, EntityState, ScenarioBlock, ScenarioBlockType } from "./types";
|
|
5
|
-
import { useDataAPI } from "./contexts/DataContext";
|
|
6
6
|
/**
|
|
7
7
|
* ブロックオプションの定義
|
|
8
8
|
* プラグインがシナリオブロックに追加できるカスタムオプションを定義
|
package/dist/types.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface ScenarioBlock {
|
|
|
16
16
|
updatedAt: Date;
|
|
17
17
|
/** Plugin-defined block options (key-value pairs) */
|
|
18
18
|
options?: BlockOptions | null;
|
|
19
|
+
characterLayoutMode?: "detailed" | "simple";
|
|
19
20
|
speaker?: {
|
|
20
21
|
id: string;
|
|
21
22
|
name: string;
|
|
@@ -100,6 +101,9 @@ export interface ScenarioBlockCharacter {
|
|
|
100
101
|
positionY?: number | null;
|
|
101
102
|
zIndex?: number | null;
|
|
102
103
|
scale?: number | null;
|
|
104
|
+
cropLeft?: number | null;
|
|
105
|
+
cropRight?: number | null;
|
|
106
|
+
cropFade?: number | null;
|
|
103
107
|
object: {
|
|
104
108
|
id: string;
|
|
105
109
|
name: string;
|
|
@@ -113,6 +117,9 @@ export interface ScenarioBlockCharacter {
|
|
|
113
117
|
scale?: number;
|
|
114
118
|
translateY?: number;
|
|
115
119
|
translateX?: number;
|
|
120
|
+
cropLeft?: number | null;
|
|
121
|
+
cropRight?: number | null;
|
|
122
|
+
cropFade?: number | null;
|
|
116
123
|
isDefault?: boolean;
|
|
117
124
|
layers?: EntityStateLayer[];
|
|
118
125
|
};
|
|
@@ -200,6 +207,9 @@ export interface DisplayedCharacter {
|
|
|
200
207
|
positionY?: number | null;
|
|
201
208
|
zIndex?: number | null;
|
|
202
209
|
scale?: number | null;
|
|
210
|
+
cropLeft?: number | null;
|
|
211
|
+
cropRight?: number | null;
|
|
212
|
+
cropFade?: number | null;
|
|
203
213
|
object: {
|
|
204
214
|
id: string;
|
|
205
215
|
name: string;
|
|
@@ -213,6 +223,9 @@ export interface DisplayedCharacter {
|
|
|
213
223
|
scale?: number;
|
|
214
224
|
translateY?: number;
|
|
215
225
|
translateX?: number;
|
|
226
|
+
cropLeft?: number | null;
|
|
227
|
+
cropRight?: number | null;
|
|
228
|
+
cropFade?: number | null;
|
|
216
229
|
isDefault?: boolean;
|
|
217
230
|
layers?: EntityStateLayer[];
|
|
218
231
|
};
|
|
@@ -311,13 +324,27 @@ export interface PlayerSettings {
|
|
|
311
324
|
defaultBackgroundFadeDuration?: number;
|
|
312
325
|
/** 全ての音声をミュートする (デフォルト: false) */
|
|
313
326
|
muteAudio?: boolean;
|
|
314
|
-
/**
|
|
327
|
+
/** 選択されたフォントファミリー(テキスト用) */
|
|
315
328
|
selectedFontFamily?: string;
|
|
329
|
+
/** 選択されたUIフォントファミリー(メニュー・ラベル等) */
|
|
330
|
+
selectedUIFontFamily?: string;
|
|
331
|
+
/** 非アクティブキャラクターの明るさ (0-1, デフォルト: 0.8) */
|
|
332
|
+
inactiveCharacterBrightness?: number;
|
|
333
|
+
/** キャラクター間隔(簡易モード用、デフォルト: 0.1) */
|
|
334
|
+
characterSpacing?: number;
|
|
316
335
|
}
|
|
317
336
|
export interface PluginConfig {
|
|
318
337
|
packageName: string;
|
|
319
|
-
|
|
338
|
+
/** リモートプラグインのバンドルURL(plugin未指定時に必須) */
|
|
339
|
+
bundleUrl?: string;
|
|
340
|
+
/** ローカルプラグイン: LunaPluginオブジェクトを直接渡す(bundleUrl不要) */
|
|
341
|
+
plugin?: import("./sdk").LunaPlugin;
|
|
320
342
|
config?: unknown;
|
|
343
|
+
/** アセットのファイル名→絶対URLマッピング(外部アプリから利用時に必要) */
|
|
344
|
+
assets?: {
|
|
345
|
+
filename: string;
|
|
346
|
+
url: string;
|
|
347
|
+
}[];
|
|
321
348
|
}
|
|
322
349
|
/** 作品のサウンドデータ(プラグインからアクセス可能) */
|
|
323
350
|
export interface WorkSound {
|