@luna-editor/engine 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/Player.d.ts +1 -1
  2. package/dist/Player.js +527 -85
  3. package/dist/api/conversationBranch.d.ts +4 -0
  4. package/dist/api/conversationBranch.js +83 -0
  5. package/dist/components/BackgroundLayer.d.ts +19 -0
  6. package/dist/components/BackgroundLayer.js +218 -0
  7. package/dist/components/ClickWaitIndicator.d.ts +10 -0
  8. package/dist/components/ClickWaitIndicator.js +31 -0
  9. package/dist/components/ConversationBranchBox.d.ts +2 -0
  10. package/dist/components/ConversationBranchBox.js +29 -0
  11. package/dist/components/DialogueBox.js +16 -1
  12. package/dist/components/FontSettingsPanel.d.ts +10 -0
  13. package/dist/components/FontSettingsPanel.js +30 -0
  14. package/dist/components/FullscreenTextBox.d.ts +6 -0
  15. package/dist/components/FullscreenTextBox.js +70 -0
  16. package/dist/components/GameScreen.d.ts +1 -0
  17. package/dist/components/GameScreen.js +363 -81
  18. package/dist/components/PluginComponentProvider.d.ts +2 -2
  19. package/dist/components/PluginComponentProvider.js +3 -3
  20. package/dist/components/TimeWaitIndicator.d.ts +15 -0
  21. package/dist/components/TimeWaitIndicator.js +17 -0
  22. package/dist/contexts/AudioContext.d.ts +14 -0
  23. package/dist/contexts/AudioContext.js +14 -0
  24. package/dist/contexts/DataContext.d.ts +4 -1
  25. package/dist/contexts/DataContext.js +82 -13
  26. package/dist/hooks/useBacklog.js +3 -0
  27. package/dist/hooks/useConversationBranch.d.ts +16 -0
  28. package/dist/hooks/useConversationBranch.js +125 -0
  29. package/dist/hooks/useFontLoader.d.ts +23 -0
  30. package/dist/hooks/useFontLoader.js +153 -0
  31. package/dist/hooks/useFullscreenText.d.ts +17 -0
  32. package/dist/hooks/useFullscreenText.js +120 -0
  33. package/dist/hooks/usePlayerLogic.d.ts +10 -3
  34. package/dist/hooks/usePlayerLogic.js +115 -18
  35. package/dist/hooks/usePluginEvents.d.ts +4 -1
  36. package/dist/hooks/usePluginEvents.js +16 -11
  37. package/dist/hooks/usePreloadImages.js +27 -7
  38. package/dist/hooks/useSoundPlayer.d.ts +15 -0
  39. package/dist/hooks/useSoundPlayer.js +209 -0
  40. package/dist/hooks/useTypewriter.d.ts +6 -2
  41. package/dist/hooks/useTypewriter.js +42 -6
  42. package/dist/hooks/useVoice.js +7 -1
  43. package/dist/index.d.ts +6 -3
  44. package/dist/index.js +3 -1
  45. package/dist/plugin/PluginManager.d.ts +66 -2
  46. package/dist/plugin/PluginManager.js +352 -79
  47. package/dist/sdk.d.ts +184 -22
  48. package/dist/sdk.js +27 -2
  49. package/dist/types.d.ts +303 -4
  50. package/dist/utils/branchBlockConverter.d.ts +2 -0
  51. package/dist/utils/branchBlockConverter.js +21 -0
  52. package/dist/utils/branchNavigator.d.ts +14 -0
  53. package/dist/utils/branchNavigator.js +55 -0
  54. package/dist/utils/facePositionCalculator.js +0 -1
  55. package/dist/utils/variableManager.d.ts +18 -0
  56. package/dist/utils/variableManager.js +159 -0
  57. package/package.json +1 -1
  58. package/dist/components/ConversationLogUI.d.ts +0 -2
  59. package/dist/components/ConversationLogUI.js +0 -115
  60. package/dist/hooks/useConversationLog.d.ts +0 -14
  61. package/dist/hooks/useConversationLog.js +0 -82
  62. package/dist/hooks/useUIVisibility.d.ts +0 -9
  63. package/dist/hooks/useUIVisibility.js +0 -19
  64. package/dist/plugin/luna-react.d.ts +0 -41
  65. package/dist/plugin/luna-react.js +0 -99
