@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.
Files changed (59) hide show
  1. package/dist/Player.d.ts +3 -0
  2. package/dist/Player.js +336 -0
  3. package/dist/atoms/screen-size.d.ts +22 -0
  4. package/dist/atoms/screen-size.js +8 -0
  5. package/dist/components/BacklogUI.d.ts +2 -0
  6. package/dist/components/BacklogUI.js +115 -0
  7. package/dist/components/ConversationLogUI.d.ts +2 -0
  8. package/dist/components/ConversationLogUI.js +115 -0
  9. package/dist/components/DebugControls.d.ts +12 -0
  10. package/dist/components/DebugControls.js +5 -0
  11. package/dist/components/DialogueBox.d.ts +2 -0
  12. package/dist/components/DialogueBox.js +28 -0
  13. package/dist/components/EndScreen.d.ts +8 -0
  14. package/dist/components/EndScreen.js +5 -0
  15. package/dist/components/GameScreen.d.ts +9 -0
  16. package/dist/components/GameScreen.js +111 -0
  17. package/dist/components/OverlayUI.d.ts +13 -0
  18. package/dist/components/OverlayUI.js +21 -0
  19. package/dist/components/PluginComponentProvider.d.ts +14 -0
  20. package/dist/components/PluginComponentProvider.js +24 -0
  21. package/dist/constants/screen-size.d.ts +3 -0
  22. package/dist/constants/screen-size.js +6 -0
  23. package/dist/contexts/DataContext.d.ts +24 -0
  24. package/dist/contexts/DataContext.js +101 -0
  25. package/dist/hooks/useBacklog.d.ts +14 -0
  26. package/dist/hooks/useBacklog.js +82 -0
  27. package/dist/hooks/useConversationLog.d.ts +14 -0
  28. package/dist/hooks/useConversationLog.js +82 -0
  29. package/dist/hooks/usePlayerLogic.d.ts +21 -0
  30. package/dist/hooks/usePlayerLogic.js +145 -0
  31. package/dist/hooks/usePluginAPI.d.ts +19 -0
  32. package/dist/hooks/usePluginAPI.js +42 -0
  33. package/dist/hooks/usePluginEvents.d.ts +14 -0
  34. package/dist/hooks/usePluginEvents.js +197 -0
  35. package/dist/hooks/usePreloadImages.d.ts +2 -0
  36. package/dist/hooks/usePreloadImages.js +56 -0
  37. package/dist/hooks/useScreenSize.d.ts +89 -0
  38. package/dist/hooks/useScreenSize.js +87 -0
  39. package/dist/hooks/useTypewriter.d.ts +11 -0
  40. package/dist/hooks/useTypewriter.js +56 -0
  41. package/dist/hooks/useUIVisibility.d.ts +9 -0
  42. package/dist/hooks/useUIVisibility.js +19 -0
  43. package/dist/hooks/useVoice.d.ts +4 -0
  44. package/dist/hooks/useVoice.js +21 -0
  45. package/dist/index.d.ts +10 -0
  46. package/dist/index.js +9 -0
  47. package/dist/plugin/PluginManager.d.ts +108 -0
  48. package/dist/plugin/PluginManager.js +851 -0
  49. package/dist/plugin/luna-react.d.ts +41 -0
  50. package/dist/plugin/luna-react.js +99 -0
  51. package/dist/sdk.d.ts +512 -0
  52. package/dist/sdk.js +64 -0
  53. package/dist/types.d.ts +186 -0
  54. package/dist/types.js +2 -0
  55. package/dist/utils/attributeNormalizer.d.ts +5 -0
  56. package/dist/utils/attributeNormalizer.js +53 -0
  57. package/dist/utils/facePositionCalculator.d.ts +29 -0
  58. package/dist/utils/facePositionCalculator.js +127 -0
  59. 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,8 @@
1
+ import type React from "react";
2
+ interface EndScreenProps {
3
+ scenarioName: string;
4
+ onRestart: () => void;
5
+ className?: string;
6
+ }
7
+ export declare const EndScreen: React.FC<EndScreenProps>;
8
+ export {};
@@ -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,3 @@
1
+ export declare const BasisWidth = 1920;
2
+ export declare const BasisHeight = 1080;
3
+ export declare const aspectRatio: number;
@@ -0,0 +1,6 @@
1
+ // Figmaデザインの基準サイズ (1920×1080)
2
+ // このサイズを基準として、実際の画面サイズに応じて座標やサイズを変換します
3
+ export const BasisWidth = 1920;
4
+ export const BasisHeight = 1080;
5
+ // アスペクト比 (16:9)
6
+ export const aspectRatio = 16 / 9;
@@ -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 {};