@luna-editor/engine 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Player.js CHANGED
@@ -24,6 +24,7 @@ import { PluginComponentProvider } from "./components/PluginComponentProvider";
24
24
  import { TimeWaitIndicator } from "./components/TimeWaitIndicator";
25
25
  import { AudioProvider } from "./contexts/AudioContext";
26
26
  import { DataProvider } from "./contexts/DataContext";
27
+ import { PlaybackTextProvider } from "./contexts/PlaybackTextContext";
27
28
  import { useBacklog } from "./hooks/useBacklog";
28
29
  import { useConversationBranch } from "./hooks/useConversationBranch";
29
30
  import { useFontLoader } from "./hooks/useFontLoader";
@@ -38,10 +39,17 @@ import { ComponentType } from "./sdk";
38
39
  import { convertBranchBlockToScenarioBlock } from "./utils/branchBlockConverter";
39
40
  import { BranchNavigator } from "./utils/branchNavigator";
40
41
  import { VariableManager } from "./utils/variableManager";
41
- export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, onScenarioEnd, onScenarioStart, onScenarioCancelled, onSettingsChange, className, autoplay = false, preventDefaultScroll = true, screenSize: screenSizeProp, disableKeyboardNavigation = false, }) => {
42
+ const EMPTY_PLUGINS = [];
43
+ const EMPTY_SOUNDS = [];
44
+ export const Player = ({ scenario: scenarioProp, settings, plugins = EMPTY_PLUGINS, sounds = EMPTY_SOUNDS, onEnd, onScenarioEnd, onScenarioStart, onScenarioCancelled, onSettingsChange, className, autoplay = false, preventDefaultScroll = true, screenSize: screenSizeProp, disableKeyboardNavigation = false, }) => {
42
45
  var _a, _b, _c, _d;
46
+ // scenario.blocks が存在しない場合は空の配列を使用
47
+ const scenario = useMemo(() => {
48
+ var _a;
49
+ return (Object.assign(Object.assign({}, scenarioProp), { blocks: (_a = scenarioProp.blocks) !== null && _a !== void 0 ? _a : [] }));
50
+ }, [scenarioProp]);
43
51
  // デフォルト値とマージ
44
- const mergedSettings = Object.assign({ textSpeed: 80, autoPlaySpeed: 3, bgmVolume: 0.5, seVolume: 1.0, voiceVolume: 1.0, effectVolume: 1.0, textSoundVolume: 1.0, defaultBackgroundFadeDuration: 0 }, settings);
52
+ const mergedSettings = Object.assign({ textSpeed: 80, autoPlaySpeed: 3, bgmVolume: 0.5, seVolume: 1.0, voiceVolume: 1.0, effectVolume: 1.0, textSoundVolume: 1.0, defaultBackgroundFadeDuration: 0, inactiveCharacterBrightness: 0.8, characterSpacing: 0.2 }, settings);
45
53
  // プラグインからの設定更新ハンドラ
46
54
  const handleSettingsUpdate = useCallback((updatedSettings) => {
47
55
  var _a, _b;
@@ -84,6 +92,7 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
84
92
  }, [pluginManager, mergedSettings.muteAudio]);
85
93
  // 画面サイズの初期化
86
94
  const [, setScreenSize] = useScreenSizeAtom();
95
+ const aspectRatioContainerRef = useRef(null);
87
96
  useEffect(() => {
88
97
  // screenSizeが明示的に指定されている場合はそれを使用(プレビュー用)
89
98
  if (screenSizeProp) {
@@ -93,16 +102,40 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
93
102
  // クライアントサイドでのみ実行
94
103
  if (typeof window === "undefined")
95
104
  return;
105
+ // 初期値として一時的にウィンドウサイズを設定
96
106
  setScreenSize({ width: window.innerWidth, height: window.innerHeight });
97
- // リサイズ監視
98
- const handleResize = () => {
107
+ const container = aspectRatioContainerRef.current;
108
+ if (!container)
109
+ return;
110
+ // アスペクト比コンテナのサイズを監視
111
+ const updateScreenSize = () => {
112
+ const rect = container.getBoundingClientRect();
113
+ // サイズが0の場合はスキップ(まだレンダリングされていない)
114
+ if (rect.width === 0 || rect.height === 0)
115
+ return;
116
+ console.log("[Player] screenSize更新:", {
117
+ width: rect.width,
118
+ height: rect.height,
119
+ windowWidth: window.innerWidth,
120
+ windowHeight: window.innerHeight,
121
+ });
99
122
  setScreenSize({
100
- width: window.innerWidth,
101
- height: window.innerHeight,
123
+ width: rect.width,
124
+ height: rect.height,
102
125
  });
103
126
  };
104
- window.addEventListener("resize", handleResize);
105
- return () => window.removeEventListener("resize", handleResize);
127
+ // 初期サイズを設定(次のフレームで実行してレンダリング完了を待つ)
128
+ requestAnimationFrame(() => {
129
+ updateScreenSize();
130
+ });
131
+ // ResizeObserverでコンテナのサイズ変更を監視
132
+ const resizeObserver = new ResizeObserver(() => {
133
+ updateScreenSize();
134
+ });
135
+ resizeObserver.observe(container);
136
+ return () => {
137
+ resizeObserver.disconnect();
138
+ };
106
139
  }, [setScreenSize, screenSizeProp]);
107
140
  // 表示可能なブロックのインデックスを事前計算
108
141
  const displayableBlockIndices = useMemo(() => {
@@ -113,7 +146,6 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
113
146
  "fullscreen_text",
114
147
  "click_wait",
115
148
  "time_wait",
116
- "action_node",
117
149
  ];
118
150
  const indices = scenario.blocks
119
151
  .map((block, index) => ({ block, index }))
@@ -269,8 +301,28 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
269
301
  }, [scenario.id, autoplay, hasStarted, pluginManager]);
270
302
  // 画像を事前読み込み
271
303
  const imagesLoaded = usePreloadImages(scenario);
272
- // フォントを読み込み
273
- const { isLoaded: fontsLoaded } = useFontLoader(scenario.fonts);
304
+ // シナリオ内の全テキストを抽出(フォントのプリロード用)
305
+ // サブセットフォントの分割ファイルを事前に読み込むことで、
306
+ // 文字送り中のフォント切り替わりを防止
307
+ const allScenarioText = useMemo(() => {
308
+ var _a;
309
+ const texts = [];
310
+ for (const block of scenario.blocks) {
311
+ // dialogue, narration, fullscreen_textのcontent
312
+ if (block.content) {
313
+ texts.push(block.content);
314
+ }
315
+ // speaker name
316
+ if ((_a = block.speaker) === null || _a === void 0 ? void 0 : _a.name) {
317
+ texts.push(block.speaker.name);
318
+ }
319
+ }
320
+ // 重複を除去してユニークな文字のみを残す
321
+ const uniqueChars = [...new Set(texts.join(""))].join("");
322
+ return uniqueChars;
323
+ }, [scenario.blocks]);
324
+ // フォントを読み込み(シナリオ内の全テキストでプリロード)
325
+ const { isLoaded: fontsLoaded } = useFontLoader(scenario.fonts, allScenarioText);
274
326
  // プラグインの読み込み状態
275
327
  const [pluginsLoaded, setPluginsLoaded] = useState(false);
276
328
  // 読み込み済みプラグインのパッケージ名を追跡
@@ -292,7 +344,7 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
292
344
  for (const plugin of newPlugins) {
293
345
  if (isCancelled)
294
346
  return;
295
- yield pluginManager.loadPlugin(plugin.packageName, plugin.bundleUrl, plugin.config);
347
+ yield pluginManager.loadPlugin(plugin.packageName, plugin.bundleUrl, plugin.config, plugin.assets, plugin.plugin);
296
348
  loadedPluginNamesRef.current.add(plugin.packageName);
297
349
  }
298
350
  if (isCancelled)
@@ -309,6 +361,9 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
309
361
  loadPlugins();
310
362
  return () => {
311
363
  isCancelled = true;
364
+ // Strict Mode再マウント時にプラグインが再ロード・再登録できるようにする
365
+ loadedPluginNamesRef.current.clear();
366
+ pluginManager.resetForRemount();
312
367
  };
313
368
  }, [plugins, sounds]);
314
369
  // 初回レンダリング完了フラグ
@@ -549,7 +604,16 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
549
604
  scenarioName: scenario.name || "scenario",
550
605
  });
