@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,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useDataAPI } from "../contexts/DataContext";
|
|
3
|
+
export const DialogueBox = () => {
|
|
4
|
+
var _a, _b;
|
|
5
|
+
const dataAPI = useDataAPI();
|
|
6
|
+
const currentBlock = dataAPI.get("playback", "currentBlock");
|
|
7
|
+
const content = dataAPI.get("playback", "displayText") || "";
|
|
8
|
+
const displayedCharacters = dataAPI.get("playback", "displayedCharacters") || [];
|
|
9
|
+
if (!currentBlock) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const speakerName = (_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name;
|
|
13
|
+
const currentSpeakerId = (_b = currentBlock.speaker) === null || _b === void 0 ? void 0 : _b.id;
|
|
14
|
+
// 話者の位置を特定
|
|
15
|
+
const speakerCharacter = displayedCharacters.find((char) => char.objectId === currentSpeakerId);
|
|
16
|
+
// 位置に基づいて名前表示のクラスを決定
|
|
17
|
+
const getNamePositionClass = () => {
|
|
18
|
+
if (!(speakerCharacter === null || speakerCharacter === void 0 ? void 0 : speakerCharacter.positionX))
|
|
19
|
+
return "justify-center";
|
|
20
|
+
const positionX = speakerCharacter.positionX;
|
|
21
|
+
if (positionX < 0.33)
|
|
22
|
+
return "justify-start -ml-[5%]"; // 左側
|
|
23
|
+
if (positionX > 0.67)
|
|
24
|
+
return "justify-end -mr-[5%]"; // 右側
|
|
25
|
+
return "justify-center"; // 中央
|
|
26
|
+
};
|
|
27
|
+
return (_jsx("div", { className: "absolute bottom-[5%] max-w-[60%] mx-auto h-[25%] left-0 right-0 bg-amber-50 border-4 rounded-xl border-amber-800 z-10 pointer-events-auto", style: { zIndex: 1000 }, "data-dialogue-element": true, children: _jsxs("div", { className: "relative space-y-3", children: [speakerName && (_jsx("div", { className: `flex items-center space-x-2 ${getNamePositionClass()}`, children: _jsx("div", { className: "bg-amber-950 text-white rounded-xl relative -top-5 w-full max-w-30 text-center border-4 border-amber-50 shadow-lg", "data-speaker-name-element": true, children: _jsx("span", { className: "font-bold text-lg", children: speakerName }) }) })), _jsx("div", { className: "absolute top-0 p-4 leading-relaxed text-2xl", children: _jsx("p", { children: content }) }), _jsx("div", { className: "flex justify-end", children: _jsx("div", { className: "text-white/60 text-sm animate-pulse", children: "\u25BC" }) })] }) }));
|
|
28
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { clsx } from "clsx";
|
|
3
|
+
export const EndScreen = ({ scenarioName, onRestart, className, }) => {
|
|
4
|
+
return (_jsxs("div", { className: clsx("flex flex-col items-center justify-center p-8 space-y-4", className), children: [_jsx("h2", { className: "text-2xl font-bold text-white", children: "\u30B7\u30CA\u30EA\u30AA\u7D42\u4E86" }), _jsx("p", { className: "text-gray-300", children: scenarioName }), _jsx("button", { type: "button", onClick: onRestart, className: "px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors", children: "\u6700\u521D\u304B\u3089\u518D\u751F" })] }));
|
|
5
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { DisplayedCharacter, PublishedScenario, ScenarioBlock } from "../types";
|
|
3
|
+
interface GameScreenProps {
|
|
4
|
+
scenario: PublishedScenario;
|
|
5
|
+
currentBlock: ScenarioBlock;
|
|
6
|
+
displayedCharacters: DisplayedCharacter[];
|
|
7
|
+
}
|
|
8
|
+
export declare const GameScreen: React.FC<GameScreenProps>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
export const GameScreen = ({ scenario, currentBlock, displayedCharacters, }) => {
|
|
4
|
+
// シナリオ全体で使用される全ての画像を収集
|
|
5
|
+
const allImages = useMemo(() => {
|
|
6
|
+
const images = new Map();
|
|
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
|
+
block.speakerId &&
|
|
12
|
+
block.speakerStateId) {
|
|
13
|
+
const key = `${block.speakerId}-${block.speakerStateId}`;
|
|
14
|
+
images.set(key, {
|
|
15
|
+
url: block.speakerState.imageUrl,
|
|
16
|
+
objectId: block.speakerId,
|
|
17
|
+
entityStateId: block.speakerStateId,
|
|
18
|
+
scale: block.speakerState.scale,
|
|
19
|
+
translateY: block.speakerState.translateY,
|
|
20
|
+
translateX: block.speakerState.translateX,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// character_entranceブロックのキャラクター画像
|
|
24
|
+
if (block.characters) {
|
|
25
|
+
block.characters.forEach((char) => {
|
|
26
|
+
if (char.entityState.imageUrl) {
|
|
27
|
+
const key = `${char.objectId}-${char.entityStateId}`;
|
|
28
|
+
images.set(key, {
|
|
29
|
+
url: char.entityState.imageUrl,
|
|
30
|
+
objectId: char.objectId,
|
|
31
|
+
entityStateId: char.entityStateId,
|
|
32
|
+
scale: char.entityState.scale,
|
|
33
|
+
translateY: char.entityState.translateY,
|
|
34
|
+
translateX: char.entityState.translateX,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return Array.from(images.values());
|
|
41
|
+
}, [scenario.blocks]);
|
|
42
|
+
// 現在の話者
|
|
43
|
+
const currentSpeaker = (currentBlock === null || currentBlock === void 0 ? void 0 : currentBlock.speakerId)
|
|
44
|
+
? displayedCharacters.find((char) => char.objectId === currentBlock.speakerId)
|
|
45
|
+
: null;
|
|
46
|
+
return (_jsx("div", { className: "w-full h-full relative", children: allImages.map((image) => {
|
|
47
|
+
var _a, _b, _c, _d, _e;
|
|
48
|
+
// 複数キャラクター表示の場合、表示されているキャラクターを探す
|
|
49
|
+
const displayedChar = displayedCharacters.find((char) => char.objectId === image.objectId &&
|
|
50
|
+
char.entityStateId === image.entityStateId);
|
|
51
|
+
// 単一キャラクター表示の場合、現在の話者かチェック
|
|
52
|
+
const isCurrentSpeaker = displayedCharacters.length === 0 &&
|
|
53
|
+
currentBlock.speakerId === image.objectId &&
|
|
54
|
+
currentBlock.speakerStateId === image.entityStateId;
|
|
55
|
+
// 表示すべきかどうか
|
|
56
|
+
const shouldDisplay = displayedChar || isCurrentSpeaker;
|
|
57
|
+
// 明るさを決定
|
|
58
|
+
let brightness = 1;
|
|
59
|
+
if (displayedCharacters.length > 0 && displayedChar) {
|
|
60
|
+
// 複数キャラクター表示で、現在の話者でない場合
|
|
61
|
+
if (currentSpeaker &&
|
|
62
|
+
currentSpeaker.objectId !== displayedChar.objectId) {
|
|
63
|
+
brightness = 0.8;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// z-indexを決定
|
|
67
|
+
const zIndex = (_a = displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.zIndex) !== null && _a !== void 0 ? _a : (displayedChar ? 200 : 0);
|
|
68
|
+
// スケールを決定
|
|
69
|
+
// displayedCharのscaleが設定されている場合はそれを使用し、EntityStateのscaleと組み合わせる
|
|
70
|
+
// 設定されていない場合はEntityStateのscaleのみ使用
|
|
71
|
+
const characterScale = (displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.scale)
|
|
72
|
+
? displayedChar.scale * ((_b = image.scale) !== null && _b !== void 0 ? _b : 1)
|
|
73
|
+
: ((_c = image.scale) !== null && _c !== void 0 ? _c : 1);
|
|
74
|
+
// デバッグ: スケール値をログ出力
|
|
75
|
+
if ((displayedChar === null || displayedChar === void 0 ? void 0 : displayedChar.scale) && displayedChar.scale !== 1) {
|
|
76
|
+
console.log(`Character ${image.objectId} - displayedChar.scale: ${displayedChar.scale}, image.scale: ${image.scale}, final: ${characterScale}`);
|
|
77
|
+
}
|
|
78
|
+
// 位置を決定(カスタム位置またはデフォルト位置)
|
|
79
|
+
let finalPosition = { x: 0.5, y: 1.0 }; // デフォルトは中央下
|
|
80
|
+
if (displayedChar) {
|
|
81
|
+
// カスタム位置が設定されている場合
|
|
82
|
+
if (displayedChar.positionX !== null &&
|
|
83
|
+
displayedChar.positionY !== null) {
|
|
84
|
+
finalPosition = {
|
|
85
|
+
x: (_d = displayedChar.positionX) !== null && _d !== void 0 ? _d : 0.5,
|
|
86
|
+
y: (_e = displayedChar.positionY) !== null && _e !== void 0 ? _e : 1.0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// カスタム位置が設定されていない場合はデフォルト位置のまま
|
|
90
|
+
}
|
|
91
|
+
// 高さ基準の位置計算(中央基準)
|
|
92
|
+
// 画面の中央(50vw)を基準として、vh単位で位置を指定
|
|
93
|
+
// positionX: 0.0 = 中央から -56.25vh(左端), 0.5 = 中央, 1.0 = 中央から +56.25vh(右端)
|
|
94
|
+
const aspectRatio = 16 / 9;
|
|
95
|
+
const maxOffsetVh = 100 / aspectRatio / 2; // 56.25vh (16:9比率での半幅)
|
|
96
|
+
const offsetFromCenterVh = (finalPosition.x - 0.5) * 2 * maxOffsetVh;
|
|
97
|
+
const topVh = finalPosition.y * 100; // vh単位での上端からの距離
|
|
98
|
+
return (_jsx("div", { className: "absolute w-auto h-4/5 flex items-end", style: {
|
|
99
|
+
visibility: shouldDisplay ? "visible" : "hidden",
|
|
100
|
+
filter: `brightness(${brightness})`,
|
|
101
|
+
zIndex: zIndex,
|
|
102
|
+
left: "50vw",
|
|
103
|
+
top: `${topVh}vh`,
|
|
104
|
+
transform: `translate(calc(-50% + ${offsetFromCenterVh}vh), -100%)`, // 中央から相対位置 + 下揃え
|
|
105
|
+
}, "data-character-id": image.objectId, "data-character-sprite": true, children: _jsx("div", { className: "w-full h-full flex items-end justify-center", children: _jsx("img", { src: image.url, alt: "", className: "max-w-full max-h-full", style: {
|
|
106
|
+
objectFit: "contain",
|
|
107
|
+
transform: `scale(${characterScale}) translateX(${image.translateX || 0}%) translateY(${image.translateY || 0}%)`,
|
|
108
|
+
transformOrigin: "bottom center",
|
|
109
|
+
} }) }) }, `${image.objectId}-${image.entityStateId}`));
|
|
110
|
+
}) }));
|
|
111
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
interface OverlayUIProps {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* OverlayUI コンポーネント
|
|
7
|
+
*
|
|
8
|
+
* 16:9のアスペクト比を維持しながら、様々な画面サイズに対応するコンテナ
|
|
9
|
+
* - スマートフォン(幅 < 1000px): フルサイズ表示
|
|
10
|
+
* - タブレット(幅 ≥ 1000px): 70%に縮小して中央配置
|
|
11
|
+
*/
|
|
12
|
+
export declare const OverlayUI: React.FC<OverlayUIProps>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useScreenSizeAtom } from "../atoms/screen-size";
|
|
3
|
+
/**
|
|
4
|
+
* OverlayUI コンポーネント
|
|
5
|
+
*
|
|
6
|
+
* 16:9のアスペクト比を維持しながら、様々な画面サイズに対応するコンテナ
|
|
7
|
+
* - スマートフォン(幅 < 1000px): フルサイズ表示
|
|
8
|
+
* - タブレット(幅 ≥ 1000px): 70%に縮小して中央配置
|
|
9
|
+
*/
|
|
10
|
+
export const OverlayUI = ({ children }) => {
|
|
11
|
+
// 16:9のアスペクト比を基準
|
|
12
|
+
let [{ height: screenHeight }] = useScreenSizeAtom();
|
|
13
|
+
let screenWidth = (screenHeight * 16) / 9;
|
|
14
|
+
// タブレット(幅1000px以上)では70%に縮小
|
|
15
|
+
const [{ width: checkWidth }] = useScreenSizeAtom();
|
|
16
|
+
if (checkWidth >= 1000) {
|
|
17
|
+
screenHeight = (screenHeight * 7) / 10;
|
|
18
|
+
screenWidth = (screenHeight * 16) / 9;
|
|
19
|
+
}
|
|
20
|
+
return (_jsx("div", { className: "absolute inset-0 pointer-events-none z-[9999] flex justify-center items-center", children: _jsx("div", { style: { width: screenWidth, height: screenHeight }, children: children }) }));
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { PluginManager } from "../plugin/PluginManager";
|
|
3
|
+
import { ComponentType } from "../sdk";
|
|
4
|
+
interface PluginComponentProviderProps {
|
|
5
|
+
type: ComponentType;
|
|
6
|
+
pluginManager: PluginManager;
|
|
7
|
+
fallback?: React.ComponentType<any>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wrapper component that renders a plugin-registered component or a fallback
|
|
11
|
+
* All data is provided via DataContext, so no props are passed
|
|
12
|
+
*/
|
|
13
|
+
export declare function PluginComponentProvider({ type, pluginManager, fallback: FallbackComponent, }: PluginComponentProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Wrapper component that renders a plugin-registered component or a fallback
|
|
4
|
+
* All data is provided via DataContext, so no props are passed
|
|
5
|
+
*/
|
|
6
|
+
export function PluginComponentProvider({ type, pluginManager, fallback: FallbackComponent, }) {
|
|
7
|
+
const Component = pluginManager.getComponent(type);
|
|
8
|
+
if (Component) {
|
|
9
|
+
return _jsx(Component, {}, `plugin-component-${type}`);
|
|
10
|
+
}
|
|
11
|
+
if (FallbackComponent) {
|
|
12
|
+
return _jsx(FallbackComponent, {}, `fallback-component-${type}`);
|
|
13
|
+
}
|
|
14
|
+
// If no component is registered and no fallback provided, show a warning
|
|
15
|
+
console.warn(`No component registered for type: ${type}`);
|
|
16
|
+
return (_jsxs("div", { style: {
|
|
17
|
+
padding: "20px",
|
|
18
|
+
backgroundColor: "#f0f0f0",
|
|
19
|
+
border: "1px dashed #999",
|
|
20
|
+
borderRadius: "4px",
|
|
21
|
+
color: "#666",
|
|
22
|
+
textAlign: "center",
|
|
23
|
+
}, children: [_jsxs("p", { children: ["Component not found: ", type] }), _jsx("p", { style: { fontSize: "12px", marginTop: "10px" }, children: "Please install a plugin that provides this component" })] }, `warning-component-${type}`));
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { DataAPI, DataContext } from "../sdk";
|
|
3
|
+
/**
|
|
4
|
+
* データプロバイダー
|
|
5
|
+
* プラグインがシナリオ情報にリアクティブにアクセスするためのProvider
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <DataProvider data={dataContext}>
|
|
10
|
+
* <PluginComponentProvider manager={pluginManager}>
|
|
11
|
+
* {children}
|
|
12
|
+
* </PluginComponentProvider>
|
|
13
|
+
* </DataProvider>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare const DataProvider: React.FC<{
|
|
17
|
+
data: DataContext;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* DataAPI実装を提供するフック
|
|
22
|
+
* プラグインから呼び出される
|
|
23
|
+
*/
|
|
24
|
+
export declare function useDataAPI(): DataAPI;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from "react";
|
|
3
|
+
const DataContextInstance = createContext(null);
|
|
4
|
+
const SubscribersContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* データプロバイダー
|
|
7
|
+
* プラグインがシナリオ情報にリアクティブにアクセスするためのProvider
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <DataProvider data={dataContext}>
|
|
12
|
+
* <PluginComponentProvider manager={pluginManager}>
|
|
13
|
+
* {children}
|
|
14
|
+
* </PluginComponentProvider>
|
|
15
|
+
* </DataProvider>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const DataProvider = ({ data, children }) => {
|
|
19
|
+
const subscribers = useMemo(() => new Map(), // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
20
|
+
[]);
|
|
21
|
+
const previousDataRef = useRef(data);
|
|
22
|
+
// データ変更時に購読者に通知
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const prevData = previousDataRef.current;
|
|
25
|
+
const currentData = data;
|
|
26
|
+
// 各カテゴリの変更をチェック
|
|
27
|
+
for (const key of Object.keys(currentData)) {
|
|
28
|
+
const prevValue = prevData[key];
|
|
29
|
+
const currentValue = currentData[key];
|
|
30
|
+
// カテゴリ全体が変更された場合
|
|
31
|
+
if (prevValue !== currentValue) {
|
|
32
|
+
// カテゴリ全体の購読者に通知
|
|
33
|
+
const categorySubscribers = subscribers.get(key);
|
|
34
|
+
categorySubscribers === null || categorySubscribers === void 0 ? void 0 : categorySubscribers.forEach((callback) => callback(currentValue));
|
|
35
|
+
// 個別プロパティの変更をチェック
|
|
36
|
+
if (typeof currentValue === "object" && currentValue !== null) {
|
|
37
|
+
for (const prop of Object.keys(currentValue)) {
|
|
38
|
+
const prevPropValue = prevValue === null || prevValue === void 0 ? void 0 : prevValue[prop]; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const currentPropValue = currentValue[prop]; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
40
|
+
if (prevPropValue !== currentPropValue) {
|
|
41
|
+
const propKey = `${key}.${prop}`;
|
|
42
|
+
const propSubscribers = subscribers.get(propKey);
|
|
43
|
+
propSubscribers === null || propSubscribers === void 0 ? void 0 : propSubscribers.forEach((callback) => callback(currentPropValue));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
previousDataRef.current = data;
|
|
50
|
+
}, [data, subscribers]);
|
|
51
|
+
return (_jsx(SubscribersContext.Provider, { value: subscribers, children: _jsx(DataContextInstance.Provider, { value: data, children: children }) }));
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* DataAPI実装を提供するフック
|
|
55
|
+
* プラグインから呼び出される
|
|
56
|
+
*/
|
|
57
|
+
export function useDataAPI() {
|
|
58
|
+
const data = useContext(DataContextInstance);
|
|
59
|
+
const subscribers = useContext(SubscribersContext);
|
|
60
|
+
if (!data || !subscribers) {
|
|
61
|
+
throw new Error("useDataAPI must be used within DataProvider");
|
|
62
|
+
}
|
|
63
|
+
const get = useCallback((key, property) => {
|
|
64
|
+
var _a;
|
|
65
|
+
if (property !== undefined) {
|
|
66
|
+
return (_a = data[key]) === null || _a === void 0 ? void 0 : _a[property];
|
|
67
|
+
}
|
|
68
|
+
return data[key];
|
|
69
|
+
}, [data]);
|
|
70
|
+
const subscribe = useCallback((key, callback) => {
|
|
71
|
+
var _a;
|
|
72
|
+
const subscriberKey = key;
|
|
73
|
+
if (!subscribers.has(subscriberKey)) {
|
|
74
|
+
subscribers.set(subscriberKey, new Set());
|
|
75
|
+
}
|
|
76
|
+
(_a = subscribers.get(subscriberKey)) === null || _a === void 0 ? void 0 : _a.add(callback);
|
|
77
|
+
// unsubscribe関数を返す
|
|
78
|
+
return () => {
|
|
79
|
+
var _a;
|
|
80
|
+
(_a = subscribers.get(subscriberKey)) === null || _a === void 0 ? void 0 : _a.delete(callback);
|
|
81
|
+
};
|
|
82
|
+
}, [subscribers]);
|
|
83
|
+
const watch = useCallback((key, property, callback) => {
|
|
84
|
+
var _a;
|
|
85
|
+
const subscriberKey = `${key}.${property}`;
|
|
86
|
+
if (!subscribers.has(subscriberKey)) {
|
|
87
|
+
subscribers.set(subscriberKey, new Set());
|
|
88
|
+
}
|
|
89
|
+
(_a = subscribers.get(subscriberKey)) === null || _a === void 0 ? void 0 : _a.add(callback);
|
|
90
|
+
// unsubscribe関数を返す
|
|
91
|
+
return () => {
|
|
92
|
+
var _a;
|
|
93
|
+
(_a = subscribers.get(subscriberKey)) === null || _a === void 0 ? void 0 : _a.delete(callback);
|
|
94
|
+
};
|
|
95
|
+
}, [subscribers]);
|
|
96
|
+
return useMemo(() => ({
|
|
97
|
+
get,
|
|
98
|
+
subscribe,
|
|
99
|
+
watch,
|
|
100
|
+
}), [get, subscribe, watch]);
|
|
101
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BacklogEntry, PublishedScenario, ScenarioBlock } from "../types";
|
|
2
|
+
interface UseBacklogOptions {
|
|
3
|
+
scenario: PublishedScenario;
|
|
4
|
+
currentBlockIndex: number;
|
|
5
|
+
currentBlock: ScenarioBlock | null;
|
|
6
|
+
}
|
|
7
|
+
interface UseBacklogReturn {
|
|
8
|
+
logs: BacklogEntry[];
|
|
9
|
+
addLogEntry: (entry: BacklogEntry) => void;
|
|
10
|
+
clearLogs: () => void;
|
|
11
|
+
buildInitialHistory: (upToIndex: number) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function useBacklog({ scenario, currentBlockIndex, currentBlock, }: UseBacklogOptions): UseBacklogReturn;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
export function useBacklog({ scenario, currentBlockIndex, currentBlock, }) {
|
|
3
|
+
const [logs, setLogs] = useState([]);
|
|
4
|
+
const hasBuiltInitialHistory = useRef(false);
|
|
5
|
+
const processedBlocks = useRef(new Set());
|
|
6
|
+
// ログエントリを追加する関数
|
|
7
|
+
const addLogEntry = useCallback((entry) => {
|
|
8
|
+
setLogs((prevLogs) => {
|
|
9
|
+
// 既存の同じブロックインデックスのエントリを削除(重複防止)
|
|
10
|
+
const filteredLogs = prevLogs.filter((log) => log.blockIndex !== entry.blockIndex);
|
|
11
|
+
// 新しいエントリを追加してソート
|
|
12
|
+
const newLogs = [...filteredLogs, entry];
|
|
13
|
+
return newLogs.sort((a, b) => a.blockIndex - b.blockIndex);
|
|
14
|
+
});
|
|
15
|
+
}, []);
|
|
16
|
+
// ログをクリアする関数
|
|
17
|
+
const clearLogs = useCallback(() => {
|
|
18
|
+
setLogs([]);
|
|
19
|
+
hasBuiltInitialHistory.current = false;
|
|
20
|
+
processedBlocks.current.clear();
|
|
21
|
+
}, []);
|
|
22
|
+
// 初期履歴を構築する関数
|
|
23
|
+
const buildInitialHistory = useCallback((upToIndex) => {
|
|
24
|
+
const conversationBlocks = scenario.blocks
|
|
25
|
+
.slice(0, upToIndex + 1)
|
|
26
|
+
.map((block, index) => ({ block, realIndex: index }))
|
|
27
|
+
.filter(({ block }) => block.blockType === "dialogue" || block.blockType === "narration");
|
|
28
|
+
console.log(`Building conversation history up to index ${upToIndex}, found ${conversationBlocks.length} conversation blocks`);
|
|
29
|
+
conversationBlocks.forEach(({ block, realIndex }) => {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
if (!processedBlocks.current.has(realIndex)) {
|
|
32
|
+
const logEntry = {
|
|
33
|
+
id: `${block.id}_init`,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
blockIndex: realIndex,
|
|
36
|
+
blockType: block.blockType,
|
|
37
|
+
content: block.content,
|
|
38
|
+
speakerName: (_a = block.speaker) === null || _a === void 0 ? void 0 : _a.name,
|
|
39
|
+
speakerState: (_b = block.speakerState) === null || _b === void 0 ? void 0 : _b.name,
|
|
40
|
+
};
|
|
41
|
+
addLogEntry(logEntry);
|
|
42
|
+
processedBlocks.current.add(realIndex);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
hasBuiltInitialHistory.current = true;
|
|
46
|
+
}, [scenario.blocks, addLogEntry]);
|
|
47
|
+
// 新しいブロックを処理
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
var _a, _b;
|
|
50
|
+
if (!currentBlock || !hasBuiltInitialHistory.current) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// 会話ブロックのみログに追加
|
|
54
|
+
if (currentBlock.blockType === "dialogue" ||
|
|
55
|
+
currentBlock.blockType === "narration") {
|
|
56
|
+
if (!processedBlocks.current.has(currentBlockIndex)) {
|
|
57
|
+
console.log("Adding new conversation block to log:", currentBlock);
|
|
58
|
+
const logEntry = {
|
|
59
|
+
id: `${currentBlock.id}_${Date.now()}`,
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
blockIndex: currentBlockIndex,
|
|
62
|
+
blockType: currentBlock.blockType,
|
|
63
|
+
content: currentBlock.content,
|
|
64
|
+
speakerName: (_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name,
|
|
65
|
+
speakerState: (_b = currentBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name,
|
|
66
|
+
};
|
|
67
|
+
addLogEntry(logEntry);
|
|
68
|
+
processedBlocks.current.add(currentBlockIndex);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, [currentBlock, currentBlockIndex, addLogEntry]);
|
|
72
|
+
// シナリオ変更時にリセット
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
clearLogs();
|
|
75
|
+
}, [scenario.id, clearLogs]);
|
|
76
|
+
return {
|
|
77
|
+
logs,
|
|
78
|
+
addLogEntry,
|
|
79
|
+
clearLogs,
|
|
80
|
+
buildInitialHistory,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConversationLogEntry, PublishedScenario, ScenarioBlock } from "../types";
|
|
2
|
+
interface UseConversationLogOptions {
|
|
3
|
+
scenario: PublishedScenario;
|
|
4
|
+
currentBlockIndex: number;
|
|
5
|
+
currentBlock: ScenarioBlock | null;
|
|
6
|
+
}
|
|
7
|
+
interface UseConversationLogReturn {
|
|
8
|
+
logs: ConversationLogEntry[];
|
|
9
|
+
addLogEntry: (entry: ConversationLogEntry) => void;
|
|
10
|
+
clearLogs: () => void;
|
|
11
|
+
buildInitialHistory: (upToIndex: number) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function useConversationLog({ scenario, currentBlockIndex, currentBlock, }: UseConversationLogOptions): UseConversationLogReturn;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
export function useConversationLog({ scenario, currentBlockIndex, currentBlock, }) {
|
|
3
|
+
const [logs, setLogs] = useState([]);
|
|
4
|
+
const hasBuiltInitialHistory = useRef(false);
|
|
5
|
+
const processedBlocks = useRef(new Set());
|
|
6
|
+
// ログエントリを追加する関数
|
|
7
|
+
const addLogEntry = useCallback((entry) => {
|
|
8
|
+
setLogs((prevLogs) => {
|
|
9
|
+
// 既存の同じブロックインデックスのエントリを削除(重複防止)
|
|
10
|
+
const filteredLogs = prevLogs.filter((log) => log.blockIndex !== entry.blockIndex);
|
|
11
|
+
// 新しいエントリを追加してソート
|
|
12
|
+
const newLogs = [...filteredLogs, entry];
|
|
13
|
+
return newLogs.sort((a, b) => a.blockIndex - b.blockIndex);
|
|
14
|
+
});
|
|
15
|
+
}, []);
|
|
16
|
+
// ログをクリアする関数
|
|
17
|
+
const clearLogs = useCallback(() => {
|
|
18
|
+
setLogs([]);
|
|
19
|
+
hasBuiltInitialHistory.current = false;
|
|
20
|
+
processedBlocks.current.clear();
|
|
21
|
+
}, []);
|
|
22
|
+
// 初期履歴を構築する関数
|
|
23
|
+
const buildInitialHistory = useCallback((upToIndex) => {
|
|
24
|
+
const conversationBlocks = scenario.blocks
|
|
25
|
+
.slice(0, upToIndex + 1)
|
|
26
|
+
.map((block, index) => ({ block, realIndex: index }))
|
|
27
|
+
.filter(({ block }) => block.blockType === "dialogue" || block.blockType === "narration");
|
|
28
|
+
console.log(`Building conversation history up to index ${upToIndex}, found ${conversationBlocks.length} conversation blocks`);
|
|
29
|
+
conversationBlocks.forEach(({ block, realIndex }) => {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
if (!processedBlocks.current.has(realIndex)) {
|
|
32
|
+
const logEntry = {
|
|
33
|
+
id: `${block.id}_init`,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
blockIndex: realIndex,
|
|
36
|
+
blockType: block.blockType,
|
|
37
|
+
content: block.content,
|
|
38
|
+
speakerName: (_a = block.speaker) === null || _a === void 0 ? void 0 : _a.name,
|
|
39
|
+
speakerState: (_b = block.speakerState) === null || _b === void 0 ? void 0 : _b.name,
|
|
40
|
+
};
|
|
41
|
+
addLogEntry(logEntry);
|
|
42
|
+
processedBlocks.current.add(realIndex);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
hasBuiltInitialHistory.current = true;
|
|
46
|
+
}, [scenario.blocks, addLogEntry]);
|
|
47
|
+
// 新しいブロックを処理
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
var _a, _b;
|
|
50
|
+
if (!currentBlock || !hasBuiltInitialHistory.current) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// 会話ブロックのみログに追加
|
|
54
|
+
if (currentBlock.blockType === "dialogue" ||
|
|
55
|
+
currentBlock.blockType === "narration") {
|
|
56
|
+
if (!processedBlocks.current.has(currentBlockIndex)) {
|
|
57
|
+
console.log("Adding new conversation block to log:", currentBlock);
|
|
58
|
+
const logEntry = {
|
|
59
|
+
id: `${currentBlock.id}_${Date.now()}`,
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
blockIndex: currentBlockIndex,
|
|
62
|
+
blockType: currentBlock.blockType,
|
|
63
|
+
content: currentBlock.content,
|
|
64
|
+
speakerName: (_a = currentBlock.speaker) === null || _a === void 0 ? void 0 : _a.name,
|
|
65
|
+
speakerState: (_b = currentBlock.speakerState) === null || _b === void 0 ? void 0 : _b.name,
|
|
66
|
+
};
|
|
67
|
+
addLogEntry(logEntry);
|
|
68
|
+
processedBlocks.current.add(currentBlockIndex);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, [currentBlock, currentBlockIndex, addLogEntry]);
|
|
72
|
+
// シナリオ変更時にリセット
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
clearLogs();
|
|
75
|
+
}, [scenario.id, clearLogs]);
|
|
76
|
+
return {
|
|
77
|
+
logs,
|
|
78
|
+
addLogEntry,
|
|
79
|
+
clearLogs,
|
|
80
|
+
buildInitialHistory,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DisplayedCharacter, PlayerState, PublishedScenario, ScenarioBlock } from "../types";
|
|
2
|
+
interface UsePlayerLogicProps {
|
|
3
|
+
state: PlayerState;
|
|
4
|
+
setState: React.Dispatch<React.SetStateAction<PlayerState>>;
|
|
5
|
+
scenario: PublishedScenario;
|
|
6
|
+
isTyping: boolean;
|
|
7
|
+
currentBlock: ScenarioBlock;
|
|
8
|
+
skipTyping: (text: string) => void;
|
|
9
|
+
onEnd?: () => void;
|
|
10
|
+
onScenarioEnd?: () => void;
|
|
11
|
+
autoplay: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const usePlayerLogic: ({ state, setState, scenario, isTyping, currentBlock, skipTyping, onEnd, onScenarioEnd, autoplay, }: UsePlayerLogicProps) => {
|
|
14
|
+
handleNext: () => void;
|
|
15
|
+
handlePrevious: () => void;
|
|
16
|
+
togglePlay: () => void;
|
|
17
|
+
restart: () => void;
|
|
18
|
+
isLastBlock: boolean;
|
|
19
|
+
displayedCharacters: DisplayedCharacter[];
|
|
20
|
+
};
|
|
21
|
+
export {};
|