@luna-editor/engine 0.1.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 +3 -0
- package/dist/Player.js +336 -0
- package/dist/atoms/screen-size.d.ts +22 -0
- package/dist/atoms/screen-size.js +8 -0
- package/dist/components/BacklogUI.d.ts +2 -0
- package/dist/components/BacklogUI.js +115 -0
- package/dist/components/ConversationLogUI.d.ts +2 -0
- package/dist/components/ConversationLogUI.js +115 -0
- package/dist/components/DebugControls.d.ts +12 -0
- package/dist/components/DebugControls.js +5 -0
- package/dist/components/DialogueBox.d.ts +2 -0
- package/dist/components/DialogueBox.js +28 -0
- package/dist/components/EndScreen.d.ts +8 -0
- package/dist/components/EndScreen.js +5 -0
- package/dist/components/GameScreen.d.ts +9 -0
- package/dist/components/GameScreen.js +111 -0
- package/dist/components/OverlayUI.d.ts +13 -0
- package/dist/components/OverlayUI.js +21 -0
- package/dist/components/PluginComponentProvider.d.ts +14 -0
- package/dist/components/PluginComponentProvider.js +24 -0
- package/dist/constants/screen-size.d.ts +3 -0
- package/dist/constants/screen-size.js +6 -0
- package/dist/contexts/DataContext.d.ts +24 -0
- package/dist/contexts/DataContext.js +101 -0
- package/dist/hooks/useBacklog.d.ts +14 -0
- package/dist/hooks/useBacklog.js +82 -0
- package/dist/hooks/useConversationLog.d.ts +14 -0
- package/dist/hooks/useConversationLog.js +82 -0
- package/dist/hooks/usePlayerLogic.d.ts +21 -0
- package/dist/hooks/usePlayerLogic.js +145 -0
- package/dist/hooks/usePluginAPI.d.ts +19 -0
- package/dist/hooks/usePluginAPI.js +42 -0
- package/dist/hooks/usePluginEvents.d.ts +14 -0
- package/dist/hooks/usePluginEvents.js +197 -0
- package/dist/hooks/usePreloadImages.d.ts +2 -0
- package/dist/hooks/usePreloadImages.js +56 -0
- package/dist/hooks/useScreenSize.d.ts +89 -0
- package/dist/hooks/useScreenSize.js +87 -0
- package/dist/hooks/useTypewriter.d.ts +11 -0
- package/dist/hooks/useTypewriter.js +56 -0
- package/dist/hooks/useUIVisibility.d.ts +9 -0
- package/dist/hooks/useUIVisibility.js +19 -0
- package/dist/hooks/useVoice.d.ts +4 -0
- package/dist/hooks/useVoice.js +21 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/plugin/PluginManager.d.ts +108 -0
- package/dist/plugin/PluginManager.js +851 -0
- package/dist/plugin/luna-react.d.ts +41 -0
- package/dist/plugin/luna-react.js +99 -0
- package/dist/sdk.d.ts +512 -0
- package/dist/sdk.js +64 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +2 -0
- package/dist/utils/attributeNormalizer.d.ts +5 -0
- package/dist/utils/attributeNormalizer.js +53 -0
- package/dist/utils/facePositionCalculator.d.ts +29 -0
- package/dist/utils/facePositionCalculator.js +127 -0
- package/package.json +55 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
// ブロックインデックスから表示すべきキャラクター状態を計算する純粋関数
|
|
3
|
+
const calculateDisplayedCharacters = (blocks, currentIndex) => {
|
|
4
|
+
let characters = [];
|
|
5
|
+
// 現在位置から前方向に探索して、最後のcharacter_entranceまたはcharacter_exitを見つける
|
|
6
|
+
for (let i = currentIndex; i >= 0; i--) {
|
|
7
|
+
const block = blocks[i];
|
|
8
|
+
if (block.blockType === "character_entrance") {
|
|
9
|
+
// character_entranceを見つけたら、そこから現在位置までのキャラクター状態を構築
|
|
10
|
+
// character_entranceから現在位置までのブロックを処理
|
|
11
|
+
for (let j = i; j <= currentIndex; j++) {
|
|
12
|
+
const processBlock = blocks[j];
|
|
13
|
+
switch (processBlock.blockType) {
|
|
14
|
+
case "character_entrance":
|
|
15
|
+
if (processBlock.characters) {
|
|
16
|
+
const newCharacters = processBlock.characters.map((char) => ({
|
|
17
|
+
objectId: char.objectId,
|
|
18
|
+
entityStateId: char.entityStateId,
|
|
19
|
+
positionX: char.positionX,
|
|
20
|
+
positionY: char.positionY,
|
|
21
|
+
zIndex: char.zIndex,
|
|
22
|
+
scale: char.scale,
|
|
23
|
+
object: char.object,
|
|
24
|
+
entityState: char.entityState,
|
|
25
|
+
}));
|
|
26
|
+
characters = [
|
|
27
|
+
...characters.filter((existing) => !newCharacters.some((newChar) => newChar.objectId === existing.objectId)),
|
|
28
|
+
...newCharacters,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
case "character_exit":
|
|
33
|
+
// character_exitに到達したら、キャラクター表示範囲外
|
|
34
|
+
return [];
|
|
35
|
+
case "dialogue":
|
|
36
|
+
if (processBlock.speakerId && processBlock.speakerStateId) {
|
|
37
|
+
characters = characters.map((char) => {
|
|
38
|
+
var _a, _b, _c, _d;
|
|
39
|
+
return char.objectId === processBlock.speakerId
|
|
40
|
+
? Object.assign(Object.assign({}, char), { entityStateId: processBlock.speakerStateId || "", entityState: {
|
|
41
|
+
id: (_a = processBlock.speakerStateId) !== null && _a !== void 0 ? _a : "",
|
|
42
|
+
name: ((_b = processBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name) || "",
|
|
43
|
+
imageUrl: ((_c = processBlock.speakerState) === null || _c === void 0 ? void 0 : _c.imageUrl) || null,
|
|
44
|
+
cropArea: ((_d = processBlock.speakerState) === null || _d === void 0 ? void 0 : _d.cropArea) || null,
|
|
45
|
+
} }) : char;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
break; // 最初のcharacter_entranceを見つけたら終了
|
|
52
|
+
}
|
|
53
|
+
if (block.blockType === "character_exit") {
|
|
54
|
+
// character_exitを見つけたら、キャラクター表示範囲外
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return characters;
|
|
59
|
+
};
|
|
60
|
+
export const usePlayerLogic = ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, }) => {
|
|
61
|
+
const isLastBlock = state.currentBlockIndex === scenario.blocks.length - 1;
|
|
62
|
+
// 現在のインデックスに基づいて表示すべきキャラクターを計算
|
|
63
|
+
const displayedCharacters = useMemo(() => calculateDisplayedCharacters(scenario.blocks, state.currentBlockIndex), [scenario.blocks, state.currentBlockIndex]);
|
|
64
|
+
const handleNext = useCallback(() => {
|
|
65
|
+
var _a;
|
|
66
|
+
// タイピング中の場合はスキップ
|
|
67
|
+
if (isTyping && currentBlock) {
|
|
68
|
+
skipTyping(currentBlock.content || "");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (isLastBlock) {
|
|
72
|
+
setState((prev) => (Object.assign(Object.assign({}, prev), { isEnded: true, isPlaying: false })));
|
|
73
|
+
onEnd === null || onEnd === void 0 ? void 0 : onEnd();
|
|
74
|
+
onScenarioEnd === null || onScenarioEnd === void 0 ? void 0 : onScenarioEnd();
|
|
75
|
+
// iframe向けのイベント発火
|
|
76
|
+
if (typeof window !== "undefined") {
|
|
77
|
+
window.postMessage({ type: "scenario-end" }, "*");
|
|
78
|
+
(_a = window.parent) === null || _a === void 0 ? void 0 : _a.postMessage({ type: "scenario-end" }, "*");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
setState((prev) => (Object.assign(Object.assign({}, prev), { currentBlockIndex: prev.currentBlockIndex + 1 })));
|
|
83
|
+
}
|
|
84
|
+
}, [
|
|
85
|
+
isLastBlock,
|
|
86
|
+
onEnd,
|
|
87
|
+
onScenarioEnd,
|
|
88
|
+
isTyping,
|
|
89
|
+
currentBlock,
|
|
90
|
+
skipTyping,
|
|
91
|
+
setState,
|
|
92
|
+
]);
|
|
93
|
+
const handlePrevious = useCallback(() => {
|
|
94
|
+
if (state.currentBlockIndex > 0) {
|
|
95
|
+
setState((prev) => (Object.assign(Object.assign({}, prev), { currentBlockIndex: prev.currentBlockIndex - 1, isEnded: false })));
|
|
96
|
+
}
|
|
97
|
+
}, [state.currentBlockIndex, setState]);
|
|
98
|
+
const togglePlay = useCallback(() => {
|
|
99
|
+
if (state.isEnded)
|
|
100
|
+
return;
|
|
101
|
+
setState((prev) => (Object.assign(Object.assign({}, prev), { isPlaying: !prev.isPlaying })));
|
|
102
|
+
}, [state.isEnded, setState]);
|
|
103
|
+
const restart = useCallback(() => {
|
|
104
|
+
console.log("restart");
|
|
105
|
+
setState({
|
|
106
|
+
currentBlockIndex: 0,
|
|
107
|
+
isPlaying: autoplay,
|
|
108
|
+
isEnded: false,
|
|
109
|
+
});
|
|
110
|
+
}, [autoplay, setState]);
|
|
111
|
+
// キーボードナビゲーション
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const handleKeyDown = (event) => {
|
|
114
|
+
switch (event.key) {
|
|
115
|
+
case "ArrowRight":
|
|
116
|
+
case " ":
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
handleNext();
|
|
119
|
+
break;
|
|
120
|
+
case "ArrowLeft":
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
handlePrevious();
|
|
123
|
+
break;
|
|
124
|
+
case "Enter":
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
togglePlay();
|
|
127
|
+
break;
|
|
128
|
+
case "Escape":
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
restart();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
135
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
136
|
+
}, [handleNext, handlePrevious, togglePlay, restart]);
|
|
137
|
+
return {
|
|
138
|
+
handleNext,
|
|
139
|
+
handlePrevious,
|
|
140
|
+
togglePlay,
|
|
141
|
+
restart,
|
|
142
|
+
isLastBlock,
|
|
143
|
+
displayedCharacters,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginManager } from "../plugin/PluginManager";
|
|
2
|
+
import type { ComponentType, UIAPI } from "../sdk";
|
|
3
|
+
/**
|
|
4
|
+
* グローバルなUIAPIとPluginManagerを設定(Player.tsxから呼ばれる)
|
|
5
|
+
*/
|
|
6
|
+
export declare function setGlobalUIAPI(api: UIAPI, pluginManager: PluginManager): void;
|
|
7
|
+
/**
|
|
8
|
+
* プラグインコンポーネント内でUIAPIにアクセスするためのフック
|
|
9
|
+
* @returns UIAPI
|
|
10
|
+
*/
|
|
11
|
+
export declare function usePluginAPI(): {
|
|
12
|
+
ui: UIAPI;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* UI コンポーネントの表示状態を監視するフック(プラグインコンポーネント用)
|
|
16
|
+
* @param type - コンポーネントタイプ
|
|
17
|
+
* @returns 表示状態
|
|
18
|
+
*/
|
|
19
|
+
export declare function useUIVisibility(type: ComponentType): boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* グローバルなPluginAPIインスタンスを保持
|
|
4
|
+
* プラグインコンポーネントから呼び出された時に使用される
|
|
5
|
+
*/
|
|
6
|
+
let globalUIAPI = null;
|
|
7
|
+
let globalPluginManager = null;
|
|
8
|
+
/**
|
|
9
|
+
* グローバルなUIAPIとPluginManagerを設定(Player.tsxから呼ばれる)
|
|
10
|
+
*/
|
|
11
|
+
export function setGlobalUIAPI(api, pluginManager) {
|
|
12
|
+
globalUIAPI = api;
|
|
13
|
+
globalPluginManager = pluginManager;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* プラグインコンポーネント内でUIAPIにアクセスするためのフック
|
|
17
|
+
* @returns UIAPI
|
|
18
|
+
*/
|
|
19
|
+
export function usePluginAPI() {
|
|
20
|
+
if (!globalUIAPI) {
|
|
21
|
+
throw new Error("UIAPI is not initialized. Make sure this hook is used within a Luna Player context.");
|
|
22
|
+
}
|
|
23
|
+
return { ui: globalUIAPI };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* UI コンポーネントの表示状態を監視するフック(プラグインコンポーネント用)
|
|
27
|
+
* @param type - コンポーネントタイプ
|
|
28
|
+
* @returns 表示状態
|
|
29
|
+
*/
|
|
30
|
+
export function useUIVisibility(type) {
|
|
31
|
+
if (!globalPluginManager) {
|
|
32
|
+
throw new Error("PluginManager is not initialized. Make sure this hook is used within a Luna Player context.");
|
|
33
|
+
}
|
|
34
|
+
const pluginManager = globalPluginManager;
|
|
35
|
+
const [isVisible, setIsVisible] = useState(() => pluginManager.getUIVisibility(type));
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// UI 状態の変更を監視
|
|
38
|
+
const unsubscribe = pluginManager.subscribeUIVisibility(type, setIsVisible);
|
|
39
|
+
return unsubscribe;
|
|
40
|
+
}, [pluginManager, type]);
|
|
41
|
+
return isVisible;
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginManager } from "../plugin/PluginManager";
|
|
2
|
+
import type { DisplayedCharacter, ScenarioBlock } from "../types";
|
|
3
|
+
interface UsePluginEventsProps {
|
|
4
|
+
pluginManager: PluginManager;
|
|
5
|
+
currentBlock?: ScenarioBlock;
|
|
6
|
+
displayedCharacters: DisplayedCharacter[];
|
|
7
|
+
blockIndex: number;
|
|
8
|
+
isFirstRenderComplete: boolean;
|
|
9
|
+
allBlocks: ScenarioBlock[];
|
|
10
|
+
realBlockIndex: number;
|
|
11
|
+
pluginsLoaded: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const usePluginEvents: ({ pluginManager, currentBlock, displayedCharacters, blockIndex, isFirstRenderComplete, allBlocks, realBlockIndex, pluginsLoaded, }: UsePluginEventsProps) => void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { normalizeAttributes } from "../utils/attributeNormalizer";
|
|
3
|
+
import { calculateFacePosition } from "../utils/facePositionCalculator";
|
|
4
|
+
export const usePluginEvents = ({ pluginManager, currentBlock, displayedCharacters, blockIndex, isFirstRenderComplete, allBlocks, realBlockIndex, pluginsLoaded, }) => {
|
|
5
|
+
const previousSpeakerRef = useRef({});
|
|
6
|
+
const previousRealBlockIndexRef = useRef(-1);
|
|
7
|
+
const scenarioReadyCalledRef = useRef(false);
|
|
8
|
+
// シナリオ準備完了イベント(初回のみ、プラグイン読み込み完了後)
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
var _a;
|
|
11
|
+
if (!isFirstRenderComplete ||
|
|
12
|
+
!currentBlock ||
|
|
13
|
+
!pluginsLoaded ||
|
|
14
|
+
scenarioReadyCalledRef.current) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
pluginManager.callHook("onScenarioReady", {
|
|
18
|
+
scenario: {
|
|
19
|
+
id: ((_a = allBlocks[0]) === null || _a === void 0 ? void 0 : _a.scenarioId) || "unknown",
|
|
20
|
+
name: "scenario", // TODO: シナリオ名を適切に取得
|
|
21
|
+
},
|
|
22
|
+
allBlocks,
|
|
23
|
+
currentBlockIndex: blockIndex,
|
|
24
|
+
currentBlock,
|
|
25
|
+
});
|
|
26
|
+
scenarioReadyCalledRef.current = true;
|
|
27
|
+
}, [
|
|
28
|
+
pluginManager,
|
|
29
|
+
currentBlock,
|
|
30
|
+
allBlocks,
|
|
31
|
+
blockIndex,
|
|
32
|
+
isFirstRenderComplete,
|
|
33
|
+
pluginsLoaded,
|
|
34
|
+
]);
|
|
35
|
+
// 通り過ぎたaction_nodeを実行
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!isFirstRenderComplete || !pluginsLoaded) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const previousIndex = previousRealBlockIndexRef.current;
|
|
41
|
+
const currentIndex = realBlockIndex;
|
|
42
|
+
// 前回から今回までに通り過ぎたブロックをチェック
|
|
43
|
+
// 初回の場合(previousIndex === -1)は0から処理、そうでなければpreviousIndex + 1から処理
|
|
44
|
+
if (currentIndex > previousIndex) {
|
|
45
|
+
const startIndex = previousIndex === -1 ? 0 : previousIndex + 1;
|
|
46
|
+
for (let i = startIndex; i <= currentIndex; i++) {
|
|
47
|
+
const block = allBlocks[i];
|
|
48
|
+
console.log("block", block);
|
|
49
|
+
if ((block === null || block === void 0 ? void 0 : block.blockType) === "action_node" && block.actionNode) {
|
|
50
|
+
// Normalize attributes from database format
|
|
51
|
+
const normalizedAttributes = normalizeAttributes(block);
|
|
52
|
+
console.log("Normalized attributes:", normalizedAttributes);
|
|
53
|
+
// Try multiple ways to find the ActionNode type - prioritize displayName matching
|
|
54
|
+
const possibleTypes = [
|
|
55
|
+
block.actionNode.nodeType,
|
|
56
|
+
block.actionNode.name, // This will be "背景" from database
|
|
57
|
+
].filter(Boolean);
|
|
58
|
+
let actionNodeDef;
|
|
59
|
+
let foundActionNodeType;
|
|
60
|
+
// First try direct type matching
|
|
61
|
+
for (const actionNodeType of possibleTypes) {
|
|
62
|
+
if (actionNodeType) {
|
|
63
|
+
actionNodeDef =
|
|
64
|
+
pluginManager.getActionNodeDefinition(actionNodeType);
|
|
65
|
+
if (actionNodeDef) {
|
|
66
|
+
foundActionNodeType = actionNodeType;
|
|
67
|
+
console.log(`Found ActionNode definition with type: ${actionNodeType}`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// If no direct match found, try displayName matching
|
|
73
|
+
if (!actionNodeDef && block.actionNode.name) {
|
|
74
|
+
actionNodeDef = pluginManager.getActionNodeDefinition(block.actionNode.name);
|
|
75
|
+
if (actionNodeDef) {
|
|
76
|
+
foundActionNodeType = block.actionNode.name;
|
|
77
|
+
console.log(`Found ActionNode definition with displayName: ${block.actionNode.name}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (actionNodeDef === null || actionNodeDef === void 0 ? void 0 : actionNodeDef.execute) {
|
|
81
|
+
try {
|
|
82
|
+
// Create a clean context object that matches plugin expectations
|
|
83
|
+
// Note: DisplayedCharacter[] is used internally but needs to match ActionExecutionContext signature
|
|
84
|
+
actionNodeDef.execute({
|
|
85
|
+
attributes: normalizedAttributes,
|
|
86
|
+
currentBlock: block,
|
|
87
|
+
currentSpeaker: block.speaker,
|
|
88
|
+
displayedCharacters,
|
|
89
|
+
api: pluginManager,
|
|
90
|
+
});
|
|
91
|
+
console.log(`Successfully executed ActionNode: ${foundActionNodeType}`);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error("Error executing ActionNode:", error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.warn(`ActionNode definition not found for: ${block.actionNode.name} (tried types: ${possibleTypes.join(", ")})`);
|
|
99
|
+
}
|
|
100
|
+
// Legacy support: also call onActionExecute hook (for backward compatibility)
|
|
101
|
+
pluginManager.callHook("onActionExecute", {
|
|
102
|
+
attributes: normalizedAttributes,
|
|
103
|
+
currentBlock: block,
|
|
104
|
+
currentSpeaker: block.speaker,
|
|
105
|
+
displayedCharacters,
|
|
106
|
+
api: pluginManager,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
previousRealBlockIndexRef.current = currentIndex;
|
|
112
|
+
}, [
|
|
113
|
+
pluginManager,
|
|
114
|
+
realBlockIndex,
|
|
115
|
+
allBlocks,
|
|
116
|
+
displayedCharacters,
|
|
117
|
+
isFirstRenderComplete,
|
|
118
|
+
pluginsLoaded,
|
|
119
|
+
]);
|
|
120
|
+
// ブロック変更イベント
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!isFirstRenderComplete || !currentBlock || !pluginsLoaded) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
pluginManager.callHook("onBlockChange", {
|
|
126
|
+
currentBlock,
|
|
127
|
+
blockIndex,
|
|
128
|
+
});
|
|
129
|
+
}, [
|
|
130
|
+
pluginManager,
|
|
131
|
+
currentBlock,
|
|
132
|
+
blockIndex,
|
|
133
|
+
isFirstRenderComplete,
|
|
134
|
+
pluginsLoaded,
|
|
135
|
+
]);
|
|
136
|
+
// キャラクター発話イベント
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
if (!isFirstRenderComplete || !currentBlock || !pluginsLoaded) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// dialogueブロックの場合のみ処理
|
|
143
|
+
if (currentBlock.blockType === "dialogue" && currentBlock.speakerId) {
|
|
144
|
+
const currentSpeaker = {
|
|
145
|
+
id: currentBlock.speakerId,
|
|
146
|
+
name: ((_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name) || "",
|
|
147
|
+
};
|
|
148
|
+
// 前の話者と異なるかチェック
|
|
149
|
+
const isNewSpeaker = previousSpeakerRef.current.id !== currentSpeaker.id;
|
|
150
|
+
// 表示されているキャラクターから話者の位置を取得
|
|
151
|
+
const speakerCharacter = displayedCharacters.find((char) => char.objectId === currentSpeaker.id);
|
|
152
|
+
// キャラクター要素を取得
|
|
153
|
+
const characterElement = document.querySelector(`[data-character-id="${currentSpeaker.id}"]`);
|
|
154
|
+
// 顔位置を計算
|
|
155
|
+
let facePosition;
|
|
156
|
+
if (characterElement && currentBlock.speakerState) {
|
|
157
|
+
const calculatedPosition = calculateFacePosition(characterElement, {
|
|
158
|
+
cropArea: currentBlock.speakerState.cropArea,
|
|
159
|
+
scale: currentBlock.speakerState.scale,
|
|
160
|
+
translateX: currentBlock.speakerState.translateX,
|
|
161
|
+
translateY: currentBlock.speakerState.translateY,
|
|
162
|
+
});
|
|
163
|
+
if (calculatedPosition) {
|
|
164
|
+
facePosition = calculatedPosition;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// characterSpeakHandlerを呼び出し
|
|
168
|
+
pluginManager.callHook("characterSpeakHandler", {
|
|
169
|
+
speaker: {
|
|
170
|
+
id: currentSpeaker.id,
|
|
171
|
+
name: currentSpeaker.name,
|
|
172
|
+
state: (_b = currentBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name,
|
|
173
|
+
positionX: speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.positionX,
|
|
174
|
+
positionY: speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.positionY,
|
|
175
|
+
zIndex: speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.zIndex,
|
|
176
|
+
scale: speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.scale,
|
|
177
|
+
},
|
|
178
|
+
dialogue: {
|
|
179
|
+
content: currentBlock.content || "",
|
|
180
|
+
isNewSpeaker,
|
|
181
|
+
},
|
|
182
|
+
speakerElement: document.querySelector("[data-speaker-name-element]"),
|
|
183
|
+
characterElement,
|
|
184
|
+
previousSpeaker: isNewSpeaker ? previousSpeakerRef.current : undefined,
|
|
185
|
+
facePosition, // 顔位置情報を追加
|
|
186
|
+
});
|
|
187
|
+
// 現在の話者を記録
|
|
188
|
+
previousSpeakerRef.current = currentSpeaker;
|
|
189
|
+
}
|
|
190
|
+
}, [
|
|
191
|
+
pluginManager,
|
|
192
|
+
currentBlock,
|
|
193
|
+
displayedCharacters,
|
|
194
|
+
isFirstRenderComplete,
|
|
195
|
+
pluginsLoaded,
|
|
196
|
+
]);
|
|
197
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
export const usePreloadImages = (scenario) => {
|
|
3
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const imageUrls = new Set();
|
|
6
|
+
// シナリオ全体から画像URLを収集
|
|
7
|
+
scenario.blocks.forEach((block) => {
|
|
8
|
+
var _a;
|
|
9
|
+
// dialogue/narrationブロックのspeaker画像
|
|
10
|
+
if ((_a = block.speakerState) === null || _a === void 0 ? void 0 : _a.imageUrl) {
|
|
11
|
+
imageUrls.add(block.speakerState.imageUrl);
|
|
12
|
+
}
|
|
13
|
+
// character_entranceブロックのキャラクター画像
|
|
14
|
+
if (block.characters) {
|
|
15
|
+
block.characters.forEach((char) => {
|
|
16
|
+
if (char.entityState.imageUrl) {
|
|
17
|
+
imageUrls.add(char.entityState.imageUrl);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
// 画像がない場合は即座に完了
|
|
23
|
+
if (imageUrls.size === 0) {
|
|
24
|
+
setIsLoaded(true);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let loadedCount = 0;
|
|
28
|
+
const totalCount = imageUrls.size;
|
|
29
|
+
// 各画像を事前読み込み
|
|
30
|
+
imageUrls.forEach((url) => {
|
|
31
|
+
const img = new Image();
|
|
32
|
+
const handleLoad = () => {
|
|
33
|
+
loadedCount++;
|
|
34
|
+
if (loadedCount === totalCount) {
|
|
35
|
+
setIsLoaded(true);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const handleError = () => {
|
|
39
|
+
console.error(`Failed to preload image: ${url}`);
|
|
40
|
+
loadedCount++;
|
|
41
|
+
if (loadedCount === totalCount) {
|
|
42
|
+
setIsLoaded(true);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
img.addEventListener("load", handleLoad);
|
|
46
|
+
img.addEventListener("error", handleError);
|
|
47
|
+
img.src = url;
|
|
48
|
+
// クリーンアップ関数で画像を破棄
|
|
49
|
+
return () => {
|
|
50
|
+
img.removeEventListener("load", handleLoad);
|
|
51
|
+
img.removeEventListener("error", handleError);
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}, [scenario]);
|
|
55
|
+
return isLoaded;
|
|
56
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figmaの基準座標(1920×1080)から実際のピクセル座標への変換を提供するフック
|
|
3
|
+
*/
|
|
4
|
+
export declare const useToPixel: () => {
|
|
5
|
+
/**
|
|
6
|
+
* 基準座標(px)を実際のピクセルに変換(高さ基準)
|
|
7
|
+
* @param basisX 基準座標での値
|
|
8
|
+
* @returns 実際のピクセル値
|
|
9
|
+
*/
|
|
10
|
+
pixel: (basisX: number) => number;
|
|
11
|
+
/**
|
|
12
|
+
* 基準座標(px)を実際のピクセルに変換(幅基準)
|
|
13
|
+
* @param basisX 基準座標での値
|
|
14
|
+
* @returns 実際のピクセル値
|
|
15
|
+
*/
|
|
16
|
+
pixelWidth: (basisX: number) => number;
|
|
17
|
+
/**
|
|
18
|
+
* アスペクト比を考慮したサイズを返す
|
|
19
|
+
* @param basisSize 基準サイズ
|
|
20
|
+
* @returns 幅と高さのピクセル値
|
|
21
|
+
*/
|
|
22
|
+
aspectSize: (basisSize: number) => {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* 2D座標を基準から実際のピクセルに変換
|
|
28
|
+
* @param basisPos 基準座標 {x, y}
|
|
29
|
+
* @returns 実際のピクセル座標
|
|
30
|
+
*/
|
|
31
|
+
pos: (basisPos: {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
}) => {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 3D座標を基準から実際のピクセルに変換
|
|
40
|
+
* @param basisPos 基準座標 {x, y, z}
|
|
41
|
+
* @returns 実際のピクセル座標
|
|
42
|
+
*/
|
|
43
|
+
vec3d: (basisPos: {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
z: number;
|
|
47
|
+
}) => {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
z: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* 相対幅(0-1の値)を実際のピクセル幅に変換
|
|
54
|
+
* @param basisWidth 基準幅(0-1)
|
|
55
|
+
* @returns 実際のピクセル幅
|
|
56
|
+
*/
|
|
57
|
+
relativeWidth: (basisWidth: number) => number;
|
|
58
|
+
/**
|
|
59
|
+
* 相対高さ(0-1の値)を実際のピクセル高さに変換
|
|
60
|
+
* @param basisHeight 基準高さ(0-1)
|
|
61
|
+
* @returns 実際のピクセル高さ
|
|
62
|
+
*/
|
|
63
|
+
relativeHeight: (basisHeight: number) => number;
|
|
64
|
+
/**
|
|
65
|
+
* 相対座標(0-1の値)を実際のピクセル座標に変換
|
|
66
|
+
* @param basisPos 相対座標 {x, y}(0-1)
|
|
67
|
+
* @returns 実際のピクセル座標
|
|
68
|
+
*/
|
|
69
|
+
relativePos: (basisPos: {
|
|
70
|
+
x: number;
|
|
71
|
+
y: number;
|
|
72
|
+
}) => {
|
|
73
|
+
x: number;
|
|
74
|
+
y: number;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* 画面サイズを取得するフック
|
|
79
|
+
* @returns 現在の画面サイズ {width, height}
|
|
80
|
+
*/
|
|
81
|
+
export declare const useScreenSize: () => {
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* 画面のスケール倍率を取得するフック
|
|
87
|
+
* @returns 基準サイズに対するスケール倍率
|
|
88
|
+
*/
|
|
89
|
+
export declare const useScreenScale: () => number;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useScreenSizeAtom } from "../atoms/screen-size";
|
|
2
|
+
import { BasisHeight, BasisWidth } from "../constants/screen-size";
|
|
3
|
+
/**
|
|
4
|
+
* Figmaの基準座標(1920×1080)から実際のピクセル座標への変換を提供するフック
|
|
5
|
+
*/
|
|
6
|
+
export const useToPixel = () => {
|
|
7
|
+
const [{ width: screenWidth, height: screenHeight }] = useScreenSizeAtom();
|
|
8
|
+
return {
|
|
9
|
+
/**
|
|
10
|
+
* 基準座標(px)を実際のピクセルに変換(高さ基準)
|
|
11
|
+
* @param basisX 基準座標での値
|
|
12
|
+
* @returns 実際のピクセル値
|
|
13
|
+
*/
|
|
14
|
+
pixel: (basisX) => (basisX / BasisHeight) * screenHeight,
|
|
15
|
+
/**
|
|
16
|
+
* 基準座標(px)を実際のピクセルに変換(幅基準)
|
|
17
|
+
* @param basisX 基準座標での値
|
|
18
|
+
* @returns 実際のピクセル値
|
|
19
|
+
*/
|
|
20
|
+
pixelWidth: (basisX) => (basisX / BasisWidth) * screenWidth,
|
|
21
|
+
/**
|
|
22
|
+
* アスペクト比を考慮したサイズを返す
|
|
23
|
+
* @param basisSize 基準サイズ
|
|
24
|
+
* @returns 幅と高さのピクセル値
|
|
25
|
+
*/
|
|
26
|
+
aspectSize: (basisSize) => ({
|
|
27
|
+
width: (basisSize / BasisHeight) * screenHeight,
|
|
28
|
+
height: (basisSize / BasisHeight) * screenHeight,
|
|
29
|
+
}),
|
|
30
|
+
/**
|
|
31
|
+
* 2D座標を基準から実際のピクセルに変換
|
|
32
|
+
* @param basisPos 基準座標 {x, y}
|
|
33
|
+
* @returns 実際のピクセル座標
|
|
34
|
+
*/
|
|
35
|
+
pos: (basisPos) => ({
|
|
36
|
+
x: (basisPos.x / BasisWidth) * screenWidth,
|
|
37
|
+
y: (basisPos.y / BasisHeight) * screenHeight,
|
|
38
|
+
}),
|
|
39
|
+
/**
|
|
40
|
+
* 3D座標を基準から実際のピクセルに変換
|
|
41
|
+
* @param basisPos 基準座標 {x, y, z}
|
|
42
|
+
* @returns 実際のピクセル座標
|
|
43
|
+
*/
|
|
44
|
+
vec3d: (basisPos) => ({
|
|
45
|
+
x: (basisPos.x / BasisWidth) * screenWidth,
|
|
46
|
+
y: (basisPos.y / BasisHeight) * screenHeight,
|
|
47
|
+
z: (basisPos.z / BasisHeight) * screenHeight,
|
|
48
|
+
}),
|
|
49
|
+
/**
|
|
50
|
+
* 相対幅(0-1の値)を実際のピクセル幅に変換
|
|
51
|
+
* @param basisWidth 基準幅(0-1)
|
|
52
|
+
* @returns 実際のピクセル幅
|
|
53
|
+
*/
|
|
54
|
+
relativeWidth: (basisWidth) => basisWidth * screenWidth,
|
|
55
|
+
/**
|
|
56
|
+
* 相対高さ(0-1の値)を実際のピクセル高さに変換
|
|
57
|
+
* @param basisHeight 基準高さ(0-1)
|
|
58
|
+
* @returns 実際のピクセル高さ
|
|
59
|
+
*/
|
|
60
|
+
relativeHeight: (basisHeight) => basisHeight * screenHeight,
|
|
61
|
+
/**
|
|
62
|
+
* 相対座標(0-1の値)を実際のピクセル座標に変換
|
|
63
|
+
* @param basisPos 相対座標 {x, y}(0-1)
|
|
64
|
+
* @returns 実際のピクセル座標
|
|
65
|
+
*/
|
|
66
|
+
relativePos: (basisPos) => ({
|
|
67
|
+
x: basisPos.x * screenWidth,
|
|
68
|
+
y: basisPos.y * screenHeight,
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* 画面サイズを取得するフック
|
|
74
|
+
* @returns 現在の画面サイズ {width, height}
|
|
75
|
+
*/
|
|
76
|
+
export const useScreenSize = () => {
|
|
77
|
+
const [screenSize] = useScreenSizeAtom();
|
|
78
|
+
return screenSize;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* 画面のスケール倍率を取得するフック
|
|
82
|
+
* @returns 基準サイズに対するスケール倍率
|
|
83
|
+
*/
|
|
84
|
+
export const useScreenScale = () => {
|
|
85
|
+
const [{ height: screenHeight }] = useScreenSizeAtom();
|
|
86
|
+
return screenHeight / BasisHeight;
|
|
87
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface UseTypewriterOptions {
|
|
2
|
+
speed?: number;
|
|
3
|
+
onComplete?: () => void;
|
|
4
|
+
}
|
|
5
|
+
export declare const useTypewriter: (options?: UseTypewriterOptions) => {
|
|
6
|
+
displayText: string;
|
|
7
|
+
isTyping: boolean;
|
|
8
|
+
startTyping: (text: string) => void;
|
|
9
|
+
skipTyping: (text: string) => void;
|
|
10
|
+
reset: () => void;
|
|
11
|
+
};
|