@luna-editor/engine 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/Player.d.ts +1 -1
  2. package/dist/Player.js +676 -95
  3. package/dist/api/conversationBranch.d.ts +4 -0
  4. package/dist/api/conversationBranch.js +83 -0
  5. package/dist/components/BackgroundLayer.d.ts +19 -0
  6. package/dist/components/BackgroundLayer.js +220 -0
  7. package/dist/components/ClickWaitIndicator.d.ts +10 -0
  8. package/dist/components/ClickWaitIndicator.js +31 -0
  9. package/dist/components/ConversationBranchBox.d.ts +2 -0
  10. package/dist/components/ConversationBranchBox.js +29 -0
  11. package/dist/components/DialogueBox.js +16 -1
  12. package/dist/components/FontSettingsPanel.d.ts +10 -0
  13. package/dist/components/FontSettingsPanel.js +30 -0
  14. package/dist/components/FullscreenTextBox.d.ts +6 -0
  15. package/dist/components/FullscreenTextBox.js +70 -0
  16. package/dist/components/GameScreen.d.ts +9 -2
  17. package/dist/components/GameScreen.js +396 -80
  18. package/dist/components/OverlayUI.d.ts +2 -3
  19. package/dist/components/OverlayUI.js +3 -14
  20. package/dist/components/PluginComponentProvider.d.ts +3 -3
  21. package/dist/components/PluginComponentProvider.js +22 -4
  22. package/dist/components/TimeWaitIndicator.d.ts +15 -0
  23. package/dist/components/TimeWaitIndicator.js +17 -0
  24. package/dist/contexts/AudioContext.d.ts +1 -0
  25. package/dist/contexts/AudioContext.js +1 -0
  26. package/dist/contexts/DataContext.d.ts +3 -1
  27. package/dist/contexts/DataContext.js +104 -17
  28. package/dist/contexts/PlaybackTextContext.d.ts +32 -0
  29. package/dist/contexts/PlaybackTextContext.js +29 -0
  30. package/dist/data-api-types.d.ts +251 -0
  31. package/dist/data-api-types.js +6 -0
  32. package/dist/emotion-effect-types.d.ts +86 -0
  33. package/dist/emotion-effect-types.js +6 -0
  34. package/dist/hooks/useBacklog.js +3 -1
  35. package/dist/hooks/useConversationBranch.d.ts +16 -0
  36. package/dist/hooks/useConversationBranch.js +125 -0
  37. package/dist/hooks/useFontLoader.d.ts +30 -0
  38. package/dist/hooks/useFontLoader.js +192 -0
  39. package/dist/hooks/useFullscreenText.d.ts +17 -0
  40. package/dist/hooks/useFullscreenText.js +120 -0
  41. package/dist/hooks/useImagePreloader.d.ts +5 -0
  42. package/dist/hooks/useImagePreloader.js +53 -0
  43. package/dist/hooks/usePlayerLogic.d.ts +10 -3
  44. package/dist/hooks/usePlayerLogic.js +115 -18
  45. package/dist/hooks/usePluginAPI.js +1 -1
  46. package/dist/hooks/usePluginEvents.d.ts +4 -1
  47. package/dist/hooks/usePluginEvents.js +16 -11
  48. package/dist/hooks/usePreloadImages.js +27 -7
  49. package/dist/hooks/useSoundPlayer.d.ts +15 -0
  50. package/dist/hooks/useSoundPlayer.js +209 -0
  51. package/dist/hooks/useTypewriter.d.ts +6 -2
  52. package/dist/hooks/useTypewriter.js +42 -6
  53. package/dist/hooks/useVoice.js +4 -1
  54. package/dist/index.d.ts +5 -3
  55. package/dist/index.js +3 -1
  56. package/dist/plugin/PluginManager.d.ts +86 -5
  57. package/dist/plugin/PluginManager.js +427 -94
  58. package/dist/sdk.d.ts +133 -162
  59. package/dist/sdk.js +39 -4
  60. package/dist/types.d.ts +300 -4
  61. package/dist/utils/branchBlockConverter.d.ts +2 -0
  62. package/dist/utils/branchBlockConverter.js +21 -0
  63. package/dist/utils/branchNavigator.d.ts +14 -0
  64. package/dist/utils/branchNavigator.js +55 -0
  65. package/dist/utils/facePositionCalculator.js +0 -1
  66. package/dist/utils/variableManager.d.ts +18 -0
  67. package/dist/utils/variableManager.js +159 -0
  68. package/package.json +6 -6
  69. package/dist/components/ConversationLogUI.d.ts +0 -2
  70. package/dist/components/ConversationLogUI.js +0 -115
  71. package/dist/hooks/useConversationLog.d.ts +0 -14
  72. package/dist/hooks/useConversationLog.js +0 -82
  73. package/dist/hooks/useUIVisibility.d.ts +0 -9
  74. package/dist/hooks/useUIVisibility.js +0 -19
