@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
@@ -25,7 +25,6 @@ export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
25
25
  .slice(0, upToIndex + 1)
26
26
  .map((block, index) => ({ block, realIndex: index }))
27
27
  .filter(({ block }) => block.blockType === "dialogue" || block.blockType === "narration");
28
- console.log(`Building conversation history up to index ${upToIndex}, found ${conversationBlocks.length} conversation blocks`);
29
28
  conversationBlocks.forEach(({ block, realIndex }) => {
30
29
  var _a, _b;
31
30
  if (!processedBlocks.current.has(realIndex)) {
@@ -37,6 +36,7 @@ export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
37
36
  content: block.content,
38
37
  speakerName: (_a = block.speaker) === null || _a === void 0 ? void 0 : _a.name,
39
38
  speakerState: (_b = block.speakerState) === null || _b === void 0 ? void 0 : _b.name,
39
+ options: block.options,
40
40
  };
41
41
  addLogEntry(logEntry);
42
42
  processedBlocks.current.add(realIndex);
@@ -63,6 +63,7 @@ export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
63
63
  content: currentBlock.content,
64
64
  speakerName: (_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name,
65
65
  speakerState: (_b = currentBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name,
66
+ options: currentBlock.options,
66
67
  };
67
68
  addLogEntry(logEntry);
68
69
  processedBlocks.current.add(currentBlockIndex);
@@ -70,6 +71,7 @@ export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
70
71
  }
71
72
  }, [currentBlock, currentBlockIndex, addLogEntry]);
72
73
  // シナリオ変更時にリセット
74
+ // biome-ignore lint/correctness/useExhaustiveDependencies: scenario.idの変更を検知してログをクリアする意図的な依存
73
75
  useEffect(() => {
74
76
  clearLogs();
75
77
  }, [scenario.id, clearLogs]);
