@luna-editor/engine 0.2.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 +504 -77
- 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 +1 -0
- package/dist/contexts/AudioContext.js +1 -0
- package/dist/contexts/DataContext.js +69 -11
- 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 +4 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/plugin/PluginManager.d.ts +66 -2
- package/dist/plugin/PluginManager.js +349 -79
- package/dist/sdk.d.ts +178 -21
- package/dist/sdk.js +27 -2
- package/dist/types.d.ts +288 -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
|
@@ -22,6 +22,8 @@ const calculateDisplayedCharacters = (blocks, currentIndex) => {
|
|
|
22
22
|
scale: char.scale,
|
|
23
23
|
object: char.object,
|
|
24
24
|
entityState: char.entityState,
|
|
25
|
+
// レイヤー機能用
|
|
26
|
+
baseBodyState: char.baseBodyState,
|
|
25
27
|
}));
|
|
26
28
|
characters = [
|
|
27
29
|
...characters.filter((existing) => !newCharacters.some((newChar) => newChar.objectId === existing.objectId)),
|
|
@@ -29,20 +31,70 @@ const calculateDisplayedCharacters = (blocks, currentIndex) => {
|
|
|
29
31
|
];
|
|
30
32
|
}
|
|
31
33
|
break;
|
|
32
|
-
case "character_exit":
|
|
33
|
-
// character_exitに到達したら、キャラクター表示範囲外
|
|
34
|
-
return [];
|
|
35
34
|
case "dialogue":
|
|
36
35
|
if (processBlock.speakerId && processBlock.speakerStateId) {
|
|
37
36
|
characters = characters.map((char) => {
|
|
38
|
-
var _a, _b, _c, _d;
|
|
37
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
39
38
|
return char.objectId === processBlock.speakerId
|
|
40
39
|
? Object.assign(Object.assign({}, char), { entityStateId: processBlock.speakerStateId || "", entityState: {
|
|
41
40
|
id: (_a = processBlock.speakerStateId) !== null && _a !== void 0 ? _a : "",
|
|
42
41
|
name: ((_b = processBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name) || "",
|
|
43
42
|
imageUrl: ((_c = processBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl) || null,
|
|
44
43
|
cropArea: ((_d = processBlock.speakerState) === null || _d === void 0 ? void 0 : _d.cropArea) || null,
|
|
45
|
-
|
|
44
|
+
scale: (_e = processBlock.speakerState) === null || _e === void 0 ? void 0 : _e.scale,
|
|
45
|
+
translateX: (_f = processBlock.speakerState) === null || _f === void 0 ? void 0 : _f.translateX,
|
|
46
|
+
translateY: (_g = processBlock.speakerState) === null || _g === void 0 ? void 0 : _g.translateY,
|
|
47
|
+
isDefault: (_h = processBlock.speakerState) === null || _h === void 0 ? void 0 : _h.isDefault,
|
|
48
|
+
// speakerStateにレイヤー情報があればそれを使用、なければ既存を保持
|
|
49
|
+
layers: (_k = (_j = processBlock.speakerState) === null || _j === void 0 ? void 0 : _j.layers) !== null && _k !== void 0 ? _k : char.entityState.layers,
|
|
50
|
+
},
|
|
51
|
+
// baseBodyStateを保持
|
|
52
|
+
baseBodyState: char.baseBodyState }) : char;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case "character_state_change":
|
|
57
|
+
// 状態変更ブロック: 登場済みキャラクターの状態のみを変更(位置は維持)
|
|
58
|
+
// characters配列がある場合はそれを使用(複数キャラクター対応)
|
|
59
|
+
if (processBlock.characters && processBlock.characters.length > 0) {
|
|
60
|
+
for (const stateChange of processBlock.characters) {
|
|
61
|
+
characters = characters.map((char) => {
|
|
62
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
63
|
+
return char.objectId === stateChange.objectId
|
|
64
|
+
? Object.assign(Object.assign({}, char), { entityStateId: stateChange.entityStateId, entityState: {
|
|
65
|
+
id: stateChange.entityStateId,
|
|
66
|
+
name: ((_a = stateChange.entityState) === null || _a === void 0 ? void 0 : _a.name) || "",
|
|
67
|
+
imageUrl: ((_b = stateChange.entityState) === null || _b === void 0 ? void 0 : _b.imageUrl) || null,
|
|
68
|
+
cropArea: ((_c = stateChange.entityState) === null || _c === void 0 ? void 0 : _c.cropArea) || null,
|
|
69
|
+
scale: (_d = stateChange.entityState) === null || _d === void 0 ? void 0 : _d.scale,
|
|
70
|
+
translateX: (_e = stateChange.entityState) === null || _e === void 0 ? void 0 : _e.translateX,
|
|
71
|
+
translateY: (_f = stateChange.entityState) === null || _f === void 0 ? void 0 : _f.translateY,
|
|
72
|
+
isDefault: (_g = stateChange.entityState) === null || _g === void 0 ? void 0 : _g.isDefault,
|
|
73
|
+
layers: (_j = (_h = stateChange.entityState) === null || _h === void 0 ? void 0 : _h.layers) !== null && _j !== void 0 ? _j : char.entityState.layers,
|
|
74
|
+
},
|
|
75
|
+
// baseBodyStateと位置情報を保持
|
|
76
|
+
baseBodyState: char.baseBodyState }) : char;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (processBlock.speakerId && processBlock.speakerStateId) {
|
|
81
|
+
// speakerId/speakerStateIdを使用した単一キャラクター状態変更(インラインエディタ形式)
|
|
82
|
+
characters = characters.map((char) => {
|
|
83
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
84
|
+
return char.objectId === processBlock.speakerId
|
|
85
|
+
? Object.assign(Object.assign({}, char), { entityStateId: processBlock.speakerStateId || "", entityState: {
|
|
86
|
+
id: (_a = processBlock.speakerStateId) !== null && _a !== void 0 ? _a : "",
|
|
87
|
+
name: ((_b = processBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name) || "",
|
|
88
|
+
imageUrl: ((_c = processBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl) || null,
|
|
89
|
+
cropArea: ((_d = processBlock.speakerState) === null || _d === void 0 ? void 0 : _d.cropArea) || null,
|
|
90
|
+
scale: (_e = processBlock.speakerState) === null || _e === void 0 ? void 0 : _e.scale,
|
|
91
|
+
translateX: (_f = processBlock.speakerState) === null || _f === void 0 ? void 0 : _f.translateX,
|
|
92
|
+
translateY: (_g = processBlock.speakerState) === null || _g === void 0 ? void 0 : _g.translateY,
|
|
93
|
+
isDefault: (_h = processBlock.speakerState) === null || _h === void 0 ? void 0 : _h.isDefault,
|
|
94
|
+
layers: (_k = (_j = processBlock.speakerState) === null || _j === void 0 ? void 0 : _j.layers) !== null && _k !== void 0 ? _k : char.entityState.layers,
|
|
95
|
+
},
|
|
96
|
+
// baseBodyStateと位置情報を保持
|
|
97
|
+
baseBodyState: char.baseBodyState }) : char;
|
|
46
98
|
});
|
|
47
99
|
}
|
|
48
100
|
break;
|
|
@@ -50,36 +102,57 @@ const calculateDisplayedCharacters = (blocks, currentIndex) => {
|
|
|
50
102
|
}
|
|
51
103
|
break; // 最初のcharacter_entranceを見つけたら終了
|
|
52
104
|
}
|
|
53
|
-
if (block.blockType === "character_exit") {
|
|
54
|
-
// character_exitを見つけたら、キャラクター表示範囲外
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
105
|
}
|
|
58
106
|
return characters;
|
|
59
107
|
};
|
|
60
|
-
export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, }) => {
|
|
108
|
+
export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, branchState, branchNavigator: _branchNavigator, customRestart, variableManager, disableKeyboardNavigation = false, }) => {
|
|
61
109
|
const isLastBlock = state.currentBlockIndex === scenario.blocks.length - 1;
|
|
62
110
|
// 現在のインデックスに基づいて表示すべきキャラクターを計算
|
|
63
111
|
const displayedCharacters = useMemo(() => calculateDisplayedCharacters(scenario.blocks, state.currentBlockIndex), [scenario.blocks, state.currentBlockIndex]);
|
|
64
112
|
const handleNext = useCallback(() => {
|
|
65
|
-
var _a;
|
|
113
|
+
var _a, _b;
|
|
66
114
|
// タイピング中の場合はスキップ
|
|
67
115
|
if (isTyping && currentBlock) {
|
|
68
|
-
skipTyping(
|
|
116
|
+
skipTyping();
|
|
69
117
|
return;
|
|
70
118
|
}
|
|
119
|
+
// 分岐ブロックでの進行制御
|
|
120
|
+
if ((currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.blockType) === "conversation_branch") {
|
|
121
|
+
// エラー時は次のブロックに進む
|
|
122
|
+
if (branchState === null || branchState === void 0 ? void 0 : branchState.errorState) {
|
|
123
|
+
// 進行を許可
|
|
124
|
+
}
|
|
125
|
+
else if (!(branchState === null || branchState === void 0 ? void 0 : branchState.selectedChoiceId)) {
|
|
126
|
+
// ローディング中または選択肢未選択の場合は停止
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
71
130
|
if (isLastBlock) {
|
|
72
131
|
setState((prev) => (Object.assign(Object.assign({}, prev), { isEnded: true, isPlaying: false })));
|
|
73
132
|
onEnd === null || onEnd === void 0 ? void 0 : onEnd();
|
|
74
133
|
onScenarioEnd === null || onScenarioEnd === void 0 ? void 0 : onScenarioEnd();
|
|
75
|
-
// iframe向けのイベント発火
|
|
76
134
|
if (typeof window !== "undefined") {
|
|
77
135
|
window.postMessage({ type: "scenario-end" }, "*");
|
|
78
136
|
(_a = window.parent) === null || _a === void 0 ? void 0 : _a.postMessage({ type: "scenario-end" }, "*");
|
|
79
137
|
}
|
|
80
138
|
}
|
|
81
139
|
else {
|
|
82
|
-
|
|
140
|
+
const nextIndex = state.currentBlockIndex + 1;
|
|
141
|
+
const nextBlock = scenario.blocks[nextIndex];
|
|
142
|
+
if ((nextBlock === null || nextBlock === void 0 ? void 0 : nextBlock.blockType) === "variable_operation" && variableManager) {
|
|
143
|
+
const operation = (_b = scenario.variableOperations) === null || _b === void 0 ? void 0 : _b.find((op) => op.blockId === nextBlock.id);
|
|
144
|
+
if (operation) {
|
|
145
|
+
variableManager.executeOperation(operation);
|
|
146
|
+
setState((prev) => (Object.assign(Object.assign({}, prev), { currentBlockIndex: nextIndex, variables: variableManager.getVariablesMap() })));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.error("変数操作ブロックに対応する操作が見つかりません:", nextBlock.id);
|
|
150
|
+
setState((prev) => (Object.assign(Object.assign({}, prev), { currentBlockIndex: nextIndex })));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
setState((prev) => (Object.assign(Object.assign({}, prev), { currentBlockIndex: nextIndex })));
|
|
155
|
+
}
|
|
83
156
|
}
|
|
84
157
|
}, [
|
|
85
158
|
isLastBlock,
|
|
@@ -89,6 +162,11 @@ export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlo
|
|
|
89
162
|
currentBlock,
|
|
90
163
|
skipTyping,
|
|
91
164
|
setState,
|
|
165
|
+
branchState,
|
|
166
|
+
state.currentBlockIndex,
|
|
167
|
+
scenario.blocks,
|
|
168
|
+
scenario.variableOperations,
|
|
169
|
+
variableManager,
|
|
92
170
|
]);
|
|
93
171
|
const handlePrevious = useCallback(() => {
|
|
94
172
|
if (state.currentBlockIndex > 0) {
|
|
@@ -101,15 +179,22 @@ export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlo
|
|
|
101
179
|
setState((prev) => (Object.assign(Object.assign({}, prev), { isPlaying: !prev.isPlaying })));
|
|
102
180
|
}, [state.isEnded, setState]);
|
|
103
181
|
const restart = useCallback(() => {
|
|
104
|
-
|
|
182
|
+
if (variableManager) {
|
|
183
|
+
variableManager.reset();
|
|
184
|
+
}
|
|
105
185
|
setState({
|
|
106
186
|
currentBlockIndex: 0,
|
|
107
187
|
isPlaying: autoplay,
|
|
108
188
|
isEnded: false,
|
|
189
|
+
variables: variableManager === null || variableManager === void 0 ? void 0 : variableManager.getVariablesMap(),
|
|
109
190
|
});
|
|
110
|
-
}, [autoplay, setState]);
|
|
191
|
+
}, [autoplay, setState, variableManager]);
|
|
111
192
|
// キーボードナビゲーション
|
|
112
193
|
useEffect(() => {
|
|
194
|
+
// プレビューモードなど、キーボードナビゲーションを無効化する場合はリスナーを追加しない
|
|
195
|
+
if (disableKeyboardNavigation) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
113
198
|
const handleKeyDown = (event) => {
|
|
114
199
|
switch (event.key) {
|
|
115
200
|
case "ArrowRight":
|
|
@@ -127,13 +212,25 @@ export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlo
|
|
|
127
212
|
break;
|
|
128
213
|
case "Escape":
|
|
129
214
|
event.preventDefault();
|
|
130
|
-
|
|
215
|
+
if (customRestart) {
|
|
216
|
+
customRestart();
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
restart();
|
|
220
|
+
}
|
|
131
221
|
break;
|
|
132
222
|
}
|
|
133
223
|
};
|
|
134
224
|
window.addEventListener("keydown", handleKeyDown);
|
|
135
225
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
136
|
-
}, [
|
|
226
|
+
}, [
|
|
227
|
+
handleNext,
|
|
228
|
+
handlePrevious,
|
|
229
|
+
togglePlay,
|
|
230
|
+
restart,
|
|
231
|
+
customRestart,
|
|
232
|
+
disableKeyboardNavigation,
|
|
233
|
+
]);
|
|
137
234
|
return {
|
|
138
235
|
handleNext,
|
|
139
236
|
handlePrevious,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PluginManager } from "../plugin/PluginManager";
|
|
2
|
+
import type { TransitionSource } from "../sdk";
|
|
2
3
|
import type { DisplayedCharacter, ScenarioBlock } from "../types";
|
|
3
4
|
interface UsePluginEventsProps {
|
|
4
5
|
pluginManager: PluginManager;
|
|
@@ -9,6 +10,8 @@ interface UsePluginEventsProps {
|
|
|
9
10
|
allBlocks: ScenarioBlock[];
|
|
10
11
|
realBlockIndex: number;
|
|
11
12
|
pluginsLoaded: boolean;
|
|
13
|
+
/** 遷移の種類(クリック、自動、時間待ち) */
|
|
14
|
+
transitionSource?: TransitionSource;
|
|
12
15
|
}
|
|
13
|
-
export declare const usePluginEvents: ({ pluginManager, currentBlock, displayedCharacters, blockIndex, isFirstRenderComplete, allBlocks, realBlockIndex, pluginsLoaded, }: UsePluginEventsProps) => void;
|
|
16
|
+
export declare const usePluginEvents: ({ pluginManager, currentBlock, displayedCharacters, blockIndex, isFirstRenderComplete, allBlocks, realBlockIndex, pluginsLoaded, transitionSource, }: UsePluginEventsProps) => void;
|
|
14
17
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import { normalizeAttributes } from "../utils/attributeNormalizer";
|
|
3
3
|
import { calculateFacePosition } from "../utils/facePositionCalculator";
|
|
4
|
-
export const usePluginEvents = ({ pluginManager, currentBlock, displayedCharacters, blockIndex, isFirstRenderComplete, allBlocks, realBlockIndex, pluginsLoaded, }) => {
|
|
4
|
+
export const usePluginEvents = ({ pluginManager, currentBlock, displayedCharacters, blockIndex, isFirstRenderComplete, allBlocks, realBlockIndex, pluginsLoaded, transitionSource = "click", }) => {
|
|
5
5
|
const previousSpeakerRef = useRef({});
|
|
6
6
|
const previousRealBlockIndexRef = useRef(-1);
|
|
7
7
|
const scenarioReadyCalledRef = useRef(false);
|
|
@@ -45,26 +45,22 @@ export const usePluginEvents = ({ pluginManager, currentBlock, displayedCharacte
|
|
|
45
45
|
const startIndex = previousIndex === -1 ? 0 : previousIndex + 1;
|
|
46
46
|
for (let i = startIndex; i <= currentIndex; i++) {
|
|
47
47
|
const block = allBlocks[i];
|
|
48
|
-
console.log("block", block);
|
|
49
48
|
if ((block === null || block === void 0 ? void 0 : block.blockType) === "action_node" && block.actionNode) {
|
|
50
|
-
// Normalize attributes from database format
|
|
51
49
|
const normalizedAttributes = normalizeAttributes(block);
|
|
52
|
-
console.log("Normalized attributes:", normalizedAttributes);
|
|
53
50
|
// Try multiple ways to find the ActionNode type - prioritize displayName matching
|
|
54
51
|
const possibleTypes = [
|
|
55
52
|
block.actionNode.nodeType,
|
|
56
53
|
block.actionNode.name, // This will be "背景" from database
|
|
57
54
|
].filter(Boolean);
|
|
58
55
|
let actionNodeDef;
|
|
59
|
-
let
|
|
56
|
+
let _foundActionNodeType;
|
|
60
57
|
// First try direct type matching
|
|
61
58
|
for (const actionNodeType of possibleTypes) {
|
|
62
59
|
if (actionNodeType) {
|
|
63
60
|
actionNodeDef =
|
|
64
61
|
pluginManager.getActionNodeDefinition(actionNodeType);
|
|
65
62
|
if (actionNodeDef) {
|
|
66
|
-
|
|
67
|
-
console.log(`Found ActionNode definition with type: ${actionNodeType}`);
|
|
63
|
+
_foundActionNodeType = actionNodeType;
|
|
68
64
|
break;
|
|
69
65
|
}
|
|
70
66
|
}
|
|
@@ -73,8 +69,7 @@ export const usePluginEvents = ({ pluginManager, currentBlock, displayedCharacte
|
|
|
73
69
|
if (!actionNodeDef && block.actionNode.name) {
|
|
74
70
|
actionNodeDef = pluginManager.getActionNodeDefinition(block.actionNode.name);
|
|
75
71
|
if (actionNodeDef) {
|
|
76
|
-
|
|
77
|
-
console.log(`Found ActionNode definition with displayName: ${block.actionNode.name}`);
|
|
72
|
+
_foundActionNodeType = block.actionNode.name;
|
|
78
73
|
}
|
|
79
74
|
}
|
|
80
75
|
if (actionNodeDef === null || actionNodeDef === void 0 ? void 0 : actionNodeDef.execute) {
|
|
@@ -88,7 +83,6 @@ export const usePluginEvents = ({ pluginManager, currentBlock, displayedCharacte
|
|
|
88
83
|
displayedCharacters,
|
|
89
84
|
api: pluginManager,
|
|
90
85
|
});
|
|
91
|
-
console.log(`Successfully executed ActionNode: ${foundActionNodeType}`);
|
|
92
86
|
}
|
|
93
87
|
catch (error) {
|
|
94
88
|
console.error("Error executing ActionNode:", error);
|
|
@@ -117,21 +111,32 @@ export const usePluginEvents = ({ pluginManager, currentBlock, displayedCharacte
|
|
|
117
111
|
isFirstRenderComplete,
|
|
118
112
|
pluginsLoaded,
|
|
119
113
|
]);
|
|
120
|
-
// ブロック変更イベント
|
|
114
|
+
// ブロック変更イベント + ビルトインクリック音
|
|
121
115
|
useEffect(() => {
|
|
122
116
|
if (!isFirstRenderComplete || !currentBlock || !pluginsLoaded) {
|
|
123
117
|
return;
|
|
124
118
|
}
|
|
119
|
+
// まずプラグインのオーバーライドフラグをリセット
|
|
120
|
+
pluginManager.resetClickSoundOverride();
|
|
121
|
+
// プラグインのonBlockChangeフックを呼び出す
|
|
122
|
+
// プラグインはここでoverrideClickSound()を呼び出してビルトインクリック音を抑制できる
|
|
125
123
|
pluginManager.callHook("onBlockChange", {
|
|
126
124
|
currentBlock,
|
|
127
125
|
blockIndex,
|
|
126
|
+
transitionSource,
|
|
128
127
|
});
|
|
128
|
+
// 時間待ちからの自動遷移でない場合のみ、ビルトインクリック音を再生
|
|
129
|
+
// プラグインがoverrideClickSound()を呼び出した場合は再生されない
|
|
130
|
+
if (transitionSource !== "time_wait" && transitionSource !== "auto") {
|
|
131
|
+
pluginManager.playClickSound();
|
|
132
|
+
}
|
|
129
133
|
}, [
|
|
130
134
|
pluginManager,
|
|
131
135
|
currentBlock,
|
|
132
136
|
blockIndex,
|
|
133
137
|
isFirstRenderComplete,
|
|
134
138
|
pluginsLoaded,
|
|
139
|
+
transitionSource,
|
|
135
140
|
]);
|
|
136
141
|
// キャラクター発話イベント
|
|
137
142
|
useEffect(() => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
export const usePreloadImages = (scenario) => {
|
|
3
3
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
4
|
+
// プリロード済みの画像URLセットを追跡(シナリオ変更で再プリロードを避ける)
|
|
5
|
+
const preloadedUrlsRef = useRef(new Set());
|
|
4
6
|
useEffect(() => {
|
|
5
7
|
const imageUrls = new Set();
|
|
6
8
|
// シナリオ全体から画像URLを収集
|
|
@@ -24,12 +26,23 @@ export const usePreloadImages = (scenario) => {
|
|
|
24
26
|
setIsLoaded(true);
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
29
|
+
// 未プリロードの画像のみをフィルタリング
|
|
30
|
+
const newImageUrls = Array.from(imageUrls).filter((url) => !preloadedUrlsRef.current.has(url));
|
|
31
|
+
// すべてプリロード済みの場合は即座に完了
|
|
32
|
+
if (newImageUrls.length === 0) {
|
|
33
|
+
setIsLoaded(true);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// 新しい画像がある場合はローディング状態にリセット
|
|
37
|
+
setIsLoaded(false);
|
|
27
38
|
let loadedCount = 0;
|
|
28
|
-
const totalCount =
|
|
39
|
+
const totalCount = newImageUrls.length;
|
|
40
|
+
const cleanupFunctions = [];
|
|
29
41
|
// 各画像を事前読み込み
|
|
30
|
-
|
|
42
|
+
for (const url of newImageUrls) {
|
|
31
43
|
const img = new Image();
|
|
32
44
|
const handleLoad = () => {
|
|
45
|
+
preloadedUrlsRef.current.add(url);
|
|
33
46
|
loadedCount++;
|
|
34
47
|
if (loadedCount === totalCount) {
|
|
35
48
|
setIsLoaded(true);
|
|
@@ -37,6 +50,7 @@ export const usePreloadImages = (scenario) => {
|
|
|
37
50
|
};
|
|
38
51
|
const handleError = () => {
|
|
39
52
|
console.error(`Failed to preload image: ${url}`);
|
|
53
|
+
preloadedUrlsRef.current.add(url); // エラーでもプリロード済みとしてマーク
|
|
40
54
|
loadedCount++;
|
|
41
55
|
if (loadedCount === totalCount) {
|
|
42
56
|
setIsLoaded(true);
|
|
@@ -45,12 +59,18 @@ export const usePreloadImages = (scenario) => {
|
|
|
45
59
|
img.addEventListener("load", handleLoad);
|
|
46
60
|
img.addEventListener("error", handleError);
|
|
47
61
|
img.src = url;
|
|
48
|
-
//
|
|
49
|
-
|
|
62
|
+
// クリーンアップ関数を配列に追加
|
|
63
|
+
cleanupFunctions.push(() => {
|
|
50
64
|
img.removeEventListener("load", handleLoad);
|
|
51
65
|
img.removeEventListener("error", handleError);
|
|
52
|
-
};
|
|
53
|
-
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// クリーンアップ時にすべてのイベントリスナーを解除
|
|
69
|
+
return () => {
|
|
70
|
+
for (const cleanup of cleanupFunctions) {
|
|
71
|
+
cleanup();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
54
74
|
}, [scenario]);
|
|
55
75
|
return isLoaded;
|
|
56
76
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScenarioBlock } from "../types";
|
|
2
|
+
interface UseSoundPlayerProps {
|
|
3
|
+
soundBlocks: ScenarioBlock[];
|
|
4
|
+
isFirstRenderComplete: boolean;
|
|
5
|
+
/** 音声をミュートするかどうか */
|
|
6
|
+
muteAudio?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const useSoundPlayer: ({ soundBlocks, isFirstRenderComplete, muteAudio, }: UseSoundPlayerProps) => {
|
|
9
|
+
playSound: (block: ScenarioBlock) => void;
|
|
10
|
+
stopBGM: () => void;
|
|
11
|
+
stopSE: (soundId: string) => void;
|
|
12
|
+
stopAllSE: () => void;
|
|
13
|
+
stopAll: () => void;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { useAudioSettings } from "../contexts/AudioContext";
|
|
3
|
+
export const useSoundPlayer = ({ soundBlocks, isFirstRenderComplete, muteAudio = false, }) => {
|
|
4
|
+
const audioSettings = useAudioSettings();
|
|
5
|
+
// 現在再生中のBGM
|
|
6
|
+
const bgmRef = useRef(null);
|
|
7
|
+
// 現在再生中のSE(複数同時再生可能)
|
|
8
|
+
const seInstancesRef = useRef(new Map());
|
|
9
|
+
// 前回処理したブロックIDセット(重複処理防止)
|
|
10
|
+
const processedBlockIdsRef = useRef(new Set());
|
|
11
|
+
// BGMかどうかを判定(SoundTypeのcategoryで判断)
|
|
12
|
+
const isBGM = useCallback((block) => {
|
|
13
|
+
var _a;
|
|
14
|
+
if (!((_a = block.sound) === null || _a === void 0 ? void 0 : _a.soundType))
|
|
15
|
+
return false;
|
|
16
|
+
return block.sound.soundType.category === "bgm";
|
|
17
|
+
}, []);
|
|
18
|
+
// Voiceかどうかを判定
|
|
19
|
+
const isVoice = useCallback((block) => {
|
|
20
|
+
var _a;
|
|
21
|
+
if (!((_a = block.sound) === null || _a === void 0 ? void 0 : _a.soundType))
|
|
22
|
+
return false;
|
|
23
|
+
return block.sound.soundType.category === "voice";
|
|
24
|
+
}, []);
|
|
25
|
+
// 音量を適用
|
|
26
|
+
const applyVolume = useCallback((audio, block) => {
|
|
27
|
+
if (isBGM(block)) {
|
|
28
|
+
audio.volume = audioSettings.bgmVolume;
|
|
29
|
+
}
|
|
30
|
+
else if (isVoice(block)) {
|
|
31
|
+
audio.volume = audioSettings.voiceVolume;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
audio.volume = audioSettings.seVolume;
|
|
35
|
+
}
|
|
36
|
+
}, [
|
|
37
|
+
audioSettings.bgmVolume,
|
|
38
|
+
audioSettings.seVolume,
|
|
39
|
+
audioSettings.voiceVolume,
|
|
40
|
+
isBGM,
|
|
41
|
+
isVoice,
|
|
42
|
+
]);
|
|
43
|
+
// BGMを停止
|
|
44
|
+
const stopBGM = useCallback(() => {
|
|
45
|
+
if (bgmRef.current) {
|
|
46
|
+
bgmRef.current.audio.pause();
|
|
47
|
+
bgmRef.current.audio.currentTime = 0;
|
|
48
|
+
bgmRef.current = null;
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
// 特定のSEを停止
|
|
52
|
+
const stopSE = useCallback((soundId) => {
|
|
53
|
+
const instance = seInstancesRef.current.get(soundId);
|
|
54
|
+
if (instance) {
|
|
55
|
+
instance.audio.pause();
|
|
56
|
+
instance.audio.currentTime = 0;
|
|
57
|
+
seInstancesRef.current.delete(soundId);
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
// 全てのSEを停止
|
|
61
|
+
const stopAllSE = useCallback(() => {
|
|
62
|
+
for (const instance of seInstancesRef.current.values()) {
|
|
63
|
+
instance.audio.pause();
|
|
64
|
+
instance.audio.currentTime = 0;
|
|
65
|
+
}
|
|
66
|
+
seInstancesRef.current.clear();
|
|
67
|
+
}, []);
|
|
68
|
+
// 全ての音を停止
|
|
69
|
+
const stopAll = useCallback(() => {
|
|
70
|
+
stopBGM();
|
|
71
|
+
stopAllSE();
|
|
72
|
+
}, [stopBGM, stopAllSE]);
|
|
73
|
+
// サウンドを再生
|
|
74
|
+
const playSound = useCallback((block) => {
|
|
75
|
+
if (!block.sound)
|
|
76
|
+
return;
|
|
77
|
+
// ミュート中は再生しない(propsまたはaudioSettings)
|
|
78
|
+
if (muteAudio || audioSettings.muteAudio)
|
|
79
|
+
return;
|
|
80
|
+
const { sound } = block;
|
|
81
|
+
const isBGMSound = isBGM(block);
|
|
82
|
+
// BGMの場合、既存のBGMを停止
|
|
83
|
+
if (isBGMSound) {
|
|
84
|
+
stopBGM();
|
|
85
|
+
}
|
|
86
|
+
const audio = new Audio(sound.url);
|
|
87
|
+
audio.loop = sound.loop;
|
|
88
|
+
applyVolume(audio, block);
|
|
89
|
+
const category = block.sound.soundType.category;
|
|
90
|
+
// エラーハンドリング(イベントリスナーを保持してクリーンアップ可能にする)
|
|
91
|
+
const handleError = () => {
|
|
92
|
+
var _a;
|
|
93
|
+
console.warn(`Sound load failed: ${sound.name} (${sound.url})`);
|
|
94
|
+
// 失敗したインスタンスをクリーンアップ
|
|
95
|
+
if (isBGMSound && ((_a = bgmRef.current) === null || _a === void 0 ? void 0 : _a.soundId) === sound.id) {
|
|
96
|
+
bgmRef.current = null;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
seInstancesRef.current.delete(sound.id);
|
|
100
|
+
}
|
|
101
|
+
// イベントリスナーを削除
|
|
102
|
+
audio.removeEventListener("error", handleError);
|
|
103
|
+
};
|
|
104
|
+
audio.addEventListener("error", handleError);
|
|
105
|
+
const instance = {
|
|
106
|
+
audio,
|
|
107
|
+
soundId: sound.id,
|
|
108
|
+
category,
|
|
109
|
+
};
|
|
110
|
+
if (isBGMSound) {
|
|
111
|
+
bgmRef.current = instance;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// SE/Voiceは既に同じIDのものがあれば停止して再生
|
|
115
|
+
// 注意: 同一サウンドの重複再生は意図的に防止している
|
|
116
|
+
// 連打などによる音の重複を避けるため
|
|
117
|
+
stopSE(sound.id);
|
|
118
|
+
seInstancesRef.current.set(sound.id, instance);
|
|
119
|
+
// SE終了時にMapから削除(イベントリスナーも削除)
|
|
120
|
+
const handleEnded = () => {
|
|
121
|
+
seInstancesRef.current.delete(sound.id);
|
|
122
|
+
audio.removeEventListener("ended", handleEnded);
|
|
123
|
+
};
|
|
124
|
+
audio.addEventListener("ended", handleEnded);
|
|
125
|
+
}
|
|
126
|
+
audio.play().catch((error) => {
|
|
127
|
+
// ネット作品エラーやソース不対応時のエラーをより詳細にログ
|
|
128
|
+
if (error.name === "NotSupportedError") {
|
|
129
|
+
console.warn(`Sound source not available: ${sound.name}`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.error("Sound playback error:", error);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}, [isBGM, stopBGM, stopSE, applyVolume, muteAudio, audioSettings.muteAudio]);
|
|
136
|
+
// soundBlocksが変更された時にprocessedBlockIdsをクリア(シナリオリセット対応)
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
// soundBlocksが空になったらprocessedBlockIdsをクリア
|
|
139
|
+
if (soundBlocks.length === 0) {
|
|
140
|
+
processedBlockIdsRef.current.clear();
|
|
141
|
+
}
|
|
142
|
+
}, [soundBlocks]);
|
|
143
|
+
// 複数のサウンドブロックを順番に処理
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!isFirstRenderComplete)
|
|
146
|
+
return;
|
|
147
|
+
for (const block of soundBlocks) {
|
|
148
|
+
// 既に処理済みのブロックはスキップ
|
|
149
|
+
if (processedBlockIdsRef.current.has(block.id))
|
|
150
|
+
continue;
|
|
151
|
+
// 処理済みとしてマーク
|
|
152
|
+
processedBlockIdsRef.current.add(block.id);
|
|
153
|
+
switch (block.blockType) {
|
|
154
|
+
case "bgm_play":
|
|
155
|
+
case "se_play":
|
|
156
|
+
if (block.sound) {
|
|
157
|
+
playSound(block);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case "bgm_stop":
|
|
161
|
+
// bgm_stopブロックはBGMを停止する
|
|
162
|
+
stopBGM();
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}, [soundBlocks, isFirstRenderComplete, playSound, stopBGM]);
|
|
167
|
+
// 音量設定の変更を反映
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (bgmRef.current) {
|
|
170
|
+
bgmRef.current.audio.volume = audioSettings.bgmVolume;
|
|
171
|
+
}
|
|
172
|
+
for (const instance of seInstancesRef.current.values()) {
|
|
173
|
+
if (instance.category === "voice") {
|
|
174
|
+
instance.audio.volume = audioSettings.voiceVolume;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
instance.audio.volume = audioSettings.seVolume;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}, [
|
|
181
|
+
audioSettings.bgmVolume,
|
|
182
|
+
audioSettings.seVolume,
|
|
183
|
+
audioSettings.voiceVolume,
|
|
184
|
+
]);
|
|
185
|
+
// クリーンアップ(直接refにアクセスして依存配列の問題を回避)
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
return () => {
|
|
188
|
+
// BGMを停止
|
|
189
|
+
if (bgmRef.current) {
|
|
190
|
+
bgmRef.current.audio.pause();
|
|
191
|
+
bgmRef.current.audio.currentTime = 0;
|
|
192
|
+
bgmRef.current = null;
|
|
193
|
+
}
|
|
194
|
+
// 全てのSEを停止
|
|
195
|
+
for (const instance of seInstancesRef.current.values()) {
|
|
196
|
+
instance.audio.pause();
|
|
197
|
+
instance.audio.currentTime = 0;
|
|
198
|
+
}
|
|
199
|
+
seInstancesRef.current.clear();
|
|
200
|
+
};
|
|
201
|
+
}, []);
|
|
202
|
+
return {
|
|
203
|
+
playSound,
|
|
204
|
+
stopBGM,
|
|
205
|
+
stopSE,
|
|
206
|
+
stopAllSE,
|
|
207
|
+
stopAll,
|
|
208
|
+
};
|
|
209
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** 連続モード: false = 連続しない, "continue" = 連続(改行なし), "continueWithNewline" = 連続(改行あり) */
|
|
2
|
+
export type ContinueMode = false | "continue" | "continueWithNewline";
|
|
1
3
|
export interface UseTypewriterOptions {
|
|
2
4
|
speed?: number;
|
|
3
5
|
onComplete?: () => void;
|
|
@@ -5,7 +7,9 @@ export interface UseTypewriterOptions {
|
|
|
5
7
|
export declare const useTypewriter: (options?: UseTypewriterOptions) => {
|
|
6
8
|
displayText: string;
|
|
7
9
|
isTyping: boolean;
|
|
8
|
-
startTyping: (text: string) => void;
|
|
9
|
-
skipTyping: (
|
|
10
|
+
startTyping: (text: string, continueMode?: ContinueMode) => void;
|
|
11
|
+
skipTyping: () => void;
|
|
10
12
|
reset: () => void;
|
|
13
|
+
resetAccumulated: () => void;
|
|
14
|
+
accumulatedText: string;
|
|
11
15
|
};
|