@@ -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
- /* eslint-disable @typescript-eslint/no-explicit-any */
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
- window.__LUNA_JSX_RUNTIME__ = {
84
+ globalWindow.__LUNA_JSX_RUNTIME__ = {
38
85
  jsx: (type, props, key) => {
39
- return React.createElement(type, key ? Object.assign(Object.assign({}, props), { key }) : props);
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
- return React.createElement(type, key ? Object.assign(Object.assign({}, props), { key }) : props);
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
- window.__LUNA_REACT_RUNTIME__ = Object.assign(Object.assign({}, React), { default: React, useDataAPI,
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
- console.log("🔧 Luna React Runtime initialized for JSX support with hooks and screen size system");
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
- console.log(`Plugin ${packageName} setup completed. Hooks:`, Object.keys(instance.hooks));
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 = window.location.hostname === "localhost" ||
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 === "__LUNA_REACT__" || moduleName === "luna-react" || moduleName === "react" || moduleName === "@luna-editor/engine") {
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 === "__LUNA_JSX_RUNTIME__" || moduleName === "luna-react/jsx-runtime" || moduleName === "react/jsx-runtime" || moduleName === "react/jsx-dev-runtime") {
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(" ").forEach((cn) => classNames.add(cn));
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(" ").forEach((cn) => classNames.add(cn));
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(" ").forEach((cn) => classNames.delete(cn));
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 && targetImage.src) {
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[key] = value;
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
- overlaysToRemove.forEach((id) => this.removeOverlay(id, instance));
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
- keysToDelete.forEach((key) => this.storage.delete(key));
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
- // プラグインアセットのAPIエンドポイントを返す(URLエンコーディング対応)
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) => __awaiter(this, void 0, void 0, function* () {
498
- const promises = filenames.map((filename) => {
499
- const url = assetAPI.getUrl(filename);
500
- if (filename.match(/\.(png|jpg|jpeg|gif|webp)$/i)) {
501
- return new Promise((resolve, reject) => {
502
- const img = new Image();
503
- img.onload = () => resolve();
504
- img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
505
- img.src = url;
506
- });
507
- }
508
- if (filename.match(/\.(mp3|wav|ogg)$/i)) {
509
- return new Promise((resolve, reject) => {
510
- const audio = new Audio();
511
- audio.oncanplaythrough = () => resolve();
512
- audio.onerror = () => reject(new Error(`Failed to load audio: ${url}`));
513
- audio.src = url;
514
- });
515
- }
516
- return Promise.resolve();
517
- });
518
- yield Promise.all(promises);
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
- console.log("playing sound", filename);
522
- console.log("url:", assetAPI.getUrl(filename));
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
- throw new Error("DataAPI.get() can only be called from within DataProvider context");
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
- element.className = classNames.join(" ");
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
- .querySelectorAll("style[data-plugin]")
708
- .forEach((el) => el.remove());
709
- document
710
- .querySelectorAll("script[data-plugin]")
711
- .forEach((el) => el.remove());
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
- this.plugins.forEach(({ instance }) => {
714
- instance.effects.forEach((effect) => effect.remove());
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
- listeners.forEach((listener) => listener(visible));
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
  }