551
606
  }
552
- }, [isFirstRenderComplete, hasStarted, currentBlock, pluginsLoaded, onScenarioStart, pluginManager, scenario.id, scenario.name]);
607
+ }, [
608
+ isFirstRenderComplete,
609
+ hasStarted,
610
+ currentBlock,
611
+ pluginsLoaded,
612
+ onScenarioStart,
613
+ pluginManager,
614
+ scenario.id,
615
+ scenario.name,
616
+ ]);
553
617
  // restartをusePlayerLogicの前に定義(分岐状態もリセット + キャンセルコールバック)
554
618
  const restart = useCallback(() => {
555
619
  // リスタート時はシナリオがキャンセルされたとみなす
@@ -712,7 +776,10 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
712
776
  displayableBlockIndices.length,
713
777
  handleNextInternal,
714
778
  onEnd,
715
- onScenarioEnd,
779
+ onScenarioEnd, // プラグインのonScenarioEndフックを呼び出し
780
+ pluginManager,
781
+ scenario.id,
782
+ scenario.name,
716
783
  ]);
717
784
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
718
785
  const _handlePrevious = useCallback(() => {
@@ -722,7 +789,16 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
722
789
  }, [state.currentBlockIndex, handlePreviousInternal]);
723
790
  // 現在の背景を計算
