@rohal12/spindle 0.51.3 → 0.52.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.
Files changed (66) hide show
  1. package/dist/pkg/format.js +1 -1
  2. package/dist/pkg/headless.js +4833 -1603
  3. package/dist/pkg/macro-registry.json +7 -7
  4. package/dist/pkg/story-variables.js +1658 -189
  5. package/package.json +5 -2
  6. package/src/automation/runner.ts +2 -1
  7. package/src/class-registry.ts +277 -90
  8. package/src/components/Passage.tsx +2 -2
  9. package/src/components/PassageDialog.tsx +2 -5
  10. package/src/components/StoryInterface.tsx +2 -4
  11. package/src/components/macros/Button.tsx +9 -32
  12. package/src/components/macros/Checkbox.tsx +10 -4
  13. package/src/components/macros/Computed.tsx +19 -13
  14. package/src/components/macros/Dialog.tsx +4 -1
  15. package/src/components/macros/For.tsx +33 -4
  16. package/src/components/macros/If.tsx +8 -0
  17. package/src/components/macros/Include.tsx +15 -24
  18. package/src/components/macros/MacroError.tsx +2 -1
  19. package/src/components/macros/MacroLink.tsx +14 -46
  20. package/src/components/macros/Meter.tsx +26 -40
  21. package/src/components/macros/Nobr.tsx +1 -0
  22. package/src/components/macros/PassageDisplay.tsx +3 -0
  23. package/src/components/macros/Print.tsx +4 -0
  24. package/src/components/macros/Radiobutton.tsx +27 -2
  25. package/src/components/macros/SaveManager.tsx +39 -14
  26. package/src/components/macros/Span.tsx +1 -0
  27. package/src/components/macros/StoryTitle.tsx +1 -0
  28. package/src/components/macros/Switch.tsx +13 -0
  29. package/src/components/macros/Unset.tsx +30 -10
  30. package/src/components/macros/VarDisplay.tsx +21 -4
  31. package/src/components/macros/Watch.tsx +88 -40
  32. package/src/components/macros/Widget.tsx +20 -1
  33. package/src/components/macros/WidgetInvocation.tsx +20 -158
  34. package/src/components/macros/arg-utils.ts +226 -0
  35. package/src/components/macros/detached-body.tsx +68 -0
  36. package/src/components/macros/option-utils.ts +10 -5
  37. package/src/define-macro.ts +44 -38
  38. package/src/execute-mutation.ts +499 -28
  39. package/src/expression.ts +88 -272
  40. package/src/hooks/use-action.ts +18 -3
  41. package/src/hooks/use-interpolate.ts +36 -5
  42. package/src/index.tsx +10 -1
  43. package/src/interpolation.ts +394 -96
  44. package/src/js-lexer.ts +1460 -0
  45. package/src/markup/code-attributes.ts +64 -0
  46. package/src/markup/markdown.ts +188 -9
  47. package/src/markup/render.tsx +552 -113
  48. package/src/markup/tokenizer.ts +601 -119
  49. package/src/prng.ts +8 -8
  50. package/src/registry.ts +35 -0
  51. package/src/saves/save-manager.ts +368 -153
  52. package/src/saves/storage.ts +24 -7
  53. package/src/saves/types.ts +20 -6
  54. package/src/store.ts +549 -137
  55. package/src/story-api.ts +46 -81
  56. package/src/story-init.ts +1 -1
  57. package/src/story-variables.ts +98 -102
  58. package/src/triggers.ts +6 -2
  59. package/src/utils/error-message.ts +12 -0
  60. package/src/utils/live-locals.ts +10 -3
  61. package/src/utils/namespace.ts +71 -0
  62. package/src/utils/object-path.ts +194 -0
  63. package/src/utils/stable-key.ts +82 -0
  64. package/src/widgets/widget-registry.ts +9 -0
  65. package/types/index.d.ts +43 -7
  66. package/types/tooling.d.ts +1 -0
@@ -4,7 +4,13 @@ import type { SaveRecord, PlaythroughRecord, StorageBackend } from './types';
4
4
  // Memory backend
5
5
  // ---------------------------------------------------------------------------
6
6
 
