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.
Files changed (69) hide show
  1. package/package.json +1 -1
  2. package/template-react-vite-muijoy/package-lock.json +12 -8
  3. package/template-react-vite-muijoy/package.json +1 -1
  4. package/template-react-vite-muijoy/src/components/loading.tsx +11 -1
  5. package/template-react-vite-muijoy/src/components/menus/choice-menus.tsx +4 -0
  6. package/template-react-vite-muijoy/src/components/menus/main-menu.tsx +1 -6
  7. package/template-react-vite-muijoy/src/components/menus/settings/index.tsx +3 -1
  8. package/template-react-vite-muijoy/src/components/modals/input-request-dialogues.tsx +1 -1
  9. package/template-react-vite-muijoy/src/components/modals/quick-actions-wheel.tsx +3 -1
  10. package/template-react-vite-muijoy/src/components/providers/alert-dialog-provider.tsx +3 -0
  11. package/template-react-vite-muijoy/src/components/screens/narration/narration-cards.tsx +40 -19
  12. package/template-react-vite-muijoy/src/lib/hooks/hotkeys-hooks.ts +111 -10
  13. package/template-react-vite-muijoy/src/lib/hooks/narration-hooks.ts +7 -1
  14. package/template-react-vite-muijoy/src/lib/query/narration-query.ts +32 -2
  15. package/template-react-vite-muijoy/src/lib/stores/alert-dialog-store.ts +13 -0
  16. package/template-react-vite-muijoy/src/lib/stores/text-display-settings-store.ts +20 -1
  17. package/template-react-vite-muijoy/src/locales/en.json +2 -0
  18. package/template-react-vite-muijoy/src/routes/__root.tsx +3 -4
  19. package/template-react-vite-muijoy-ink/package-lock.json +12 -8
  20. package/template-react-vite-muijoy-ink/package.json +1 -1
  21. package/template-react-vite-muijoy-ink/src/components/loading.tsx +11 -1
  22. package/template-react-vite-muijoy-ink/src/components/menus/choice-menus.tsx +4 -0
  23. package/template-react-vite-muijoy-ink/src/components/menus/main-menu.tsx +1 -6
  24. package/template-react-vite-muijoy-ink/src/components/menus/settings/index.tsx +3 -1
  25. package/template-react-vite-muijoy-ink/src/components/modals/input-request-dialogues.tsx +1 -1
  26. package/template-react-vite-muijoy-ink/src/components/modals/quick-actions-wheel.tsx +3 -1
  27. package/template-react-vite-muijoy-ink/src/components/providers/alert-dialog-provider.tsx +3 -0
  28. package/template-react-vite-muijoy-ink/src/components/screens/narration/narration-cards.tsx +40 -19
  29. package/template-react-vite-muijoy-ink/src/lib/hooks/hotkeys-hooks.ts +111 -10
  30. package/template-react-vite-muijoy-ink/src/lib/hooks/narration-hooks.ts +7 -1
  31. package/template-react-vite-muijoy-ink/src/lib/query/narration-query.ts +32 -2
  32. package/template-react-vite-muijoy-ink/src/lib/stores/alert-dialog-store.ts +13 -0
  33. package/template-react-vite-muijoy-ink/src/lib/stores/text-display-settings-store.ts +20 -1
  34. package/template-react-vite-muijoy-ink/src/locales/en.json +2 -0
  35. package/template-react-vite-muijoy-ink/src/routes/__root.tsx +3 -4
  36. package/template-react-vite-muijoy-ink-tauri/package-lock.json +12 -8
  37. package/template-react-vite-muijoy-ink-tauri/package.json +1 -1
  38. package/template-react-vite-muijoy-ink-tauri/src/components/loading.tsx +11 -1
  39. package/template-react-vite-muijoy-ink-tauri/src/components/menus/choice-menus.tsx +4 -0
  40. package/template-react-vite-muijoy-ink-tauri/src/components/menus/main-menu.tsx +1 -6
  41. package/template-react-vite-muijoy-ink-tauri/src/components/menus/settings/index.tsx +3 -1
  42. package/template-react-vite-muijoy-ink-tauri/src/components/modals/input-request-dialogues.tsx +1 -1
  43. package/template-react-vite-muijoy-ink-tauri/src/components/modals/quick-actions-wheel.tsx +3 -1
  44. package/template-react-vite-muijoy-ink-tauri/src/components/providers/alert-dialog-provider.tsx +3 -0
  45. package/template-react-vite-muijoy-ink-tauri/src/components/screens/narration/narration-cards.tsx +40 -19
  46. package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/hotkeys-hooks.ts +111 -10
  47. package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/narration-hooks.ts +7 -1
  48. package/template-react-vite-muijoy-ink-tauri/src/lib/query/narration-query.ts +32 -2
  49. package/template-react-vite-muijoy-ink-tauri/src/lib/stores/alert-dialog-store.ts +13 -0
  50. package/template-react-vite-muijoy-ink-tauri/src/lib/stores/text-display-settings-store.ts +20 -1
  51. package/template-react-vite-muijoy-ink-tauri/src/locales/en.json +2 -0
  52. package/template-react-vite-muijoy-ink-tauri/src/routes/__root.tsx +3 -4
  53. package/template-react-vite-muijoy-tauri/package-lock.json +12 -8
  54. package/template-react-vite-muijoy-tauri/package.json +1 -1
  55. package/template-react-vite-muijoy-tauri/src/components/loading.tsx +11 -1
  56. package/template-react-vite-muijoy-tauri/src/components/menus/choice-menus.tsx +4 -0
  57. package/template-react-vite-muijoy-tauri/src/components/menus/main-menu.tsx +1 -6
  58. package/template-react-vite-muijoy-tauri/src/components/menus/settings/index.tsx +3 -1
  59. package/template-react-vite-muijoy-tauri/src/components/modals/input-request-dialogues.tsx +1 -1
  60. package/template-react-vite-muijoy-tauri/src/components/modals/quick-actions-wheel.tsx +3 -1
  61. package/template-react-vite-muijoy-tauri/src/components/providers/alert-dialog-provider.tsx +3 -0
  62. package/template-react-vite-muijoy-tauri/src/components/screens/narration/narration-cards.tsx +40 -19
  63. package/template-react-vite-muijoy-tauri/src/lib/hooks/hotkeys-hooks.ts +111 -10
  64. package/template-react-vite-muijoy-tauri/src/lib/hooks/narration-hooks.ts +7 -1
  65. package/template-react-vite-muijoy-tauri/src/lib/query/narration-query.ts +32 -2
  66. package/template-react-vite-muijoy-tauri/src/lib/stores/alert-dialog-store.ts +13 -0
  67. package/template-react-vite-muijoy-tauri/src/lib/stores/text-display-settings-store.ts +20 -1
  68. package/template-react-vite-muijoy-tauri/src/locales/en.json +2 -0
  69. package/template-react-vite-muijoy-tauri/src/routes/__root.tsx +3 -4