724
791
  const currentBackground = useMemo(() => calculateCurrentBackground(actualBlockIndex), [calculateCurrentBackground, actualBlockIndex]);
725
- // DataContext の構築 - displayedCharactersが必要なため usePlayerLogic の後に配置
792
+ // displayText JSX に変換(PlaybackTextProvider 用)
793
+ const displayTextElement = useMemo(() => {
794
+ if (displayText.includes("\n")) {
795
+ return displayText.split("\n").map((line, index, array) => (_jsxs(React.Fragment, { children: [line, index < array.length - 1 && _jsx("br", {})] }, index)));
796
+ }
797
+ return displayText;
798
+ }, [displayText]);
799
+ // DataContext の構築
800
+ // DataRefContext で安定した参照を提供することで、
801
+ // useDataAPI() を使うコンポーネントの再レンダリングを防ぐ
726
802
  const dataContext = useMemo(() => {
727
803
  var _a, _b, _c;
728
804
  return ({
@@ -732,9 +808,7 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
732
808
  scenarioId: scenario.id,
733
809
  scenarioName: scenario.name,
734
810
  currentBlock: currentBlock || null,
735
- displayText: displayText.includes("\n")
736
- ? displayText.split("\n").map((line, index, array) => (_jsxs(React.Fragment, { children: [line, index < array.length - 1 && _jsx("br", {})] }, index)))
737
- : displayText,
811
+ displayText: displayTextElement,
738
812
  isTyping,
739
813
  displayedCharacters,
740
814
  },
@@ -753,6 +827,8 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
753
827
  seVolume: mergedSettings.seVolume,
754
828
  voiceVolume: mergedSettings.voiceVolume,
755
829
  skipMode: "unread",
830
+ selectedFontFamily: mergedSettings.selectedFontFamily,
831
+ selectedUIFontFamily: mergedSettings.selectedUIFontFamily,
756
832
  },
757
833
  pluginAssets: {
758
834
  getAssetUrl: (pluginName, filename) => {
@@ -766,6 +842,7 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
766
842
  fonts: {
767
843
  fonts: (_c = scenario.fonts) !== null && _c !== void 0 ? _c : [],
768
844
  selectedFontFamily: mergedSettings.selectedFontFamily,
845
+ selectedUIFontFamily: mergedSettings.selectedUIFontFamily,
769
846
  isLoaded: fontsLoaded,
770
847
  },
771
848
  emotionEffect: emotionEffectState,
@@ -775,7 +852,7 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
775
852
  actualBlockIndex,
776
853
  scenario,
777
854
  currentBlock,
778
- displayText,
855
+ displayTextElement,
779
856
  isTyping,
780
857
  displayedCharacters,
781
858
  backlog,
@@ -789,6 +866,7 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
789
866
  mergedSettings.textSpeed,
790
867
  mergedSettings.voiceVolume,
791
868
  mergedSettings.selectedFontFamily,
869
+ mergedSettings.selectedUIFontFamily,
792
870
  currentBackground,
793
871
  fontsLoaded,
794
872
  ]);
@@ -828,6 +906,12 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
828
906
  const [width, height] = settings.aspectRatio.split(":").map(Number);
829
907
  return `${width}/${height}`;
830
908
  };
909
+ const getAspectRatioValue = () => {
910
+ if (!(settings === null || settings === void 0 ? void 0 : settings.aspectRatio))
911
+ return 16 / 9;
912
+ const [width, height] = settings.aspectRatio.split(":").map(Number);
913
+ return width / height;
914
+ };
831
915
  // 条件付きレンダリングを JSX で処理(フックの後、early return なし)
832
916
  return (_jsxs(_Fragment, { children: [!currentBlock && !state.isEnded && (_jsx("div", { className: clsx("flex items-center justify-center p-8", className), children: _jsx("p", { className: "text-gray-500", children: "\u30B7\u30CA\u30EA\u30AA\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" }) })), state.isEnded && (_jsx(EndScreen, { scenarioName: scenario.name, onRestart: restart, className: className })), currentBlock && !state.isEnded && (_jsxs(_Fragment, { children: [(!imagesLoaded ||
833
917
  !fontsLoaded ||
@@ -844,16 +928,27 @@ export const Player = ({ scenario, settings, plugins = [], sounds = [], onEnd, o
844
928
  touchAction: "none",
845
929
  userSelect: "none",
846
930
  WebkitUserSelect: "none",
847
- }, children: _jsx("div", { className: "relative bg-white flex flex-col w-full overflow-hidden h-full", style: { aspectRatio: getAspectRatio() }, children: _jsx(DataProvider, { data: dataContext, onSettingsUpdate: handleSettingsUpdate, onEmotionEffectUpdate: handleEmotionEffectUpdate, onCancelScenario: cancelScenario, children: _jsxs(AudioProvider, { settings: {
931
+ }, children: _jsx("div", { ref: aspectRatioContainerRef, className: "relative bg-white flex flex-col overflow-hidden", style: screenSizeProp
932
+ ? {
933
+ // プレビューモード: 親コンテナ(固定論理サイズ)を100%で埋める
934
+ width: "100%",
935
+ height: "100%",
936
+ }
937
+ : {
938
+ // 通常再生: ビューポートに合わせてアスペクト比を維持
939
+ aspectRatio: getAspectRatio(),
940
+ width: `min(100vw, calc(100vh * ${getAspectRatioValue()}))`,
941
+ height: `min(100vh, calc(100vw / ${getAspectRatioValue()}))`,
942
+ }, children: _jsx(DataProvider, { data: dataContext, onSettingsUpdate: handleSettingsUpdate, onEmotionEffectUpdate: handleEmotionEffectUpdate, onCancelScenario: cancelScenario, children: _jsx(AudioProvider, { settings: {
848
943
  bgmVolume: mergedSettings.bgmVolume,
849
944
  seVolume: mergedSettings.seVolume,
850
945
  voiceVolume: mergedSettings.voiceVolume,
851
946
  effectVolume: mergedSettings.effectVolume,
852
947
  textSoundVolume: mergedSettings.textSoundVolume,
853
948
  muteAudio: (_b = mergedSettings.muteAudio) !== null && _b !== void 0 ? _b : false,
854
- }, children: [_jsxs("div", { className: "h-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.Background, pluginManager: pluginManager, fallback: BackgroundLayer }, "background"), _jsx(GameScreen, { scenario: scenario, currentBlock: currentBlock, previousBlock: previousBlock, displayedCharacters: displayedCharacters }, "game-screen")] }), _jsx(OverlayUI, { children: _jsxs("div", { className: "h-full w-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.DialogueBox, pluginManager: pluginManager, fallback: DialogueBox }, "dialogue-box"), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "conversation_branch" && (_jsx(PluginComponentProvider, { type: ComponentType.ConversationBranch, pluginManager: pluginManager, fallback: ConversationBranchBox }, "conversation-branch")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "fullscreen_text" && (_jsx(FullscreenTextBox, { onComplete: () => handleNext("click") }, "fullscreen-text")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "click_wait" && (_jsx(ClickWaitIndicator, {}, "click-wait")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "time_wait" && (_jsx(TimeWaitIndicator, { duration: (_d = (_c = currentBlock.options) === null || _c === void 0 ? void 0 : _c.duration) !== null && _d !== void 0 ? _d : 1, onComplete: () => handleNext("time_wait") }, "time-wait")), pluginManager
855
- .getRegisteredComponents()
856
- .filter((type) => type !== ComponentType.DialogueBox &&
857
- type !== ComponentType.ConversationBranch)
858
- .map((componentType) => (_jsx(PluginComponentProvider, { type: componentType, pluginManager: pluginManager }, componentType)))] }) })] }) }) }) })] }))] }));
949
+ }, children: _jsxs(PlaybackTextProvider, { displayText: displayTextElement, isTyping: isTyping, children: [_jsxs("div", { className: "h-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.Background, pluginManager: pluginManager, fallback: BackgroundLayer }, "background"), _jsx(GameScreen, { scenario: scenario, currentBlock: currentBlock, previousBlock: previousBlock, displayedCharacters: displayedCharacters, inactiveCharacterBrightness: mergedSettings.inactiveCharacterBrightness, characterSpacing: mergedSettings.characterSpacing }, "game-screen")] }), _jsx(OverlayUI, { children: _jsxs("div", { className: "h-full w-full relative", children: [_jsx(PluginComponentProvider, { type: ComponentType.DialogueBox, pluginManager: pluginManager, fallback: DialogueBox }, "dialogue-box"), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "conversation_branch" && (_jsx(PluginComponentProvider, { type: ComponentType.ConversationBranch, pluginManager: pluginManager, fallback: ConversationBranchBox }, "conversation-branch")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "fullscreen_text" && (_jsx(FullscreenTextBox, { onComplete: () => handleNext("click") }, "fullscreen-text")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "click_wait" && (_jsx(ClickWaitIndicator, {}, "click-wait")), (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "time_wait" && (_jsx(TimeWaitIndicator, { duration: (_d = (_c = currentBlock.options) === null || _c === void 0 ? void 0 : _c.duration) !== null && _d !== void 0 ? _d : 1, onComplete: () => handleNext("time_wait") }, "time-wait")), pluginManager
950
+ .getRegisteredComponents()
951
+ .filter((type) => type !== ComponentType.DialogueBox &&
952
+ type !== ComponentType.ConversationBranch)
953
+ .map((componentType) => (_jsx(PluginComponentProvider, { type: componentType, pluginManager: pluginManager }, componentType)))] }) })] }) }) }) }) })] }))] }));
859
954
  };
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { memo, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { useDataAPI } from "../contexts/DataContext";
4
4
  /**
5
5
  * iOS/macOSかどうかを判定
@@ -56,8 +56,10 @@ function getMediaType(url) {
56
56
  }
57
57
  /**
58
58
  * 背景メディアコンポーネント
59
+ * React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
60
+ * これにより displayText の更新による不要な再描画を防止
59
61
  */
60
- const BackgroundMedia = ({ background, opacity, zIndex, useAppleFormat }) => {
62
+ const BackgroundMedia = memo(function BackgroundMedia({ background, opacity, zIndex, useAppleFormat }) {
61
63
  var _a, _b;
62
64
  // OS判定に基づいて使用するURLを決定
63
65
  // iOS/macOS: mp4 (imageUrl)
@@ -84,7 +86,7 @@ const BackgroundMedia = ({ background, opacity, zIndex, useAppleFormat }) => {
84
86
  transition: "none", // requestAnimationFrameでアニメーション
85
87
  pointerEvents: isHighLayer ? "none" : undefined,
86
88
  }, "data-background-object-id": background.objectId, "data-background-state-id": background.stateId, "data-background-layer": layer, children: mediaType === "video" ? (_jsx("video", { src: videoUrl, className: `w-full h-full ${objectFitClass}`, autoPlay: true, muted: true, playsInline: true, loop: background.loop }, videoUrl)) : (_jsx("img", { src: background.imageUrl, alt: background.stateName, className: `w-full h-full ${objectFitClass}` }, background.imageUrl)) }));
87
- };
89
+ });
88
90
  /**
89
91
  * 背景の配列が同じかどうかを比較
90
92
  */
