@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
|
@@ -7,54 +7,174 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
|
|
10
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
11
|
+
var t = {};
|
|
12
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
13
|
+
t[p] = s[p];
|
|
14
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
15
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
16
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
17
|
+
t[p[i]] = s[p[i]];
|
|
18
|
+
}
|
|
19
|
+
return t;
|
|
20
|
+
};
|
|
11
21
|
import React from "react";
|
|
12
22
|
import { useScreenSizeAtom } from "../atoms/screen-size";
|
|
13
23
|
import { useDataAPI } from "../contexts/DataContext";
|
|
24
|
+
import { getFontFamilyStyle, getUIFontFamilyStyle, } from "../hooks/useFontLoader";
|
|
14
25
|
import { usePluginAPI, useUIVisibility } from "../hooks/usePluginAPI";
|
|
15
26
|
import { useScreenScale, useScreenSize, useToPixel, } from "../hooks/useScreenSize";
|
|
16
|
-
import { ComponentType, } from "../sdk";
|
|
27
|
+
import { ComponentType, definePlugin, } from "../sdk";
|
|
28
|
+
// グローバルなロード状態管理(複数のPluginManagerインスタンス間で共有)
|
|
29
|
+
const globalLoadingPlugins = new Map();
|
|
30
|
+
const globalLoadedPlugins = new Map();
|
|
17
31
|
export class PluginManager {
|
|
18
32
|
constructor() {
|
|
19
33
|
this.plugins = new Map();
|
|
20
34
|
this.styleElements = new Map();
|
|
35
|
+
this.originalClassNames = new Map(); // 元のクラス名を保持
|
|
21
36
|
this.injectedStyles = new Set();
|
|
22
37
|
this.storage = new Map();
|
|
23
38
|
this.componentRegistry = new Map();
|
|
24
39
|
this.uiVisibilityState = new Map();
|
|
25
40
|
this.uiVisibilityListeners = new Map();
|
|
41
|
+
this.selectChoiceCallback = null;
|
|
42
|
+
this.getBranchStateCallback = null;
|
|
43
|
+
this.emotionEffectUpdaterCallback = null;
|
|
44
|
+
this.dataContextGetter = null;
|
|
45
|
+
this.preloadPromises = [];
|
|
46
|
+
this.muteAudio = false;
|
|
47
|
+
// 作品のサウンドデータ(ID → URL)
|
|
48
|
+
this.workSounds = new Map();
|
|
49
|
+
// ビルトインのクリック音設定
|
|
50
|
+
this.clickSoundUrl = null;
|
|
51
|
+
this.clickSoundVolume = 0.5;
|
|
52
|
+
// プラグインによるクリック音の上書きフラグ
|
|
53
|
+
this.clickSoundOverridden = false;
|
|
26
54
|
this.initializeReactRuntime();
|
|
27
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* 開発環境かどうかを判定
|
|
58
|
+
* localhost、127.0.0.1、またはポート3000で実行されている場合に true を返す
|
|
59
|
+
*/
|
|
60
|
+
isDevelopment() {
|
|
61
|
+
if (typeof window === "undefined") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return (window.location.hostname === "localhost" ||
|
|
65
|
+
window.location.hostname === "127.0.0.1" ||
|
|
66
|
+
window.location.port === "3000");
|
|
67
|
+
}
|
|
28
68
|
/**
|
|
29
69
|
* グローバルReactランタイムを初期化(クライアントサイドのみ)
|
|
70
|
+
* 既に初期化済みの場合はスキップ
|
|
30
71
|
*/
|
|
31
72
|
initializeReactRuntime() {
|
|
32
73
|
// サーバーサイドではスキップ
|
|
33
74
|
if (typeof window === "undefined") {
|
|
34
75
|
return;
|
|
35
76
|
}
|
|
77
|
+
const globalWindow = window;
|
|
78
|
+
// 既に初期化済みの場合はスキップ
|
|
79
|
+
if (globalWindow.__LUNA_REACT_RUNTIME_INITIALIZED__) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
globalWindow.__LUNA_REACT_RUNTIME_INITIALIZED__ = true;
|
|
36
83
|
// JSXランタイム
|
|
37
|
-
|
|
84
|
+
globalWindow.__LUNA_JSX_RUNTIME__ = {
|
|
38
85
|
jsx: (type, props, key) => {
|
|
39
|
-
|
|
86
|
+
const { children } = props, restProps = __rest(props, ["children"]);
|
|
87
|
+
const propsWithKey = key ? Object.assign(Object.assign({}, restProps), { key }) : restProps;
|
|
88
|
+
return children !== undefined
|
|
89
|
+
? React.createElement(type, propsWithKey, children)
|
|
90
|
+
: React.createElement(type, propsWithKey);
|
|
40
91
|
},
|
|
41
92
|
jsxs: (type, props, key) => {
|
|
42
|
-
|
|
93
|
+
const { children } = props, restProps = __rest(props, ["children"]);
|
|
94
|
+
const propsWithKey = key ? Object.assign(Object.assign({}, restProps), { key }) : restProps;
|
|
95
|
+
// jsxsはchildren配列をスプレッドして渡す
|
|
96
|
+
return Array.isArray(children)
|
|
97
|
+
? React.createElement(type, propsWithKey, ...children)
|
|
98
|
+
: React.createElement(type, propsWithKey, children);
|
|
43
99
|
},
|
|
44
100
|
Fragment: React.Fragment,
|
|
45
101
|
};
|
|
46
102
|
// Reactランタイム (useDataAPIフックと画面サイズフックを含む)
|
|
47
|
-
|
|
103
|
+
globalWindow.__LUNA_REACT_RUNTIME__ = Object.assign(Object.assign({}, React), { default: React, useDataAPI,
|
|
48
104
|
usePluginAPI,
|
|
49
105
|
useUIVisibility,
|
|
50
106
|
useToPixel,
|
|
51
107
|
useScreenSize,
|
|
52
108
|
useScreenScale,
|
|
53
109
|
useScreenSizeAtom,
|
|
54
|
-
ComponentType
|
|
55
|
-
|
|
110
|
+
ComponentType,
|
|
111
|
+
definePlugin,
|
|
112
|
+
getFontFamilyStyle,
|
|
113
|
+
getUIFontFamilyStyle });
|
|
56
114
|
}
|
|
57
|
-
loadPlugin(packageName, bundleUrl, config) {
|
|
115
|
+
loadPlugin(packageName, bundleUrl, config, assets) {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
// 開発環境ではキャッシュをバイパスして常に最新コードをロード
|
|
118
|
+
const isDevelopment = this.isDevelopment();
|
|
119
|
+
if (isDevelopment) {
|
|
120
|
+
// 開発環境では強制リロード(キャッシュをクリア)
|
|
121
|
+
globalLoadedPlugins.delete(packageName);
|
|
122
|
+
this.plugins.delete(packageName);
|
|
123
|
+
// ロード中のPromiseもクリア(再ロードを確実にするため)
|
|
124
|
+
globalLoadingPlugins.delete(packageName);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// 本番環境のみキャッシュを使用
|
|
128
|
+
// グローバルに既にロード済みの場合はローカルにコピーしてスキップ
|
|
129
|
+
const globalLoaded = globalLoadedPlugins.get(packageName);
|
|
130
|
+
if (globalLoaded) {
|
|
131
|
+
// 新しいconfigで更新したコピーを作成
|
|
132
|
+
const updatedPlugin = Object.assign(Object.assign({}, globalLoaded), { config: config !== null && config !== void 0 ? config : globalLoaded.config });
|
|
133
|
+
this.plugins.set(packageName, updatedPlugin);
|
|
134
|
+
// コンポーネントレジストリにも登録
|
|
135
|
+
for (const [type, component] of globalLoaded.instance.components) {
|
|
136
|
+
this.componentRegistry.set(type, component);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!isDevelopment) {
|
|
142
|
+
// 本番環境のみ:ローカルに既にロード済みの場合はconfigだけ更新してスキップ
|
|
143
|
+
const localLoaded = this.plugins.get(packageName);
|
|
144
|
+
if (localLoaded) {
|
|
145
|
+
// configを更新
|
|
146
|
+
localLoaded.config = config !== null && config !== void 0 ? config : localLoaded.config;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// 本番環境のみ:グローバルでロード中の場合はそのPromiseを待つ
|
|
150
|
+
const existingLoadPromise = globalLoadingPlugins.get(packageName);
|
|
151
|
+
if (existingLoadPromise) {
|
|
152
|
+
yield existingLoadPromise;
|
|
153
|
+
// ロード完了後にグローバルからコピー(新しいconfigで更新)
|
|
154
|
+
const loaded = globalLoadedPlugins.get(packageName);
|
|
155
|
+
if (loaded) {
|
|
156
|
+
const updatedPlugin = Object.assign(Object.assign({}, loaded), { config: config !== null && config !== void 0 ? config : loaded.config });
|
|
157
|
+
this.plugins.set(packageName, updatedPlugin);
|
|
158
|
+
for (const [type, component] of loaded.instance.components) {
|
|
159
|
+
this.componentRegistry.set(type, component);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ロード処理をPromiseとして保存
|
|
166
|
+
const loadPromise = this.doLoadPlugin(packageName, bundleUrl, config, assets);
|
|
167
|
+
globalLoadingPlugins.set(packageName, loadPromise);
|
|
168
|
+
try {
|
|
169
|
+
yield loadPromise;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
// ロード完了後にマップから削除
|
|
173
|
+
globalLoadingPlugins.delete(packageName);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
doLoadPlugin(packageName, bundleUrl, config, assets) {
|
|
58
178
|
return __awaiter(this, void 0, void 0, function* () {
|
|
59
179
|
try {
|
|
60
180
|
// スクリプトタグで動的にプラグインを読み込む
|
|
@@ -68,17 +188,25 @@ export class PluginManager {
|
|
|
68
188
|
components: new Map(),
|
|
69
189
|
assetUrls: new Map(),
|
|
70
190
|
};
|
|
191
|
+
// 外部アプリから渡されたアセットURLを事前にキャッシュ
|
|
192
|
+
if (assets) {
|
|
193
|
+
for (const asset of assets) {
|
|
194
|
+
instance.assetUrls.set(asset.filename, asset.url);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
71
197
|
// プラグインAPIを作成
|
|
72
198
|
const api = this.createPluginAPI(packageName, instance);
|
|
73
199
|
// プラグインをセットアップ
|
|
74
200
|
plugin.setup(api);
|
|
75
|
-
|
|
76
|
-
this.plugins.set(packageName, {
|
|
201
|
+
const loadedPlugin = {
|
|
77
202
|
plugin,
|
|
78
203
|
instance,
|
|
79
204
|
enabled: true,
|
|
80
205
|
config: config || {},
|
|
81
|
-
}
|
|
206
|
+
};
|
|
207
|
+
// ローカルとグローバル両方に保存
|
|
208
|
+
this.plugins.set(packageName, loadedPlugin);
|
|
209
|
+
globalLoadedPlugins.set(packageName, loadedPlugin);
|
|
82
210
|
}
|
|
83
211
|
catch (error) {
|
|
84
212
|
console.error(`Failed to load plugin ${packageName}:`, error);
|
|
@@ -108,9 +236,7 @@ export class PluginManager {
|
|
|
108
236
|
};
|
|
109
237
|
// プラグインバンドルをfetchで取得して実行
|
|
110
238
|
// 開発環境ではキャッシュを無効化してホットリロードを確実に動作させる
|
|
111
|
-
const isDevelopment =
|
|
112
|
-
window.location.hostname === "127.0.0.1" ||
|
|
113
|
-
window.location.port === "3000";
|
|
239
|
+
const isDevelopment = this.isDevelopment();
|
|
114
240
|
const fetchOptions = isDevelopment
|
|
115
241
|
? {
|
|
116
242
|
cache: "no-cache",
|
|
@@ -120,7 +246,6 @@ export class PluginManager {
|
|
|
120
246
|
},
|
|
121
247
|
}
|
|
122
248
|
: {};
|
|
123
|
-
console.log(`🔄 Loading plugin ${packageName} (dev: ${isDevelopment}, cache: ${isDevelopment ? "disabled" : "enabled"})`);
|
|
124
249
|
fetch(url, fetchOptions)
|
|
125
250
|
.then((response) => {
|
|
126
251
|
if (!response.ok) {
|
|
@@ -133,12 +258,7 @@ export class PluginManager {
|
|
|
133
258
|
// import文を削除
|
|
134
259
|
.replace(/^import\s+.*?from\s+['"].*?['"];?\s*$/gm, "")
|
|
135
260
|
.replace(/export\s*{[^}]*}\s*;?\s*$/g, "")
|
|
136
|
-
.replace(/export\s+default\s+/g, "")
|
|
137
|
-
// luna-react/jsxランタイムへの参照を置換
|
|
138
|
-
.replace(/from\s+['"]luna-react\/jsx-runtime['"]/, 'from "__LUNA_JSX_RUNTIME__"')
|
|
139
|
-
.replace(/from\s+['"]luna-react['"]/, 'from "__LUNA_REACT__"')
|
|
140
|
-
.replace(/require\(['"]luna-react\/jsx-runtime['"]\)/, 'require("__LUNA_JSX_RUNTIME__")')
|
|
141
|
-
.replace(/require\(['"]luna-react['"]\)/, 'require("__LUNA_REACT__")');
|
|
261
|
+
.replace(/export\s+default\s+/g, "");
|
|
142
262
|
// ES modules形式のプラグインコードを処理
|
|
143
263
|
const wrappedCode = `
|
|
144
264
|
(function() {
|
|
@@ -156,10 +276,10 @@ export class PluginManager {
|
|
|
156
276
|
|
|
157
277
|
// Luna React Runtime を提供
|
|
158
278
|
const require = function(moduleName) {
|
|
159
|
-
if (moduleName === "
|
|
279
|
+
if (moduleName === "react" || moduleName === "luna-react" || moduleName === "@luna-editor/engine" || moduleName === "@luna-editor/engine/sdk" || moduleName === "@luna-editor/plugin-sdk") {
|
|
160
280
|
return window.__LUNA_REACT_RUNTIME__;
|
|
161
281
|
}
|
|
162
|
-
if (moduleName === "
|
|
282
|
+
if (moduleName === "react/jsx-runtime" || moduleName === "react/jsx-dev-runtime" || moduleName === "luna-react/jsx-runtime" || moduleName === "luna-react/jsx-dev-runtime") {
|
|
163
283
|
return window.__LUNA_JSX_RUNTIME__;
|
|
164
284
|
}
|
|
165
285
|
throw new Error('Module not found: ' + moduleName);
|
|
@@ -190,19 +310,6 @@ export class PluginManager {
|
|
|
190
310
|
scriptElement.type = "text/javascript";
|
|
191
311
|
scriptElement.dataset.plugin = packageName;
|
|
192
312
|
scriptElement.textContent = wrappedCode;
|
|
193
|
-
// タイムアウトを設定(グローバルコールバックが5秒以内に呼ばれない場合はエラー)
|
|
194
|
-
const timeoutId = setTimeout(() => {
|
|
195
|
-
if (window[globalName]) {
|
|
196
|
-
delete window[globalName];
|
|
197
|
-
reject(new Error(`Plugin ${packageName} did not initialize within timeout`));
|
|
198
|
-
}
|
|
199
|
-
}, 5000);
|
|
200
|
-
// グローバルコールバック関数にタイムアウトクリアを追加
|
|
201
|
-
const originalCallback = window[globalName];
|
|
202
|
-
window[globalName] = (plugin) => {
|
|
203
|
-
clearTimeout(timeoutId);
|
|
204
|
-
originalCallback(plugin);
|
|
205
|
-
};
|
|
206
313
|
// DOMに追加(この時点で同期的に実行される)
|
|
207
314
|
document.head.appendChild(scriptElement);
|
|
208
315
|
})
|
|
@@ -220,17 +327,23 @@ export class PluginManager {
|
|
|
220
327
|
return {
|
|
221
328
|
setClassName: (className) => {
|
|
222
329
|
classNames.clear();
|
|
223
|
-
className.split(" ")
|
|
330
|
+
for (const cn of className.split(" ")) {
|
|
331
|
+
classNames.add(cn);
|
|
332
|
+
}
|
|
224
333
|
instance.styles.set(elementName, Array.from(classNames));
|
|
225
334
|
this.applyStyles(elementName, Array.from(classNames));
|
|
226
335
|
},
|
|
227
336
|
addClassName: (className) => {
|
|
228
|
-
className.split(" ")
|
|
337
|
+
for (const cn of className.split(" ")) {
|
|
338
|
+
classNames.add(cn);
|
|
339
|
+
}
|
|
229
340
|
instance.styles.set(elementName, Array.from(classNames));
|
|
230
341
|
this.applyStyles(elementName, Array.from(classNames));
|
|
231
342
|
},
|
|
232
343
|
removeClassName: (className) => {
|
|
233
|
-
className.split(" ")
|
|
344
|
+
for (const cn of className.split(" ")) {
|
|
345
|
+
classNames.delete(cn);
|
|
346
|
+
}
|
|
234
347
|
instance.styles.set(elementName, Array.from(classNames));
|
|
235
348
|
this.applyStyles(elementName, Array.from(classNames));
|
|
236
349
|
},
|
|
@@ -281,7 +394,7 @@ export class PluginManager {
|
|
|
281
394
|
// ターゲット要素内の画像を探してマスクを適用
|
|
282
395
|
const targetImage = targetElement.querySelector("img");
|
|
283
396
|
let maskStyles = {};
|
|
284
|
-
if (targetImage
|
|
397
|
+
if (targetImage === null || targetImage === void 0 ? void 0 : targetImage.src) {
|
|
285
398
|
// 画像のスタイルプロパティを取得
|
|
286
399
|
const computedImageStyle = getComputedStyle(targetImage);
|
|
287
400
|
const objectFit = computedImageStyle.objectFit;
|
|
@@ -312,7 +425,7 @@ export class PluginManager {
|
|
|
312
425
|
// スタイルを個別に適用
|
|
313
426
|
Object.entries(overlayStyles).forEach(([key, value]) => {
|
|
314
427
|
if (typeof value === "string") {
|
|
315
|
-
overlay.style[
|
|
428
|
+
overlay.style.setProperty(key.replace(/([A-Z])/g, "-$1").toLowerCase(), value);
|
|
316
429
|
}
|
|
317
430
|
});
|
|
318
431
|
overlay.dataset.plugin = packageName;
|
|
@@ -332,7 +445,9 @@ export class PluginManager {
|
|
|
332
445
|
},
|
|
333
446
|
clearOverlays: () => {
|
|
334
447
|
const overlaysToRemove = Array.from(instance.overlays.keys()).filter((id) => id.includes(`-overlay-${elementName}-`));
|
|
335
|
-
|
|
448
|
+
for (const id of overlaysToRemove) {
|
|
449
|
+
this.removeOverlay(id, instance);
|
|
450
|
+
}
|
|
336
451
|
},
|
|
337
452
|
};
|
|
338
453
|
};
|
|
@@ -344,6 +459,8 @@ export class PluginManager {
|
|
|
344
459
|
gameScreen: createStyleElement("gameScreen"),
|
|
345
460
|
characterSprite: createStyleElement("characterSprite"),
|
|
346
461
|
background: createStyleElement("background"),
|
|
462
|
+
fullscreenTextOverlay: createStyleElement("fullscreenTextOverlay"),
|
|
463
|
+
fullscreenTextContainer: createStyleElement("fullscreenTextContainer"),
|
|
347
464
|
},
|
|
348
465
|
injectCSS: (css) => {
|
|
349
466
|
if (this.injectedStyles.has(css))
|
|
@@ -481,45 +598,80 @@ export class PluginManager {
|
|
|
481
598
|
keysToDelete.push(key);
|
|
482
599
|
}
|
|
483
600
|
}
|
|
484
|
-
|
|
601
|
+
for (const key of keysToDelete) {
|
|
602
|
+
this.storage.delete(key);
|
|
603
|
+
}
|
|
485
604
|
},
|
|
486
605
|
};
|
|
487
606
|
const assetAPI = {
|
|
488
607
|
getUrl: (filename) => {
|
|
489
|
-
//
|
|
608
|
+
// キャッシュに最終URL(リダイレクト後)があればそれを返す
|
|
609
|
+
const cachedUrl = instance.assetUrls.get(filename);
|
|
610
|
+
if (cachedUrl) {
|
|
611
|
+
return cachedUrl;
|
|
612
|
+
}
|
|
613
|
+
// なければAPIエンドポイントを返す(URLエンコーディング対応)
|
|
490
614
|
const encodedPackageName = encodeURIComponent(packageName);
|
|
491
615
|
const encodedFilename = encodeURIComponent(filename);
|
|
492
616
|
const url = `/api/plugin/${encodedPackageName}/assets/${encodedFilename}`;
|
|
493
|
-
// instance内にURLを保存(Context API経由でアクセス可能にするため)
|
|
494
|
-
instance.assetUrls.set(filename, url);
|
|
495
617
|
return url;
|
|
496
618
|
},
|
|
497
|
-
preload: (filenames) =>
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
619
|
+
preload: (filenames) => {
|
|
620
|
+
const preloadPromise = (() => __awaiter(this, void 0, void 0, function* () {
|
|
621
|
+
const promises = filenames.map((filename) => __awaiter(this, void 0, void 0, function* () {
|
|
622
|
+
// 既にキャッシュされたURLがあればAPIフェッチをスキップ
|
|
623
|
+
let finalUrl = instance.assetUrls.get(filename);
|
|
624
|
+
if (!finalUrl) {
|
|
625
|
+
// APIエンドポイントURL
|
|
626
|
+
const encodedPackageName = encodeURIComponent(packageName);
|
|
627
|
+
const encodedFilename = encodeURIComponent(filename);
|
|
628
|
+
const apiUrl = `/api/plugin/${encodedPackageName}/assets/${encodedFilename}`;
|
|
629
|
+
// fetchでリダイレクト後の最終URLを取得
|
|
630
|
+
try {
|
|
631
|
+
const response = yield fetch(apiUrl, { method: "HEAD" });
|
|
632
|
+
finalUrl = response.url; // リダイレクト後の最終URL
|
|
633
|
+
// 最終URLをキャッシュに保存
|
|
634
|
+
instance.assetUrls.set(filename, finalUrl);
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
console.error(`Failed to preload asset: ${filename}`, error);
|
|
638
|
+
return Promise.resolve();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
// 画像をプリロード
|
|
643
|
+
if (filename.match(/\.(png|jpg|jpeg|gif|webp)$/i)) {
|
|
644
|
+
return new Promise((resolve, reject) => {
|
|
645
|
+
const img = new Image();
|
|
646
|
+
img.onload = () => resolve();
|
|
647
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${finalUrl}`));
|
|
648
|
+
img.src = finalUrl;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
// 音声をプリロード
|
|
652
|
+
if (filename.match(/\.(mp3|wav|ogg)$/i)) {
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
654
|
+
const audio = new Audio();
|
|
655
|
+
audio.oncanplaythrough = () => resolve();
|
|
656
|
+
audio.onerror = () => reject(new Error(`Failed to load audio: ${finalUrl}`));
|
|
657
|
+
audio.src = finalUrl;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
console.error(`Failed to preload asset: ${filename}`, error);
|
|
663
|
+
}
|
|
664
|
+
}));
|
|
665
|
+
yield Promise.all(promises);
|
|
666
|
+
}))();
|
|
667
|
+
// プリロードPromiseを追跡
|
|
668
|
+
this.preloadPromises.push(preloadPromise);
|
|
669
|
+
return preloadPromise;
|
|
670
|
+
},
|
|
520
671
|
playSound: (filename, options) => {
|
|
521
|
-
|
|
522
|
-
|
|
672
|
+
// ミュート中は再生しない
|
|
673
|
+
if (this.muteAudio)
|
|
674
|
+
return;
|
|
523
675
|
const audio = new Audio(assetAPI.getUrl(filename));
|
|
524
676
|
if ((options === null || options === void 0 ? void 0 : options.volume) !== undefined) {
|
|
525
677
|
audio.volume = options.volume;
|
|
@@ -531,10 +683,30 @@ export class PluginManager {
|
|
|
531
683
|
console.error("Failed to play sound:", error);
|
|
532
684
|
});
|
|
533
685
|
},
|
|
686
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
534
687
|
stopSound: (_filename) => {
|
|
535
|
-
// eslint-disable-line @typescript-eslint/no-unused-vars
|
|
536
688
|
// TODO: 音声停止の実装
|
|
537
689
|
},
|
|
690
|
+
playWorkSound: (soundId, options) => {
|
|
691
|
+
// ミュート中は再生しない
|
|
692
|
+
if (this.muteAudio)
|
|
693
|
+
return;
|
|
694
|
+
const soundUrl = this.getWorkSoundUrl(soundId);
|
|
695
|
+
if (!soundUrl) {
|
|
696
|
+
console.warn(`Work sound not found: ${soundId}`);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const audio = new Audio(soundUrl);
|
|
700
|
+
if ((options === null || options === void 0 ? void 0 : options.volume) !== undefined) {
|
|
701
|
+
audio.volume = options.volume;
|
|
702
|
+
}
|
|
703
|
+
if (options === null || options === void 0 ? void 0 : options.loop) {
|
|
704
|
+
audio.loop = true;
|
|
705
|
+
}
|
|
706
|
+
audio.play().catch((error) => {
|
|
707
|
+
console.error("Failed to play work sound:", error);
|
|
708
|
+
});
|
|
709
|
+
},
|
|
538
710
|
};
|
|
539
711
|
const componentAPI = {
|
|
540
712
|
registerComponent: (type, component) => {
|
|
@@ -546,7 +718,6 @@ export class PluginManager {
|
|
|
546
718
|
}
|
|
547
719
|
instance.components.set(type, component);
|
|
548
720
|
this.componentRegistry.set(type, component);
|
|
549
|
-
console.log(`Plugin ${packageName} registered component: ${type}`);
|
|
550
721
|
},
|
|
551
722
|
};
|
|
552
723
|
const uiAPI = {
|
|
@@ -579,6 +750,23 @@ export class PluginManager {
|
|
|
579
750
|
const pluginEntry = this.plugins.get(packageName);
|
|
580
751
|
return (pluginEntry === null || pluginEntry === void 0 ? void 0 : pluginEntry.config) || {};
|
|
581
752
|
},
|
|
753
|
+
selectChoice: (choiceId) => {
|
|
754
|
+
if (this.selectChoiceCallback) {
|
|
755
|
+
this.selectChoiceCallback(choiceId);
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
console.warn("selectChoice callback is not registered. Make sure to call setSelectChoiceCallback.");
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
getBranchState: () => {
|
|
762
|
+
if (this.getBranchStateCallback) {
|
|
763
|
+
return this.getBranchStateCallback();
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
},
|
|
767
|
+
overrideClickSound: () => {
|
|
768
|
+
this.overrideClickSound();
|
|
769
|
+
},
|
|
582
770
|
hooks: {
|
|
583
771
|
onInit: (callback) => {
|
|
584
772
|
instance.hooks.onInit = callback;
|
|
@@ -592,9 +780,6 @@ export class PluginManager {
|
|
|
592
780
|
onCharacterEnter: (callback) => {
|
|
593
781
|
instance.hooks.onCharacterEnter = callback;
|
|
594
782
|
},
|
|
595
|
-
onCharacterExit: (callback) => {
|
|
596
|
-
instance.hooks.onCharacterExit = callback;
|
|
597
|
-
},
|
|
598
783
|
onDialogueShow: (callback) => {
|
|
599
784
|
instance.hooks.onDialogueShow = callback;
|
|
600
785
|
},
|
|
@@ -607,13 +792,37 @@ export class PluginManager {
|
|
|
607
792
|
onScenarioEnd: (callback) => {
|
|
608
793
|
instance.hooks.onScenarioEnd = callback;
|
|
609
794
|
},
|
|
795
|
+
onBranchStart: (callback) => {
|
|
796
|
+
instance.hooks.onBranchStart = callback;
|
|
797
|
+
},
|
|
798
|
+
onChoiceSelected: (callback) => {
|
|
799
|
+
instance.hooks.onChoiceSelected = callback;
|
|
800
|
+
},
|
|
801
|
+
onBranchEnd: (callback) => {
|
|
802
|
+
instance.hooks.onBranchEnd = callback;
|
|
803
|
+
},
|
|
804
|
+
onBranchBlockChange: (callback) => {
|
|
805
|
+
instance.hooks.onBranchBlockChange = callback;
|
|
806
|
+
},
|
|
610
807
|
},
|
|
611
808
|
effects: effectAPI,
|
|
612
809
|
storage: storageAPI,
|
|
613
810
|
assets: assetAPI,
|
|
811
|
+
blockOptions: {
|
|
812
|
+
register: () => {
|
|
813
|
+
// ランタイムでは何もしない(サーバー側での抽出時のみ使用)
|
|
814
|
+
// プラグインのsetup()が呼ばれた時点でBlockOptionsはすでにDBに保存済み
|
|
815
|
+
},
|
|
816
|
+
},
|
|
614
817
|
data: {
|
|
615
|
-
get: () => {
|
|
616
|
-
|
|
818
|
+
get: (key) => {
|
|
819
|
+
if (this.dataContextGetter) {
|
|
820
|
+
const context = this.dataContextGetter();
|
|
821
|
+
const value = context === null || context === void 0 ? void 0 : context[key];
|
|
822
|
+
return value;
|
|
823
|
+
}
|
|
824
|
+
console.warn("DataContext getter is not registered. Make sure to call setDataContextGetter.");
|
|
825
|
+
return undefined;
|
|
617
826
|
},
|
|
618
827
|
subscribe: () => {
|
|
619
828
|
throw new Error("DataAPI.subscribe() can only be called from within DataProvider context");
|
|
@@ -624,13 +833,41 @@ export class PluginManager {
|
|
|
624
833
|
updateSettings: () => {
|
|
625
834
|
throw new Error("DataAPI.updateSettings() can only be called from within DataProvider context");
|
|
626
835
|
},
|
|
836
|
+
getBlockOption: () => {
|
|
837
|
+
throw new Error("DataAPI.getBlockOption() can only be called from within DataProvider context");
|
|
838
|
+
},
|
|
839
|
+
setBgmVolume: () => {
|
|
840
|
+
throw new Error("DataAPI.setBgmVolume() can only be called from within DataProvider context");
|
|
841
|
+
},
|
|
842
|
+
setSeVolume: () => {
|
|
843
|
+
throw new Error("DataAPI.setSeVolume() can only be called from within DataProvider context");
|
|
844
|
+
},
|
|
845
|
+
setVoiceVolume: () => {
|
|
846
|
+
throw new Error("DataAPI.setVoiceVolume() can only be called from within DataProvider context");
|
|
847
|
+
},
|
|
848
|
+
setVolumes: () => {
|
|
849
|
+
throw new Error("DataAPI.setVolumes() can only be called from within DataProvider context");
|
|
850
|
+
},
|
|
851
|
+
updateEmotionEffect: (state) => {
|
|
852
|
+
if (this.emotionEffectUpdaterCallback) {
|
|
853
|
+
this.emotionEffectUpdaterCallback(state);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
console.warn("updateEmotionEffect callback is not registered. Make sure to call setEmotionEffectUpdaterCallback.");
|
|
857
|
+
}
|
|
858
|
+
},
|
|
627
859
|
},
|
|
628
860
|
};
|
|
629
861
|
}
|
|
630
862
|
applyStyles(elementName, classNames) {
|
|
631
863
|
const element = this.styleElements.get(elementName);
|
|
632
864
|
if (element) {
|
|
633
|
-
|
|
865
|
+
// 元のクラス名を取得し、プラグインから追加されたクラスと結合
|
|
866
|
+
const originalClassName = this.originalClassNames.get(elementName) || "";
|
|
867
|
+
const pluginClassNames = classNames.join(" ");
|
|
868
|
+
element.className = originalClassName
|
|
869
|
+
? `${originalClassName} ${pluginClassNames}`
|
|
870
|
+
: pluginClassNames;
|
|
634
871
|
}
|
|
635
872
|
}
|
|
636
873
|
hideEffect(effectId, instance) {
|
|
@@ -651,17 +888,17 @@ export class PluginManager {
|
|
|
651
888
|
// DOM要素にdata-style-element-name属性を追加
|
|
652
889
|
element.setAttribute("data-style-element-name", name);
|
|
653
890
|
this.styleElements.set(name, element);
|
|
891
|
+
// 元のクラス名を保存(初回登録時のみ)
|
|
892
|
+
if (!this.originalClassNames.has(name)) {
|
|
893
|
+
this.originalClassNames.set(name, element.className);
|
|
894
|
+
}
|
|
654
895
|
}
|
|
655
896
|
// フック呼び出しメソッド
|
|
656
897
|
callHook(hookName, ...args) {
|
|
657
|
-
console.log(`Calling hook: ${hookName}`, args);
|
|
658
|
-
console.log(`Number of plugins: ${this.plugins.size}`);
|
|
659
898
|
this.plugins.forEach((loadedPlugin, packageName) => {
|
|
660
899
|
const { instance, enabled } = loadedPlugin;
|
|
661
|
-
console.log(`Plugin ${packageName}: enabled=${enabled}, has hook=${!!instance.hooks[hookName]}`);
|
|
662
900
|
if (enabled && instance.hooks[hookName]) {
|
|
663
901
|
try {
|
|
664
|
-
console.log(`Executing hook ${hookName} for plugin ${packageName}`);
|
|
665
902
|
const hook = instance.hooks[hookName];
|
|
666
903
|
if (hook) {
|
|
667
904
|
hook(...args);
|
|
@@ -703,16 +940,18 @@ export class PluginManager {
|
|
|
703
940
|
// クリーンアップ
|
|
704
941
|
cleanup() {
|
|
705
942
|
// 注入されたスタイルを削除
|
|
706
|
-
document
|
|
707
|
-
.
|
|
708
|
-
|
|
709
|
-
document
|
|
710
|
-
.
|
|
711
|
-
|
|
943
|
+
for (const el of document.querySelectorAll("style[data-plugin]")) {
|
|
944
|
+
el.remove();
|
|
945
|
+
}
|
|
946
|
+
for (const el of document.querySelectorAll("script[data-plugin]")) {
|
|
947
|
+
el.remove();
|
|
948
|
+
}
|
|
712
949
|
// エフェクトを削除
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
950
|
+
for (const { instance } of this.plugins.values()) {
|
|
951
|
+
for (const effect of instance.effects.values()) {
|
|
952
|
+
effect.remove();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
716
955
|
this.plugins.clear();
|
|
717
956
|
this.styleElements.clear();
|
|
718
957
|
this.injectedStyles.clear();
|
|
@@ -779,7 +1018,9 @@ export class PluginManager {
|
|
|
779
1018
|
// リスナーに通知
|
|
780
1019
|
const listeners = this.uiVisibilityListeners.get(type);
|
|
781
1020
|
if (listeners) {
|
|
782
|
-
|
|
1021
|
+
for (const listener of listeners) {
|
|
1022
|
+
listener(visible);
|
|
1023
|
+
}
|
|
783
1024
|
}
|
|
784
1025
|
}
|
|
785
1026
|
/**
|
|
@@ -801,6 +1042,8 @@ export class PluginManager {
|
|
|
801
1042
|
this.uiVisibilityListeners.set(type, new Set());
|
|
802
1043
|
}
|
|
803
1044
|
const listeners = this.uiVisibilityListeners.get(type);
|
|
1045
|
+
if (!listeners)
|
|
1046
|
+
return () => { };
|
|
804
1047
|
listeners.add(listener);
|
|
805
1048
|
// 現在の状態を即座に通知
|
|
806
1049
|
listener(this.uiVisibilityState.get(type) || false);
|
|
@@ -851,4 +1094,94 @@ export class PluginManager {
|
|
|
851
1094
|
hideUI(type) {
|
|
852
1095
|
this.setUIVisibility(type, false);
|
|
853
1096
|
}
|
|
1097
|
+
setSelectChoiceCallback(callback) {
|
|
1098
|
+
this.selectChoiceCallback = callback;
|
|
1099
|
+
}
|
|
1100
|
+
setGetBranchStateCallback(callback) {
|
|
1101
|
+
this.getBranchStateCallback = callback;
|
|
1102
|
+
}
|
|
1103
|
+
setEmotionEffectUpdaterCallback(callback) {
|
|
1104
|
+
this.emotionEffectUpdaterCallback = callback;
|
|
1105
|
+
}
|
|
1106
|
+
setDataContextGetter(callback) {
|
|
1107
|
+
this.dataContextGetter = callback;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* 全てのアセットプリロードが完了するまで待機
|
|
1111
|
+
* @returns プリロード完了時にresolveするPromise
|
|
1112
|
+
*/
|
|
1113
|
+
waitForPreloads() {
|
|
1114
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1115
|
+
yield Promise.all(this.preloadPromises);
|
|
1116
|
+
this.preloadPromises = [];
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* 音声のミュート状態を設定
|
|
1121
|
+
* @param mute - ミュートするかどうか
|
|
1122
|
+
*/
|
|
1123
|
+
setMuteAudio(mute) {
|
|
1124
|
+
this.muteAudio = mute;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* 作品のサウンドデータを設定
|
|
1128
|
+
* @param sounds - サウンドIDをキーとしたURL/名前のマップ
|
|
1129
|
+
*/
|
|
1130
|
+
setWorkSounds(sounds) {
|
|
1131
|
+
this.workSounds.clear();
|
|
1132
|
+
for (const sound of sounds) {
|
|
1133
|
+
this.workSounds.set(sound.id, { url: sound.url, name: sound.name });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* 作品のサウンドURLを取得
|
|
1138
|
+
* @param soundId - サウンドID
|
|
1139
|
+
*/
|
|
1140
|
+
getWorkSoundUrl(soundId) {
|
|
1141
|
+
var _a, _b;
|
|
1142
|
+
return (_b = (_a = this.workSounds.get(soundId)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : null;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* ビルトインのクリック音を設定
|
|
1146
|
+
* @param url - サウンドファイルのURL(nullで無効化)
|
|
1147
|
+
* @param volume - 音量(0.0 - 1.0)
|
|
1148
|
+
*/
|
|
1149
|
+
setClickSound(url, volume = 0.5) {
|
|
1150
|
+
this.clickSoundUrl = url;
|
|
1151
|
+
this.clickSoundVolume = volume;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* プラグインがクリック音を上書きする
|
|
1155
|
+
* これを呼び出すと、ビルトインのクリック音は再生されなくなる
|
|
1156
|
+
*/
|
|
1157
|
+
overrideClickSound() {
|
|
1158
|
+
this.clickSoundOverridden = true;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* プラグインのクリック音上書きをリセット
|
|
1162
|
+
* 各ブロック遷移の前に呼び出す
|
|
1163
|
+
*/
|
|
1164
|
+
resetClickSoundOverride() {
|
|
1165
|
+
this.clickSoundOverridden = false;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* クリック音が上書きされているかを取得
|
|
1169
|
+
*/
|
|
1170
|
+
isClickSoundOverridden() {
|
|
1171
|
+
return this.clickSoundOverridden;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* ビルトインのクリック音を再生
|
|
1175
|
+
* プラグインで上書きされている場合やミュート中は再生しない
|
|
1176
|
+
*/
|
|
1177
|
+
playClickSound() {
|
|
1178
|
+
if (this.muteAudio || this.clickSoundOverridden || !this.clickSoundUrl) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
const audio = new Audio(this.clickSoundUrl);
|
|
1182
|
+
audio.volume = this.clickSoundVolume;
|
|
1183
|
+
audio.play().catch((error) => {
|
|
1184
|
+
console.error("Failed to play click sound:", error);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
854
1187
|
}
|