@@ -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 { useCallback } from "react";
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: "Control+K",
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: "Control+H",
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 { data: { isRequired } = {} } = useQueryInputValue<string | number>();
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
- return useQuery<DialogueModel>({
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({
@@ -37,7 +37,7 @@
37
37
  "react-dom": "^19.2.6",
38
38
  "react-i18next": "^17.0.8",
39
39
  "react-markdown": "^10.1.0",
40
- "react-markdown-typewriter": "^1.2.0",
40
+ "react-markdown-typewriter": "^1.3.4",
41
41
  "react-resizable-panels": "^4.11.2",
42
42
  "rehype-raw": "^7.0.0",
43
43
  "remark-gfm": "^4.0.1",
@@ -10358,6 +10358,7 @@
10358
10358
  "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
10359
10359
  "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
10360
10360
  "license": "MIT",
10361
+ "peer": true,
10361
10362
  "dependencies": {
10362
10363
  "motion-dom": "^12.38.0",
10363
10364
  "motion-utils": "^12.36.0",
@@ -14021,6 +14022,7 @@
14021
14022
  "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
14022
14023
  "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
14023
14024
  "license": "MIT",
14025
+ "peer": true,
14024
14026
  "dependencies": {
14025
14027
  "framer-motion": "^12.38.0",
14026
14028
  "tslib": "^2.4.0"
@@ -14047,6 +14049,7 @@
14047
14049
  "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
14048
14050
  "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
14049
14051
  "license": "MIT",
14052
+ "peer": true,
14050
14053
  "dependencies": {
14051
14054
  "motion-utils": "^12.36.0"
14052
14055
  }
@@ -14055,7 +14058,8 @@
14055
14058
  "version": "12.36.0",
14056
14059
  "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
14057
14060
  "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
14058
- "license": "MIT"
14061
+ "license": "MIT",
14062
+ "peer": true
14059
14063
  },
14060
14064
  "node_modules/ms": {
14061
14065
  "version": "2.1.3",
@@ -15142,12 +15146,12 @@
15142
15146
  }
15143
15147
  },
15144
15148
  "node_modules/react-markdown-typewriter": {
15145
- "version": "1.2.0",
15146
- "resolved": "https://registry.npmjs.org/react-markdown-typewriter/-/react-markdown-typewriter-1.2.0.tgz",
15147
- "integrity": "sha512-jztq8XBfYB+CwG/fRk3VxE8A63LhVbcO7ZqjCYCg97cbn3Kl+hUFyA57LJ4vxG2b7z/FCqCuE/9hzBOhNiyzeA==",
15148
- "dependencies": {
15149
- "motion": "^12.38.0"
15150
- },
15149
+ "version": "1.3.4",
15150
+ "resolved": "https://registry.npmjs.org/react-markdown-typewriter/-/react-markdown-typewriter-1.3.4.tgz",
15151
+ "integrity": "sha512-LE/7xzyta4A6Gz6CfeTP0saCgVsxbCEVj4le4xobrkM/IG9MVWdrpyGFGFdsKmZ1k5otnRF3HP52YXbMv7P1rg==",
15152
+ "workspaces": [
15153
+ "playground"
15154
+ ],
15151
15155
  "peerDependencies": {
15152
15156
  "motion": ">=12.9.0",
15153
15157
  "react": ">=18.0.0",
@@ -51,7 +51,7 @@
51
51
  "react-dom": "^19.2.6",
52
52
  "react-i18next": "^17.0.8",
53
53
  "react-markdown": "^10.1.0",
54
- "react-markdown-typewriter": "^1.2.0",
54
+ "react-markdown-typewriter": "^1.3.4",
55
55
  "react-resizable-panels": "^4.11.2",
56
56
  "rehype-raw": "^7.0.0",
57
57
  "remark-gfm": "^4.0.1",
@@ -1,4 +1,5 @@
1
1
  import { Spinner } from "@/components/ui/spinner";
2
+ import { useEffect, useState } from "react";
2
3
 
3
4
  export function PendingComponent() {
4
5
  return (
@@ -12,10 +13,19 @@ export function PendingComponent() {
12
13
 
13
14
  export function AnimatedDots() {
14
15
  return (
15
- <span className="flex pointer-events-none select-none">
16
+ <span className="inline-flex pointer-events-none select-none">
16
17
  <span className="animate-bounce [animation-delay:0s]">.</span>
17
18
  <span className="animate-bounce [animation-delay:0.2s]">.</span>
18
19
  <span className="animate-bounce [animation-delay:0.4s]">.</span>
19
20
  </span>
20
21
  );
21
22
  }
23
+
24
+ export function DelayedAnimatedDots({ delay = 300 }: { delay?: number }) {
25
+ const [visible, setVisible] = useState(false);
26
+ useEffect(() => {
27
+ const id = setTimeout(() => setVisible(true), delay);
28
+ return () => clearTimeout(id);
29
+ }, [delay]);
30
+ return visible ? <AnimatedDots /> : null;
31
+ }
@@ -1,4 +1,5 @@
1
1
  import { Button } from "@/components/ui/button";
2
+ import { useChoiceMenuHotkeys } from "@/lib/hooks/hotkeys-hooks";
2
3
  import { useNarrationFunctions } from "@/lib/hooks/narration-hooks";
3
4
  import { useQueryChoiceMenuOptions } from "@/lib/query/narration-query";
4
5
  import { GameStatus } from "@/lib/stores/game-status-store";
@@ -13,9 +14,11 @@ export function ChoiceMenu() {
13
14
  const isTyping = useSelector(TextDisplaySettings.store, (state) => state.inProgress);
14
15
  const { selectChoice } = useNarrationFunctions();
15
16
  const [debouncedMenu] = useDebouncedValue(isTyping ? [] : menu, { wait: 50 });
17
+ const { menuRef } = useChoiceMenuHotkeys(debouncedMenu.length);
16
18
 
17
19
  return (
18
20
  <div
21
+ ref={menuRef}
19
22
  className="flex flex-col items-center justify-center gap-2 w-full h-full overflow-auto"
20
23
  role="menu"
21
24
  >
@@ -28,6 +31,7 @@ export function ChoiceMenu() {
28
31
  style={{ animationDelay: `${index * 150}ms` }}
29
32
  >
30
33
  <Button
34
+ role="menuitem"
31
35
  disabled={loading}
32
36
  onClick={() => selectChoice(item)}
33
37
  size="sm"
@@ -27,7 +27,6 @@ const menuButtonClass =
27
27
  "justify-start hover:scale-105 focus-visible:scale-105 transition-transform duration-150 ease-out";
28
28
 
29
29
  export function MainMenu() {
30
- const queryClient = useQueryClient();
31
30
  const gameProps = useGameProps();
32
31
  const { uiTransition: t, navigate } = gameProps;
33
32
  const setSettings = useSetSearchParamState<boolean>("settings");
@@ -149,11 +148,7 @@ export function MainMenu() {
149
148
  setLoading(true);
150
149
  await navigate({ to: "/game/narration" });
151
150
  Game.start("start", gameProps)
152
- .then(() =>
153
- queryClient.invalidateQueries({
154
- queryKey: [INTERFACE_DATA_USE_QUERY_KEY],
155
- }),
156
- )
151
+ .then(() => gameProps.invalidateInterfaceData())
157
152
  .finally(() => setLoading(false));
158
153
  }}
159
154
  disabled={loading}
@@ -208,7 +208,9 @@ export function SettingsDialogue() {
208
208
  onOpenChange={(isOpen) => {
209
209
  setOpen(isOpen || undefined);
210
210
  if (!isOpen) {
211
- setSettingsTab(undefined);
211
+ // Delay clearing the tab until after the 100ms close animation
212
+ // to avoid briefly flashing the main settings view during close.
213
+ setTimeout(() => setSettingsTab(undefined), 150);
212
214
  }
213
215
  }}
214
216
  >
@@ -14,7 +14,7 @@ import rehypeRaw from "rehype-raw";
14
14
  import remarkGfm from "remark-gfm";
15
15
 
16
16
  export function InputRequestDialog() {
17
- const { data: { animatedText: text } = {} } = useQueryDialogue();
17
+ const { data: { lastText: text } = {} } = useQueryDialogue();
18
18
  const {
19
19
  data: { isRequired, type, currentValue } = { currentValue: undefined, isRequired: false },
20
20
  } = useQueryInputValue<string | number>();
@@ -223,7 +223,9 @@ function QuickActionsWheelContent({
223
223
  >
224
224
  <span className="font-semibold">{registration.label}</span>
225
225
  <span className="text-[11px] opacity-85">
226
- {registration.hotkeyLabel}
226
+ {registration.hotkeyLabel.length > 15
227
+ ? `${registration.hotkeyLabel.slice(0, 12)}…`
228
+ : registration.hotkeyLabel}
227
229
  </span>
228
230
  </button>
229
231
  );