@@ -107,7 +109,7 @@ function areBackgroundsEqual(a, b) {
107
109
  * プラグインによって置き換え可能
108
110
  * フェードイン・フェードアウト対応
109
111
  */
110
- export const BackgroundLayer = ({ className, }) => {
112
+ export const BackgroundLayer = memo(({ className }) => {
111
113
  const dataAPI = useDataAPI();
112
114
  // Apple環境かどうかを判定(iOS/macOSではmp4を使用)
113
115
  const useAppleFormat = useMemo(() => isApplePlatform(), []);
@@ -214,5 +216,5 @@ export const BackgroundLayer = ({ className, }) => {
214
216
  const zIndex = getLayerZIndex(layer, index);
215
217
  return (_jsx(BackgroundMedia, { background: bg, opacity: fadeState.isFading ? fadeState.fadeProgress : 1, zIndex: zIndex, useAppleFormat: useAppleFormat }, `curr-${bg.objectId}-${bg.stateId}-${index}`));
216
218
  })] }));
217
- };
219
+ });
218
220
  export { getMediaType };
@@ -1,10 +1,16 @@
1
- import type React from "react";
2
1
  import type { DisplayedCharacter, PublishedScenario, ScenarioBlock } from "../types";
3
2
  interface GameScreenProps {
4
3
  scenario: PublishedScenario;
5
4
  currentBlock: ScenarioBlock;
6
5
  previousBlock?: ScenarioBlock | null;
7
6
  displayedCharacters: DisplayedCharacter[];
7
+ inactiveCharacterBrightness?: number;
8
+ characterSpacing?: number;
8
9
  }
9
- export declare const GameScreen: React.FC<GameScreenProps>;
10
+ /**
11
+ * ゲームスクリーンコンポーネント
12
+ * React.memo でラップして、props が変わらない限り再レンダリングを防ぐ
13
+ * これにより displayText の更新による不要な再描画を防止
14
+ */
15
+ export declare const GameScreen: import("react").NamedExoticComponent<GameScreenProps>;
10
16
  export {};