@rohal12/spindle 0.59.9 → 0.59.11
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/dist/pkg/headless.js +639 -371
- package/dist/pkg/macro-registry.json +1 -0
- package/dist/pkg/story-variables.js +480 -319
- package/package.json +1 -1
- package/src/components/macros/If.tsx +2 -7
- package/src/components/macros/PassageDisplay.tsx +56 -10
- package/src/components/macros/SaveManager.tsx +5 -2
- package/src/components/macros/Switch.tsx +4 -2
- package/src/components/macros/WidgetInvocation.tsx +17 -1
- package/src/components/macros/branches.ts +20 -0
- package/src/components/macros/detached-body.tsx +22 -16
- package/src/components/macros/input-macro.ts +18 -3
- package/src/hooks/use-merged-locals.ts +5 -2
- package/src/hooks/use-modal-focus.ts +6 -1
- package/src/hooks/use-story-fields.ts +16 -1
- package/src/js-lexer.ts +18 -1
- package/src/markup/render.tsx +38 -14
- package/src/markup/spindle.peggy +11 -0
- package/src/saves/save-manager.ts +47 -3
- package/src/store.ts +17 -7
- package/src/styles.css +5 -0
- package/src/triggers.ts +4 -2
|
@@ -147,6 +147,7 @@ async function startNewPlaythroughNow(
|
|
|
147
147
|
await backend.setMeta(playthroughCountKey(ifid), num);
|
|
148
148
|
});
|
|
149
149
|
await backend.setMeta(currentPlaythroughKey(ifid), id);
|
|
150
|
+
saveSessionPlaythrough(ifid, id);
|
|
150
151
|
return id;
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -202,6 +203,7 @@ async function adoptPlaythroughNow(ifid: string, id: string): Promise<void> {
|
|
|
202
203
|
if ((await backend.getMeta<string>(key)) !== id) {
|
|
203
204
|
await backend.setMeta(key, id);
|
|
204
205
|
}
|
|
206
|
+
saveSessionPlaythrough(ifid, id);
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
/**
|
|
@@ -231,9 +233,18 @@ export function establishPlaythrough(
|
|
|
231
233
|
): Promise<{ id: string; knownSaves: Record<string, true> }> {
|
|
232
234
|
return inOrder(async () => {
|
|
233
235
|
const backend = await getBackend();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
236
|
+
// A tab that restores its session continues the session's playthrough;
|
|
237
|
+
// the shared current one is the default for a visit with no session,
|
|
238
|
+
// and may belong to what another tab has done since (#356).
|
|
239
|
+
const own = loadSessionPlaythrough(ifid);
|
|
240
|
+
const ownExists =
|
|
241
|
+
own !== undefined &&
|
|
242
|
+
(await backend.getPlaythroughsByIfid(ifid)).some((p) => p.id === own);
|
|
243
|
+
const id = ownExists
|
|
244
|
+
? own
|
|
245
|
+
: ((await backend.getMeta<string>(currentPlaythroughKey(ifid))) ??
|
|
246
|
+
(await startNewPlaythroughNow(ifid)));
|
|
247
|
+
saveSessionPlaythrough(ifid, id);
|
|
237
248
|
return { id, knownSaves: await populateKnownSavesNow(ifid) };
|
|
238
249
|
});
|
|
239
250
|
}
|
|
@@ -673,15 +684,20 @@ export const loadQuickSave = readingSlot(livePayload);
|
|
|
673
684
|
* (see adoptPlaythrough) in the same operation: operations issued after the
|
|
674
685
|
* load take effect in the loaded playthrough. Resolves to the live payload
|
|
675
686
|
* and its playthrough, or undefined if the slot is empty.
|
|
687
|
+
*
|
|
688
|
+
* `check` is run on the payload first: when it throws, the load rejects and
|
|
689
|
+
* the game stays in its playthrough (#357).
|
|
676
690
|
*/
|
|
677
691
|
export function loadSlotSave(
|
|
678
692
|
ifid: string,
|
|
679
693
|
slot?: string,
|
|
694
|
+
check?: (payload: SavePayload) => void,
|
|
680
695
|
): Promise<{ payload: SavePayload; playthroughId: string } | undefined> {
|
|
681
696
|
return inOrder(async () => {
|
|
682
697
|
const record = await slotRecord(ifid, slot);
|
|
683
698
|
if (!record) return undefined;
|
|
684
699
|
const payload = decodePayload(record.payload);
|
|
700
|
+
check?.(payload);
|
|
685
701
|
const { playthroughId } = record.meta;
|
|
686
702
|
if (playthroughId) await adoptPlaythroughNow(ifid, playthroughId);
|
|
687
703
|
return { payload, playthroughId };
|
|
@@ -795,6 +811,34 @@ export function clearSession(ifid: string): void {
|
|
|
795
811
|
}
|
|
796
812
|
}
|
|
797
813
|
|
|
814
|
+
/**
|
|
815
|
+
* The playthrough this tab's game is in, kept with its session: the shared
|
|
816
|
+
* current playthrough (a stored meta value) can be changed by another tab.
|
|
817
|
+
* Written where this tab's storage operations set the current playthrough,
|
|
818
|
+
* so it follows them in call order. Under the session prefix, so clearing
|
|
819
|
+
* all data clears it.
|
|
820
|
+
*/
|
|
821
|
+
const SESSION_PLAYTHROUGH_PREFIX = `${SESSION_KEY_PREFIX}playthrough.`;
|
|
822
|
+
|
|
823
|
+
function saveSessionPlaythrough(ifid: string, id: string): void {
|
|
824
|
+
try {
|
|
825
|
+
sessionStorage.setItem(`${SESSION_PLAYTHROUGH_PREFIX}${ifid}`, id);
|
|
826
|
+
} catch {
|
|
827
|
+
// the session cannot be stored: a reload takes the shared playthrough
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function loadSessionPlaythrough(ifid: string): string | undefined {
|
|
832
|
+
try {
|
|
833
|
+
return (
|
|
834
|
+
sessionStorage.getItem(`${SESSION_PLAYTHROUGH_PREFIX}${ifid}`) ||
|
|
835
|
+
undefined
|
|
836
|
+
);
|
|
837
|
+
} catch {
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
798
842
|
// --- Export / Import ---
|
|
799
843
|
|
|
800
844
|
/** The export of a stored save, if there is one. */
|
package/src/store.ts
CHANGED
|
@@ -965,6 +965,19 @@ export function missingPassage(
|
|
|
965
965
|
);
|
|
966
966
|
}
|
|
967
967
|
|
|
968
|
+
/** Throw if `payload` refers to a passage `storyData` does not have. */
|
|
969
|
+
function assertPassagesExist(
|
|
970
|
+
storyData: StoryData | null | undefined,
|
|
971
|
+
payload: SavePayload,
|
|
972
|
+
): void {
|
|
973
|
+
const missing = missingPassage(storyData, payload);
|
|
974
|
+
if (missing !== undefined) {
|
|
975
|
+
throw new Error(
|
|
976
|
+
`The save refers to the passage "${missing}", which this version of the story does not have`,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
968
981
|
const NAMESPACE_KEYS = ['variables', 'temporary', 'transient'] as const;
|
|
969
982
|
|
|
970
983
|
type StoryRecipe = (draft: Draft<StoryState>) => void;
|
|
@@ -1449,7 +1462,9 @@ export const useStoryStore = create<StoryState>()(
|
|
|
1449
1462
|
// the game on, as usual). An empty slot leaves the playthrough as it
|
|
1450
1463
|
// is.
|
|
1451
1464
|
const previous = resolvePlaythroughId();
|
|
1452
|
-
const read = loadSlotSave(storyData.ifid, slot)
|
|
1465
|
+
const read = loadSlotSave(storyData.ifid, slot, (payload) =>
|
|
1466
|
+
assertPassagesExist(storyData, payload),
|
|
1467
|
+
);
|
|
1453
1468
|
const switched = switchToLookedUpPlaythrough(
|
|
1454
1469
|
Promise.all([previous, read.catch(() => undefined)]).then(
|
|
1455
1470
|
([prev, loaded]) => loaded?.playthroughId || prev,
|
|
@@ -1646,12 +1661,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
1646
1661
|
}
|
|
1647
1662
|
// Before anything is replaced: an incompatible payload leaves the
|
|
1648
1663
|
// running game as it is
|
|
1649
|
-
|
|
1650
|
-
if (missing !== undefined) {
|
|
1651
|
-
throw new Error(
|
|
1652
|
-
`The save refers to the passage "${missing}", which this version of the story does not have`,
|
|
1653
|
-
);
|
|
1654
|
-
}
|
|
1664
|
+
assertPassagesExist(get().storyData, payload);
|
|
1655
1665
|
latestStateApplied = replacement;
|
|
1656
1666
|
|
|
1657
1667
|
emit('beforeload', slot);
|
package/src/styles.css
CHANGED
package/src/triggers.ts
CHANGED
|
@@ -251,7 +251,7 @@ export function checkTriggersOnNavigation(): void {
|
|
|
251
251
|
|
|
252
252
|
/**
|
|
253
253
|
* Re-evaluate watchers whenever a namespace their conditions can read
|
|
254
|
-
* ($variables, _temporary, %transient)
|
|
254
|
+
* ($variables, _temporary, %transient) or the render counts change. Navigation is left to the
|
|
255
255
|
* store: navigate() checks watchers once the new moment is complete
|
|
256
256
|
* (checkTriggersOnNavigation), while history traversal and loads
|
|
257
257
|
* reinitialize watcher state instead of firing.
|
|
@@ -269,7 +269,9 @@ export function connectTriggersToStore(): () => void {
|
|
|
269
269
|
if (
|
|
270
270
|
state.variables === before.variables &&
|
|
271
271
|
state.temporary === before.temporary &&
|
|
272
|
-
state.transient === before.transient
|
|
272
|
+
state.transient === before.transient &&
|
|
273
|
+
// hasRendered() / rendered() read the counts an {include} bumps
|
|
274
|
+
state.renderCounts === before.renderCounts
|
|
273
275
|
) {
|
|
274
276
|
return;
|
|
275
277
|
}
|