@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.
- package/dist/Player.d.ts +1 -1
- package/dist/Player.js +527 -85
- package/dist/api/conversationBranch.d.ts +4 -0
- package/dist/api/conversationBranch.js +83 -0
- package/dist/components/BackgroundLayer.d.ts +19 -0
- package/dist/components/BackgroundLayer.js +218 -0
- package/dist/components/ClickWaitIndicator.d.ts +10 -0
- package/dist/components/ClickWaitIndicator.js +31 -0
- package/dist/components/ConversationBranchBox.d.ts +2 -0
- package/dist/components/ConversationBranchBox.js +29 -0
- package/dist/components/DialogueBox.js +16 -1
- package/dist/components/FontSettingsPanel.d.ts +10 -0
- package/dist/components/FontSettingsPanel.js +30 -0
- package/dist/components/FullscreenTextBox.d.ts +6 -0
- package/dist/components/FullscreenTextBox.js +70 -0
- package/dist/components/GameScreen.d.ts +1 -0
- package/dist/components/GameScreen.js +363 -81
- package/dist/components/PluginComponentProvider.d.ts +2 -2
- package/dist/components/PluginComponentProvider.js +3 -3
- package/dist/components/TimeWaitIndicator.d.ts +15 -0
- package/dist/components/TimeWaitIndicator.js +17 -0
- package/dist/contexts/AudioContext.d.ts +14 -0
- package/dist/contexts/AudioContext.js +14 -0
- package/dist/contexts/DataContext.d.ts +4 -1
- package/dist/contexts/DataContext.js +82 -13
- package/dist/hooks/useBacklog.js +3 -0
- package/dist/hooks/useConversationBranch.d.ts +16 -0
- package/dist/hooks/useConversationBranch.js +125 -0
- package/dist/hooks/useFontLoader.d.ts +23 -0
- package/dist/hooks/useFontLoader.js +153 -0
- package/dist/hooks/useFullscreenText.d.ts +17 -0
- package/dist/hooks/useFullscreenText.js +120 -0
- package/dist/hooks/usePlayerLogic.d.ts +10 -3
- package/dist/hooks/usePlayerLogic.js +115 -18
- package/dist/hooks/usePluginEvents.d.ts +4 -1
- package/dist/hooks/usePluginEvents.js +16 -11
- package/dist/hooks/usePreloadImages.js +27 -7
- package/dist/hooks/useSoundPlayer.d.ts +15 -0
- package/dist/hooks/useSoundPlayer.js +209 -0
- package/dist/hooks/useTypewriter.d.ts +6 -2
- package/dist/hooks/useTypewriter.js +42 -6
- package/dist/hooks/useVoice.js +7 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/plugin/PluginManager.d.ts +66 -2
- package/dist/plugin/PluginManager.js +352 -79
- package/dist/sdk.d.ts +184 -22
- package/dist/sdk.js +27 -2
- package/dist/types.d.ts +303 -4
- package/dist/utils/branchBlockConverter.d.ts +2 -0
- package/dist/utils/branchBlockConverter.js +21 -0
- package/dist/utils/branchNavigator.d.ts +14 -0
- package/dist/utils/branchNavigator.js +55 -0
- package/dist/utils/facePositionCalculator.js +0 -1
- package/dist/utils/variableManager.d.ts +18 -0
- package/dist/utils/variableManager.js +159 -0
- package/package.json +1 -1
- package/dist/components/ConversationLogUI.d.ts +0 -2
- package/dist/components/ConversationLogUI.js +0 -115
- package/dist/hooks/useConversationLog.d.ts +0 -14
- package/dist/hooks/useConversationLog.js +0 -82
- package/dist/hooks/useUIVisibility.d.ts +0 -9
- package/dist/hooks/useUIVisibility.js +0 -19
- package/dist/plugin/luna-react.d.ts +0 -41
- package/dist/plugin/luna-react.js +0 -99
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from "react";
|
|
3
3
|
const DataContextInstance = createContext(null);
|
|
4
4
|
const SubscribersContext = createContext(null);
|
|
5
|
+
const SettingsUpdaterContext = createContext(null);
|
|
5
6
|
/**
|
|
6
7
|
* データプロバイダー
|
|
7
8
|
* プラグインがシナリオ情報にリアクティブにアクセスするためのProvider
|
|
@@ -15,9 +16,8 @@ const SubscribersContext = createContext(null);
|
|
|
15
16
|
* </DataProvider>
|
|
16
17
|
* ```
|
|
17
18
|
*/
|
|
18
|
-
export const DataProvider = ({ data, children }) => {
|
|
19
|
-
const subscribers = useMemo(() => new Map(),
|
|
20
|
-
[]);
|
|
19
|
+
export const DataProvider = ({ data, onSettingsUpdate, children }) => {
|
|
20
|
+
const subscribers = useMemo(() => new Map(), []);
|
|
21
21
|
const previousDataRef = useRef(data);
|
|
22
22
|
// データ変更時に購読者に通知
|
|
23
23
|
useEffect(() => {
|
|
@@ -31,16 +31,20 @@ export const DataProvider = ({ data, children }) => {
|
|
|
31
31
|
if (prevValue !== currentValue) {
|
|
32
32
|
// カテゴリ全体の購読者に通知
|
|
33
33
|
const categorySubscribers = subscribers.get(key);
|
|
34
|
-
categorySubscribers === null || categorySubscribers === void 0 ? void 0 : categorySubscribers.forEach((callback) =>
|
|
34
|
+
categorySubscribers === null || categorySubscribers === void 0 ? void 0 : categorySubscribers.forEach((callback) => {
|
|
35
|
+
callback(currentValue);
|
|
36
|
+
});
|
|
35
37
|
// 個別プロパティの変更をチェック
|
|
36
38
|
if (typeof currentValue === "object" && currentValue !== null) {
|
|
37
39
|
for (const prop of Object.keys(currentValue)) {
|
|
38
|
-
const prevPropValue = prevValue === null || prevValue === void 0 ? void 0 : prevValue[prop];
|
|
39
|
-
const currentPropValue = currentValue[prop];
|
|
40
|
+
const prevPropValue = prevValue === null || prevValue === void 0 ? void 0 : prevValue[prop];
|
|
41
|
+
const currentPropValue = currentValue[prop];
|
|
40
42
|
if (prevPropValue !== currentPropValue) {
|
|
41
43
|
const propKey = `${key}.${prop}`;
|
|
42
44
|
const propSubscribers = subscribers.get(propKey);
|
|
43
|
-
propSubscribers === null || propSubscribers === void 0 ? void 0 : propSubscribers.forEach((callback) =>
|
|
45
|
+
propSubscribers === null || propSubscribers === void 0 ? void 0 : propSubscribers.forEach((callback) => {
|
|
46
|
+
callback(currentPropValue);
|
|
47
|
+
});
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -48,7 +52,7 @@ export const DataProvider = ({ data, children }) => {
|
|
|
48
52
|
}
|
|
49
53
|
previousDataRef.current = data;
|
|
50
54
|
}, [data, subscribers]);
|
|
51
|
-
return (_jsx(SubscribersContext.Provider, { value: subscribers, children: _jsx(DataContextInstance.Provider, { value: data, children: children }) }));
|
|
55
|
+
return (_jsx(SubscribersContext.Provider, { value: subscribers, children: _jsx(SettingsUpdaterContext.Provider, { value: onSettingsUpdate !== null && onSettingsUpdate !== void 0 ? onSettingsUpdate : null, children: _jsx(DataContextInstance.Provider, { value: data, children: children }) }) }));
|
|
52
56
|
};
|
|
53
57
|
/**
|
|
54
58
|
* DataAPI実装を提供するフック
|
|
@@ -57,13 +61,17 @@ export const DataProvider = ({ data, children }) => {
|
|
|
57
61
|
export function useDataAPI() {
|
|
58
62
|
const data = useContext(DataContextInstance);
|
|
59
63
|
const subscribers = useContext(SubscribersContext);
|
|
64
|
+
const settingsUpdater = useContext(SettingsUpdaterContext);
|
|
60
65
|
if (!data || !subscribers) {
|
|
61
66
|
throw new Error("useDataAPI must be used within DataProvider");
|
|
62
67
|
}
|
|
63
68
|
const get = useCallback((key, property) => {
|
|
64
|
-
var _a;
|
|
65
69
|
if (property !== undefined) {
|
|
66
|
-
|
|
70
|
+
const value = data[key];
|
|
71
|
+
if (value && typeof value === "object" && property in value) {
|
|
72
|
+
return value[property];
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
67
75
|
}
|
|
68
76
|
return data[key];
|
|
69
77
|
}, [data]);
|
|
@@ -77,7 +85,8 @@ export function useDataAPI() {
|
|
|
77
85
|
// unsubscribe関数を返す
|
|
78
86
|
return () => {
|
|
79
87
|
var _a;
|
|
80
|
-
(_a = subscribers
|
|
88
|
+
(_a = subscribers
|
|
89
|
+
.get(subscriberKey)) === null || _a === void 0 ? void 0 : _a.delete(callback);
|
|
81
90
|
};
|
|
82
91
|
}, [subscribers]);
|
|
83
92
|
const watch = useCallback((key, property, callback) => {
|
|
@@ -90,12 +99,72 @@ export function useDataAPI() {
|
|
|
90
99
|
// unsubscribe関数を返す
|
|
91
100
|
return () => {
|
|
92
101
|
var _a;
|
|
93
|
-
(_a = subscribers
|
|
102
|
+
(_a = subscribers
|
|
103
|
+
.get(subscriberKey)) === null || _a === void 0 ? void 0 : _a.delete(callback);
|
|
94
104
|
};
|
|
95
105
|
}, [subscribers]);
|
|
106
|
+
const updateSettings = useCallback((settings) => {
|
|
107
|
+
if (settingsUpdater) {
|
|
108
|
+
settingsUpdater(settings);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.warn("updateSettings called but no settings updater provided");
|
|
112
|
+
}
|
|
113
|
+
}, [settingsUpdater]);
|
|
114
|
+
const setBgmVolume = useCallback((volume) => {
|
|
115
|
+
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
116
|
+
updateSettings({ bgmVolume: clampedVolume });
|
|
117
|
+
}, [updateSettings]);
|
|
118
|
+
const setSeVolume = useCallback((volume) => {
|
|
119
|
+
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
120
|
+
updateSettings({ seVolume: clampedVolume });
|
|
121
|
+
}, [updateSettings]);
|
|
122
|
+
const setVoiceVolume = useCallback((volume) => {
|
|
123
|
+
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
124
|
+
updateSettings({ voiceVolume: clampedVolume });
|
|
125
|
+
}, [updateSettings]);
|
|
126
|
+
const setVolumes = useCallback((volumes) => {
|
|
127
|
+
const settings = {};
|
|
128
|
+
if (volumes.bgm !== undefined) {
|
|
129
|
+
settings.bgmVolume = Math.max(0, Math.min(1, volumes.bgm));
|
|
130
|
+
}
|
|
131
|
+
if (volumes.se !== undefined) {
|
|
132
|
+
settings.seVolume = Math.max(0, Math.min(1, volumes.se));
|
|
133
|
+
}
|
|
134
|
+
if (volumes.voice !== undefined) {
|
|
135
|
+
settings.voiceVolume = Math.max(0, Math.min(1, volumes.voice));
|
|
136
|
+
}
|
|
137
|
+
if (Object.keys(settings).length > 0) {
|
|
138
|
+
updateSettings(settings);
|
|
139
|
+
}
|
|
140
|
+
}, [updateSettings]);
|
|
141
|
+
const getBlockOption = useCallback((key) => {
|
|
142
|
+
var _a;
|
|
143
|
+
const currentBlock = (_a = data.playback) === null || _a === void 0 ? void 0 : _a.currentBlock;
|
|
144
|
+
if (!(currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.options)) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return currentBlock.options[key];
|
|
148
|
+
}, [data]);
|
|
96
149
|
return useMemo(() => ({
|
|
97
150
|
get,
|
|
98
151
|
subscribe,
|
|
99
152
|
watch,
|
|
100
|
-
|
|
153
|
+
updateSettings,
|
|
154
|
+
setBgmVolume,
|
|
155
|
+
setSeVolume,
|
|
156
|
+
setVoiceVolume,
|
|
157
|
+
setVolumes,
|
|
158
|
+
getBlockOption,
|
|
159
|
+
}), [
|
|
160
|
+
get,
|
|
161
|
+
subscribe,
|
|
162
|
+
watch,
|
|
163
|
+
updateSettings,
|
|
164
|
+
setBgmVolume,
|
|
165
|
+
setSeVolume,
|
|
166
|
+
setVoiceVolume,
|
|
167
|
+
setVolumes,
|
|
168
|
+
getBlockOption,
|
|
169
|
+
]);
|
|
101
170
|
}
|
package/dist/hooks/useBacklog.js
CHANGED
|
@@ -37,6 +37,7 @@ export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
|
|
|
37
37
|
content: block.content,
|
|
38
38
|
speakerName: (_a = block.speaker) === null || _a === void 0 ? void 0 : _a.name,
|
|
39
39
|
speakerState: (_b = block.speakerState) === null || _b === void 0 ? void 0 : _b.name,
|
|
40
|
+
options: block.options,
|
|
40
41
|
};
|
|
41
42
|
addLogEntry(logEntry);
|
|
42
43
|
processedBlocks.current.add(realIndex);
|
|
@@ -63,6 +64,7 @@ export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
|
|
|
63
64
|
content: currentBlock.content,
|
|
64
65
|
speakerName: (_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name,
|
|
65
66
|
speakerState: (_b = currentBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name,
|
|
67
|
+
options: currentBlock.options,
|
|
66
68
|
};
|
|
67
69
|
addLogEntry(logEntry);
|
|
68
70
|
processedBlocks.current.add(currentBlockIndex);
|
|
@@ -70,6 +72,7 @@ export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
|
|
|
70
72
|
}
|
|
71
73
|
}, [currentBlock, currentBlockIndex, addLogEntry]);
|
|
72
74
|
// シナリオ変更時にリセット
|
|
75
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: scenario.idの変更を検知してログをクリアする意図的な依存
|
|
73
76
|
useEffect(() => {
|
|
74
77
|
clearLogs();
|
|
75
78
|
}, [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,23 @@
|
|
|
1
|
+
import type { WorkFont } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* フォントを読み込むカスタムフック
|
|
4
|
+
* - Googleフォント: <link>タグで読み込み + document.fonts.load()でプリロード
|
|
5
|
+
* - カスタムフォント: FontFace APIで読み込み
|
|
6
|
+
* - すべてのフォントがプリロード&キャッシュされてからisLoaded=trueになる
|
|
7
|
+
*/
|
|
8
|
+
export declare function useFontLoader(fonts: WorkFont[] | undefined): {
|
|
9
|
+
isLoaded: boolean;
|
|
10
|
+
loadedFonts: string[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* フォントファミリーに基づいてCSSのfont-family値を生成
|
|
14
|
+
*/
|
|
15
|
+
export declare function getFontFamilyStyle(selectedFontFamily: string | undefined, fonts: WorkFont[] | undefined): string;
|
|
16
|
+
/**
|
|
17
|
+
* フォントがキャッシュに存在するかチェック
|
|
18
|
+
*/
|
|
19
|
+
export declare function isFontCached(fontFamily: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* キャッシュをクリア(主にテスト用)
|
|
22
|
+
*/
|
|
23
|
+
export declare function clearFontCache(): void;
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
* フォントを読み込むカスタムフック
|
|
15
|
+
* - Googleフォント: <link>タグで読み込み + document.fonts.load()でプリロード
|
|
16
|
+
* - カスタムフォント: FontFace APIで読み込み
|
|
17
|
+
* - すべてのフォントがプリロード&キャッシュされてからisLoaded=trueになる
|
|
18
|
+
*/
|
|
19
|
+
export function useFontLoader(fonts) {
|
|
20
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
21
|
+
const [loadedFonts, setLoadedFonts] = useState([]);
|
|
22
|
+
const loadingRef = useRef(false);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!fonts || fonts.length === 0) {
|
|
25
|
+
setIsLoaded(true);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// 既に読み込み中の場合はスキップ
|
|
29
|
+
if (loadingRef.current)
|
|
30
|
+
return;
|
|
31
|
+
loadingRef.current = true;
|
|
32
|
+
const loadFonts = () => __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
const loadedFontFamilies = [];
|
|
34
|
+
const fontLoadPromises = [];
|
|
35
|
+
for (const font of fonts) {
|
|
36
|
+
// キャッシュにあればスキップ
|
|
37
|
+
if (fontCache.has(font.fontFamily)) {
|
|
38
|
+
loadedFontFamilies.push(font.fontFamily);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
if (font.type === "google") {
|
|
43
|
+
// Googleフォントを<link>タグで読み込み
|
|
44
|
+
const existingLink = document.querySelector(`link[data-font-family="${font.fontFamily}"]`);
|
|
45
|
+
if (!existingLink) {
|
|
46
|
+
const link = document.createElement("link");
|
|
47
|
+
link.rel = "stylesheet";
|
|
48
|
+
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(font.fontFamily)}:wght@400;700&display=swap`;
|
|
49
|
+
link.setAttribute("data-font-family", font.fontFamily);
|
|
50
|
+
document.head.appendChild(link);
|
|
51
|
+
// CSSファイルの読み込み完了を待つ
|
|
52
|
+
yield new Promise((resolve) => {
|
|
53
|
+
link.onload = () => resolve();
|
|
54
|
+
link.onerror = () => {
|
|
55
|
+
console.warn(`Failed to load Google font CSS: ${font.fontFamily}`);
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// フォントの実際の読み込み(プリロード)を開始
|
|
61
|
+
// document.fonts.load()でフォントデータを事前にダウンロード
|
|
62
|
+
const preloadPromise = document.fonts
|
|
63
|
+
.load(`16px "${font.fontFamily}"`)
|
|
64
|
+
.then(() => {
|
|
65
|
+
fontCache.add(font.fontFamily);
|
|
66
|
+
loadedFontFamilies.push(font.fontFamily);
|
|
67
|
+
})
|
|
68
|
+
.catch((error) => {
|
|
69
|
+
console.warn(`Failed to preload Google font: ${font.fontFamily}`, error);
|
|
70
|
+
// エラーでもキャッシュに追加(再試行防止)
|
|
71
|
+
fontCache.add(font.fontFamily);
|
|
72
|
+
loadedFontFamilies.push(font.fontFamily);
|
|
73
|
+
});
|
|
74
|
+
fontLoadPromises.push(preloadPromise);
|
|
75
|
+
}
|
|
76
|
+
else if (font.type === "custom" && font.url) {
|
|
77
|
+
// カスタムフォントをFontFace APIで読み込み
|
|
78
|
+
const loadCustomFont = () => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
try {
|
|
80
|
+
// 既にdocument.fontsに登録されているかチェック
|
|
81
|
+
let fontExists = false;
|
|
82
|
+
for (const existingFont of document.fonts) {
|
|
83
|
+
if (existingFont.family === font.fontFamily) {
|
|
84
|
+
fontExists = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!fontExists) {
|
|
89
|
+
const fontFace = new FontFace(font.fontFamily, `url(${font.url})`);
|
|
90
|
+
// フォントデータをダウンロード&パース
|
|
91
|
+
yield fontFace.load();
|
|
92
|
+
// document.fontsに追加(キャッシュとして機能)
|
|
93
|
+
document.fonts.add(fontFace);
|
|
94
|
+
}
|
|
95
|
+
fontCache.add(font.fontFamily);
|
|
96
|
+
loadedFontFamilies.push(font.fontFamily);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.warn(`Failed to load custom font: ${font.name}`, error);
|
|
100
|
+
// エラーでもキャッシュに追加(再試行防止)
|
|
101
|
+
fontCache.add(font.fontFamily);
|
|
102
|
+
loadedFontFamilies.push(font.fontFamily);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
fontLoadPromises.push(loadCustomFont());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.warn(`Failed to load font: ${font.name}`, error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// すべてのフォント読み込みを並列で待つ
|
|
113
|
+
yield Promise.all(fontLoadPromises);
|
|
114
|
+
// document.fonts.readyで全フォントの準備完了を確認
|
|
115
|
+
yield document.fonts.ready;
|
|
116
|
+
setLoadedFonts(loadedFontFamilies);
|
|
117
|
+
setIsLoaded(true);
|
|
118
|
+
loadingRef.current = false;
|
|
119
|
+
});
|
|
120
|
+
loadFonts();
|
|
121
|
+
}, [fonts]);
|
|
122
|
+
return { isLoaded, loadedFonts };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* フォントファミリーに基づいてCSSのfont-family値を生成
|
|
126
|
+
*/
|
|
127
|
+
export function getFontFamilyStyle(selectedFontFamily, fonts) {
|
|
128
|
+
if (!fonts || fonts.length === 0) {
|
|
129
|
+
// フォントが設定されていない場合はデフォルトフォント
|
|
130
|
+
return "sans-serif";
|
|
131
|
+
}
|
|
132
|
+
// 選択されたフォントファミリーが指定されている場合
|
|
133
|
+
if (selectedFontFamily) {
|
|
134
|
+
const selectedFont = fonts.find((f) => f.fontFamily === selectedFontFamily);
|
|
135
|
+
if (selectedFont) {
|
|
136
|
+
return `"${selectedFontFamily}", sans-serif`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
|
|
140
|
+
return `"${fonts[0].fontFamily}", sans-serif`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* フォントがキャッシュに存在するかチェック
|
|
144
|
+
*/
|
|
145
|
+
export function isFontCached(fontFamily) {
|
|
146
|
+
return fontCache.has(fontFamily);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* キャッシュをクリア(主にテスト用)
|
|
150
|
+
*/
|
|
151
|
+
export function clearFontCache() {
|
|
152
|
+
fontCache.clear();
|
|
153
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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: (
|
|
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;
|