create-pixi-vn 2.0.14 → 2.0.15
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/package.json +1 -1
- package/template-react-vite-muijoy/package-lock.json +12 -8
- package/template-react-vite-muijoy/package.json +1 -1
- package/template-react-vite-muijoy/src/components/loading.tsx +11 -1
- package/template-react-vite-muijoy/src/components/menus/choice-menus.tsx +4 -0
- package/template-react-vite-muijoy/src/components/menus/main-menu.tsx +1 -6
- package/template-react-vite-muijoy/src/components/menus/settings/index.tsx +3 -1
- package/template-react-vite-muijoy/src/components/modals/input-request-dialogues.tsx +1 -1
- package/template-react-vite-muijoy/src/components/modals/quick-actions-wheel.tsx +3 -1
- package/template-react-vite-muijoy/src/components/providers/alert-dialog-provider.tsx +3 -0
- package/template-react-vite-muijoy/src/components/screens/narration/narration-cards.tsx +40 -19
- package/template-react-vite-muijoy/src/lib/hooks/hotkeys-hooks.ts +111 -10
- package/template-react-vite-muijoy/src/lib/hooks/narration-hooks.ts +7 -1
- package/template-react-vite-muijoy/src/lib/query/narration-query.ts +32 -2
- package/template-react-vite-muijoy/src/lib/stores/alert-dialog-store.ts +13 -0
- package/template-react-vite-muijoy/src/lib/stores/text-display-settings-store.ts +20 -1
- package/template-react-vite-muijoy/src/locales/en.json +2 -0
- package/template-react-vite-muijoy/src/routes/__root.tsx +3 -4
- package/template-react-vite-muijoy-ink/package-lock.json +12 -8
- package/template-react-vite-muijoy-ink/package.json +1 -1
- package/template-react-vite-muijoy-ink/src/components/loading.tsx +11 -1
- package/template-react-vite-muijoy-ink/src/components/menus/choice-menus.tsx +4 -0
- package/template-react-vite-muijoy-ink/src/components/menus/main-menu.tsx +1 -6
- package/template-react-vite-muijoy-ink/src/components/menus/settings/index.tsx +3 -1
- package/template-react-vite-muijoy-ink/src/components/modals/input-request-dialogues.tsx +1 -1
- package/template-react-vite-muijoy-ink/src/components/modals/quick-actions-wheel.tsx +3 -1
- package/template-react-vite-muijoy-ink/src/components/providers/alert-dialog-provider.tsx +3 -0
- package/template-react-vite-muijoy-ink/src/components/screens/narration/narration-cards.tsx +40 -19
- package/template-react-vite-muijoy-ink/src/lib/hooks/hotkeys-hooks.ts +111 -10
- package/template-react-vite-muijoy-ink/src/lib/hooks/narration-hooks.ts +7 -1
- package/template-react-vite-muijoy-ink/src/lib/query/narration-query.ts +32 -2
- package/template-react-vite-muijoy-ink/src/lib/stores/alert-dialog-store.ts +13 -0
- package/template-react-vite-muijoy-ink/src/lib/stores/text-display-settings-store.ts +20 -1
- package/template-react-vite-muijoy-ink/src/locales/en.json +2 -0
- package/template-react-vite-muijoy-ink/src/routes/__root.tsx +3 -4
- package/template-react-vite-muijoy-ink-tauri/package-lock.json +12 -8
- package/template-react-vite-muijoy-ink-tauri/package.json +1 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/loading.tsx +11 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/choice-menus.tsx +4 -0
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/main-menu.tsx +1 -6
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/settings/index.tsx +3 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/modals/input-request-dialogues.tsx +1 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/modals/quick-actions-wheel.tsx +3 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/providers/alert-dialog-provider.tsx +3 -0
- package/template-react-vite-muijoy-ink-tauri/src/components/screens/narration/narration-cards.tsx +40 -19
- package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/hotkeys-hooks.ts +111 -10
- package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/narration-hooks.ts +7 -1
- package/template-react-vite-muijoy-ink-tauri/src/lib/query/narration-query.ts +32 -2
- package/template-react-vite-muijoy-ink-tauri/src/lib/stores/alert-dialog-store.ts +13 -0
- package/template-react-vite-muijoy-ink-tauri/src/lib/stores/text-display-settings-store.ts +20 -1
- package/template-react-vite-muijoy-ink-tauri/src/locales/en.json +2 -0
- package/template-react-vite-muijoy-ink-tauri/src/routes/__root.tsx +3 -4
- package/template-react-vite-muijoy-tauri/package-lock.json +12 -8
- package/template-react-vite-muijoy-tauri/package.json +1 -1
- package/template-react-vite-muijoy-tauri/src/components/loading.tsx +11 -1
- package/template-react-vite-muijoy-tauri/src/components/menus/choice-menus.tsx +4 -0
- package/template-react-vite-muijoy-tauri/src/components/menus/main-menu.tsx +1 -6
- package/template-react-vite-muijoy-tauri/src/components/menus/settings/index.tsx +3 -1
- package/template-react-vite-muijoy-tauri/src/components/modals/input-request-dialogues.tsx +1 -1
- package/template-react-vite-muijoy-tauri/src/components/modals/quick-actions-wheel.tsx +3 -1
- package/template-react-vite-muijoy-tauri/src/components/providers/alert-dialog-provider.tsx +3 -0
- package/template-react-vite-muijoy-tauri/src/components/screens/narration/narration-cards.tsx +40 -19
- package/template-react-vite-muijoy-tauri/src/lib/hooks/hotkeys-hooks.ts +111 -10
- package/template-react-vite-muijoy-tauri/src/lib/hooks/narration-hooks.ts +7 -1
- package/template-react-vite-muijoy-tauri/src/lib/query/narration-query.ts +32 -2
- package/template-react-vite-muijoy-tauri/src/lib/stores/alert-dialog-store.ts +13 -0
- package/template-react-vite-muijoy-tauri/src/lib/stores/text-display-settings-store.ts +20 -1
- package/template-react-vite-muijoy-tauri/src/locales/en.json +2 -0
- package/template-react-vite-muijoy-tauri/src/routes/__root.tsx +3 -4
package/template-react-vite-muijoy-ink-tauri/src/components/providers/alert-dialog-provider.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AlertDialogState } from "@/lib/stores/alert-dialog-store";
|
|
1
2
|
import { createContext, type ReactNode, useCallback, useContext, useState } from "react";
|
|
2
3
|
import { useTranslation } from "react-i18next";
|
|
3
4
|
import {
|
|
@@ -47,6 +48,7 @@ export function AlertDialogProvider({ children }: { children: ReactNode }) {
|
|
|
47
48
|
|
|
48
49
|
const closeDialog = useCallback((id: string) => {
|
|
49
50
|
setDialogs((prev) => prev.map((d) => (d.id === id ? { ...d, open: false } : d)));
|
|
51
|
+
AlertDialogState.decrement();
|
|
50
52
|
setTimeout(() => {
|
|
51
53
|
setDialogs((prev) => prev.filter((d) => d.id !== id));
|
|
52
54
|
}, DIALOG_CLOSE_ANIMATION_DURATION_MS);
|
|
@@ -54,6 +56,7 @@ export function AlertDialogProvider({ children }: { children: ReactNode }) {
|
|
|
54
56
|
|
|
55
57
|
const openAlertDialog = useCallback((options: AlertDialogOptions) => {
|
|
56
58
|
const id = generateId();
|
|
59
|
+
AlertDialogState.increment();
|
|
57
60
|
setDialogs((prev) => [...prev, { id, open: true, loading: false, options }]);
|
|
58
61
|
}, []);
|
|
59
62
|
|
package/template-react-vite-muijoy-ink-tauri/src/components/screens/narration/narration-cards.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DelayedAnimatedDots } from "@/components/loading";
|
|
2
2
|
import { QuickTools } from "@/components/quick-tools";
|
|
3
3
|
import { Card, CardContent } from "@/components/ui/card";
|
|
4
4
|
import { Image } from "@/components/ui/image";
|
|
@@ -8,7 +8,7 @@ import { useNarrationPointerHandlers } from "@/lib/hooks/narration-hooks";
|
|
|
8
8
|
import { useQueryDialogue } from "@/lib/query/narration-query";
|
|
9
9
|
import { TextDisplaySettings } from "@/lib/stores/text-display-settings-store";
|
|
10
10
|
import { useSelector } from "@tanstack/react-store";
|
|
11
|
-
import { type RefObject, useCallback, useRef } from "react";
|
|
11
|
+
import { type RefObject, memo, useCallback, useMemo, useRef } from "react";
|
|
12
12
|
import Markdown from "react-markdown";
|
|
13
13
|
import { MarkdownTypewriterHooks } from "react-markdown-typewriter";
|
|
14
14
|
import rehypeRaw from "rehype-raw";
|
|
@@ -63,9 +63,13 @@ export function NarrationCards() {
|
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export
|
|
66
|
+
export const Text = memo(function Text({
|
|
67
|
+
paragraphRef,
|
|
68
|
+
}: {
|
|
69
|
+
paragraphRef: RefObject<HTMLDivElement | null>;
|
|
70
|
+
}) {
|
|
67
71
|
const typewriterDelay = useSelector(TextDisplaySettings.store, (state) => state.delay);
|
|
68
|
-
const { data: { animatedText, text } = {} } = useQueryDialogue();
|
|
72
|
+
const { data: { animatedText, text } = {}, finalizeDialogue } = useQueryDialogue();
|
|
69
73
|
|
|
70
74
|
const handleCharacterAnimationComplete = useCallback(
|
|
71
75
|
(ref: { current: HTMLSpanElement | null }) => {
|
|
@@ -74,7 +78,6 @@ export function Text({ paragraphRef }: { paragraphRef: RefObject<HTMLDivElement
|
|
|
74
78
|
if (container && char) {
|
|
75
79
|
const containerRect = container.getBoundingClientRect();
|
|
76
80
|
const charRect = char.getBoundingClientRect();
|
|
77
|
-
// Position of the character relative to the top of the scroll container
|
|
78
81
|
const charOffsetInContainer =
|
|
79
82
|
charRect.top - containerRect.top + container.scrollTop;
|
|
80
83
|
const scrollTop = charOffsetInContainer - container.clientHeight / 2;
|
|
@@ -87,15 +90,40 @@ export function Text({ paragraphRef }: { paragraphRef: RefObject<HTMLDivElement
|
|
|
87
90
|
[paragraphRef],
|
|
88
91
|
);
|
|
89
92
|
|
|
93
|
+
const markdownComponents = useMemo(() => ({ p: (props: object) => <span {...props} /> }), []);
|
|
94
|
+
|
|
95
|
+
const motionProps = useMemo(
|
|
96
|
+
() => ({
|
|
97
|
+
onAnimationStart: TextDisplaySettings.start,
|
|
98
|
+
onAnimationComplete: (definition: "visible" | "hidden") => {
|
|
99
|
+
if (definition === "visible") {
|
|
100
|
+
finalizeDialogue();
|
|
101
|
+
TextDisplaySettings.end();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
onCharacterAnimationComplete: handleCharacterAnimationComplete,
|
|
105
|
+
}),
|
|
106
|
+
[finalizeDialogue, handleCharacterAnimationComplete],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const specialCharacters = useMemo(
|
|
110
|
+
() => ({
|
|
111
|
+
".": { delayAfter: typewriterDelay + 300 },
|
|
112
|
+
"!": { delayAfter: typewriterDelay + 300 },
|
|
113
|
+
"?": { delayAfter: typewriterDelay + 300 },
|
|
114
|
+
",": { delayAfter: typewriterDelay + 75 },
|
|
115
|
+
":": { delay: typewriterDelay + 50, delayAfter: typewriterDelay + 150 },
|
|
116
|
+
}),
|
|
117
|
+
[typewriterDelay],
|
|
118
|
+
);
|
|
119
|
+
|
|
90
120
|
return (
|
|
91
121
|
<p className="prose dark:prose-invert m-0 max-w-full p-0">
|
|
92
122
|
<span>
|
|
93
123
|
<Markdown
|
|
94
124
|
remarkPlugins={[remarkGfm]}
|
|
95
125
|
rehypePlugins={[rehypeRaw]}
|
|
96
|
-
components={
|
|
97
|
-
p: (props) => <span {...props} />,
|
|
98
|
-
}}
|
|
126
|
+
components={markdownComponents}
|
|
99
127
|
>
|
|
100
128
|
{text}
|
|
101
129
|
</Markdown>
|
|
@@ -106,23 +134,16 @@ export function Text({ paragraphRef }: { paragraphRef: RefObject<HTMLDivElement
|
|
|
106
134
|
remarkPlugins={[remarkGfm]}
|
|
107
135
|
rehypePlugins={[rehypeRaw]}
|
|
108
136
|
delay={typewriterDelay}
|
|
109
|
-
motionProps={
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (definition === "visible") {
|
|
113
|
-
TextDisplaySettings.end();
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
onCharacterAnimationComplete: handleCharacterAnimationComplete,
|
|
117
|
-
}}
|
|
118
|
-
fallback={<AnimatedDots />}
|
|
137
|
+
motionProps={motionProps}
|
|
138
|
+
fallback={<DelayedAnimatedDots />}
|
|
139
|
+
specialCharacters={specialCharacters}
|
|
119
140
|
>
|
|
120
141
|
{animatedText}
|
|
121
142
|
</MarkdownTypewriterHooks>
|
|
122
143
|
</span>
|
|
123
144
|
</p>
|
|
124
145
|
);
|
|
125
|
-
}
|
|
146
|
+
});
|
|
126
147
|
|
|
127
148
|
export function CharacterIcon({ alt, icon }: { icon: string; alt: string }) {
|
|
128
149
|
return (
|
|
@@ -2,22 +2,47 @@ import { useAlertDialog } from "@/components/providers/alert-dialog-provider";
|
|
|
2
2
|
import { useNarrationFunctions } from "@/lib/hooks/narration-hooks";
|
|
3
3
|
import { useSetSearchParamState } from "@/lib/hooks/navigation-hooks";
|
|
4
4
|
import { useGameProps } from "@/lib/hooks/props-hooks";
|
|
5
|
-
import { useQueryInputValue } from "@/lib/query/narration-query";
|
|
5
|
+
import { useQueryCanGoNext, useQueryInputValue } from "@/lib/query/narration-query";
|
|
6
6
|
import {
|
|
7
7
|
LAST_SAVE_USE_QUERY_KEY,
|
|
8
8
|
SAVES_USE_QUERY_KEY,
|
|
9
9
|
useQueryLastSave,
|
|
10
10
|
} from "@/lib/query/save-query";
|
|
11
|
+
import { AlertDialogState } from "@/lib/stores/alert-dialog-store";
|
|
11
12
|
import { QuickActionsWheelState } from "@/lib/stores/quick-actions-wheel-store";
|
|
13
|
+
import { SearchParams } from "@/lib/stores/search-param-store";
|
|
12
14
|
import { SkipSettings } from "@/lib/stores/skip-settings-store";
|
|
15
|
+
import { TextDisplaySettings } from "@/lib/stores/text-display-settings-store";
|
|
13
16
|
import { loadSave, saveGameToIndexDB } from "@/lib/utils/save-utility";
|
|
17
|
+
import { narration } from "@drincs/pixi-vn";
|
|
14
18
|
import { useHotkeys } from "@tanstack/react-hotkeys";
|
|
15
19
|
import { useQueryClient } from "@tanstack/react-query";
|
|
16
20
|
import { useLocation } from "@tanstack/react-router";
|
|
17
|
-
import {
|
|
21
|
+
import { useSelector } from "@tanstack/react-store";
|
|
22
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
18
23
|
import { useTranslation } from "react-i18next";
|
|
19
24
|
import { toast } from "sonner";
|
|
20
25
|
|
|
26
|
+
function useMenuDialogState() {
|
|
27
|
+
const searchParams = useSelector(SearchParams.store, (state) => state);
|
|
28
|
+
const alertDialogCount = useSelector(AlertDialogState.store, (state) => state.count);
|
|
29
|
+
const { data: { isRequired } = {} } = useQueryInputValue<string | number>();
|
|
30
|
+
|
|
31
|
+
/** true when the settings menu (or any sub-page) is open */
|
|
32
|
+
const isSettingsOpen = useMemo(
|
|
33
|
+
() => Object.values(searchParams).some((v) => v !== undefined),
|
|
34
|
+
[searchParams],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** true when an alert confirmation dialog or the input-request dialog is blocking interaction */
|
|
38
|
+
const isAlertOrInputOpen = alertDialogCount > 0 || !!isRequired;
|
|
39
|
+
|
|
40
|
+
/** true when any menu or dialog that should disable hotkeys is open */
|
|
41
|
+
const isAnyMenuOrDialogOpen = isSettingsOpen || isAlertOrInputOpen;
|
|
42
|
+
|
|
43
|
+
return { isAnyMenuOrDialogOpen, isSettingsOpen, isAlertOrInputOpen };
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
/**
|
|
22
47
|
* useSaveHotkeys
|
|
23
48
|
*
|
|
@@ -40,6 +65,7 @@ export function useSaveHotkeys(): null {
|
|
|
40
65
|
const { data: lastSave = null } = useQueryLastSave();
|
|
41
66
|
const { openAlertDialog } = useAlertDialog();
|
|
42
67
|
const gameProps = useGameProps();
|
|
68
|
+
const { isSettingsOpen } = useMenuDialogState();
|
|
43
69
|
|
|
44
70
|
const quickSave = useCallback(() => {
|
|
45
71
|
if (location.pathname === "/") {
|
|
@@ -87,6 +113,7 @@ export function useSaveHotkeys(): null {
|
|
|
87
113
|
hotkey: "F5",
|
|
88
114
|
callback: quickSave,
|
|
89
115
|
options: {
|
|
116
|
+
enabled: !isSettingsOpen,
|
|
90
117
|
meta: {
|
|
91
118
|
name: t("quick_save"),
|
|
92
119
|
description: t("quick_save_hotkey_description"),
|
|
@@ -97,6 +124,7 @@ export function useSaveHotkeys(): null {
|
|
|
97
124
|
hotkey: "Control+S",
|
|
98
125
|
callback: quickSave,
|
|
99
126
|
options: {
|
|
127
|
+
enabled: !isSettingsOpen,
|
|
100
128
|
meta: {
|
|
101
129
|
name: t("quick_save"),
|
|
102
130
|
description: t("quick_save_hotkey_alternative_description"),
|
|
@@ -107,6 +135,7 @@ export function useSaveHotkeys(): null {
|
|
|
107
135
|
hotkey: "F9",
|
|
108
136
|
callback: quickLoad,
|
|
109
137
|
options: {
|
|
138
|
+
enabled: !isSettingsOpen,
|
|
110
139
|
meta: {
|
|
111
140
|
name: t("load_last_save"),
|
|
112
141
|
description: t("quick_load_hotkey_description"),
|
|
@@ -117,6 +146,7 @@ export function useSaveHotkeys(): null {
|
|
|
117
146
|
hotkey: "Control+L",
|
|
118
147
|
callback: quickLoad,
|
|
119
148
|
options: {
|
|
149
|
+
enabled: !isSettingsOpen,
|
|
120
150
|
meta: {
|
|
121
151
|
name: t("load_last_save"),
|
|
122
152
|
description: t("quick_load_hotkey_alternative_description"),
|
|
@@ -132,6 +162,7 @@ export function useSettingsHotkeys(): null {
|
|
|
132
162
|
const { t } = useTranslation(["ui"]);
|
|
133
163
|
const setSettingsOpen = useSetSearchParamState<boolean>("settings");
|
|
134
164
|
const setSettingsTab = useSetSearchParamState<string>("settings_tab");
|
|
165
|
+
const { isAnyMenuOrDialogOpen } = useMenuDialogState();
|
|
135
166
|
|
|
136
167
|
const openControlsPage = useCallback(() => {
|
|
137
168
|
setSettingsOpen(true);
|
|
@@ -150,9 +181,10 @@ export function useSettingsHotkeys(): null {
|
|
|
150
181
|
},
|
|
151
182
|
},
|
|
152
183
|
{
|
|
153
|
-
hotkey: "
|
|
184
|
+
hotkey: "K",
|
|
154
185
|
callback: openControlsPage,
|
|
155
186
|
options: {
|
|
187
|
+
enabled: !isAnyMenuOrDialogOpen,
|
|
156
188
|
meta: {
|
|
157
189
|
name: t("hotkeys_menu"),
|
|
158
190
|
description: t("hotkeys_menu_shortcut_description"),
|
|
@@ -169,6 +201,7 @@ export function useGameHotkeys(): null {
|
|
|
169
201
|
const setSettingsTab = useSetSearchParamState<string>("settings_tab");
|
|
170
202
|
const setHistory = useSetSearchParamState<boolean>("history");
|
|
171
203
|
const { t } = useTranslation(["ui"]);
|
|
204
|
+
const { isAnyMenuOrDialogOpen } = useMenuDialogState();
|
|
172
205
|
|
|
173
206
|
const openHistoryPage = useCallback(() => {
|
|
174
207
|
setHistory(undefined);
|
|
@@ -187,9 +220,10 @@ export function useGameHotkeys(): null {
|
|
|
187
220
|
|
|
188
221
|
useHotkeys([
|
|
189
222
|
{
|
|
190
|
-
hotkey: "
|
|
223
|
+
hotkey: "H",
|
|
191
224
|
callback: openHistoryPage,
|
|
192
225
|
options: {
|
|
226
|
+
enabled: !isAnyMenuOrDialogOpen,
|
|
193
227
|
meta: {
|
|
194
228
|
name: t("history"),
|
|
195
229
|
description: t("history_hotkey_description"),
|
|
@@ -200,6 +234,7 @@ export function useGameHotkeys(): null {
|
|
|
200
234
|
hotkey: "Tab",
|
|
201
235
|
callback: toggleQuickActionsWheel,
|
|
202
236
|
options: {
|
|
237
|
+
enabled: !isAnyMenuOrDialogOpen,
|
|
203
238
|
meta: {
|
|
204
239
|
name: t("quick_actions"),
|
|
205
240
|
description: t("quick_actions_open_description"),
|
|
@@ -211,38 +246,104 @@ export function useGameHotkeys(): null {
|
|
|
211
246
|
return null;
|
|
212
247
|
}
|
|
213
248
|
|
|
249
|
+
export function useChoiceMenuHotkeys(menuLength: number) {
|
|
250
|
+
const { t } = useTranslation(["ui"]);
|
|
251
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
252
|
+
|
|
253
|
+
function getMenuItems(): HTMLButtonElement[] {
|
|
254
|
+
if (!menuRef.current) return [];
|
|
255
|
+
return Array.from(
|
|
256
|
+
menuRef.current.querySelectorAll<HTMLButtonElement>(
|
|
257
|
+
"button[role='menuitem']:not(:disabled)",
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function focusMenuItem(direction: "up" | "down") {
|
|
263
|
+
const items = getMenuItems();
|
|
264
|
+
if (!items.length) return;
|
|
265
|
+
const active = document.activeElement as HTMLElement;
|
|
266
|
+
const currentIndex = items.indexOf(active as HTMLButtonElement);
|
|
267
|
+
let next: number;
|
|
268
|
+
if (direction === "down") {
|
|
269
|
+
next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
270
|
+
} else {
|
|
271
|
+
next = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
272
|
+
}
|
|
273
|
+
items[next].focus();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
useHotkeys([
|
|
277
|
+
{
|
|
278
|
+
hotkey: "ArrowDown",
|
|
279
|
+
callback: () => focusMenuItem("down"),
|
|
280
|
+
options: {
|
|
281
|
+
enabled: menuLength > 0,
|
|
282
|
+
preventDefault: true,
|
|
283
|
+
meta: {
|
|
284
|
+
name: t("choice_navigation"),
|
|
285
|
+
description: t("choice_navigation_description"),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
hotkey: "ArrowUp",
|
|
291
|
+
callback: () => focusMenuItem("up"),
|
|
292
|
+
options: {
|
|
293
|
+
enabled: menuLength > 0,
|
|
294
|
+
preventDefault: true,
|
|
295
|
+
meta: {
|
|
296
|
+
name: t("choice_navigation"),
|
|
297
|
+
description: t("choice_navigation_description"),
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
return { menuRef };
|
|
304
|
+
}
|
|
305
|
+
|
|
214
306
|
export function useNarrationHotkeys(): null {
|
|
215
307
|
const { t } = useTranslation(["ui"]);
|
|
216
308
|
const { goNext } = useNarrationFunctions();
|
|
217
|
-
const
|
|
309
|
+
const typewriterInProgress = useSelector(
|
|
310
|
+
TextDisplaySettings.store,
|
|
311
|
+
(state) => state.inProgress,
|
|
312
|
+
);
|
|
313
|
+
const { isAnyMenuOrDialogOpen } = useMenuDialogState();
|
|
314
|
+
const { data: canGoNext } = useQueryCanGoNext();
|
|
218
315
|
|
|
219
316
|
const onSkipKeyDown = useCallback(() => SkipSettings.setEnabled(true), []);
|
|
220
317
|
const onSkipKeyUp = useCallback(() => {
|
|
221
318
|
SkipSettings.setEnabled(false);
|
|
319
|
+
if (typewriterInProgress && !narration.dialogGlue) {
|
|
320
|
+
TextDisplaySettings.complete();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
222
323
|
goNext();
|
|
223
|
-
}, [goNext]);
|
|
324
|
+
}, [goNext, typewriterInProgress]);
|
|
224
325
|
|
|
225
326
|
useHotkeys([
|
|
226
327
|
{
|
|
227
328
|
hotkey: "Enter",
|
|
228
329
|
callback: onSkipKeyDown,
|
|
229
330
|
options: {
|
|
331
|
+
enabled: !isAnyMenuOrDialogOpen && canGoNext,
|
|
230
332
|
meta: {
|
|
231
333
|
name: t("skip"),
|
|
232
334
|
description: t("skip_hold_description"),
|
|
233
335
|
},
|
|
234
|
-
enabled: !isRequired,
|
|
235
336
|
},
|
|
236
337
|
},
|
|
237
338
|
{
|
|
238
339
|
hotkey: "Space",
|
|
239
340
|
callback: onSkipKeyDown,
|
|
240
341
|
options: {
|
|
342
|
+
enabled: !isAnyMenuOrDialogOpen && canGoNext,
|
|
241
343
|
meta: {
|
|
242
344
|
name: t("skip"),
|
|
243
345
|
description: t("skip_hold_space_description"),
|
|
244
346
|
},
|
|
245
|
-
enabled: !isRequired,
|
|
246
347
|
},
|
|
247
348
|
},
|
|
248
349
|
{
|
|
@@ -251,11 +352,11 @@ export function useNarrationHotkeys(): null {
|
|
|
251
352
|
options: {
|
|
252
353
|
eventType: "keyup",
|
|
253
354
|
conflictBehavior: "allow",
|
|
355
|
+
enabled: !isAnyMenuOrDialogOpen && canGoNext,
|
|
254
356
|
meta: {
|
|
255
357
|
name: t("next"),
|
|
256
358
|
description: t("skip_release_description"),
|
|
257
359
|
},
|
|
258
|
-
enabled: !isRequired,
|
|
259
360
|
},
|
|
260
361
|
},
|
|
261
362
|
{
|
|
@@ -264,11 +365,11 @@ export function useNarrationHotkeys(): null {
|
|
|
264
365
|
options: {
|
|
265
366
|
eventType: "keyup",
|
|
266
367
|
conflictBehavior: "allow",
|
|
368
|
+
enabled: !isAnyMenuOrDialogOpen && canGoNext,
|
|
267
369
|
meta: {
|
|
268
370
|
name: t("next"),
|
|
269
371
|
description: t("skip_release_space_description"),
|
|
270
372
|
},
|
|
271
|
-
enabled: !isRequired,
|
|
272
373
|
},
|
|
273
374
|
},
|
|
274
375
|
]);
|
|
@@ -22,6 +22,7 @@ export function useNarrationFunctions() {
|
|
|
22
22
|
|
|
23
23
|
const goNext = useCallback(async () => {
|
|
24
24
|
if (hasOpenMenu) return;
|
|
25
|
+
TextDisplaySettings.resetForNext();
|
|
25
26
|
GameStatus.setLoading(true);
|
|
26
27
|
try {
|
|
27
28
|
if (!narration.canContinue) {
|
|
@@ -94,6 +95,7 @@ const isDragGesture = (dx: number, dy: number) =>
|
|
|
94
95
|
export function useNarrationPointerHandlers() {
|
|
95
96
|
const { goNext } = useNarrationFunctions();
|
|
96
97
|
const skipEnabled = useSelector(SkipSettings.store, (state) => state.enabled);
|
|
98
|
+
const typewriterInProgress = useSelector(TextDisplaySettings.store, (state) => state.inProgress);
|
|
97
99
|
const pointerDownPos = useRef<{ x: number; y: number } | null>(null);
|
|
98
100
|
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
99
101
|
const longPressTriggered = useRef(false);
|
|
@@ -166,9 +168,13 @@ export function useNarrationPointerHandlers() {
|
|
|
166
168
|
if (skipEnabled) {
|
|
167
169
|
SkipSettings.setEnabled(false);
|
|
168
170
|
}
|
|
171
|
+
if (typewriterInProgress && !narration.dialogGlue) {
|
|
172
|
+
TextDisplaySettings.complete();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
169
175
|
goNext();
|
|
170
176
|
},
|
|
171
|
-
[clearLongPressTimer, goNext, skipEnabled],
|
|
177
|
+
[clearLongPressTimer, goNext, skipEnabled, typewriterInProgress],
|
|
172
178
|
);
|
|
173
179
|
|
|
174
180
|
const handlePointerMove = useCallback(
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { INTERFACE_DATA_USE_QUERY_KEY } from "@/constants";
|
|
2
|
+
import { TextDisplaySettings } from "@/lib/stores/text-display-settings-store";
|
|
2
3
|
import { type CharacterInterface, narration, stepHistory } from "@drincs/pixi-vn";
|
|
3
4
|
import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { useSelector } from "@tanstack/react-store";
|
|
6
|
+
import { useCallback } from "react";
|
|
4
7
|
import { useTranslation } from "react-i18next";
|
|
5
8
|
|
|
6
9
|
const CAN_GO_BACK_USE_QUERY_KEY = "can_go_back_use_query_key";
|
|
@@ -24,7 +27,6 @@ export function useQueryChoiceMenuOptions() {
|
|
|
24
27
|
? t(option.text)
|
|
25
28
|
: option.text.map((text) => t(text)).join(" "),
|
|
26
29
|
})) || [],
|
|
27
|
-
placeholderData: keepPreviousData,
|
|
28
30
|
});
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -45,13 +47,30 @@ type DialogueModel = {
|
|
|
45
47
|
animatedText?: string;
|
|
46
48
|
text?: string;
|
|
47
49
|
character?: CharacterInterface;
|
|
50
|
+
lastText?: string;
|
|
48
51
|
};
|
|
49
52
|
const DIALOGUE_USE_QUERY_KEY = "dialogue_use_query_key";
|
|
50
53
|
export function useQueryDialogue() {
|
|
51
54
|
const { t } = useTranslation(["narration"]);
|
|
52
55
|
const queryClient = useQueryClient();
|
|
56
|
+
const forceComplete = useSelector(TextDisplaySettings.store, (state) => state.forceComplete);
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
const finalizeDialogue = useCallback(() => {
|
|
59
|
+
queryClient.setQueryData<DialogueModel>(
|
|
60
|
+
[INTERFACE_DATA_USE_QUERY_KEY, DIALOGUE_USE_QUERY_KEY],
|
|
61
|
+
(prev) => {
|
|
62
|
+
if (!prev) return prev;
|
|
63
|
+
const fullText = (prev.text || "") + (prev.animatedText || "");
|
|
64
|
+
return {
|
|
65
|
+
...prev,
|
|
66
|
+
text: fullText || undefined,
|
|
67
|
+
animatedText: undefined,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}, [queryClient]);
|
|
72
|
+
|
|
73
|
+
const query = useQuery<DialogueModel>({
|
|
55
74
|
queryKey: [INTERFACE_DATA_USE_QUERY_KEY, DIALOGUE_USE_QUERY_KEY],
|
|
56
75
|
queryFn: async ({ queryKey }) => {
|
|
57
76
|
const dialogue = narration.dialogue;
|
|
@@ -78,6 +97,7 @@ export function useQueryDialogue() {
|
|
|
78
97
|
}
|
|
79
98
|
return {
|
|
80
99
|
animatedText: newText,
|
|
100
|
+
lastText: newText,
|
|
81
101
|
text: oldText,
|
|
82
102
|
character: character,
|
|
83
103
|
};
|
|
@@ -85,11 +105,21 @@ export function useQueryDialogue() {
|
|
|
85
105
|
|
|
86
106
|
return {
|
|
87
107
|
animatedText: text,
|
|
108
|
+
lastText: text,
|
|
88
109
|
character: character,
|
|
89
110
|
};
|
|
90
111
|
},
|
|
91
112
|
placeholderData: keepPreviousData,
|
|
113
|
+
select: forceComplete
|
|
114
|
+
? (data) => ({
|
|
115
|
+
...data,
|
|
116
|
+
text: (data.text || "") + (data.animatedText || "") || undefined,
|
|
117
|
+
animatedText: undefined,
|
|
118
|
+
})
|
|
119
|
+
: undefined,
|
|
92
120
|
});
|
|
121
|
+
|
|
122
|
+
return { ...query, finalizeDialogue };
|
|
93
123
|
}
|
|
94
124
|
|
|
95
125
|
const CAN_GO_NEXT_USE_QUERY_KEY = "can_go_next_use_query_key";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Store } from "@tanstack/store";
|
|
2
|
+
|
|
3
|
+
export namespace AlertDialogState {
|
|
4
|
+
export const store = new Store<{ count: number }>({ count: 0 });
|
|
5
|
+
|
|
6
|
+
export function increment() {
|
|
7
|
+
store.setState((state) => ({ count: state.count + 1 }));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function decrement() {
|
|
11
|
+
store.setState((state) => ({ count: Math.max(0, state.count - 1) }));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -9,6 +9,10 @@ type TextDisplayStorage = {
|
|
|
9
9
|
* Whether the typewriter effect is in progress
|
|
10
10
|
*/
|
|
11
11
|
inProgress: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the typewriter effect should be force-completed instantly
|
|
14
|
+
*/
|
|
15
|
+
forceComplete: boolean;
|
|
12
16
|
/**
|
|
13
17
|
* The font size in percent (100 = default)
|
|
14
18
|
*/
|
|
@@ -22,6 +26,7 @@ export namespace TextDisplaySettings {
|
|
|
22
26
|
export const store = new Store<TextDisplayStorage>({
|
|
23
27
|
delay: Number(localStorage.getItem("typewriter_delay_millisecond") ?? 10),
|
|
24
28
|
inProgress: false,
|
|
29
|
+
forceComplete: false,
|
|
25
30
|
fontSize: initialFontSize,
|
|
26
31
|
});
|
|
27
32
|
|
|
@@ -57,6 +62,20 @@ export namespace TextDisplaySettings {
|
|
|
57
62
|
* End the typewriter effect
|
|
58
63
|
*/
|
|
59
64
|
export function end() {
|
|
60
|
-
store.setState((state) => ({ ...state, inProgress: false }));
|
|
65
|
+
store.setState((state) => ({ ...state, inProgress: false, forceComplete: false }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Force the typewriter effect to complete instantly
|
|
70
|
+
*/
|
|
71
|
+
export function complete() {
|
|
72
|
+
store.setState((state) => ({ ...state, forceComplete: true, inProgress: false }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clear the forceComplete flag before advancing to the next step
|
|
77
|
+
*/
|
|
78
|
+
export function resetForNext() {
|
|
79
|
+
store.setState((state) => ({ ...state, forceComplete: false }));
|
|
61
80
|
}
|
|
62
81
|
}
|
|
@@ -81,6 +81,8 @@
|
|
|
81
81
|
"menu_navigation_up_description": "Focus the previous menu item.",
|
|
82
82
|
"menu_navigation_home_description": "Focus the first menu item.",
|
|
83
83
|
"menu_navigation_end_description": "Focus the last menu item.",
|
|
84
|
+
"choice_navigation": "Choices",
|
|
85
|
+
"choice_navigation_description": "Use ↑ / ↓ to select an choice, then Enter to run it.",
|
|
84
86
|
"success_delete": "Delete successful",
|
|
85
87
|
"success_load": "Load successful",
|
|
86
88
|
"success_save": "Save successful",
|
|
@@ -12,14 +12,13 @@ import { defineAssets } from "@/lib/utils/assets-utility";
|
|
|
12
12
|
import { initializeIndexedDB } from "@/lib/utils/db-utility";
|
|
13
13
|
import { loadRefreshSave } from "@/lib/utils/save-utility";
|
|
14
14
|
import type { RouterContext } from "@/router";
|
|
15
|
+
import { narration } from "@drincs/pixi-vn";
|
|
15
16
|
import { setupInkHmrListener } from "@drincs/pixi-vn-ink/vite-listener";
|
|
16
17
|
import { setupPixivnViteData } from "@drincs/pixi-vn/vite-listener";
|
|
17
18
|
import { createRootRouteWithContext, ErrorComponent, Outlet } from "@tanstack/react-router";
|
|
18
19
|
import { lazy, Suspense } from "react";
|
|
19
20
|
|
|
20
|
-
const DevDevtools = import.meta.env.DEV
|
|
21
|
-
? lazy(() => import("@/components/dev-devtools"))
|
|
22
|
-
: null;
|
|
21
|
+
const DevDevtools = import.meta.env.DEV ? lazy(() => import("@/components/dev-devtools")) : null;
|
|
23
22
|
|
|
24
23
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
25
24
|
validateSearch: (search) => SearchParams.setMany(search),
|
|
@@ -30,7 +29,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|
|
30
29
|
await Promise.all([import("@/content"), initializeIndexedDB(), defineAssets(), useI18n()]);
|
|
31
30
|
await setupPixivnViteData();
|
|
32
31
|
await setupInkHmrListener();
|
|
33
|
-
if (location.pathname !== "/") {
|
|
32
|
+
if (location.pathname !== "/" && narration.stepCounter === 0) {
|
|
34
33
|
const isRefreshSaveExist = await loadRefreshSave();
|
|
35
34
|
if (isRefreshSaveExist) {
|
|
36
35
|
await context.queryClient.invalidateQueries({
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"react-dom": "^19.2.6",
|
|
37
37
|
"react-i18next": "^17.0.8",
|
|
38
38
|
"react-markdown": "^10.1.0",
|
|
39
|
-
"react-markdown-typewriter": "^1.
|
|
39
|
+
"react-markdown-typewriter": "^1.3.4",
|
|
40
40
|
"react-resizable-panels": "^4.11.2",
|
|
41
41
|
"rehype-raw": "^7.0.0",
|
|
42
42
|
"remark-gfm": "^4.0.1",
|
|
@@ -10179,6 +10179,7 @@
|
|
|
10179
10179
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
|
10180
10180
|
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
|
10181
10181
|
"license": "MIT",
|
|
10182
|
+
"peer": true,
|
|
10182
10183
|
"dependencies": {
|
|
10183
10184
|
"motion-dom": "^12.38.0",
|
|
10184
10185
|
"motion-utils": "^12.36.0",
|
|
@@ -13842,6 +13843,7 @@
|
|
|
13842
13843
|
"resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
|
|
13843
13844
|
"integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
|
|
13844
13845
|
"license": "MIT",
|
|
13846
|
+
"peer": true,
|
|
13845
13847
|
"dependencies": {
|
|
13846
13848
|
"framer-motion": "^12.38.0",
|
|
13847
13849
|
"tslib": "^2.4.0"
|
|
@@ -13868,6 +13870,7 @@
|
|
|
13868
13870
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
|
13869
13871
|
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
|
13870
13872
|
"license": "MIT",
|
|
13873
|
+
"peer": true,
|
|
13871
13874
|
"dependencies": {
|
|
13872
13875
|
"motion-utils": "^12.36.0"
|
|
13873
13876
|
}
|
|
@@ -13876,7 +13879,8 @@
|
|
|
13876
13879
|
"version": "12.36.0",
|
|
13877
13880
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
|
13878
13881
|
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
|
13879
|
-
"license": "MIT"
|
|
13882
|
+
"license": "MIT",
|
|
13883
|
+
"peer": true
|
|
13880
13884
|
},
|
|
13881
13885
|
"node_modules/ms": {
|
|
13882
13886
|
"version": "2.1.3",
|
|
@@ -14963,12 +14967,12 @@
|
|
|
14963
14967
|
}
|
|
14964
14968
|
},
|
|
14965
14969
|
"node_modules/react-markdown-typewriter": {
|
|
14966
|
-
"version": "1.
|
|
14967
|
-
"resolved": "https://registry.npmjs.org/react-markdown-typewriter/-/react-markdown-typewriter-1.
|
|
14968
|
-
"integrity": "sha512-
|
|
14969
|
-
"
|
|
14970
|
-
"
|
|
14971
|
-
|
|
14970
|
+
"version": "1.3.4",
|
|
14971
|
+
"resolved": "https://registry.npmjs.org/react-markdown-typewriter/-/react-markdown-typewriter-1.3.4.tgz",
|
|
14972
|
+
"integrity": "sha512-LE/7xzyta4A6Gz6CfeTP0saCgVsxbCEVj4le4xobrkM/IG9MVWdrpyGFGFdsKmZ1k5otnRF3HP52YXbMv7P1rg==",
|
|
14973
|
+
"workspaces": [
|
|
14974
|
+
"playground"
|
|
14975
|
+
],
|
|
14972
14976
|
"peerDependencies": {
|
|
14973
14977
|
"motion": ">=12.9.0",
|
|
14974
14978
|
"react": ">=18.0.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"react-dom": "^19.2.6",
|
|
51
51
|
"react-i18next": "^17.0.8",
|
|
52
52
|
"react-markdown": "^10.1.0",
|
|
53
|
-
"react-markdown-typewriter": "^1.
|
|
53
|
+
"react-markdown-typewriter": "^1.3.4",
|
|
54
54
|
"react-resizable-panels": "^4.11.2",
|
|
55
55
|
"rehype-raw": "^7.0.0",
|
|
56
56
|
"remark-gfm": "^4.0.1",
|