@rohal12/spindle 0.31.2 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/components/macros/MenubarButtons.tsx +1 -1
- package/src/saves/save-manager.ts +132 -8
- package/src/saves/types.ts +10 -0
- package/src/store.ts +61 -15
- package/src/story-api.ts +19 -4
- package/types/index.d.ts +30 -2
package/package.json
CHANGED
|
@@ -119,7 +119,7 @@ defineMenubarAction({
|
|
|
119
119
|
setup: () => {
|
|
120
120
|
const load = useStoryStore((s) => s.load);
|
|
121
121
|
const hasSave = useStoryStore((s) => s.hasSave);
|
|
122
|
-
useStoryStore((s) => s.
|
|
122
|
+
useStoryStore((s) => s.knownSaves);
|
|
123
123
|
return { perform: () => load(), disabled: !hasSave() };
|
|
124
124
|
},
|
|
125
125
|
});
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
SavePayload,
|
|
3
3
|
SaveMeta,
|
|
4
4
|
SaveRecord,
|
|
5
|
+
SaveInfo,
|
|
5
6
|
PlaythroughRecord,
|
|
6
7
|
SaveExport,
|
|
7
8
|
} from './types';
|
|
@@ -134,6 +135,7 @@ export async function createSave(
|
|
|
134
135
|
export async function overwriteSave(
|
|
135
136
|
saveId: string,
|
|
136
137
|
payload: SavePayload,
|
|
138
|
+
custom?: Record<string, unknown>,
|
|
137
139
|
): Promise<SaveRecord | undefined> {
|
|
138
140
|
const existing = await getSave(saveId);
|
|
139
141
|
if (!existing) return undefined;
|
|
@@ -149,6 +151,9 @@ export async function overwriteSave(
|
|
|
149
151
|
...existing.meta,
|
|
150
152
|
updatedAt: new Date().toISOString(),
|
|
151
153
|
passage: payload.passage,
|
|
154
|
+
...(custom != null
|
|
155
|
+
? { custom: { ...existing.meta.custom, ...custom } }
|
|
156
|
+
: {}),
|
|
152
157
|
},
|
|
153
158
|
payload: serializedPayload,
|
|
154
159
|
};
|
|
@@ -258,33 +263,58 @@ export async function getSavesGrouped(
|
|
|
258
263
|
return result;
|
|
259
264
|
}
|
|
260
265
|
|
|
261
|
-
// --- Quick Save ---
|
|
266
|
+
// --- Quick Save / Slot Save ---
|
|
262
267
|
|
|
263
268
|
const AUTOSAVE_KEY_PREFIX = 'autosave.';
|
|
269
|
+
const SLOT_KEY_PREFIX = 'slot.';
|
|
270
|
+
const SLOT_INDEX_KEY_PREFIX = 'slotIndex.';
|
|
271
|
+
|
|
272
|
+
function slotMetaKey(ifid: string, slot?: string): string {
|
|
273
|
+
return slot != null
|
|
274
|
+
? `${SLOT_KEY_PREFIX}${slot}.${ifid}`
|
|
275
|
+
: `${AUTOSAVE_KEY_PREFIX}${ifid}`;
|
|
276
|
+
}
|
|
264
277
|
|
|
265
278
|
export async function quickSave(
|
|
266
279
|
ifid: string,
|
|
267
280
|
playthroughId: string,
|
|
268
281
|
payload: SavePayload,
|
|
282
|
+
slot?: string,
|
|
283
|
+
custom?: Record<string, unknown>,
|
|
269
284
|
): Promise<SaveRecord> {
|
|
270
|
-
const metaKey =
|
|
285
|
+
const metaKey = slotMetaKey(ifid, slot);
|
|
271
286
|
const existingId = await getMeta<string>(metaKey);
|
|
272
287
|
|
|
273
288
|
if (existingId) {
|
|
274
|
-
const updated = await overwriteSave(existingId, payload);
|
|
289
|
+
const updated = await overwriteSave(existingId, payload, custom);
|
|
275
290
|
if (updated) return updated;
|
|
276
291
|
}
|
|
277
292
|
|
|
278
|
-
// Create new
|
|
293
|
+
// Create new save
|
|
279
294
|
const record = await createSave(ifid, playthroughId, payload, {
|
|
280
|
-
isAutosave:
|
|
295
|
+
isAutosave: !slot,
|
|
296
|
+
...(slot != null ? { slot } : {}),
|
|
297
|
+
...custom,
|
|
281
298
|
});
|
|
282
299
|
await setMeta(metaKey, record.meta.id);
|
|
300
|
+
|
|
301
|
+
// Track named slots in the index
|
|
302
|
+
if (slot != null) {
|
|
303
|
+
const indexKey = `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
304
|
+
const existing = (await getMeta<string[]>(indexKey)) ?? [];
|
|
305
|
+
if (!existing.includes(slot)) {
|
|
306
|
+
await setMeta(indexKey, [...existing, slot]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
283
310
|
return record;
|
|
284
311
|
}
|
|
285
312
|
|
|
286
|
-
export async function hasQuickSave(
|
|
287
|
-
|
|
313
|
+
export async function hasQuickSave(
|
|
314
|
+
ifid: string,
|
|
315
|
+
slot?: string,
|
|
316
|
+
): Promise<boolean> {
|
|
317
|
+
const metaKey = slotMetaKey(ifid, slot);
|
|
288
318
|
const existingId = await getMeta<string>(metaKey);
|
|
289
319
|
if (!existingId) return false;
|
|
290
320
|
const record = await getSave(existingId);
|
|
@@ -293,13 +323,107 @@ export async function hasQuickSave(ifid: string): Promise<boolean> {
|
|
|
293
323
|
|
|
294
324
|
export async function loadQuickSave(
|
|
295
325
|
ifid: string,
|
|
326
|
+
slot?: string,
|
|
296
327
|
): Promise<SavePayload | undefined> {
|
|
297
|
-
const metaKey =
|
|
328
|
+
const metaKey = slotMetaKey(ifid, slot);
|
|
298
329
|
const existingId = await getMeta<string>(metaKey);
|
|
299
330
|
if (!existingId) return undefined;
|
|
300
331
|
return loadSave(existingId);
|
|
301
332
|
}
|
|
302
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Check IDB for all known saves and return a map of slot keys to true.
|
|
336
|
+
* The default (autosave) slot uses empty string as key.
|
|
337
|
+
*/
|
|
338
|
+
export async function populateKnownSaves(
|
|
339
|
+
ifid: string,
|
|
340
|
+
): Promise<Record<string, true>> {
|
|
341
|
+
const result: Record<string, true> = {};
|
|
342
|
+
|
|
343
|
+
// Check default autosave
|
|
344
|
+
if (await hasQuickSave(ifid)) {
|
|
345
|
+
result[''] = true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check named slots from the index
|
|
349
|
+
const indexKey = `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
350
|
+
const slots = (await getMeta<string[]>(indexKey)) ?? [];
|
|
351
|
+
for (const slot of slots) {
|
|
352
|
+
if (await hasQuickSave(ifid, slot)) {
|
|
353
|
+
result[slot] = true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get metadata for a specific save slot.
|
|
362
|
+
* Returns null if no save exists for that slot.
|
|
363
|
+
*/
|
|
364
|
+
export async function getSlotSaveInfo(
|
|
365
|
+
ifid: string,
|
|
366
|
+
slot?: string,
|
|
367
|
+
): Promise<SaveInfo | null> {
|
|
368
|
+
const metaKey = slotMetaKey(ifid, slot);
|
|
369
|
+
const existingId = await getMeta<string>(metaKey);
|
|
370
|
+
if (!existingId) return null;
|
|
371
|
+
const record = await getSave(existingId);
|
|
372
|
+
if (!record) return null;
|
|
373
|
+
return {
|
|
374
|
+
slot: slot ?? '',
|
|
375
|
+
title: record.meta.title,
|
|
376
|
+
passage: record.meta.passage,
|
|
377
|
+
createdAt: record.meta.createdAt,
|
|
378
|
+
updatedAt: record.meta.updatedAt,
|
|
379
|
+
custom: record.meta.custom ?? {},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* List metadata for all known save slots (default + named).
|
|
385
|
+
*/
|
|
386
|
+
export async function listSlotSaves(ifid: string): Promise<SaveInfo[]> {
|
|
387
|
+
const result: SaveInfo[] = [];
|
|
388
|
+
|
|
389
|
+
// Check default autosave
|
|
390
|
+
const defaultInfo = await getSlotSaveInfo(ifid);
|
|
391
|
+
if (defaultInfo) result.push(defaultInfo);
|
|
392
|
+
|
|
393
|
+
// Check named slots from the index
|
|
394
|
+
const indexKey = `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
395
|
+
const slots = (await getMeta<string[]>(indexKey)) ?? [];
|
|
396
|
+
for (const slot of slots) {
|
|
397
|
+
const info = await getSlotSaveInfo(ifid, slot);
|
|
398
|
+
if (info) result.push(info);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Delete a save by slot name. Removes from slot index if named.
|
|
406
|
+
*/
|
|
407
|
+
export async function deleteSlotSave(
|
|
408
|
+
ifid: string,
|
|
409
|
+
slot?: string,
|
|
410
|
+
): Promise<void> {
|
|
411
|
+
const metaKey = slotMetaKey(ifid, slot);
|
|
412
|
+
const existingId = await getMeta<string>(metaKey);
|
|
413
|
+
if (!existingId) return;
|
|
414
|
+
|
|
415
|
+
await idbDeleteSave(existingId);
|
|
416
|
+
await setMeta(metaKey, undefined);
|
|
417
|
+
|
|
418
|
+
// Remove from slot index if named
|
|
419
|
+
if (slot != null) {
|
|
420
|
+
const indexKey = `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
421
|
+
const existing = (await getMeta<string[]>(indexKey)) ?? [];
|
|
422
|
+
const updated = existing.filter((s) => s !== slot);
|
|
423
|
+
await setMeta(indexKey, updated);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
303
427
|
// --- Session Persistence (survives F5, cleared on tab close) ---
|
|
304
428
|
|
|
305
429
|
const SESSION_KEY_PREFIX = 'spindle.session.';
|
package/src/saves/types.ts
CHANGED
|
@@ -41,6 +41,16 @@ export interface PlaythroughRecord {
|
|
|
41
41
|
label: string;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** Public-facing save metadata for the Story API. */
|
|
45
|
+
export interface SaveInfo {
|
|
46
|
+
slot: string;
|
|
47
|
+
title: string;
|
|
48
|
+
passage: string;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
custom: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
export interface SaveExport {
|
|
45
55
|
version: 1;
|
|
46
56
|
ifid: string;
|
package/src/store.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from 'immer';
|
|
9
9
|
import type { StoryData } from './parser';
|
|
10
10
|
import type { TransitionConfig } from './transition';
|
|
11
|
-
import type { SavePayload, SaveHistoryMoment } from './saves/types';
|
|
11
|
+
import type { SavePayload, SaveHistoryMoment, SaveInfo } from './saves/types';
|
|
12
12
|
import { executeStoryInit } from './story-init';
|
|
13
13
|
import { resetTriggers } from './triggers';
|
|
14
14
|
import {
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
getCurrentPlaythroughId,
|
|
18
18
|
quickSave,
|
|
19
19
|
loadQuickSave,
|
|
20
|
+
populateKnownSaves,
|
|
21
|
+
getSlotSaveInfo,
|
|
22
|
+
listSlotSaves,
|
|
23
|
+
deleteSlotSave,
|
|
20
24
|
saveSession,
|
|
21
25
|
clearSession,
|
|
22
26
|
} from './saves/save-manager';
|
|
@@ -198,7 +202,7 @@ export interface StoryState {
|
|
|
198
202
|
historyIndex: number;
|
|
199
203
|
visitCounts: Record<string, number>;
|
|
200
204
|
renderCounts: Record<string, number>;
|
|
201
|
-
|
|
205
|
+
knownSaves: Record<string, true>;
|
|
202
206
|
playthroughId: string;
|
|
203
207
|
maxHistory: number;
|
|
204
208
|
saveError: string | null;
|
|
@@ -221,9 +225,12 @@ export interface StoryState {
|
|
|
221
225
|
deleteTemporary: (name: string) => void;
|
|
222
226
|
trackRender: (passageName: string) => void;
|
|
223
227
|
restart: () => void;
|
|
224
|
-
save: (slot?: string) => void;
|
|
228
|
+
save: (slot?: string, custom?: Record<string, unknown>) => void;
|
|
225
229
|
load: (slot?: string) => void;
|
|
226
230
|
hasSave: (slot?: string) => boolean;
|
|
231
|
+
getSaveInfo: (slot?: string) => Promise<SaveInfo | null>;
|
|
232
|
+
listSaves: () => Promise<SaveInfo[]>;
|
|
233
|
+
deleteSave: (slot?: string) => void;
|
|
227
234
|
getSavePayload: () => SavePayload;
|
|
228
235
|
loadFromPayload: (payload: SavePayload) => void;
|
|
229
236
|
getHistoryVariables: (index: number) => Record<string, unknown>;
|
|
@@ -243,7 +250,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
243
250
|
historyIndex: -1,
|
|
244
251
|
visitCounts: {},
|
|
245
252
|
renderCounts: {},
|
|
246
|
-
|
|
253
|
+
knownSaves: {},
|
|
247
254
|
playthroughId: '',
|
|
248
255
|
maxHistory: 40,
|
|
249
256
|
saveError: null,
|
|
@@ -307,6 +314,14 @@ export const useStoryStore = create<StoryState>()(
|
|
|
307
314
|
state.playthroughId = newId;
|
|
308
315
|
});
|
|
309
316
|
}
|
|
317
|
+
|
|
318
|
+
// Populate knownSaves from IDB so hasSave() works after reload
|
|
319
|
+
const saves = await populateKnownSaves(ifid);
|
|
320
|
+
if (Object.keys(saves).length > 0) {
|
|
321
|
+
set((state) => {
|
|
322
|
+
state.knownSaves = saves;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
310
325
|
})
|
|
311
326
|
.catch((err) =>
|
|
312
327
|
console.error('spindle: failed to init save system', err),
|
|
@@ -487,7 +502,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
487
502
|
);
|
|
488
503
|
},
|
|
489
504
|
|
|
490
|
-
save: () => {
|
|
505
|
+
save: (slot?: string, custom?: Record<string, unknown>) => {
|
|
491
506
|
const { storyData, playthroughId } = get();
|
|
492
507
|
if (!storyData) return;
|
|
493
508
|
|
|
@@ -496,14 +511,17 @@ export const useStoryStore = create<StoryState>()(
|
|
|
496
511
|
set((state) => {
|
|
497
512
|
state.saveError = null;
|
|
498
513
|
});
|
|
499
|
-
quickSave(storyData.ifid, playthroughId, payload)
|
|
514
|
+
quickSave(storyData.ifid, playthroughId, payload, slot, custom)
|
|
500
515
|
.then(() => {
|
|
501
516
|
set((state) => {
|
|
502
|
-
state.
|
|
517
|
+
state.knownSaves = {
|
|
518
|
+
...state.knownSaves,
|
|
519
|
+
[slot ?? '']: true,
|
|
520
|
+
};
|
|
503
521
|
});
|
|
504
522
|
})
|
|
505
523
|
.catch((err) => {
|
|
506
|
-
console.error('spindle: failed to
|
|
524
|
+
console.error('spindle: failed to save', err);
|
|
507
525
|
set((state) => {
|
|
508
526
|
state.saveError =
|
|
509
527
|
err instanceof Error ? err.message : 'Failed to save';
|
|
@@ -511,20 +529,20 @@ export const useStoryStore = create<StoryState>()(
|
|
|
511
529
|
});
|
|
512
530
|
},
|
|
513
531
|
|
|
514
|
-
load: () => {
|
|
532
|
+
load: (slot?: string) => {
|
|
515
533
|
const { storyData } = get();
|
|
516
534
|
if (!storyData) return;
|
|
517
535
|
|
|
518
536
|
set((state) => {
|
|
519
537
|
state.loadError = null;
|
|
520
538
|
});
|
|
521
|
-
loadQuickSave(storyData.ifid)
|
|
539
|
+
loadQuickSave(storyData.ifid, slot)
|
|
522
540
|
.then((payload) => {
|
|
523
541
|
if (!payload) return;
|
|
524
542
|
get().loadFromPayload(payload);
|
|
525
543
|
})
|
|
526
544
|
.catch((err) => {
|
|
527
|
-
console.error('spindle: failed to load
|
|
545
|
+
console.error('spindle: failed to load save', err);
|
|
528
546
|
set((state) => {
|
|
529
547
|
state.loadError =
|
|
530
548
|
err instanceof Error ? err.message : 'Failed to load';
|
|
@@ -532,11 +550,39 @@ export const useStoryStore = create<StoryState>()(
|
|
|
532
550
|
});
|
|
533
551
|
},
|
|
534
552
|
|
|
535
|
-
hasSave: () => {
|
|
536
|
-
|
|
537
|
-
const { storyData, saveVersion } = get();
|
|
553
|
+
hasSave: (slot?: string) => {
|
|
554
|
+
const { storyData, knownSaves } = get();
|
|
538
555
|
if (!storyData) return false;
|
|
539
|
-
return
|
|
556
|
+
return (slot ?? '') in knownSaves;
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
getSaveInfo: async (slot?: string): Promise<SaveInfo | null> => {
|
|
560
|
+
const { storyData } = get();
|
|
561
|
+
if (!storyData) return null;
|
|
562
|
+
return getSlotSaveInfo(storyData.ifid, slot);
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
listSaves: async (): Promise<SaveInfo[]> => {
|
|
566
|
+
const { storyData } = get();
|
|
567
|
+
if (!storyData) return [];
|
|
568
|
+
return listSlotSaves(storyData.ifid);
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
deleteSave: (slot?: string) => {
|
|
572
|
+
const { storyData } = get();
|
|
573
|
+
if (!storyData) return;
|
|
574
|
+
|
|
575
|
+
deleteSlotSave(storyData.ifid, slot)
|
|
576
|
+
.then(() => {
|
|
577
|
+
set((state) => {
|
|
578
|
+
const key = slot ?? '';
|
|
579
|
+
const { [key]: _, ...rest } = state.knownSaves;
|
|
580
|
+
state.knownSaves = rest as Record<string, true>;
|
|
581
|
+
});
|
|
582
|
+
})
|
|
583
|
+
.catch((err) => {
|
|
584
|
+
console.error('spindle: failed to delete save', err);
|
|
585
|
+
});
|
|
540
586
|
},
|
|
541
587
|
|
|
542
588
|
getSavePayload: (): SavePayload => {
|
package/src/story-api.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useStoryStore } from './store';
|
|
2
2
|
import type { Passage } from './parser';
|
|
3
3
|
import { settings } from './settings';
|
|
4
|
-
import type { SavePayload } from './saves/types';
|
|
4
|
+
import type { SavePayload, SaveInfo } from './saves/types';
|
|
5
5
|
import { setTitleGenerator } from './saves/save-manager';
|
|
6
6
|
import { registerClass } from './class-registry';
|
|
7
7
|
import { defineMacro } from './define-macro';
|
|
@@ -48,9 +48,12 @@ export interface StoryAPI {
|
|
|
48
48
|
back(): void;
|
|
49
49
|
forward(): void;
|
|
50
50
|
restart(): void;
|
|
51
|
-
save(slot?: string): void;
|
|
51
|
+
save(slot?: string, custom?: Record<string, unknown>): void;
|
|
52
52
|
load(slot?: string): void;
|
|
53
53
|
hasSave(slot?: string): boolean;
|
|
54
|
+
getSaveInfo(slot?: string): Promise<SaveInfo | null>;
|
|
55
|
+
listSaves(): Promise<SaveInfo[]>;
|
|
56
|
+
deleteSave(slot?: string): void;
|
|
54
57
|
visited(name?: string): number;
|
|
55
58
|
hasVisited(name?: string): boolean;
|
|
56
59
|
hasVisitedAny(...names: string[]): boolean;
|
|
@@ -137,8 +140,8 @@ function createStoryAPI(): StoryAPI {
|
|
|
137
140
|
useStoryStore.getState().restart();
|
|
138
141
|
},
|
|
139
142
|
|
|
140
|
-
save(slot?: string): void {
|
|
141
|
-
useStoryStore.getState().save(slot);
|
|
143
|
+
save(slot?: string, custom?: Record<string, unknown>): void {
|
|
144
|
+
useStoryStore.getState().save(slot, custom);
|
|
142
145
|
},
|
|
143
146
|
|
|
144
147
|
load(slot?: string): void {
|
|
@@ -149,6 +152,18 @@ function createStoryAPI(): StoryAPI {
|
|
|
149
152
|
return useStoryStore.getState().hasSave(slot);
|
|
150
153
|
},
|
|
151
154
|
|
|
155
|
+
getSaveInfo(slot?: string): Promise<SaveInfo | null> {
|
|
156
|
+
return useStoryStore.getState().getSaveInfo(slot);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
listSaves(): Promise<SaveInfo[]> {
|
|
160
|
+
return useStoryStore.getState().listSaves();
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
deleteSave(slot?: string): void {
|
|
164
|
+
useStoryStore.getState().deleteSave(slot);
|
|
165
|
+
},
|
|
166
|
+
|
|
152
167
|
visited(name?: string): number {
|
|
153
168
|
const state = useStoryStore.getState();
|
|
154
169
|
return state.visitCounts[name ?? state.currentPassage] ?? 0;
|
package/types/index.d.ts
CHANGED
|
@@ -102,6 +102,25 @@ export interface Passage {
|
|
|
102
102
|
content: string;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Metadata about a save slot, returned by `getSaveInfo()` and `listSaves()`.
|
|
107
|
+
* @see {@link ../../src/saves/types.ts} for the implementation.
|
|
108
|
+
*/
|
|
109
|
+
export interface SaveInfo {
|
|
110
|
+
/** Slot name (empty string for the default autosave slot). */
|
|
111
|
+
slot: string;
|
|
112
|
+
/** Save title (generated or custom). */
|
|
113
|
+
title: string;
|
|
114
|
+
/** Passage name at the time of saving. */
|
|
115
|
+
passage: string;
|
|
116
|
+
/** ISO 8601 timestamp when the save was first created. */
|
|
117
|
+
createdAt: string;
|
|
118
|
+
/** ISO 8601 timestamp when the save was last updated. */
|
|
119
|
+
updatedAt: string;
|
|
120
|
+
/** Custom metadata passed when saving. */
|
|
121
|
+
custom: Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
|
|
105
124
|
/**
|
|
106
125
|
* The main Story API available as `window.Story` at runtime.
|
|
107
126
|
* Provides access to variables, navigation, save/load, and visit tracking.
|
|
@@ -128,8 +147,8 @@ export interface StoryAPI {
|
|
|
128
147
|
/** Restart the story from the beginning. */
|
|
129
148
|
restart(): void;
|
|
130
149
|
|
|
131
|
-
/** Save the current state
|
|
132
|
-
save(slot?: string): void;
|
|
150
|
+
/** Save the current state. Pass `slot` for a named save, `custom` for metadata. */
|
|
151
|
+
save(slot?: string, custom?: Record<string, unknown>): void;
|
|
133
152
|
|
|
134
153
|
/** Load a saved state (quick load). */
|
|
135
154
|
load(slot?: string): void;
|
|
@@ -137,6 +156,15 @@ export interface StoryAPI {
|
|
|
137
156
|
/** Check whether a save exists. */
|
|
138
157
|
hasSave(slot?: string): boolean;
|
|
139
158
|
|
|
159
|
+
/** Get metadata for a specific save slot. Returns null if no save exists. */
|
|
160
|
+
getSaveInfo(slot?: string): Promise<SaveInfo | null>;
|
|
161
|
+
|
|
162
|
+
/** List metadata for all known save slots. */
|
|
163
|
+
listSaves(): Promise<SaveInfo[]>;
|
|
164
|
+
|
|
165
|
+
/** Delete a save by slot name. */
|
|
166
|
+
deleteSave(slot?: string): void;
|
|
167
|
+
|
|
140
168
|
/** Return the number of times a passage has been visited. */
|
|
141
169
|
visited(name: string): number;
|
|
142
170
|
|