@@ -7,13 +7,26 @@ 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";
14
24
  import { usePluginAPI, useUIVisibility } from "../hooks/usePluginAPI";
15
25
  import { useScreenScale, useScreenSize, useToPixel, } from "../hooks/useScreenSize";
16
- import { ComponentType, } from "../sdk";
26
+ import { ComponentType, definePlugin, } from "../sdk";
27
+ // グローバルなロード状態管理(複数のPluginManagerインスタンス間で共有)
28
+ const globalLoadingPlugins = new Map();
29
+ const globalLoadedPlugins = new Map();
17
30
  export class PluginManager {
18
31
  constructor() {
19
32
  this.plugins = new Map();
@@ -23,38 +36,117 @@ export class PluginManager {
23
36
  this.componentRegistry = new Map();
24
37
  this.uiVisibilityState = new Map();
25
38
  this.uiVisibilityListeners = new Map();
39
+ this.selectChoiceCallback = null;
40
+ this.getBranchStateCallback = null;
41
+ this.preloadPromises = [];
42
+ this.muteAudio = false;
43
+ // 作品のサウンドデータ(ID → URL)
44
+ this.workSounds = new Map();
45
+ // ビルトインのクリック音設定
46
+ this.clickSoundUrl = null;
47
+ this.clickSoundVolume = 0.5;
48
+ // プラグインによるクリック音の上書きフラグ
49
+ this.clickSoundOverridden = false;
26
50
  this.initializeReactRuntime();
27
51
  }
28
52
  /**
29
53
  * グローバルReactランタイムを初期化(クライアントサイドのみ)
54
+ * 既に初期化済みの場合はスキップ
30
55
  */
31
56
  initializeReactRuntime() {
32
57
  // サーバーサイドではスキップ
33
58
  if (typeof window === "undefined") {
34
59
  return;
35
60
  }
61
+ const globalWindow = window;
62
+ // 既に初期化済みの場合はスキップ
63
+ if (globalWindow.__LUNA_REACT_RUNTIME_INITIALIZED__) {
64
+ return;
65
+ }
66
+ globalWindow.__LUNA_REACT_RUNTIME_INITIALIZED__ = true;
36
67
  // JSXランタイム
37
- window.__LUNA_JSX_RUNTIME__ = {
68
+ globalWindow.__LUNA_JSX_RUNTIME__ = {
38
69
  jsx: (type, props, key) => {
39
- return React.createElement(type, key ? Object.assign(Object.assign({}, props), { key }) : props);
70
+ const { children } = props, restProps = __rest(props, ["children"]);
71
+ const propsWithKey = key ? Object.assign(Object.assign({}, restProps), { key }) : restProps;
72
+ return children !== undefined
73
+ ? React.createElement(type, propsWithKey, children)
74
+ : React.createElement(type, propsWithKey);
40
75
  },
41
76
  jsxs: (type, props, key) => {
42
- return React.createElement(type, key ? Object.assign(Object.assign({}, props), { key }) : props);
77
+ const { children } = props, restProps = __rest(props, ["children"]);
78
+ const propsWithKey = key ? Object.assign(Object.assign({}, restProps), { key }) : restProps;
79
+ // jsxsはchildren配列をスプレッドして渡す
80
+ return Array.isArray(children)
81
+ ? React.createElement(type, propsWithKey, ...children)
82
+ : React.createElement(type, propsWithKey, children);
43
83
  },
44
84
  Fragment: React.Fragment,
45
85
  };
46
86
  // Reactランタイム (useDataAPIフックと画面サイズフックを含む)
47
- window.__LUNA_REACT_RUNTIME__ = Object.assign(Object.assign({}, React), { default: React, useDataAPI,
87
+ globalWindow.__LUNA_REACT_RUNTIME__ = Object.assign(Object.assign({}, React), { default: React, useDataAPI,
48
88
  usePluginAPI,
49
89
  useUIVisibility,
50
90
  useToPixel,
51
91
  useScreenSize,
52
92
  useScreenScale,
53
93
  useScreenSizeAtom,
54
- ComponentType });
94
+ ComponentType,
95
+ definePlugin });
55
96
  console.log("🔧 Luna React Runtime initialized for JSX support with hooks and screen size system");
56
97
  }
57
98
  loadPlugin(packageName, bundleUrl, config) {
99
+ return __awaiter(this, void 0, void 0, function* () {
100
+ // グローバルに既にロード済みの場合はローカルにコピーしてスキップ
101
+ const globalLoaded = globalLoadedPlugins.get(packageName);
102
+ if (globalLoaded) {
103
+ console.log(`Plugin ${packageName} is already loaded globally, reusing.`);
104
+ // 新しいconfigで更新したコピーを作成
105
+ const updatedPlugin = Object.assign(Object.assign({}, globalLoaded), { config: config !== null && config !== void 0 ? config : globalLoaded.config });
106
+ this.plugins.set(packageName, updatedPlugin);
107
+ // コンポーネントレジストリにも登録
108
+ for (const [type, component] of globalLoaded.instance.components) {
109
+ this.componentRegistry.set(type, component);
110
+ }
111
+ return;
112
+ }
113
+ // ローカルに既にロード済みの場合はconfigだけ更新してスキップ
114
+ const localLoaded = this.plugins.get(packageName);
115
+ if (localLoaded) {
116
+ console.log(`Plugin ${packageName} is already loaded, updating config.`);
117
+ // configを更新
118
+ localLoaded.config = config !== null && config !== void 0 ? config : localLoaded.config;
119
+ return;
120
+ }
121
+ // グローバルでロード中の場合はそのPromiseを待つ
122
+ const existingLoadPromise = globalLoadingPlugins.get(packageName);
123
+ if (existingLoadPromise) {
124
+ console.log(`Plugin ${packageName} is currently loading, waiting...`);
125
+ yield existingLoadPromise;
126
+ // ロード完了後にグローバルからコピー(新しいconfigで更新)
127
+ const loaded = globalLoadedPlugins.get(packageName);
128
+ if (loaded) {
129
+ const updatedPlugin = Object.assign(Object.assign({}, loaded), { config: config !== null && config !== void 0 ? config : loaded.config });
130
+ this.plugins.set(packageName, updatedPlugin);
131
+ for (const [type, component] of loaded.instance.components) {
132
+ this.componentRegistry.set(type, component);
133
+ }
134
+ }
135
+ return;
136
+ }
137
+ // ロード処理をPromiseとして保存
138
+ const loadPromise = this.doLoadPlugin(packageName, bundleUrl, config);
139
+ globalLoadingPlugins.set(packageName, loadPromise);
140
+ try {
141
+ yield loadPromise;
142
+ }
143
+ finally {
144
+ // ロード完了後にマップから削除
145
+ globalLoadingPlugins.delete(packageName);
146
+ }
147
+ });
148
+ }
149
+ doLoadPlugin(packageName, bundleUrl, config) {
58
150
  return __awaiter(this, void 0, void 0, function* () {
59
151
  try {
60
152
  // スクリプトタグで動的にプラグインを読み込む
@@ -73,12 +165,15 @@ export class PluginManager {
73
165
  // プラグインをセットアップ
74
166
  plugin.setup(api);
75
167
  console.log(`Plugin ${packageName} setup completed. Hooks:`, Object.keys(instance.hooks));
76
- this.plugins.set(packageName, {
168
+ const loadedPlugin = {
77
169
  plugin,
78
170
  instance,
79
171
  enabled: true,
80
172
  config: config || {},
81
- });
173
+ };
174
+ // ローカルとグローバル両方に保存
175
+ this.plugins.set(packageName, loadedPlugin);
176
+ globalLoadedPlugins.set(packageName, loadedPlugin);
82
177
  }
83
178
  catch (error) {
84
179
  console.error(`Failed to load plugin ${packageName}:`, error);
@@ -133,12 +228,7 @@ export class PluginManager {
133
228
  // import文を削除
134
229
  .replace(/^import\s+.*?from\s+['"].*?['"];?\s*$/gm, "")
135
230
  .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__")');
231
+ .replace(/export\s+default\s+/g, "");
142
232
  // ES modules形式のプラグインコードを処理
143
233
  const wrappedCode = `
144
234
  (function() {
@@ -156,10 +246,10 @@ export class PluginManager {
156
246
 
157
247
  // Luna React Runtime を提供
158
248
  const require = function(moduleName) {
159
- if (moduleName === "__LUNA_REACT__" || moduleName === "luna-react" || moduleName === "react" || moduleName === "@luna-editor/engine") {
249
+ if (moduleName === "react" || moduleName === "luna-react" || moduleName === "@luna-editor/engine" || moduleName === "@luna-editor/engine/sdk" || moduleName === "@luna-editor/plugin-sdk") {
160
250
  return window.__LUNA_REACT_RUNTIME__;
161
251
  }
162
- if (moduleName === "__LUNA_JSX_RUNTIME__" || moduleName === "luna-react/jsx-runtime" || moduleName === "react/jsx-runtime" || moduleName === "react/jsx-dev-runtime") {
252
+ if (moduleName === "react/jsx-runtime" || moduleName === "react/jsx-dev-runtime" || moduleName === "luna-react/jsx-runtime" || moduleName === "luna-react/jsx-dev-runtime") {
163
253
  return window.__LUNA_JSX_RUNTIME__;
164
254
  }
165
255
  throw new Error('Module not found: ' + moduleName);
@@ -190,19 +280,6 @@ export class PluginManager {
190
280
  scriptElement.type = "text/javascript";
191
281
  scriptElement.dataset.plugin = packageName;
192
282
  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
283
  // DOMに追加(この時点で同期的に実行される)
207
284
  document.head.appendChild(scriptElement);
208
285
  })
@@ -220,17 +297,23 @@ export class PluginManager {
220
297
  return {
221
298
  setClassName: (className) => {
222
299
  classNames.clear();
223
- className.split(" ").forEach((cn) => classNames.add(cn));
300
+ for (const cn of className.split(" ")) {
301
+ classNames.add(cn);
302
+ }
224
303
  instance.styles.set(elementName, Array.from(classNames));
225
304
  this.applyStyles(elementName, Array.from(classNames));
226
305
  },
227
306
  addClassName: (className) => {
228
- className.split(" ").forEach((cn) => classNames.add(cn));
307
+ for (const cn of className.split(" ")) {
308
+ classNames.add(cn);
309
+ }
229
310
  instance.styles.set(elementName, Array.from(classNames));
230
311
  this.applyStyles(elementName, Array.from(classNames));
231
312
  },
232
313
  removeClassName: (className) => {
233
- className.split(" ").forEach((cn) => classNames.delete(cn));
314
+ for (const cn of className.split(" ")) {
315
+ classNames.delete(cn);
316
+ }
234
317
  instance.styles.set(elementName, Array.from(classNames));
235
318
  this.applyStyles(elementName, Array.from(classNames));
236
319
  },
@@ -281,7 +364,7 @@ export class PluginManager {
281
364
  // ターゲット要素内の画像を探してマスクを適用
282
365
  const targetImage = targetElement.querySelector("img");
283
366
  let maskStyles = {};
284
- if (targetImage && targetImage.src) {
367
+ if (targetImage === null || targetImage === void 0 ? void 0 : targetImage.src) {
285
368
  // 画像のスタイルプロパティを取得
286
369
  const computedImageStyle = getComputedStyle(targetImage);
287
370
  const objectFit = computedImageStyle.objectFit;
@@ -312,7 +395,7 @@ export class PluginManager {
312
395
  // スタイルを個別に適用
313
396
  Object.entries(overlayStyles).forEach(([key, value]) => {
314
397
  if (typeof value === "string") {
315
- overlay.style[key] = value;
398
+ overlay.style.setProperty(key.replace(/([A-Z])/g, "-$1").toLowerCase(), value);
316
399
  }
317
400
  });
318
401
  overlay.dataset.plugin = packageName;
@@ -332,7 +415,9 @@ export class PluginManager {
332
415
  },
333
416
  clearOverlays: () => {
334
417
  const overlaysToRemove = Array.from(instance.overlays.keys()).filter((id) => id.includes(`-overlay-${elementName}-`));
335
- overlaysToRemove.forEach((id) => this.removeOverlay(id, instance));
418
+ for (const id of overlaysToRemove) {
419
+ this.removeOverlay(id, instance);
420
+ }
336
421
  },
337
422
  };
338
423
  };
@@ -344,6 +429,8 @@ export class PluginManager {
344
429
  gameScreen: createStyleElement("gameScreen"),
345
430
  characterSprite: createStyleElement("characterSprite"),
346
431
  background: createStyleElement("background"),
432
+ fullscreenTextOverlay: createStyleElement("fullscreenTextOverlay"),
433
+ fullscreenTextContainer: createStyleElement("fullscreenTextContainer"),
347
434
  },
348
435
  injectCSS: (css) => {
349
436
  if (this.injectedStyles.has(css))
@@ -481,45 +568,71 @@ export class PluginManager {
481
568
  keysToDelete.push(key);
482
569
  }
483
570
  }
484
- keysToDelete.forEach((key) => this.storage.delete(key));
571
+ for (const key of keysToDelete) {
572
+ this.storage.delete(key);
573
+ }
485
574
  },
486
575
  };
487
576
  const assetAPI = {
488
577
  getUrl: (filename) => {
489
- // プラグインアセットのAPIエンドポイントを返す(URLエンコーディング対応)
578
+ // キャッシュに最終URL(リダイレクト後)があればそれを返す
579
+ const cachedUrl = instance.assetUrls.get(filename);
580
+ if (cachedUrl) {
581
+ return cachedUrl;
582
+ }
583
+ // なければAPIエンドポイントを返す(URLエンコーディング対応)
490
584
  const encodedPackageName = encodeURIComponent(packageName);
491
585
  const encodedFilename = encodeURIComponent(filename);
492
586
  const url = `/api/plugin/${encodedPackageName}/assets/${encodedFilename}`;
493
- // instance内にURLを保存(Context API経由でアクセス可能にするため)
494
- instance.assetUrls.set(filename, url);
495
587
  return url;
496
588
  },
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
- }),
589
+ preload: (filenames) => {
590
+ const preloadPromise = (() => __awaiter(this, void 0, void 0, function* () {
591
+ const promises = filenames.map((filename) => __awaiter(this, void 0, void 0, function* () {
592
+ // APIエンドポイントURL
593
+ const encodedPackageName = encodeURIComponent(packageName);
594
+ const encodedFilename = encodeURIComponent(filename);
595
+ const apiUrl = `/api/plugin/${encodedPackageName}/assets/${encodedFilename}`;
596
+ // fetchでリダイレクト後の最終URLを取得
597
+ try {
598
+ const response = yield fetch(apiUrl, { method: "HEAD" });
599
+ const finalUrl = response.url; // リダイレクト後の最終URL
600
+ // 最終URLをキャッシュに保存
601
+ instance.assetUrls.set(filename, finalUrl);
602
+ // 画像をプリロード
603
+ if (filename.match(/\.(png|jpg|jpeg|gif|webp)$/i)) {
604
+ return new Promise((resolve, reject) => {
605
+ const img = new Image();
606
+ img.onload = () => resolve();
607
+ img.onerror = () => reject(new Error(`Failed to load image: ${finalUrl}`));
608
+ img.src = finalUrl;
609
+ });
610
+ }
611
+ // 音声をプリロード
612
+ if (filename.match(/\.(mp3|wav|ogg)$/i)) {
613
+ return new Promise((resolve, reject) => {
614
+ const audio = new Audio();
615
+ audio.oncanplaythrough = () => resolve();
616
+ audio.onerror = () => reject(new Error(`Failed to load audio: ${finalUrl}`));
617
+ audio.src = finalUrl;
618
+ });
619
+ }
620
+ }
621
+ catch (error) {
622
+ console.error(`Failed to preload asset: ${filename}`, error);
623
+ }
624
+ return Promise.resolve();
625
+ }));
626
+ yield Promise.all(promises);
627
+ }))();
628
+ // プリロードPromiseを追跡
629
+ this.preloadPromises.push(preloadPromise);
630
+ return preloadPromise;
631
+ },
520
632
  playSound: (filename, options) => {
521
- console.log("playing sound", filename);
522
- console.log("url:", assetAPI.getUrl(filename));
633
+ // ミュート中は再生しない
634
+ if (this.muteAudio)
635
+ return;
523
636
  const audio = new Audio(assetAPI.getUrl(filename));
524
637
  if ((options === null || options === void 0 ? void 0 : options.volume) !== undefined) {
525
638
  audio.volume = options.volume;
@@ -531,10 +644,30 @@ export class PluginManager {
531
644
  console.error("Failed to play sound:", error);
532
645
  });
533
646
  },
647
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
534
648
  stopSound: (_filename) => {
535
- // eslint-disable-line @typescript-eslint/no-unused-vars
536
649
  // TODO: 音声停止の実装
537
650
  },
651
+ playWorkSound: (soundId, options) => {
652
+ // ミュート中は再生しない
653
+ if (this.muteAudio)
654
+ return;
655
+ const soundUrl = this.getWorkSoundUrl(soundId);
656
+ if (!soundUrl) {
657
+ console.warn(`Work sound not found: ${soundId}`);
658
+ return;
659
+ }
660
+ const audio = new Audio(soundUrl);
661
+ if ((options === null || options === void 0 ? void 0 : options.volume) !== undefined) {
662
+ audio.volume = options.volume;
663
+ }
664
+ if (options === null || options === void 0 ? void 0 : options.loop) {
665
+ audio.loop = true;
666
+ }
667
+ audio.play().catch((error) => {
668
+ console.error("Failed to play work sound:", error);
669
+ });
670
+ },
538
671
  };
539
672
  const componentAPI = {
540
673
  registerComponent: (type, component) => {
@@ -579,6 +712,23 @@ export class PluginManager {
579
712
  const pluginEntry = this.plugins.get(packageName);
580
713
  return (pluginEntry === null || pluginEntry === void 0 ? void 0 : pluginEntry.config) || {};
581
714
  },
715
+ selectChoice: (choiceId) => {
716
+ if (this.selectChoiceCallback) {
717
+ this.selectChoiceCallback(choiceId);
718
+ }
719
+ else {
720
+ console.warn("selectChoice callback is not registered. Make sure to call setSelectChoiceCallback.");
721
+ }
722
+ },
723
+ getBranchState: () => {
724
+ if (this.getBranchStateCallback) {
725
+ return this.getBranchStateCallback();
726
+ }
727
+ return null;
728
+ },
729
+ overrideClickSound: () => {
730
+ this.overrideClickSound();
731
+ },
582
732
  hooks: {
583
733
  onInit: (callback) => {
584
734
  instance.hooks.onInit = callback;
@@ -592,9 +742,6 @@ export class PluginManager {
592
742
  onCharacterEnter: (callback) => {
593
743
  instance.hooks.onCharacterEnter = callback;
594
744
  },
595
- onCharacterExit: (callback) => {
596
- instance.hooks.onCharacterExit = callback;
597
- },
598
745
  onDialogueShow: (callback) => {
599
746
  instance.hooks.onDialogueShow = callback;
600
747
  },
@@ -607,10 +754,28 @@ export class PluginManager {
607
754
  onScenarioEnd: (callback) => {
608
755
  instance.hooks.onScenarioEnd = callback;
609
756
  },
757
+ onBranchStart: (callback) => {
758
+ instance.hooks.onBranchStart = callback;
759
+ },
760
+ onChoiceSelected: (callback) => {
761
+ instance.hooks.onChoiceSelected = callback;
762
+ },
763
+ onBranchEnd: (callback) => {
764
+ instance.hooks.onBranchEnd = callback;
765
+ },
766
+ onBranchBlockChange: (callback) => {
767
+ instance.hooks.onBranchBlockChange = callback;
768
+ },
610
769
  },
611
770
  effects: effectAPI,
612
771
  storage: storageAPI,
613
772
  assets: assetAPI,
773
+ blockOptions: {
774
+ register: () => {
775
+ // ランタイムでは何もしない(サーバー側での抽出時のみ使用)
776
+ // プラグインのsetup()が呼ばれた時点でBlockOptionsはすでにDBに保存済み
777
+ },
778
+ },
614
779
  data: {
615
780
  get: () => {
616
781
  throw new Error("DataAPI.get() can only be called from within DataProvider context");
@@ -621,6 +786,24 @@ export class PluginManager {
621
786
  watch: () => {
622
787
  throw new Error("DataAPI.watch() can only be called from within DataProvider context");
623
788
  },
789
+ updateSettings: () => {
790
+ throw new Error("DataAPI.updateSettings() can only be called from within DataProvider context");
791
+ },
792
+ getBlockOption: () => {
793
+ throw new Error("DataAPI.getBlockOption() can only be called from within DataProvider context");
794
+ },
795
+ setBgmVolume: () => {
796
+ throw new Error("DataAPI.setBgmVolume() can only be called from within DataProvider context");
797
+ },
798
+ setSeVolume: () => {
799
+ throw new Error("DataAPI.setSeVolume() can only be called from within DataProvider context");
800
+ },
801
+ setVoiceVolume: () => {
802
+ throw new Error("DataAPI.setVoiceVolume() can only be called from within DataProvider context");
803
+ },
804
+ setVolumes: () => {
805
+ throw new Error("DataAPI.setVolumes() can only be called from within DataProvider context");
806
+ },
624
807
  },
625
808
  };
626
809
  }
@@ -700,16 +883,18 @@ export class PluginManager {
700
883
  // クリーンアップ
701
884
  cleanup() {
702
885
  // 注入されたスタイルを削除
703
- document
704
- .querySelectorAll("style[data-plugin]")
705
- .forEach((el) => el.remove());
706
- document
707
- .querySelectorAll("script[data-plugin]")
708
- .forEach((el) => el.remove());
886
+ for (const el of document.querySelectorAll("style[data-plugin]")) {
887
+ el.remove();
888
+ }
889
+ for (const el of document.querySelectorAll("script[data-plugin]")) {
890
+ el.remove();
891
+ }
709
892
  // エフェクトを削除
710
- this.plugins.forEach(({ instance }) => {
711
- instance.effects.forEach((effect) => effect.remove());
712
- });
893
+ for (const { instance } of this.plugins.values()) {
894
+ for (const effect of instance.effects.values()) {
895
+ effect.remove();
896
+ }
897
+ }
713
898
  this.plugins.clear();
714
899
  this.styleElements.clear();
715
900
  this.injectedStyles.clear();
@@ -776,7 +961,9 @@ export class PluginManager {
776
961
  // リスナーに通知
777
962
  const listeners = this.uiVisibilityListeners.get(type);
778
963
  if (listeners) {
779
- listeners.forEach((listener) => listener(visible));
964
+ for (const listener of listeners) {
965
+ listener(visible);
966
+ }
780
967
  }
781
968
  }
782
969
  /**
@@ -798,6 +985,8 @@ export class PluginManager {
798
985
  this.uiVisibilityListeners.set(type, new Set());
799
986
  }
800
987
  const listeners = this.uiVisibilityListeners.get(type);
988
+ if (!listeners)
989
+ return () => { };
801
990
  listeners.add(listener);
802
991
  // 現在の状態を即座に通知
803
992
  listener(this.uiVisibilityState.get(type) || false);
@@ -848,4 +1037,88 @@ export class PluginManager {
848
1037
  hideUI(type) {
849
1038
  this.setUIVisibility(type, false);
850
1039
  }
1040
+ setSelectChoiceCallback(callback) {
1041
+ this.selectChoiceCallback = callback;
1042
+ }
1043
+ setGetBranchStateCallback(callback) {
1044
+ this.getBranchStateCallback = callback;
1045
+ }
1046
+ /**
1047
+ * 全てのアセットプリロードが完了するまで待機
1048
+ * @returns プリロード完了時にresolveするPromise
1049
+ */
1050
+ waitForPreloads() {
1051
+ return __awaiter(this, void 0, void 0, function* () {
1052
+ yield Promise.all(this.preloadPromises);
1053
+ this.preloadPromises = [];
1054
+ });
1055
+ }
1056
+ /**
1057
+ * 音声のミュート状態を設定
1058
+ * @param mute - ミュートするかどうか
1059
+ */
1060
+ setMuteAudio(mute) {
1061
+ this.muteAudio = mute;
1062
+ }
1063
+ /**
1064
+ * 作品のサウンドデータを設定
1065
+ * @param sounds - サウンドIDをキーとしたURL/名前のマップ
1066
+ */
1067
+ setWorkSounds(sounds) {
1068
+ this.workSounds.clear();
1069
+ for (const sound of sounds) {
1070
+ this.workSounds.set(sound.id, { url: sound.url, name: sound.name });
1071
+ }
1072
+ }
1073
+ /**
1074
+ * 作品のサウンドURLを取得
1075
+ * @param soundId - サウンドID
1076
+ */
1077
+ getWorkSoundUrl(soundId) {
1078
+ var _a, _b;
1079
+ return (_b = (_a = this.workSounds.get(soundId)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : null;
1080
+ }
1081
+ /**
1082
+ * ビルトインのクリック音を設定
1083
+ * @param url - サウンドファイルのURL(nullで無効化)
1084
+ * @param volume - 音量(0.0 - 1.0)
1085
+ */
1086
+ setClickSound(url, volume = 0.5) {
1087
+ this.clickSoundUrl = url;
1088
+ this.clickSoundVolume = volume;
1089
+ }
1090
+ /**
1091
+ * プラグインがクリック音を上書きする
1092
+ * これを呼び出すと、ビルトインのクリック音は再生されなくなる
1093
+ */
1094
+ overrideClickSound() {
1095
+ this.clickSoundOverridden = true;
1096
+ }
1097
+ /**
1098
+ * プラグインのクリック音上書きをリセット
1099
+ * 各ブロック遷移の前に呼び出す
1100
+ */
1101
+ resetClickSoundOverride() {
1102
+ this.clickSoundOverridden = false;
1103
+ }
1104
+ /**
1105
+ * クリック音が上書きされているかを取得
1106
+ */
1107
+ isClickSoundOverridden() {
1108
+ return this.clickSoundOverridden;
1109
+ }
1110
+ /**
1111
+ * ビルトインのクリック音を再生
1112
+ * プラグインで上書きされている場合やミュート中は再生しない
1113
+ */
1114
+ playClickSound() {
1115
+ if (this.muteAudio || this.clickSoundOverridden || !this.clickSoundUrl) {
1116
+ return;
1117
+ }
1118
+ const audio = new Audio(this.clickSoundUrl);
1119
+ audio.volume = this.clickSoundVolume;
1120
+ audio.play().catch((error) => {
1121
+ console.error("Failed to play click sound:", error);
1122
+ });
1123
+ }
851
1124
  }