7
+ /**
8
+ * In-memory storage. Like the other backends it stores copies: records and
9
+ * values go in and come out cloned, so changing a record after storing it,
10
+ * or one that was read, changes nothing stored.
11
+ */
7
12
  export function createMemoryBackend(): StorageBackend {
13
+ const copy = <T>(value: T): T => structuredClone(value);
8
14
  const saves = new Map<string, SaveRecord>();
9
15
  const playthroughs = new Map<string, PlaythroughRecord>();
10
16
  const meta = new Map<string, unknown>();
@@ -13,11 +19,12 @@ export function createMemoryBackend(): StorageBackend {
13
19
  type: 'memory',
14
20
 
15
21
  async putSave(record: SaveRecord): Promise<void> {
16
- saves.set(record.meta.id, record);
22
+ saves.set(record.meta.id, copy(record));
17
23
  },
18
24
 
19
25
  async getSave(id: string): Promise<SaveRecord | undefined> {
20
- return saves.get(id);
26
+ const record = saves.get(id);
27
+ return record && copy(record);
21
28
  },
22
29
 
23
30
  async deleteSave(id: string): Promise<void> {
@@ -25,7 +32,7 @@ export function createMemoryBackend(): StorageBackend {
25
32
  },
26
33
 
27
34
  async getSavesByIfid(ifid: string): Promise<SaveRecord[]> {
28
- return [...saves.values()].filter((s) => s.meta.ifid === ifid);
35
+ return [...saves.values()].filter((s) => s.meta.ifid === ifid).map(copy);
29
36
  },
30
37
 
31
38
  async deleteSavesByIfid(ifid: string): Promise<void> {
@@ -48,11 +55,13 @@ export function createMemoryBackend(): StorageBackend {
48
55
  },
49
56
 
50
57
  async putPlaythrough(record: PlaythroughRecord): Promise<void> {
51
- playthroughs.set(record.id, record);
58
+ playthroughs.set(record.id, copy(record));
52
59
  },
53
60
 
54
61
  async getPlaythroughsByIfid(ifid: string): Promise<PlaythroughRecord[]> {
55
- return [...playthroughs.values()].filter((p) => p.ifid === ifid);
62
+ return [...playthroughs.values()]
63
+ .filter((p) => p.ifid === ifid)
64
+ .map(copy);
56
65
  },
57
66
 
58
67
  async deletePlaythroughsByIfid(ifid: string): Promise<void> {
@@ -67,11 +76,11 @@ export function createMemoryBackend(): StorageBackend {
67
76
  },
68
77
 
69
78
  async getMeta<T = unknown>(key: string): Promise<T | undefined> {
70
- return meta.get(key) as T | undefined;
79
+ return copy(meta.get(key)) as T | undefined;
71
80
  },
72
81
 
73
82
  async setMeta(key: string, value: unknown): Promise<void> {
74
- meta.set(key, value);
83
+ meta.set(key, copy(value));
75
84
  },
76
85
 
77
86
  async deleteMeta(key: string): Promise<void> {
@@ -154,6 +163,14 @@ export function createLocalStorageBackend(): StorageBackend {
154
163
 
155
164
  async putSave(record: SaveRecord): Promise<void> {
156
165
  const id = record.meta.id;
166
+ // A save rewritten under another playthrough (an overwrite after a
167
+ // restart) leaves its old playthrough's index
168
+ const previous = lsGet<SaveRecord>(`${LS_SAVE_PREFIX}${id}`);
169
+ if (previous && previous.meta.playthroughId !== record.meta.playthroughId)
170
+ lsIndexRemove(
171
+ `${LS_IDX_SAVES_PT_PREFIX}${previous.meta.playthroughId}`,
172
+ id,
173
+ );
157
174
  lsSet(`${LS_SAVE_PREFIX}${id}`, record);
158
175
  lsIndexAdd(`${LS_IDX_SAVES_PREFIX}${record.meta.ifid}`, id);
159
176
  lsIndexAdd(`${LS_IDX_SAVES_PT_PREFIX}${record.meta.playthroughId}`, id);
@@ -1,4 +1,5 @@
1
1
  import type { PRNGSnapshot } from '../prng';
2
+ import { isDeserializable } from '../class-registry';
2
3
 
3
4
  /** History moment as persisted in saves (full variable snapshots). */
4
5
  export interface SaveHistoryMoment {
@@ -25,6 +26,11 @@ export interface SaveMeta {
25
26
  createdAt: string;
26
27
  updatedAt: string;
27
28
  title: string;
29
+ /**
30
+ * True once the player has named the save (renameSave). Overwriting the
31
+ * save keeps such a title; a generated one is generated anew.
32
+ */
33
+ userTitle?: boolean;
28
34
  passage: string;
29
35
  custom: Record<string, unknown>;
30
36
  estimatedBytes?: number;
@@ -97,11 +103,16 @@ function isOptionalPRNGSnapshot(value: unknown): boolean {
97
103
  );
98
104
  }
99
105
 
106
+ /** A variable container whose values deserialize() can restore. */
107
+ function isSerializedVariables(value: unknown): boolean {
108
+ return isRecord(value) && isDeserializable(value);
109
+ }
110
+
100
111
  function isSaveHistoryMoment(value: unknown): value is SaveHistoryMoment {
101
112
  return (
102
113
  isRecord(value) &&
103
114
  typeof value.passage === 'string' &&
104
- isRecord(value.variables) &&
115
+ isSerializedVariables(value.variables) &&
105
116
  typeof value.timestamp === 'number' &&
106
117
  isOptionalPRNGSnapshot(value.prng)
107
118
  );
@@ -109,18 +120,21 @@ function isSaveHistoryMoment(value: unknown): value is SaveHistoryMoment {
109
120
 
110
121
  /**
111
122
  * Full check of a payload from outside the running story (an imported save):
112
- * every history moment is well formed, and `historyIndex` is an integer that
113
- * points at a moment of the payload's passage. Anything that passes can be
114
- * stored and later loaded.
123
+ * every history moment is well formed, `historyIndex` is an integer that
124
+ * points at a moment of the payload's passage, and the encoded values (Map,
125
+ * Set, Date, RegExp, class instances) in the live variables and every
126
+ * moment's variables have the shape deserialize() expects. Anything that
127
+ * passes can be stored and later loaded.
115
128
  */
116
129
  function isImportablePayload(value: unknown): value is SavePayload {
117
130
  if (!isRecord(value)) return false;
118
131
  if (typeof value.passage !== 'string') return false;
119
- if (!isRecord(value.variables)) return false;
132
+ if (!isSerializedVariables(value.variables)) return false;
120
133
 
121
134
  const { history, historyIndex } = value;
122
135
  if (!Array.isArray(history) || history.length === 0) return false;
123
- if (!history.every(isSaveHistoryMoment)) return false;
136
+ // Array.from: every() skips the holes of a sparse array
137
+ if (!Array.from(history).every(isSaveHistoryMoment)) return false;
124
138
  if (
125
139
  typeof historyIndex !== 'number' ||
126
140
  !Number.isInteger(historyIndex) ||