create-pixi-vn 2.1.2 → 2.1.4
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-lib/package.json +5 -5
- package/template-nqtr-react-vite-muijoy/.assetpack.ts +82 -2
- package/template-nqtr-react-vite-muijoy/src/components/menus/save-menu/index.tsx +47 -13
- package/template-nqtr-react-vite-muijoy/src/components/menus/save-menu/save-slots.tsx +3 -3
- package/template-nqtr-react-vite-muijoy/src/components/menus/settings/menus/save-load.tsx +18 -2
- package/template-nqtr-react-vite-muijoy/src/components/quick-tools.tsx +4 -8
- package/template-nqtr-react-vite-muijoy/src/lib/hooks/hotkeys-hooks.ts +3 -3
- package/template-nqtr-react-vite-muijoy/src/lib/hooks/save-hooks.tsx +3 -2
- package/template-nqtr-react-vite-muijoy/src/lib/utils/save-utility.ts +57 -0
- package/template-nqtr-react-vite-muijoy/src/locales/en.json +1 -0
- package/template-nqtr-react-vite-muijoy-ink/.assetpack.ts +82 -2
- package/template-nqtr-react-vite-muijoy-ink/package.json +1 -1
- package/template-nqtr-react-vite-muijoy-ink/src/components/menus/save-menu/index.tsx +47 -13
- package/template-nqtr-react-vite-muijoy-ink/src/components/menus/save-menu/save-slots.tsx +3 -3
- package/template-nqtr-react-vite-muijoy-ink/src/components/menus/settings/menus/save-load.tsx +18 -2
- package/template-nqtr-react-vite-muijoy-ink/src/components/quick-tools.tsx +4 -8
- package/template-nqtr-react-vite-muijoy-ink/src/lib/hooks/hotkeys-hooks.ts +3 -3
- package/template-nqtr-react-vite-muijoy-ink/src/lib/hooks/save-hooks.tsx +3 -2
- package/template-nqtr-react-vite-muijoy-ink/src/lib/utils/save-utility.ts +57 -0
- package/template-nqtr-react-vite-muijoy-ink/src/locales/en.json +1 -0
- package/template-nqtr-react-vite-muijoy-ink-tauri/.assetpack.ts +82 -2
- package/template-nqtr-react-vite-muijoy-ink-tauri/package.json +1 -1
- package/template-nqtr-react-vite-muijoy-ink-tauri/src/components/menus/save-menu/index.tsx +47 -13
- package/template-nqtr-react-vite-muijoy-ink-tauri/src/components/menus/save-menu/save-slots.tsx +3 -3
- package/template-nqtr-react-vite-muijoy-ink-tauri/src/components/menus/settings/menus/save-load.tsx +18 -2
- package/template-nqtr-react-vite-muijoy-ink-tauri/src/components/quick-tools.tsx +4 -8
- package/template-nqtr-react-vite-muijoy-ink-tauri/src/lib/hooks/hotkeys-hooks.ts +3 -3
- package/template-nqtr-react-vite-muijoy-ink-tauri/src/lib/hooks/save-hooks.tsx +3 -2
- package/template-nqtr-react-vite-muijoy-ink-tauri/src/lib/utils/save-utility.ts +57 -0
- package/template-nqtr-react-vite-muijoy-ink-tauri/src/locales/en.json +1 -0
- package/template-nqtr-react-vite-muijoy-tauri/.assetpack.ts +82 -2
- package/template-nqtr-react-vite-muijoy-tauri/src/components/menus/save-menu/index.tsx +47 -13
- package/template-nqtr-react-vite-muijoy-tauri/src/components/menus/save-menu/save-slots.tsx +3 -3
- package/template-nqtr-react-vite-muijoy-tauri/src/components/menus/settings/menus/save-load.tsx +18 -2
- package/template-nqtr-react-vite-muijoy-tauri/src/components/quick-tools.tsx +4 -8
- package/template-nqtr-react-vite-muijoy-tauri/src/lib/hooks/hotkeys-hooks.ts +3 -3
- package/template-nqtr-react-vite-muijoy-tauri/src/lib/hooks/save-hooks.tsx +3 -2
- package/template-nqtr-react-vite-muijoy-tauri/src/lib/utils/save-utility.ts +57 -0
- package/template-nqtr-react-vite-muijoy-tauri/src/locales/en.json +1 -0
- package/template-react-vite-muijoy/.assetpack.ts +82 -2
- package/template-react-vite-muijoy/src/components/menus/save-menu/index.tsx +47 -13
- package/template-react-vite-muijoy/src/components/menus/save-menu/save-slots.tsx +3 -3
- package/template-react-vite-muijoy/src/components/menus/settings/menus/save-load.tsx +18 -2
- package/template-react-vite-muijoy/src/components/quick-tools.tsx +4 -8
- package/template-react-vite-muijoy/src/lib/hooks/hotkeys-hooks.ts +3 -3
- package/template-react-vite-muijoy/src/lib/hooks/save-hooks.tsx +3 -2
- package/template-react-vite-muijoy/src/lib/utils/save-utility.ts +57 -0
- package/template-react-vite-muijoy/src/locales/en.json +1 -0
- package/template-react-vite-muijoy-ink/.assetpack.ts +82 -2
- package/template-react-vite-muijoy-ink/package.json +1 -1
- package/template-react-vite-muijoy-ink/src/components/menus/save-menu/index.tsx +47 -13
- package/template-react-vite-muijoy-ink/src/components/menus/save-menu/save-slots.tsx +3 -3
- package/template-react-vite-muijoy-ink/src/components/menus/settings/menus/save-load.tsx +18 -2
- package/template-react-vite-muijoy-ink/src/components/quick-tools.tsx +4 -8
- package/template-react-vite-muijoy-ink/src/lib/hooks/hotkeys-hooks.ts +3 -3
- package/template-react-vite-muijoy-ink/src/lib/hooks/save-hooks.tsx +3 -2
- package/template-react-vite-muijoy-ink/src/lib/utils/save-utility.ts +57 -0
- package/template-react-vite-muijoy-ink/src/locales/en.json +1 -0
- package/template-react-vite-muijoy-ink-tauri/.assetpack.ts +82 -2
- package/template-react-vite-muijoy-ink-tauri/package.json +1 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/save-menu/index.tsx +47 -13
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/save-menu/save-slots.tsx +3 -3
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/settings/menus/save-load.tsx +18 -2
- package/template-react-vite-muijoy-ink-tauri/src/components/quick-tools.tsx +4 -8
- package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/hotkeys-hooks.ts +3 -3
- package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/save-hooks.tsx +3 -2
- package/template-react-vite-muijoy-ink-tauri/src/lib/utils/save-utility.ts +57 -0
- package/template-react-vite-muijoy-ink-tauri/src/locales/en.json +1 -0
- package/template-react-vite-muijoy-tauri/.assetpack.ts +82 -2
- package/template-react-vite-muijoy-tauri/src/components/menus/save-menu/index.tsx +47 -13
- package/template-react-vite-muijoy-tauri/src/components/menus/save-menu/save-slots.tsx +3 -3
- package/template-react-vite-muijoy-tauri/src/components/menus/settings/menus/save-load.tsx +18 -2
- package/template-react-vite-muijoy-tauri/src/components/quick-tools.tsx +4 -8
- package/template-react-vite-muijoy-tauri/src/lib/hooks/hotkeys-hooks.ts +3 -3
- package/template-react-vite-muijoy-tauri/src/lib/hooks/save-hooks.tsx +3 -2
- package/template-react-vite-muijoy-tauri/src/lib/utils/save-utility.ts +57 -0
- package/template-react-vite-muijoy-tauri/src/locales/en.json +1 -0
|
@@ -15,7 +15,7 @@ import { AutoSettings } from "@/lib/stores/auto-settings-store";
|
|
|
15
15
|
import { GameStatus } from "@/lib/stores/game-status-store";
|
|
16
16
|
import { SkipSettings } from "@/lib/stores/skip-settings-store";
|
|
17
17
|
import { cn } from "@/lib/utils";
|
|
18
|
-
import { loadSave,
|
|
18
|
+
import { getSaveSlotLabel, loadSave, quickSaveGameToIndexDB } from "@/lib/utils/save-utility";
|
|
19
19
|
import { useQueryClient } from "@tanstack/react-query";
|
|
20
20
|
import { useSelector } from "@tanstack/react-store";
|
|
21
21
|
import { useTranslation } from "react-i18next";
|
|
@@ -39,11 +39,7 @@ export function QuickTools() {
|
|
|
39
39
|
const setSettingsTab = useSetSearchParamState<string>("settings_tab");
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
|
-
<div
|
|
43
|
-
className={cn(
|
|
44
|
-
"flex flex-wrap items-center justify-end gap-0.5 sm:gap-1",
|
|
45
|
-
)}
|
|
46
|
-
>
|
|
42
|
+
<div className={cn("flex flex-wrap items-center justify-end gap-0.5 sm:gap-1")}>
|
|
47
43
|
<Button
|
|
48
44
|
variant="ghost"
|
|
49
45
|
size="xs"
|
|
@@ -104,7 +100,7 @@ export function QuickTools() {
|
|
|
104
100
|
size="xs"
|
|
105
101
|
className="h-5 px-1 text-[10px] sm:h-6 sm:px-2 sm:text-xs"
|
|
106
102
|
onClick={() => {
|
|
107
|
-
const savePromise =
|
|
103
|
+
const savePromise = quickSaveGameToIndexDB().then((save) => {
|
|
108
104
|
queryClient.setQueryData([SAVES_USE_QUERY_KEY, save.id], save);
|
|
109
105
|
queryClient.setQueryData([LAST_SAVE_USE_QUERY_KEY], save);
|
|
110
106
|
});
|
|
@@ -126,7 +122,7 @@ export function QuickTools() {
|
|
|
126
122
|
openAlertDialog({
|
|
127
123
|
head: t("load"),
|
|
128
124
|
content: t("you_sure_to_load_save", {
|
|
129
|
-
name: lastSave.name ||
|
|
125
|
+
name: lastSave.name || getSaveSlotLabel(lastSave.id, t),
|
|
130
126
|
}),
|
|
131
127
|
onConfirm: () =>
|
|
132
128
|
loadSave(lastSave)
|
|
@@ -17,7 +17,7 @@ import { QuickActionsWheelState } from "@/lib/stores/quick-actions-wheel-store";
|
|
|
17
17
|
import { SearchParams } from "@/lib/stores/search-param-store";
|
|
18
18
|
import { SkipSettings } from "@/lib/stores/skip-settings-store";
|
|
19
19
|
import { TextDisplaySettings } from "@/lib/stores/text-display-settings-store";
|
|
20
|
-
import { loadSave,
|
|
20
|
+
import { getSaveSlotLabel, loadSave, quickSaveGameToIndexDB } from "@/lib/utils/save-utility";
|
|
21
21
|
import { navigator, type RoomIdType } from "@drincs/nqtr";
|
|
22
22
|
import { narration } from "@drincs/pixi-vn";
|
|
23
23
|
import type { RegisterableHotkey } from "@tanstack/hotkeys";
|
|
@@ -78,7 +78,7 @@ export function useSaveHotkeys(): null {
|
|
|
78
78
|
console.log("Can't save on home page");
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
-
const savePromise =
|
|
81
|
+
const savePromise = quickSaveGameToIndexDB().then((save) => {
|
|
82
82
|
queryClient.setQueryData([SAVES_USE_QUERY_KEY, save.id], save);
|
|
83
83
|
queryClient.setQueryData([LAST_SAVE_USE_QUERY_KEY], save);
|
|
84
84
|
});
|
|
@@ -97,7 +97,7 @@ export function useSaveHotkeys(): null {
|
|
|
97
97
|
openAlertDialog({
|
|
98
98
|
head: t("load"),
|
|
99
99
|
content: t("you_sure_to_load_save", {
|
|
100
|
-
name: lastSave.name ||
|
|
100
|
+
name: lastSave.name || getSaveSlotLabel(lastSave.id, t),
|
|
101
101
|
}),
|
|
102
102
|
onConfirm: () =>
|
|
103
103
|
loadSave(lastSave)
|
|
@@ -6,6 +6,7 @@ import { LAST_SAVE_USE_QUERY_KEY, SAVES_USE_QUERY_KEY } from "@/lib/query/save-q
|
|
|
6
6
|
import {
|
|
7
7
|
addRefreshSave,
|
|
8
8
|
deleteSaveFromIndexDB,
|
|
9
|
+
getSaveSlotLabel,
|
|
9
10
|
loadSave,
|
|
10
11
|
saveGameToIndexDB,
|
|
11
12
|
} from "@/lib/utils/save-utility";
|
|
@@ -31,7 +32,7 @@ export function useSaveActions() {
|
|
|
31
32
|
openAlertDialog({
|
|
32
33
|
head: t("load"),
|
|
33
34
|
content: t("you_sure_to_load_save", {
|
|
34
|
-
name: data.name ||
|
|
35
|
+
name: data.name || getSaveSlotLabel(data.id, t),
|
|
35
36
|
}),
|
|
36
37
|
onConfirm: () =>
|
|
37
38
|
loadSave(data)
|
|
@@ -57,7 +58,7 @@ export function useSaveActions() {
|
|
|
57
58
|
openAlertDialog({
|
|
58
59
|
head: t("delete"),
|
|
59
60
|
content: t("you_sure_to_delete_save", {
|
|
60
|
-
name:
|
|
61
|
+
name: getSaveSlotLabel(id, t),
|
|
61
62
|
}),
|
|
62
63
|
onConfirm: () =>
|
|
63
64
|
deleteSaveFromIndexDB(id)
|
|
@@ -12,6 +12,40 @@ import { canvas, Game } from "@drincs/pixi-vn";
|
|
|
12
12
|
const SAVE_FILE_EXTENSION = "json";
|
|
13
13
|
const REFRESH_SAVE_LOCAL_STORAGE_KEY = "refresh_save";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Quick saves live in a fixed, reserved range of negative ids (`-2`, `-3`, ...) so they
|
|
17
|
+
* never collide with the auto-incrementing ids used by manual saves (`0`, `1`, ...) or
|
|
18
|
+
* with the `-1` id reserved for the "refresh save" (see {@link getLastSaveFromIndexDB}).
|
|
19
|
+
*/
|
|
20
|
+
const QUICK_SAVE_ID_START = -2;
|
|
21
|
+
/** Number of quick-save slots. */
|
|
22
|
+
const QUICK_SAVE_SLOTS = 6;
|
|
23
|
+
|
|
24
|
+
function getQuickSaveId(slotIndex: number): number {
|
|
25
|
+
return QUICK_SAVE_ID_START - slotIndex;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getQuickSaveIds(): number[] {
|
|
29
|
+
return Array.from({ length: QUICK_SAVE_SLOTS }, (_, index) => getQuickSaveId(index));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isQuickSaveId(id: number): boolean {
|
|
33
|
+
return id <= QUICK_SAVE_ID_START;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 1-based slot number for a quick-save id, for display purposes. */
|
|
37
|
+
export function getQuickSaveSlotNumber(id: number): number {
|
|
38
|
+
return QUICK_SAVE_ID_START - id + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Human-readable label for a save slot, e.g. "File 01" or "Quick Save 2". */
|
|
42
|
+
export function getSaveSlotLabel(id: number, t: (key: string) => string): string {
|
|
43
|
+
if (isQuickSaveId(id)) {
|
|
44
|
+
return `${t("quick_save")} ${getQuickSaveSlotNumber(id)}`;
|
|
45
|
+
}
|
|
46
|
+
return `${t("save_slot")} ${String(id + 1).padStart(2, "0")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
15
49
|
export function createGameSave(options?: { image?: string; name?: string }): GameSaveData {
|
|
16
50
|
const { image, name = "" } = options || {};
|
|
17
51
|
return {
|
|
@@ -54,6 +88,29 @@ export async function saveGameToIndexDB(
|
|
|
54
88
|
return (await getLastSaveFromIndexDB()) as GameSaveData & { id: number };
|
|
55
89
|
}
|
|
56
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Saves into the dedicated quick-save slots (see {@link getQuickSaveIds}). Fills the first
|
|
93
|
+
* empty slot; once all slots are full, overwrites the least recently saved one.
|
|
94
|
+
*/
|
|
95
|
+
export async function quickSaveGameToIndexDB(): Promise<GameSaveData & { id: number }> {
|
|
96
|
+
const ids = getQuickSaveIds();
|
|
97
|
+
const slots = await Promise.all(ids.map((id) => getSaveFromIndexDB(id)));
|
|
98
|
+
|
|
99
|
+
let targetIndex = slots.findIndex((slot) => !slot);
|
|
100
|
+
if (targetIndex === -1) {
|
|
101
|
+
targetIndex = 0;
|
|
102
|
+
for (let index = 1; index < slots.length; index++) {
|
|
103
|
+
const slot = slots[index];
|
|
104
|
+
const oldest = slots[targetIndex];
|
|
105
|
+
if (slot && oldest && new Date(slot.date) < new Date(oldest.date)) {
|
|
106
|
+
targetIndex = index;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return saveGameToIndexDB({ id: ids[targetIndex] });
|
|
112
|
+
}
|
|
113
|
+
|
|
57
114
|
export async function getSaveFromIndexDB(
|
|
58
115
|
id: number,
|
|
59
116
|
): Promise<(GameSaveData & { id: number }) | null> {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"quick_save_hotkey_description": "Save the current progress immediately.",
|
|
41
41
|
"quick_save_hotkey_alternative_description": "Save the current progress with Ctrl+S.",
|
|
42
42
|
"quick_save_restricted": "Q.Save",
|
|
43
|
+
"quick_saves": "Quick Saves",
|
|
43
44
|
"return_main_menu": "Return to Main Menu",
|
|
44
45
|
"save": "Save",
|
|
45
46
|
"saving": "Saving…",
|
|
@@ -1,9 +1,87 @@
|
|
|
1
|
-
import type { AssetPackConfig } from "@assetpack/core";
|
|
1
|
+
import type { AssetPackConfig, AssetPipe } from "@assetpack/core";
|
|
2
|
+
import { path } from "@assetpack/core";
|
|
2
3
|
import { pixiPipes } from "@assetpack/core/pixi";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
3
5
|
|
|
4
6
|
// TAURI_ENV_TARGET_TRIPLE is set by `tauri build` before running beforeBuildCommand
|
|
5
7
|
const isTauri = !!process.env.TAURI_ENV_TARGET_TRIPLE;
|
|
6
8
|
|
|
9
|
+
const manifestOutput = "src/assets/manifest.gen.json";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AssetPack only creates a manifest bundle for folders explicitly tagged
|
|
13
|
+
* with "{m}" (see pixiManifest's `manifest` tag); everything else lands in
|
|
14
|
+
* the single "default" bundle. This splits the "default" bundle into one
|
|
15
|
+
* bundle per top-level folder under the assets entry, named after that
|
|
16
|
+
* folder, so bundles mirror the folder structure without needing to tag
|
|
17
|
+
* every folder manually. Files directly at the entry root (no folder) stay
|
|
18
|
+
* in "default". Runs after the manifest has been written to disk, so it
|
|
19
|
+
* re-reads and rewrites the file.
|
|
20
|
+
*/
|
|
21
|
+
function groupBundlesByFolder(options: { output: string }): AssetPipe<{ output: string }> {
|
|
22
|
+
return {
|
|
23
|
+
name: "group-bundles-by-folder",
|
|
24
|
+
defaultOptions: options,
|
|
25
|
+
async finish(_asset, opts) {
|
|
26
|
+
const manifest = JSON.parse(await fs.readFile(opts.output, "utf-8"));
|
|
27
|
+
|
|
28
|
+
const bundles = new Map<string, { name: string; assets: unknown[] }>();
|
|
29
|
+
for (const bundle of manifest.bundles) {
|
|
30
|
+
if (bundle.name !== "default") {
|
|
31
|
+
bundles.set(bundle.name, bundle);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
for (const asset of bundle.assets) {
|
|
35
|
+
const folder = path.dirname(asset.src[0]).split("/")[0];
|
|
36
|
+
const bundleName = folder === "." ? "default" : folder;
|
|
37
|
+
const target = bundles.get(bundleName) ?? { name: bundleName, assets: [] };
|
|
38
|
+
target.assets.push(asset);
|
|
39
|
+
bundles.set(bundleName, target);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
manifest.bundles = [...bundles.values()];
|
|
43
|
+
|
|
44
|
+
await fs.writeFile(opts.output, JSON.stringify(manifest, null, 2));
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Removes file extensions from the aliases generated by the pixi manifest
|
|
51
|
+
* pipe and replaces path separators with underscores (e.g.
|
|
52
|
+
* "yard/yard1.png" -> "yard_yard1"). Runs after the manifest has been
|
|
53
|
+
* written to disk, so it re-reads and rewrites the file.
|
|
54
|
+
*
|
|
55
|
+
* If two assets end up with the same normalized alias, the later one is
|
|
56
|
+
* prefixed with its bundle name (e.g. "yard_yard_yard1") to keep aliases
|
|
57
|
+
* unique across the whole manifest.
|
|
58
|
+
*/
|
|
59
|
+
function normalizeAliases(options: { output: string }): AssetPipe<{ output: string }> {
|
|
60
|
+
return {
|
|
61
|
+
name: "normalize-aliases",
|
|
62
|
+
defaultOptions: options,
|
|
63
|
+
async finish(_asset, opts) {
|
|
64
|
+
const manifest = JSON.parse(await fs.readFile(opts.output, "utf-8"));
|
|
65
|
+
|
|
66
|
+
const seen = new Set<string>();
|
|
67
|
+
for (const bundle of manifest.bundles) {
|
|
68
|
+
for (const asset of bundle.assets) {
|
|
69
|
+
asset.alias = asset.alias.map((alias: string) => {
|
|
70
|
+
const normalized = path.trimExt(alias).replaceAll("/", "_");
|
|
71
|
+
const unique = seen.has(normalized)
|
|
72
|
+
? `${bundle.name}_${normalized}`
|
|
73
|
+
: normalized;
|
|
74
|
+
seen.add(normalized);
|
|
75
|
+
return unique;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await fs.writeFile(opts.output, JSON.stringify(manifest, null, 2));
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
7
85
|
const config: AssetPackConfig = {
|
|
8
86
|
entry: "./src/assets",
|
|
9
87
|
output: "./public/assets",
|
|
@@ -11,7 +89,7 @@ const config: AssetPackConfig = {
|
|
|
11
89
|
pipes: [
|
|
12
90
|
...pixiPipes({
|
|
13
91
|
manifest: {
|
|
14
|
-
output:
|
|
92
|
+
output: manifestOutput,
|
|
15
93
|
createShortcuts: true,
|
|
16
94
|
},
|
|
17
95
|
// For Tauri: skip @0.5x mipmaps (unused on desktop, WebView handles DPR)
|
|
@@ -21,6 +99,8 @@ const config: AssetPackConfig = {
|
|
|
21
99
|
? { png: true, jpg: true, webp: { quality: 88, alphaQuality: 88 } }
|
|
22
100
|
: undefined,
|
|
23
101
|
}),
|
|
102
|
+
groupBundlesByFolder({ output: manifestOutput }),
|
|
103
|
+
normalizeAliases({ output: manifestOutput }),
|
|
24
104
|
],
|
|
25
105
|
};
|
|
26
106
|
|
|
@@ -10,42 +10,71 @@ import {
|
|
|
10
10
|
} from "@/components/ui/pagination";
|
|
11
11
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
12
12
|
import { SaveMenuPagination } from "@/lib/stores/save-menu-pagination-store";
|
|
13
|
+
import { getQuickSaveIds } from "@/lib/utils/save-utility";
|
|
13
14
|
import { useSelector } from "@tanstack/react-store";
|
|
14
15
|
|
|
15
|
-
const TOTAL_PAGES =
|
|
16
|
+
const TOTAL_PAGES = 10;
|
|
17
|
+
/** Number of save slots shown per page. */
|
|
18
|
+
const SAVES_PER_PAGE = 6;
|
|
19
|
+
const QUICK_SAVE_IDS = getQuickSaveIds();
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
/** Number of consecutive page numbers shown around the current page on desktop. */
|
|
22
|
+
const DESKTOP_WINDOW_SIZE = 4;
|
|
23
|
+
|
|
24
|
+
type PageEntry = number | "ellipsis-start" | "ellipsis-end";
|
|
25
|
+
|
|
26
|
+
function getPageNumbers(current: number, total: number): PageEntry[] {
|
|
18
27
|
if (total <= 7) {
|
|
19
28
|
return Array.from({ length: total }, (_, i) => i + 1);
|
|
20
29
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
const start = Math.max(1, Math.min(current - 1, total - DESKTOP_WINDOW_SIZE));
|
|
31
|
+
const end = start + DESKTOP_WINDOW_SIZE - 1;
|
|
32
|
+
|
|
33
|
+
const pages: PageEntry[] = [];
|
|
34
|
+
if (start > 1) {
|
|
35
|
+
pages.push(1);
|
|
36
|
+
if (start > 2) pages.push("ellipsis-start");
|
|
37
|
+
}
|
|
38
|
+
for (let i = start; i <= end; i++) {
|
|
24
39
|
pages.push(i);
|
|
25
40
|
}
|
|
26
|
-
if (
|
|
27
|
-
|
|
41
|
+
if (end < total) {
|
|
42
|
+
if (end < total - 1) pages.push("ellipsis-end");
|
|
43
|
+
pages.push(total);
|
|
44
|
+
}
|
|
28
45
|
return pages;
|
|
29
46
|
}
|
|
30
47
|
|
|
31
|
-
export function GameSaveMenu() {
|
|
48
|
+
export function GameSaveMenu({ quickSaves }: { quickSaves: boolean }) {
|
|
32
49
|
const page = useSelector(SaveMenuPagination.store, (state) => state.page);
|
|
33
50
|
|
|
34
51
|
const currentPage = page + 1;
|
|
35
52
|
const pageNumbers = getPageNumbers(currentPage, TOTAL_PAGES);
|
|
36
53
|
|
|
54
|
+
if (quickSaves) {
|
|
55
|
+
return (
|
|
56
|
+
<ScrollArea className="min-h-0 flex-1">
|
|
57
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
|
|
58
|
+
{QUICK_SAVE_IDS.map((id) => (
|
|
59
|
+
<SaveSlot key={`SaveFile${id}`} saveId={id} />
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</ScrollArea>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
return (
|
|
38
67
|
<div className="flex flex-1 min-h-0 flex-col">
|
|
39
68
|
<ScrollArea className="min-h-0 flex-1">
|
|
40
69
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
|
|
41
|
-
{Array.from({ length:
|
|
42
|
-
const id = page *
|
|
70
|
+
{Array.from({ length: SAVES_PER_PAGE }).map((_, index) => {
|
|
71
|
+
const id = page * SAVES_PER_PAGE + index;
|
|
43
72
|
return <SaveSlot key={`SaveFile${id}`} saveId={id} />;
|
|
44
73
|
})}
|
|
45
74
|
</div>
|
|
46
75
|
</ScrollArea>
|
|
47
76
|
<Pagination className="shrink-0 border-t py-1.5">
|
|
48
|
-
<PaginationContent>
|
|
77
|
+
<PaginationContent className="w-full justify-between px-2 sm:w-auto sm:justify-center sm:px-0">
|
|
49
78
|
<PaginationItem>
|
|
50
79
|
<PaginationPrevious
|
|
51
80
|
onClick={(e) => {
|
|
@@ -56,9 +85,14 @@ export function GameSaveMenu() {
|
|
|
56
85
|
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
|
|
57
86
|
/>
|
|
58
87
|
</PaginationItem>
|
|
88
|
+
<PaginationItem className="sm:hidden">
|
|
89
|
+
<span className="px-2 text-sm text-muted-foreground">
|
|
90
|
+
{currentPage} / {TOTAL_PAGES}
|
|
91
|
+
</span>
|
|
92
|
+
</PaginationItem>
|
|
59
93
|
{pageNumbers.map((p) =>
|
|
60
|
-
p === "ellipsis" ? (
|
|
61
|
-
<PaginationItem key={
|
|
94
|
+
p === "ellipsis-start" || p === "ellipsis-end" ? (
|
|
95
|
+
<PaginationItem key={p} className="hidden sm:list-item">
|
|
62
96
|
<PaginationEllipsis />
|
|
63
97
|
</PaginationItem>
|
|
64
98
|
) : (
|
|
@@ -6,7 +6,7 @@ import { overlayTextShadowClass } from "@/constants";
|
|
|
6
6
|
import { useSaveActions } from "@/lib/hooks/save-hooks";
|
|
7
7
|
import { useQuerySaves } from "@/lib/query/save-query";
|
|
8
8
|
import { cn } from "@/lib/utils";
|
|
9
|
-
import { downloadGameSave } from "@/lib/utils/save-utility";
|
|
9
|
+
import { downloadGameSave, getSaveSlotLabel } from "@/lib/utils/save-utility";
|
|
10
10
|
import type { FileRouteTypes } from "@/routeTree.gen";
|
|
11
11
|
import { useLocation } from "@tanstack/react-router";
|
|
12
12
|
import { Download, Save, SquarePen, Trash2 } from "lucide-react";
|
|
@@ -54,7 +54,7 @@ export function SaveSlot({ saveId }: { saveId: number }) {
|
|
|
54
54
|
src={saveData.image}
|
|
55
55
|
layout="fullWidth"
|
|
56
56
|
alt={saveData.name}
|
|
57
|
-
className="absolute inset-0 size-full object-contain pointer-events-none select-none rounded-
|
|
57
|
+
className="absolute inset-0 size-full object-contain pointer-events-none select-none rounded-md"
|
|
58
58
|
/>
|
|
59
59
|
{/* top-left metadata */}
|
|
60
60
|
<div className="absolute top-2.5 left-2.5 flex flex-col gap-0.5 pointer-events-none">
|
|
@@ -73,7 +73,7 @@ export function SaveSlot({ saveId }: { saveId: number }) {
|
|
|
73
73
|
{saveData.date.toLocaleTimeString()}
|
|
74
74
|
</span>
|
|
75
75
|
<span className={cn("text-sm text-neutral-300", overlayTextShadowClass)}>
|
|
76
|
-
{
|
|
76
|
+
{getSaveSlotLabel(saveId, t)}
|
|
77
77
|
</span>
|
|
78
78
|
</div>
|
|
79
79
|
{/* top-right delete */}
|
package/template-nqtr-react-vite-muijoy-tauri/src/components/menus/settings/menus/save-load.tsx
CHANGED
|
@@ -5,7 +5,8 @@ import { useGameProps } from "@/lib/hooks/props-hooks";
|
|
|
5
5
|
import { downloadGameSave, loadGameSaveFromFile } from "@/lib/utils/save-utility";
|
|
6
6
|
import type { FileRouteTypes } from "@/routeTree.gen";
|
|
7
7
|
import { useLocation } from "@tanstack/react-router";
|
|
8
|
-
import { Download, FolderOpen } from "lucide-react";
|
|
8
|
+
import { Download, FolderOpen, Zap } from "lucide-react";
|
|
9
|
+
import { useState } from "react";
|
|
9
10
|
import { useTranslation } from "react-i18next";
|
|
10
11
|
import { toast } from "sonner";
|
|
11
12
|
|
|
@@ -13,10 +14,25 @@ export function SaveLoadSettingsPage() {
|
|
|
13
14
|
const { t } = useTranslation(["ui"]);
|
|
14
15
|
const gameProps = useGameProps();
|
|
15
16
|
const location = useLocation();
|
|
17
|
+
const [showQuickSaves, setShowQuickSaves] = useState(false);
|
|
16
18
|
|
|
17
19
|
const toolbar = (
|
|
18
20
|
<TooltipProvider>
|
|
19
21
|
<div className="flex items-center gap-1">
|
|
22
|
+
<Tooltip>
|
|
23
|
+
<TooltipTrigger render={<span />}>
|
|
24
|
+
<Button
|
|
25
|
+
variant={showQuickSaves ? "secondary" : "ghost"}
|
|
26
|
+
size="icon-lg"
|
|
27
|
+
onClick={() => setShowQuickSaves((v) => !v)}
|
|
28
|
+
aria-pressed={showQuickSaves}
|
|
29
|
+
aria-label={t("quick_saves")}
|
|
30
|
+
>
|
|
31
|
+
<Zap className="size-6" />
|
|
32
|
+
</Button>
|
|
33
|
+
</TooltipTrigger>
|
|
34
|
+
<TooltipContent>{t("quick_saves")}</TooltipContent>
|
|
35
|
+
</Tooltip>
|
|
20
36
|
<Tooltip>
|
|
21
37
|
<TooltipTrigger render={<span />}>
|
|
22
38
|
<Button
|
|
@@ -60,7 +76,7 @@ export function SaveLoadSettingsPage() {
|
|
|
60
76
|
return (
|
|
61
77
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
62
78
|
<div className="border-b p-4">{toolbar}</div>
|
|
63
|
-
<GameSaveMenu />
|
|
79
|
+
<GameSaveMenu quickSaves={showQuickSaves} />
|
|
64
80
|
</div>
|
|
65
81
|
);
|
|
66
82
|
}
|
|
@@ -15,7 +15,7 @@ import { AutoSettings } from "@/lib/stores/auto-settings-store";
|
|
|
15
15
|
import { GameStatus } from "@/lib/stores/game-status-store";
|
|
16
16
|
import { SkipSettings } from "@/lib/stores/skip-settings-store";
|
|
17
17
|
import { cn } from "@/lib/utils";
|
|
18
|
-
import { loadSave,
|
|
18
|
+
import { getSaveSlotLabel, loadSave, quickSaveGameToIndexDB } from "@/lib/utils/save-utility";
|
|
19
19
|
import { useQueryClient } from "@tanstack/react-query";
|
|
20
20
|
import { useSelector } from "@tanstack/react-store";
|
|
21
21
|
import { useTranslation } from "react-i18next";
|
|
@@ -39,11 +39,7 @@ export function QuickTools() {
|
|
|
39
39
|
const setSettingsTab = useSetSearchParamState<string>("settings_tab");
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
|
-
<div
|
|
43
|
-
className={cn(
|
|
44
|
-
"flex flex-wrap items-center justify-end gap-0.5 sm:gap-1",
|
|
45
|
-
)}
|
|
46
|
-
>
|
|
42
|
+
<div className={cn("flex flex-wrap items-center justify-end gap-0.5 sm:gap-1")}>
|
|
47
43
|
<Button
|
|
48
44
|
variant="ghost"
|
|
49
45
|
size="xs"
|
|
@@ -104,7 +100,7 @@ export function QuickTools() {
|
|
|
104
100
|
size="xs"
|
|
105
101
|
className="h-5 px-1 text-[10px] sm:h-6 sm:px-2 sm:text-xs"
|
|
106
102
|
onClick={() => {
|
|
107
|
-
const savePromise =
|
|
103
|
+
const savePromise = quickSaveGameToIndexDB().then((save) => {
|
|
108
104
|
queryClient.setQueryData([SAVES_USE_QUERY_KEY, save.id], save);
|
|
109
105
|
queryClient.setQueryData([LAST_SAVE_USE_QUERY_KEY], save);
|
|
110
106
|
});
|
|
@@ -126,7 +122,7 @@ export function QuickTools() {
|
|
|
126
122
|
openAlertDialog({
|
|
127
123
|
head: t("load"),
|
|
128
124
|
content: t("you_sure_to_load_save", {
|
|
129
|
-
name: lastSave.name ||
|
|
125
|
+
name: lastSave.name || getSaveSlotLabel(lastSave.id, t),
|
|
130
126
|
}),
|
|
131
127
|
onConfirm: () =>
|
|
132
128
|
loadSave(lastSave)
|
|
@@ -17,7 +17,7 @@ import { QuickActionsWheelState } from "@/lib/stores/quick-actions-wheel-store";
|
|
|
17
17
|
import { SearchParams } from "@/lib/stores/search-param-store";
|
|
18
18
|
import { SkipSettings } from "@/lib/stores/skip-settings-store";
|
|
19
19
|
import { TextDisplaySettings } from "@/lib/stores/text-display-settings-store";
|
|
20
|
-
import { loadSave,
|
|
20
|
+
import { getSaveSlotLabel, loadSave, quickSaveGameToIndexDB } from "@/lib/utils/save-utility";
|
|
21
21
|
import { navigator, type RoomIdType } from "@drincs/nqtr";
|
|
22
22
|
import { narration } from "@drincs/pixi-vn";
|
|
23
23
|
import type { RegisterableHotkey } from "@tanstack/hotkeys";
|
|
@@ -78,7 +78,7 @@ export function useSaveHotkeys(): null {
|
|
|
78
78
|
console.log("Can't save on home page");
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
-
const savePromise =
|
|
81
|
+
const savePromise = quickSaveGameToIndexDB().then((save) => {
|
|
82
82
|
queryClient.setQueryData([SAVES_USE_QUERY_KEY, save.id], save);
|
|
83
83
|
queryClient.setQueryData([LAST_SAVE_USE_QUERY_KEY], save);
|
|
84
84
|
});
|
|
@@ -97,7 +97,7 @@ export function useSaveHotkeys(): null {
|
|
|
97
97
|
openAlertDialog({
|
|
98
98
|
head: t("load"),
|
|
99
99
|
content: t("you_sure_to_load_save", {
|
|
100
|
-
name: lastSave.name ||
|
|
100
|
+
name: lastSave.name || getSaveSlotLabel(lastSave.id, t),
|
|
101
101
|
}),
|
|
102
102
|
onConfirm: () =>
|
|
103
103
|
loadSave(lastSave)
|
|
@@ -6,6 +6,7 @@ import { LAST_SAVE_USE_QUERY_KEY, SAVES_USE_QUERY_KEY } from "@/lib/query/save-q
|
|
|
6
6
|
import {
|
|
7
7
|
addRefreshSave,
|
|
8
8
|
deleteSaveFromIndexDB,
|
|
9
|
+
getSaveSlotLabel,
|
|
9
10
|
loadSave,
|
|
10
11
|
saveGameToIndexDB,
|
|
11
12
|
} from "@/lib/utils/save-utility";
|
|
@@ -31,7 +32,7 @@ export function useSaveActions() {
|
|
|
31
32
|
openAlertDialog({
|
|
32
33
|
head: t("load"),
|
|
33
34
|
content: t("you_sure_to_load_save", {
|
|
34
|
-
name: data.name ||
|
|
35
|
+
name: data.name || getSaveSlotLabel(data.id, t),
|
|
35
36
|
}),
|
|
36
37
|
onConfirm: () =>
|
|
37
38
|
loadSave(data)
|
|
@@ -57,7 +58,7 @@ export function useSaveActions() {
|
|
|
57
58
|
openAlertDialog({
|
|
58
59
|
head: t("delete"),
|
|
59
60
|
content: t("you_sure_to_delete_save", {
|
|
60
|
-
name:
|
|
61
|
+
name: getSaveSlotLabel(id, t),
|
|
61
62
|
}),
|
|
62
63
|
onConfirm: () =>
|
|
63
64
|
deleteSaveFromIndexDB(id)
|
|
@@ -12,6 +12,40 @@ import { canvas, Game } from "@drincs/pixi-vn";
|
|
|
12
12
|
const SAVE_FILE_EXTENSION = "json";
|
|
13
13
|
const REFRESH_SAVE_LOCAL_STORAGE_KEY = "refresh_save";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Quick saves live in a fixed, reserved range of negative ids (`-2`, `-3`, ...) so they
|
|
17
|
+
* never collide with the auto-incrementing ids used by manual saves (`0`, `1`, ...) or
|
|
18
|
+
* with the `-1` id reserved for the "refresh save" (see {@link getLastSaveFromIndexDB}).
|
|
19
|
+
*/
|
|
20
|
+
const QUICK_SAVE_ID_START = -2;
|
|
21
|
+
/** Number of quick-save slots. */
|
|
22
|
+
const QUICK_SAVE_SLOTS = 6;
|
|
23
|
+
|
|
24
|
+
function getQuickSaveId(slotIndex: number): number {
|
|
25
|
+
return QUICK_SAVE_ID_START - slotIndex;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getQuickSaveIds(): number[] {
|
|
29
|
+
return Array.from({ length: QUICK_SAVE_SLOTS }, (_, index) => getQuickSaveId(index));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isQuickSaveId(id: number): boolean {
|
|
33
|
+
return id <= QUICK_SAVE_ID_START;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 1-based slot number for a quick-save id, for display purposes. */
|
|
37
|
+
export function getQuickSaveSlotNumber(id: number): number {
|
|
38
|
+
return QUICK_SAVE_ID_START - id + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Human-readable label for a save slot, e.g. "File 01" or "Quick Save 2". */
|
|
42
|
+
export function getSaveSlotLabel(id: number, t: (key: string) => string): string {
|
|
43
|
+
if (isQuickSaveId(id)) {
|
|
44
|
+
return `${t("quick_save")} ${getQuickSaveSlotNumber(id)}`;
|
|
45
|
+
}
|
|
46
|
+
return `${t("save_slot")} ${String(id + 1).padStart(2, "0")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
15
49
|
export function createGameSave(options?: { image?: string; name?: string }): GameSaveData {
|
|
16
50
|
const { image, name = "" } = options || {};
|
|
17
51
|
return {
|
|
@@ -54,6 +88,29 @@ export async function saveGameToIndexDB(
|
|
|
54
88
|
return (await getLastSaveFromIndexDB()) as GameSaveData & { id: number };
|
|
55
89
|
}
|
|
56
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Saves into the dedicated quick-save slots (see {@link getQuickSaveIds}). Fills the first
|
|
93
|
+
* empty slot; once all slots are full, overwrites the least recently saved one.
|
|
94
|
+
*/
|
|
95
|
+
export async function quickSaveGameToIndexDB(): Promise<GameSaveData & { id: number }> {
|
|
96
|
+
const ids = getQuickSaveIds();
|
|
97
|
+
const slots = await Promise.all(ids.map((id) => getSaveFromIndexDB(id)));
|
|
98
|
+
|
|
99
|
+
let targetIndex = slots.findIndex((slot) => !slot);
|
|
100
|
+
if (targetIndex === -1) {
|
|
101
|
+
targetIndex = 0;
|
|
102
|
+
for (let index = 1; index < slots.length; index++) {
|
|
103
|
+
const slot = slots[index];
|
|
104
|
+
const oldest = slots[targetIndex];
|
|
105
|
+
if (slot && oldest && new Date(slot.date) < new Date(oldest.date)) {
|
|
106
|
+
targetIndex = index;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return saveGameToIndexDB({ id: ids[targetIndex] });
|
|
112
|
+
}
|
|
113
|
+
|
|
57
114
|
export async function getSaveFromIndexDB(
|
|
58
115
|
id: number,
|
|
59
116
|
): Promise<(GameSaveData & { id: number }) | null> {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"quick_save_hotkey_description": "Save the current progress immediately.",
|
|
41
41
|
"quick_save_hotkey_alternative_description": "Save the current progress with Ctrl+S.",
|
|
42
42
|
"quick_save_restricted": "Q.Save",
|
|
43
|
+
"quick_saves": "Quick Saves",
|
|
43
44
|
"return_main_menu": "Return to Main Menu",
|
|
44
45
|
"save": "Save",
|
|
45
46
|
"saving": "Saving…",
|