@@ -0,0 +1,16 @@
1
+ import type { ConversationBranchState, ConversationChoice, PublishedScenario } from "../types";
2
+ import type { VariableManager } from "../utils/variableManager";
3
+ interface UseConversationBranchProps {
4
+ apiBaseUrl: string;
5
+ variableManager?: VariableManager | null;
6
+ scenario?: PublishedScenario;
7
+ }
8
+ interface UseConversationBranchReturn {
9
+ branchState: ConversationBranchState;
10
+ loadBranch: (blockId: string) => Promise<void>;
11
+ selectChoice: (choiceId: string) => void;
12
+ getSelectedChoiceFullData: (choiceId?: string) => ConversationChoice | null;
13
+ resetBranch: () => void;
14
+ }
15
+ export declare function useConversationBranch({ apiBaseUrl, variableManager, scenario, }: UseConversationBranchProps): UseConversationBranchReturn;
16
+ export {};
@@ -0,0 +1,125 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { useCallback, useRef, useState } from "react";
11
+ import { fetchConversationBranch } from "../api/conversationBranch";
12
+ function validateBranchStructure(branch) {
13
+ if (!branch.branchChoices || branch.branchChoices.length === 0) {
14
+ return "DATA_MISSING";
15
+ }
16
+ if (branch.branchChoices.length < 2 || branch.branchChoices.length > 3) {
17
+ return "INVALID_STRUCTURE";
18
+ }
19
+ for (const choice of branch.branchChoices) {
20
+ if (!choice.id) {
21
+ return "INVALID_STRUCTURE";
22
+ }
23
+ if (branch.branchType === "choice" && !choice.choiceText) {
24
+ return "INVALID_STRUCTURE";
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+ export function useConversationBranch({ apiBaseUrl, variableManager, scenario, }) {
30
+ const [branchState, setBranchState] = useState({
31
+ isActive: false,
32
+ currentChoices: [],
33
+ selectedChoiceId: null,
34
+ errorState: null,
35
+ });
36
+ const fullBranchDataRef = useRef(null);
37
+ const loadBranch = useCallback((blockId) => __awaiter(this, void 0, void 0, function* () {
38
+ var _a;
39
+ try {
40
+ const branchData = yield fetchConversationBranch(blockId, apiBaseUrl);
41
+ fullBranchDataRef.current = branchData;
42
+ const validationError = validateBranchStructure(branchData);
43
+ if (validationError) {
44
+ setBranchState({
45
+ isActive: false,
46
+ currentChoices: [],
47
+ selectedChoiceId: null,
48
+ errorState: validationError,
49
+ });
50
+ return;
51
+ }
52
+ if (branchData.branchType === "variable" &&
53
+ variableManager &&
54
+ scenario) {
55
+ const conditions = ((_a = scenario.variableBranchConditions) === null || _a === void 0 ? void 0 : _a.filter((c) => c.branchId === branchData.id).sort((a, b) => a.order - b.order)) || [];
56
+ let selectedChoiceId = null;
57
+ for (const condition of conditions) {
58
+ if (variableManager.evaluateCondition(condition)) {
59
+ selectedChoiceId = condition.choiceId;
60
+ break;
61
+ }
62
+ }
63
+ setBranchState({
64
+ isActive: true,
65
+ branchType: "variable",
66
+ currentChoices: [],
67
+ selectedChoiceId,
68
+ errorState: selectedChoiceId ? null : "NO_MATCHING_CONDITION",
69
+ });
70
+ }
71
+ else {
72
+ const choices = branchData.branchChoices
73
+ .map((choice) => ({
74
+ id: choice.id,
75
+ text: choice.choiceText,
76
+ order: choice.order,
77
+ isDisabled: false,
78
+ }))
79
+ .sort((a, b) => a.order - b.order);
80
+ setBranchState({
81
+ isActive: true,
82
+ branchType: "choice",
83
+ currentChoices: choices,
84
+ selectedChoiceId: null,
85
+ errorState: null,
86
+ });
87
+ }
88
+ }
89
+ catch (_b) {
90
+ setBranchState({
91
+ isActive: false,
92
+ currentChoices: [],
93
+ selectedChoiceId: null,
94
+ errorState: "NETWORK_ERROR",
95
+ });
96
+ }
97
+ }), [apiBaseUrl, variableManager, scenario]);
98
+ const selectChoice = useCallback((choiceId) => {
99
+ setBranchState((prev) => (Object.assign(Object.assign({}, prev), { selectedChoiceId: choiceId })));
100
+ }, []);
101
+ const getSelectedChoiceFullData = useCallback((choiceId) => {
102
+ const targetChoiceId = choiceId || branchState.selectedChoiceId;
103
+ if (!fullBranchDataRef.current || !targetChoiceId) {
104
+ return null;
105
+ }
106
+ const choice = fullBranchDataRef.current.branchChoices.find((c) => c.id === targetChoiceId);
107
+ return choice || null;
108
+ }, [branchState.selectedChoiceId]);
109
+ const resetBranch = useCallback(() => {
110
+ setBranchState({
111
+ isActive: false,
112
+ currentChoices: [],
113
+ selectedChoiceId: null,
114
+ errorState: null,
115
+ });
116
+ fullBranchDataRef.current = null;
117
+ }, []);
118
+ return {
119
+ branchState,
120
+ loadBranch,
121
+ selectChoice,
122
+ getSelectedChoiceFullData,
123
+ resetBranch,
124
+ };
125
+ }
@@ -0,0 +1,30 @@
1
+ import type { WorkFont } from "../types";
2
+ /**
3
+ * フォントを読み込むカスタムフック
4
+ * - Googleフォント: <link>タグで読み込み + document.fonts.load()でプリロード
5
+ * - カスタムフォント: FontFace APIで読み込み
6
+ * - すべてのフォントが完全にダウンロード&キャッシュされてからisLoaded=trueになる
7
+ * - 再生開始前に全フォントを一括ダウンロードし、再生中のフォントスワップを防止する
8
+ * - preloadText: シナリオ内の全テキストを渡すと、使用される全ての文字でフォントをプリロード
9
+ */
10
+ export declare function useFontLoader(fonts: WorkFont[] | undefined, preloadText?: string): {
11
+ isLoaded: boolean;
12
+ loadedFonts: string[];
13
+ };
14
+ /**
15
+ * フォントファミリーに基づいてCSSのfont-family値を生成
16
+ */
17
+ export declare function getFontFamilyStyle(selectedFontFamily: string | undefined, fonts: WorkFont[] | undefined): string;
18
+ /**
19
+ * UIフォントファミリーに基づいてCSSのfont-family値を生成
20
+ * メニュー、ボタン、ラベルなどのUI要素に使用
21
+ */
22
+ export declare function getUIFontFamilyStyle(selectedUIFontFamily: string | undefined, fonts: WorkFont[] | undefined): string;
23
+ /**
24
+ * フォントがキャッシュに存在するかチェック
25
+ */
26
+ export declare function isFontCached(fontFamily: string): boolean;
27
+ /**
28
+ * キャッシュをクリア(主にテスト用)
29
+ */
30
+ export declare function clearFontCache(): void;
@@ -0,0 +1,192 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { useEffect, useRef, useState } from "react";
11
+ // グローバルなフォントキャッシュ(ページ内で共有)
12
+ const fontCache = new Set();
13
+ /**
14
+ * Googleフォントの<link>タグを挿入し、CSSの読み込み完了を待つ
15
+ */
16
+ function loadGoogleFontCSS(fontFamily) {
17
+ const existingLink = document.querySelector(`link[data-font-family="${fontFamily}"]`);
18
+ if (existingLink) {
19
+ return Promise.resolve();
20
+ }
21
+ const link = document.createElement("link");
22
+ link.rel = "stylesheet";
23
+ // display=blockでフォント読み込みまでテキストを非表示にする(FOUTを防止)
24
+ link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@400;700&display=block`;
25
+ link.setAttribute("data-font-family", fontFamily);
26
+ document.head.appendChild(link);
27
+ return new Promise((resolve) => {
28
+ link.onload = () => resolve();
29
+ link.onerror = () => {
30
+ console.warn(`Failed to load Google font CSS: ${fontFamily}`);
31
+ resolve();
32
+ };
33
+ });
34
+ }
35
+ /**
36
+ * Googleフォントのフォントデータを事前にダウンロード
37
+ * 400/700両方のウェイトをプリロードする
38
+ */
39
+ function preloadGoogleFont(fontFamily, textToPreload) {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ try {
42
+ // 400と700両方のウェイトをプリロード
43
+ yield Promise.all([
44
+ document.fonts.load(`400 16px "${fontFamily}"`, textToPreload),
45
+ document.fonts.load(`700 16px "${fontFamily}"`, textToPreload),
46
+ ]);
47
+ }
48
+ catch (error) {
49
+ console.warn(`Failed to preload Google font: ${fontFamily}`, error);
50
+ }
51
+ });
52
+ }
53
+ /**
54
+ * カスタムフォントをFontFace APIで読み込み
55
+ */
56
+ function loadCustomFont(fontFamily, url, fontName) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ try {
59
+ // 既にdocument.fontsに登録されているかチェック
60
+ let fontExists = false;
61
+ for (const existingFont of document.fonts) {
62
+ if (existingFont.family === fontFamily) {
63
+ fontExists = true;
64
+ break;
65
+ }
66
+ }
67
+ if (!fontExists) {
68
+ const fontFace = new FontFace(fontFamily, `url(${url})`);
69
+ yield fontFace.load();
70
+ document.fonts.add(fontFace);
71
+ }
72
+ }
73
+ catch (error) {
74
+ console.warn(`Failed to load custom font: ${fontName}`, error);
75
+ }
76
+ });
77
+ }
78
+ /**
79
+ * フォントを読み込むカスタムフック
80
+ * - Googleフォント: <link>タグで読み込み + document.fonts.load()でプリロード
81
+ * - カスタムフォント: FontFace APIで読み込み
82
+ * - すべてのフォントが完全にダウンロード&キャッシュされてからisLoaded=trueになる
83
+ * - 再生開始前に全フォントを一括ダウンロードし、再生中のフォントスワップを防止する
84
+ * - preloadText: シナリオ内の全テキストを渡すと、使用される全ての文字でフォントをプリロード
85
+ */
86
+ export function useFontLoader(fonts, preloadText) {
87
+ const [isLoaded, setIsLoaded] = useState(false);
88
+ const [loadedFonts, setLoadedFonts] = useState([]);
89
+ // 現在のロード処理をキャンセルするための世代カウンター
90
+ const generationRef = useRef(0);
91
+ useEffect(() => {
92
+ if (!fonts || fonts.length === 0) {
93
+ setIsLoaded(true);
94
+ return;
95
+ }
96
+ // 新しいロード処理を開始する際、前回の結果を無効化
97
+ const generation = ++generationRef.current;
98
+ setIsLoaded(false);
99
+ const loadFonts = () => __awaiter(this, void 0, void 0, function* () {
100
+ var _a;
101
+ const loadedFontFamilies = [];
102
+ const textToPreload = preloadText || "あいうえおABCabc123";
103
+ // すべてのGoogle FontsのCSS読み込みを並列で開始
104
+ const googleFonts = fonts.filter((f) => f.type === "google" && !fontCache.has(f.fontFamily));
105
+ const customFonts = fonts.filter((f) => f.type === "custom" && f.url && !fontCache.has(f.fontFamily));
106
+ const cachedFonts = fonts.filter((f) => fontCache.has(f.fontFamily));
107
+ // キャッシュ済みフォントは即座に追加
108
+ for (const font of cachedFonts) {
109
+ loadedFontFamilies.push(font.fontFamily);
110
+ }
111
+ // Google FontsのCSS読み込みを並列実行
112
+ yield Promise.all(googleFonts.map((font) => loadGoogleFontCSS(font.fontFamily)));
113
+ // キャンセルチェック
114
+ if (generation !== generationRef.current)
115
+ return;
116
+ // Google Fontsのフォントデータプリロード + カスタムフォント読み込みを並列実行
117
+ const allFontPromises = [];
118
+ for (const font of googleFonts) {
119
+ allFontPromises.push(preloadGoogleFont(font.fontFamily, textToPreload).then(() => {
120
+ fontCache.add(font.fontFamily);
121
+ loadedFontFamilies.push(font.fontFamily);
122
+ }));
123
+ }
124
+ for (const font of customFonts) {
125
+ allFontPromises.push(loadCustomFont(font.fontFamily, (_a = font.url) !== null && _a !== void 0 ? _a : "", font.name).then(() => {
126
+ fontCache.add(font.fontFamily);
127
+ loadedFontFamilies.push(font.fontFamily);
128
+ }));
129
+ }
130
+ yield Promise.all(allFontPromises);
131
+ // キャンセルチェック
132
+ if (generation !== generationRef.current)
133
+ return;
134
+ // document.fonts.readyで全フォントの準備完了を確認
135
+ yield document.fonts.ready;
136
+ // キャンセルチェック
137
+ if (generation !== generationRef.current)
138
+ return;
139
+ setLoadedFonts(loadedFontFamilies);
140
+ setIsLoaded(true);
141
+ });
142
+ loadFonts();
143
+ }, [fonts, preloadText]);
144
+ return { isLoaded, loadedFonts };
145
+ }
146
+ /**
147
+ * フォントファミリーに基づいてCSSのfont-family値を生成
148
+ */
149
+ export function getFontFamilyStyle(selectedFontFamily, fonts) {
150
+ if (!fonts || fonts.length === 0) {
151
+ // フォントが設定されていない場合はデフォルトフォント
152
+ return "sans-serif";
153
+ }
154
+ // 選択されたフォントファミリーが指定されている場合
155
+ if (selectedFontFamily) {
156
+ const selectedFont = fonts.find((f) => f.fontFamily === selectedFontFamily);
157
+ if (selectedFont) {
158
+ return `"${selectedFontFamily}", sans-serif`;
159
+ }
160
+ }
161
+ // 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
162
+ return `"${fonts[0].fontFamily}", sans-serif`;
163
+ }
164
+ /**
165
+ * UIフォントファミリーに基づいてCSSのfont-family値を生成
166
+ * メニュー、ボタン、ラベルなどのUI要素に使用
167
+ */
168
+ export function getUIFontFamilyStyle(selectedUIFontFamily, fonts) {
169
+ if (!fonts || fonts.length === 0) {
170
+ return "sans-serif";
171
+ }
172
+ if (selectedUIFontFamily) {
173
+ const selectedFont = fonts.find((f) => f.fontFamily === selectedUIFontFamily);
174
+ if (selectedFont) {
175
+ return `"${selectedUIFontFamily}", sans-serif`;
176
+ }
177
+ }
178
+ // 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
179
+ return `"${fonts[0].fontFamily}", sans-serif`;
180
+ }
181
+ /**
182
+ * フォントがキャッシュに存在するかチェック
183
+ */
184
+ export function isFontCached(fontFamily) {
185
+ return fontCache.has(fontFamily);
186
+ }
187
+ /**
188
+ * キャッシュをクリア(主にテスト用)
189
+ */
190
+ export function clearFontCache() {
191
+ fontCache.clear();
192
+ }
@@ -0,0 +1,17 @@
1
+ import type { ScenarioBlock } from "../types";
2
+ export interface UseFullscreenTextOptions {
3
+ textSpeed?: number;
4
+ onParagraphComplete?: (index: number) => void;
5
+ onAllComplete?: () => void;
6
+ }
7
+ export interface UseFullscreenTextReturn {
8
+ paragraphs: string[];
9
+ currentParagraphIndex: number;
10
+ displayText: string;
11
+ isTyping: boolean;
12
+ isComplete: boolean;
13
+ advanceParagraph: () => boolean;
14
+ skipTypewriter: () => void;
15
+ reset: () => void;
16
+ }
17
+ export declare function useFullscreenText(block: ScenarioBlock | null, options?: UseFullscreenTextOptions): UseFullscreenTextReturn;
@@ -0,0 +1,120 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ export function useFullscreenText(block, options = {}) {
3
+ const { textSpeed = 80, onParagraphComplete, onAllComplete } = options;
4
+ const [paragraphs, setParagraphs] = useState([]);
5
+ const [currentParagraphIndex, setCurrentParagraphIndex] = useState(0);
6
+ const [displayText, setDisplayText] = useState("");
7
+ const [isTyping, setIsTyping] = useState(false);
8
+ const [completedParagraphs, setCompletedParagraphs] = useState(new Set());
9
+ const typingTimeoutRef = useRef(null);
10
+ // Parse content into paragraphs
11
+ useEffect(() => {
12
+ if (!block || block.blockType !== "fullscreen_text") {
13
+ setParagraphs([]);
14
+ setCurrentParagraphIndex(0);
15
+ setDisplayText("");
16
+ setCompletedParagraphs(new Set());
17
+ return;
18
+ }
19
+ const content = block.content || "";
20
+ const parsed = content.split(/\n\n+/).filter((p) => p.trim());
21
+ setParagraphs(parsed);
22
+ setCurrentParagraphIndex(0);
23
+ setDisplayText("");
24
+ setCompletedParagraphs(new Set());
25
+ }, [block]);
26
+ // Typewriter effect
27
+ useEffect(() => {
28
+ if (paragraphs.length === 0)
29
+ return;
30
+ if (currentParagraphIndex >= paragraphs.length)
31
+ return;
32
+ if (completedParagraphs.has(currentParagraphIndex)) {
33
+ setDisplayText(paragraphs[currentParagraphIndex]);
34
+ return;
35
+ }
36
+ if (textSpeed <= 0) {
37
+ setDisplayText(paragraphs[currentParagraphIndex]);
38
+ setCompletedParagraphs((prev) => new Set(prev).add(currentParagraphIndex));
39
+ onParagraphComplete === null || onParagraphComplete === void 0 ? void 0 : onParagraphComplete(currentParagraphIndex);
40
+ return;
41
+ }
42
+ setIsTyping(true);
43
+ const text = paragraphs[currentParagraphIndex];
44
+ let charIndex = 0;
45
+ const typeChar = () => {
46
+ if (charIndex < text.length) {
47
+ setDisplayText(text.slice(0, charIndex + 1));
48
+ charIndex++;
49
+ typingTimeoutRef.current = setTimeout(typeChar, textSpeed);
50
+ }
51
+ else {
52
+ setIsTyping(false);
53
+ setCompletedParagraphs((prev) => new Set(prev).add(currentParagraphIndex));
54
+ onParagraphComplete === null || onParagraphComplete === void 0 ? void 0 : onParagraphComplete(currentParagraphIndex);
55
+ }
56
+ };
57
+ typeChar();
58
+ return () => {
59
+ if (typingTimeoutRef.current) {
60
+ clearTimeout(typingTimeoutRef.current);
61
+ }
62
+ };
63
+ }, [
64
+ paragraphs,
65
+ currentParagraphIndex,
66
+ textSpeed,
67
+ completedParagraphs,
68
+ onParagraphComplete,
69
+ ]);
70
+ const skipTypewriter = useCallback(() => {
71
+ if (typingTimeoutRef.current) {
72
+ clearTimeout(typingTimeoutRef.current);
73
+ }
74
+ if (paragraphs[currentParagraphIndex]) {
75
+ setDisplayText(paragraphs[currentParagraphIndex]);
76
+ setIsTyping(false);
77
+ setCompletedParagraphs((prev) => new Set(prev).add(currentParagraphIndex));
78
+ onParagraphComplete === null || onParagraphComplete === void 0 ? void 0 : onParagraphComplete(currentParagraphIndex);
79
+ }
80
+ }, [paragraphs, currentParagraphIndex, onParagraphComplete]);
81
+ const advanceParagraph = useCallback(() => {
82
+ if (isTyping) {
83
+ skipTypewriter();
84
+ return true;
85
+ }
86
+ if (currentParagraphIndex < paragraphs.length - 1) {
87
+ setCurrentParagraphIndex((prev) => prev + 1);
88
+ return true;
89
+ }
90
+ onAllComplete === null || onAllComplete === void 0 ? void 0 : onAllComplete();
91
+ return false;
92
+ }, [
93
+ isTyping,
94
+ currentParagraphIndex,
95
+ paragraphs.length,
96
+ skipTypewriter,
97
+ onAllComplete,
98
+ ]);
99
+ const reset = useCallback(() => {
100
+ setCurrentParagraphIndex(0);
101
+ setDisplayText("");
102
+ setCompletedParagraphs(new Set());
103
+ setIsTyping(false);
104
+ if (typingTimeoutRef.current) {
105
+ clearTimeout(typingTimeoutRef.current);
106
+ }
107
+ }, []);
108
+ return {
109
+ paragraphs,
110
+ currentParagraphIndex,
111
+ displayText,
112
+ isTyping,
113
+ isComplete: currentParagraphIndex >= paragraphs.length - 1 &&
114
+ !isTyping &&
115
+ paragraphs.length > 0,
116
+ advanceParagraph,
117
+ skipTypewriter,
118
+ reset,
119
+ };
120
+ }
@@ -0,0 +1,5 @@
1
+ import type { DisplayedCharacter } from "../types";
2
+ export declare const useImagePreloader: (
3
+ displayedCharacters: DisplayedCharacter[],
4
+ singleImageUrl?: string
5
+ ) => boolean;
@@ -0,0 +1,53 @@
1
+ import { useEffect, useState } from "react";
2
+ export var useImagePreloader = function (displayedCharacters, singleImageUrl) {
3
+ var _a = useState(true),
4
+ isLoading = _a[0],
5
+ setIsLoading = _a[1];
6
+ useEffect(
7
+ function () {
8
+ var imageUrls = [];
9
+ // 複数キャラクター表示の場合
10
+ if (displayedCharacters.length > 0) {
11
+ displayedCharacters.forEach(function (char) {
12
+ if (char.entityState.imageUrl) {
13
+ imageUrls.push(char.entityState.imageUrl);
14
+ }
15
+ });
16
+ } else if (singleImageUrl) {
17
+ // 単一キャラクター表示の場合
18
+ imageUrls.push(singleImageUrl);
19
+ }
20
+ // 画像がない場合は即座に完了
21
+ if (imageUrls.length === 0) {
22
+ setIsLoading(false);
23
+ return;
24
+ }
25
+ setIsLoading(true);
26
+ // すべての画像を並列でプリロード
27
+ var loadPromises = imageUrls.map(function (url) {
28
+ return new Promise(function (resolve, reject) {
29
+ var img = new Image();
30
+ img.onload = function () {
31
+ return resolve();
32
+ };
33
+ img.onerror = function () {
34
+ return reject(new Error("Failed to load image: ".concat(url)));
35
+ };
36
+ img.src = url;
37
+ });
38
+ });
39
+ // すべての画像の読み込みを待つ
40
+ Promise.all(loadPromises)
41
+ .then(function () {
42
+ setIsLoading(false);
43
+ })
44
+ .catch(function (error) {
45
+ console.error("Image preload error:", error);
46
+ // エラーが発生しても続行
47
+ setIsLoading(false);
48
+ });
49
+ },
50
+ [displayedCharacters, singleImageUrl]
51
+ );
52
+ return isLoading;
53
+ };
@@ -1,16 +1,23 @@
1
- import type { DisplayedCharacter, PlayerState, PublishedScenario, ScenarioBlock } from "../types";
1
+ import type { ConversationBranchState, DisplayedCharacter, PlayerState, PublishedScenario, ScenarioBlock } from "../types";
2
+ import type { BranchNavigator } from "../utils/branchNavigator";
3
+ import type { VariableManager } from "../utils/variableManager";
2
4
  interface UsePlayerLogicProps {
3
5
  state: PlayerState;
4
6
  setState: React.Dispatch<React.SetStateAction<PlayerState>>;
5
7
  scenario: PublishedScenario;
6
8
  isTyping: boolean;
7
9
  currentBlock: ScenarioBlock;
8
- skipTyping: (text: string) => void;
10
+ skipTyping: () => void;
9
11
  onEnd?: () => void;
10
12
  onScenarioEnd?: () => void;
11
13
  autoplay: boolean;
14
+ branchState?: ConversationBranchState;
15
+ branchNavigator?: BranchNavigator;
16
+ customRestart?: () => void;
17
+ variableManager?: VariableManager | null;
18
+ disableKeyboardNavigation?: boolean;
12
19
  }
13
- export declare const usePlayerLogic: ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, }: UsePlayerLogicProps) => {
20
+ export declare const usePlayerLogic: ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, branchState, branchNavigator: _branchNavigator, customRestart, variableManager, disableKeyboardNavigation, }: UsePlayerLogicProps) => {
14
21
  handleNext: () => void;
15
22
  handlePrevious: () => void;
16
23
  togglePlay: () => void;