@rohal12/spindle 0.3.2 → 0.4.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/README.md +9 -0
- package/dist/pkg/format.js +1 -1
- package/package.json +8 -2
- package/src/automation/runner.ts +1 -1
- package/src/components/SaveLoadDialog.tsx +8 -3
- package/src/components/macros/Checkbox.tsx +2 -2
- package/src/components/macros/Computed.tsx +22 -17
- package/src/components/macros/For.tsx +12 -19
- package/src/components/macros/If.tsx +3 -15
- package/src/components/macros/MacroLink.tsx +3 -3
- package/src/components/macros/Meter.tsx +12 -22
- package/src/components/macros/Numberbox.tsx +1 -1
- package/src/components/macros/Print.tsx +3 -15
- package/src/components/macros/Radiobutton.tsx +5 -5
- package/src/components/macros/Switch.tsx +5 -15
- package/src/components/macros/Textarea.tsx +1 -1
- package/src/components/macros/Textbox.tsx +1 -1
- package/src/components/macros/Timed.tsx +13 -14
- package/src/components/macros/VarDisplay.tsx +3 -2
- package/src/expression.ts +82 -10
- package/src/hooks/use-merged-locals.ts +26 -0
- package/src/markup/ast.ts +12 -7
- package/src/markup/render.tsx +13 -6
- package/src/markup/tokenizer.ts +12 -12
- package/src/parser.ts +16 -1
- package/src/prng.ts +128 -0
- package/src/saves/save-manager.ts +25 -10
- package/src/saves/types.ts +31 -0
- package/src/settings.ts +26 -1
- package/src/store.ts +101 -35
- package/src/story-api.ts +63 -0
- package/src/story-variables.ts +18 -9
package/src/settings.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type SettingDef =
|
|
|
26
26
|
|
|
27
27
|
const definitions = new Map<string, SettingDef>();
|
|
28
28
|
let values: Record<string, unknown> = {};
|
|
29
|
+
let storageLoaded = false;
|
|
29
30
|
|
|
30
31
|
function storageKey(): string {
|
|
31
32
|
const storyData = useStoryStore.getState().storyData;
|
|
@@ -38,10 +39,19 @@ function persist(): void {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
function loadFromStorage(): void {
|
|
42
|
+
if (storageLoaded) return;
|
|
43
|
+
storageLoaded = true;
|
|
41
44
|
try {
|
|
42
45
|
const raw = localStorage.getItem(storageKey());
|
|
43
46
|
if (raw) {
|
|
44
|
-
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (
|
|
49
|
+
typeof parsed === 'object' &&
|
|
50
|
+
parsed !== null &&
|
|
51
|
+
!Array.isArray(parsed)
|
|
52
|
+
) {
|
|
53
|
+
values = { ...values, ...parsed };
|
|
54
|
+
}
|
|
45
55
|
}
|
|
46
56
|
} catch {
|
|
47
57
|
// ignore corrupted data
|
|
@@ -77,6 +87,21 @@ export const settings = {
|
|
|
77
87
|
return values[name];
|
|
78
88
|
},
|
|
79
89
|
|
|
90
|
+
getToggle(name: string): boolean {
|
|
91
|
+
const v = values[name];
|
|
92
|
+
return typeof v === 'boolean' ? v : false;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
getList(name: string): string {
|
|
96
|
+
const v = values[name];
|
|
97
|
+
return typeof v === 'string' ? v : '';
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getRange(name: string): number {
|
|
101
|
+
const v = values[name];
|
|
102
|
+
return typeof v === 'number' ? v : 0;
|
|
103
|
+
},
|
|
104
|
+
|
|
80
105
|
set(name: string, value: unknown): void {
|
|
81
106
|
values[name] = value;
|
|
82
107
|
persist();
|
package/src/store.ts
CHANGED
|
@@ -11,11 +11,27 @@ import {
|
|
|
11
11
|
loadQuickSave,
|
|
12
12
|
} from './saves/save-manager';
|
|
13
13
|
import { deepClone, deserialize } from './class-registry';
|
|
14
|
+
import {
|
|
15
|
+
snapshotPRNG,
|
|
16
|
+
restorePRNG,
|
|
17
|
+
resetPRNG,
|
|
18
|
+
type PRNGSnapshot,
|
|
19
|
+
} from './prng';
|
|
20
|
+
|
|
21
|
+
/** Restore or reset PRNG from a history moment's snapshot. */
|
|
22
|
+
function restorePRNGFromMoment(moment: HistoryMoment | undefined): void {
|
|
23
|
+
if (moment?.prng) {
|
|
24
|
+
restorePRNG(moment.prng.seed, moment.prng.pull);
|
|
25
|
+
} else if (moment) {
|
|
26
|
+
resetPRNG();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
14
29
|
|
|
15
30
|
export interface HistoryMoment {
|
|
16
31
|
passage: string;
|
|
17
32
|
variables: Record<string, unknown>;
|
|
18
33
|
timestamp: number;
|
|
34
|
+
prng?: PRNGSnapshot | null;
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
export interface StoryState {
|
|
@@ -30,6 +46,8 @@ export interface StoryState {
|
|
|
30
46
|
renderCounts: Record<string, number>;
|
|
31
47
|
saveVersion: number;
|
|
32
48
|
playthroughId: string;
|
|
49
|
+
saveError: string | null;
|
|
50
|
+
loadError: string | null;
|
|
33
51
|
|
|
34
52
|
init: (
|
|
35
53
|
storyData: StoryData,
|
|
@@ -64,6 +82,8 @@ export const useStoryStore = create<StoryState>()(
|
|
|
64
82
|
renderCounts: {},
|
|
65
83
|
saveVersion: 0,
|
|
66
84
|
playthroughId: '',
|
|
85
|
+
saveError: null,
|
|
86
|
+
loadError: null,
|
|
67
87
|
|
|
68
88
|
init: (
|
|
69
89
|
storyData: StoryData,
|
|
@@ -98,19 +118,23 @@ export const useStoryStore = create<StoryState>()(
|
|
|
98
118
|
|
|
99
119
|
// Init save system (fire-and-forget — DB will be ready before user opens dialog)
|
|
100
120
|
const ifid = storyData.ifid;
|
|
101
|
-
initSaveSystem()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
state
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
state
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
initSaveSystem()
|
|
122
|
+
.then(async () => {
|
|
123
|
+
const existingId = await getCurrentPlaythroughId(ifid);
|
|
124
|
+
if (existingId) {
|
|
125
|
+
set((state) => {
|
|
126
|
+
state.playthroughId = existingId;
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
const newId = await startNewPlaythrough(ifid);
|
|
130
|
+
set((state) => {
|
|
131
|
+
state.playthroughId = newId;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.catch((err) =>
|
|
136
|
+
console.error('spindle: failed to init save system', err),
|
|
137
|
+
);
|
|
114
138
|
},
|
|
115
139
|
|
|
116
140
|
navigate: (passageName: string) => {
|
|
@@ -133,6 +157,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
133
157
|
passage: passageName,
|
|
134
158
|
variables: deepClone(state.variables),
|
|
135
159
|
timestamp: Date.now(),
|
|
160
|
+
prng: snapshotPRNG(),
|
|
136
161
|
});
|
|
137
162
|
state.historyIndex = state.history.length - 1;
|
|
138
163
|
state.visitCounts[passageName] =
|
|
@@ -146,22 +171,24 @@ export const useStoryStore = create<StoryState>()(
|
|
|
146
171
|
set((state) => {
|
|
147
172
|
if (state.historyIndex <= 0) return;
|
|
148
173
|
state.historyIndex--;
|
|
149
|
-
const moment = state.history[state.historyIndex]
|
|
174
|
+
const moment = state.history[state.historyIndex]!;
|
|
150
175
|
state.currentPassage = moment.passage;
|
|
151
176
|
state.variables = deepClone(moment.variables);
|
|
152
177
|
state.temporary = {};
|
|
153
178
|
});
|
|
179
|
+
restorePRNGFromMoment(get().history[get().historyIndex]);
|
|
154
180
|
},
|
|
155
181
|
|
|
156
182
|
goForward: () => {
|
|
157
183
|
set((state) => {
|
|
158
184
|
if (state.historyIndex >= state.history.length - 1) return;
|
|
159
185
|
state.historyIndex++;
|
|
160
|
-
const moment = state.history[state.historyIndex]
|
|
186
|
+
const moment = state.history[state.historyIndex]!;
|
|
161
187
|
state.currentPassage = moment.passage;
|
|
162
188
|
state.variables = deepClone(moment.variables);
|
|
163
189
|
state.temporary = {};
|
|
164
190
|
});
|
|
191
|
+
restorePRNGFromMoment(get().history[get().historyIndex]);
|
|
165
192
|
},
|
|
166
193
|
|
|
167
194
|
setVariable: (name: string, value: unknown) => {
|
|
@@ -202,6 +229,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
202
229
|
const startPassage = storyData.passagesById.get(storyData.startNode);
|
|
203
230
|
if (!startPassage) return;
|
|
204
231
|
|
|
232
|
+
resetPRNG();
|
|
205
233
|
const initialVars = deepClone(variableDefaults);
|
|
206
234
|
|
|
207
235
|
set((state) => {
|
|
@@ -223,11 +251,15 @@ export const useStoryStore = create<StoryState>()(
|
|
|
223
251
|
executeStoryInit();
|
|
224
252
|
|
|
225
253
|
// Start a new playthrough on restart
|
|
226
|
-
startNewPlaythrough(storyData.ifid)
|
|
227
|
-
|
|
228
|
-
state
|
|
229
|
-
|
|
230
|
-
|
|
254
|
+
startNewPlaythrough(storyData.ifid)
|
|
255
|
+
.then((newId) => {
|
|
256
|
+
set((state) => {
|
|
257
|
+
state.playthroughId = newId;
|
|
258
|
+
});
|
|
259
|
+
})
|
|
260
|
+
.catch((err) =>
|
|
261
|
+
console.error('spindle: failed to start new playthrough', err),
|
|
262
|
+
);
|
|
231
263
|
},
|
|
232
264
|
|
|
233
265
|
save: () => {
|
|
@@ -250,31 +282,59 @@ export const useStoryStore = create<StoryState>()(
|
|
|
250
282
|
historyIndex,
|
|
251
283
|
visitCounts: { ...visitCounts },
|
|
252
284
|
renderCounts: { ...renderCounts },
|
|
285
|
+
prng: snapshotPRNG(),
|
|
253
286
|
};
|
|
254
287
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
state.saveVersion++;
|
|
258
|
-
});
|
|
288
|
+
set((state) => {
|
|
289
|
+
state.saveError = null;
|
|
259
290
|
});
|
|
291
|
+
quickSave(storyData.ifid, playthroughId, payload)
|
|
292
|
+
.then(() => {
|
|
293
|
+
set((state) => {
|
|
294
|
+
state.saveVersion++;
|
|
295
|
+
});
|
|
296
|
+
})
|
|
297
|
+
.catch((err) => {
|
|
298
|
+
console.error('spindle: failed to quick save', err);
|
|
299
|
+
set((state) => {
|
|
300
|
+
state.saveError =
|
|
301
|
+
err instanceof Error ? err.message : 'Failed to save';
|
|
302
|
+
});
|
|
303
|
+
});
|
|
260
304
|
},
|
|
261
305
|
|
|
262
306
|
load: () => {
|
|
263
307
|
const { storyData } = get();
|
|
264
308
|
if (!storyData) return;
|
|
265
309
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
set((state) => {
|
|
269
|
-
state.currentPassage = payload.passage;
|
|
270
|
-
state.variables = payload.variables;
|
|
271
|
-
state.history = payload.history;
|
|
272
|
-
state.historyIndex = payload.historyIndex;
|
|
273
|
-
state.visitCounts = payload.visitCounts ?? {};
|
|
274
|
-
state.renderCounts = payload.renderCounts ?? {};
|
|
275
|
-
state.temporary = {};
|
|
276
|
-
});
|
|
310
|
+
set((state) => {
|
|
311
|
+
state.loadError = null;
|
|
277
312
|
});
|
|
313
|
+
loadQuickSave(storyData.ifid)
|
|
314
|
+
.then((payload) => {
|
|
315
|
+
if (!payload) return;
|
|
316
|
+
set((state) => {
|
|
317
|
+
state.currentPassage = payload.passage;
|
|
318
|
+
state.variables = payload.variables;
|
|
319
|
+
state.history = payload.history;
|
|
320
|
+
state.historyIndex = payload.historyIndex;
|
|
321
|
+
state.visitCounts = payload.visitCounts ?? {};
|
|
322
|
+
state.renderCounts = payload.renderCounts ?? {};
|
|
323
|
+
state.temporary = {};
|
|
324
|
+
});
|
|
325
|
+
if (payload.prng) {
|
|
326
|
+
restorePRNG(payload.prng.seed, payload.prng.pull);
|
|
327
|
+
} else {
|
|
328
|
+
resetPRNG();
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
.catch((err) => {
|
|
332
|
+
console.error('spindle: failed to load quick save', err);
|
|
333
|
+
set((state) => {
|
|
334
|
+
state.loadError =
|
|
335
|
+
err instanceof Error ? err.message : 'Failed to load';
|
|
336
|
+
});
|
|
337
|
+
});
|
|
278
338
|
},
|
|
279
339
|
|
|
280
340
|
hasSave: () => {
|
|
@@ -300,6 +360,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
300
360
|
historyIndex,
|
|
301
361
|
visitCounts: { ...visitCounts },
|
|
302
362
|
renderCounts: { ...renderCounts },
|
|
363
|
+
prng: snapshotPRNG(),
|
|
303
364
|
};
|
|
304
365
|
},
|
|
305
366
|
|
|
@@ -316,6 +377,11 @@ export const useStoryStore = create<StoryState>()(
|
|
|
316
377
|
state.renderCounts = payload.renderCounts ?? {};
|
|
317
378
|
state.temporary = {};
|
|
318
379
|
});
|
|
380
|
+
if (payload.prng) {
|
|
381
|
+
restorePRNG(payload.prng.seed, payload.prng.pull);
|
|
382
|
+
} else {
|
|
383
|
+
resetPRNG();
|
|
384
|
+
}
|
|
319
385
|
},
|
|
320
386
|
})),
|
|
321
387
|
);
|
package/src/story-api.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from './store';
|
|
2
|
+
import type { Passage } from './parser';
|
|
2
3
|
import { settings } from './settings';
|
|
3
4
|
import type { SavePayload } from './saves/types';
|
|
4
5
|
import { setTitleGenerator } from './saves/save-manager';
|
|
@@ -9,6 +10,15 @@ import {
|
|
|
9
10
|
onActionsChanged,
|
|
10
11
|
type StoryAction,
|
|
11
12
|
} from './action-registry';
|
|
13
|
+
import {
|
|
14
|
+
initPRNG,
|
|
15
|
+
isPRNGEnabled,
|
|
16
|
+
getPRNGSeed,
|
|
17
|
+
getPRNGPull,
|
|
18
|
+
random,
|
|
19
|
+
randomInt,
|
|
20
|
+
snapshotPRNG,
|
|
21
|
+
} from './prng';
|
|
12
22
|
|
|
13
23
|
export type { StoryAction };
|
|
14
24
|
|
|
@@ -37,6 +47,8 @@ export interface StoryAPI {
|
|
|
37
47
|
hasRendered(name: string): boolean;
|
|
38
48
|
hasRenderedAny(...names: string[]): boolean;
|
|
39
49
|
hasRenderedAll(...names: string[]): boolean;
|
|
50
|
+
currentPassage(): Passage | undefined;
|
|
51
|
+
previousPassage(): Passage | undefined;
|
|
40
52
|
readonly title: string;
|
|
41
53
|
readonly passage: string;
|
|
42
54
|
readonly settings: typeof settings;
|
|
@@ -50,6 +62,14 @@ export interface StoryAPI {
|
|
|
50
62
|
on(event: 'actionsChanged', callback: ActionsChangedCallback): () => void;
|
|
51
63
|
on(event: 'variableChanged', callback: VariableChangedCallback): () => void;
|
|
52
64
|
waitForActions(): Promise<StoryAction[]>;
|
|
65
|
+
random(): number;
|
|
66
|
+
randomInt(min: number, max: number): number;
|
|
67
|
+
readonly prng: {
|
|
68
|
+
init(seed?: string, useEntropy?: boolean): void;
|
|
69
|
+
isEnabled(): boolean;
|
|
70
|
+
readonly seed: string;
|
|
71
|
+
readonly pull: number;
|
|
72
|
+
};
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
function createStoryAPI(): StoryAPI {
|
|
@@ -133,6 +153,18 @@ function createStoryAPI(): StoryAPI {
|
|
|
133
153
|
return names.every((n) => (renderCounts[n] ?? 0) > 0);
|
|
134
154
|
},
|
|
135
155
|
|
|
156
|
+
currentPassage(): Passage | undefined {
|
|
157
|
+
const state = useStoryStore.getState();
|
|
158
|
+
return state.storyData?.passages.get(state.currentPassage);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
previousPassage(): Passage | undefined {
|
|
162
|
+
const state = useStoryStore.getState();
|
|
163
|
+
if (state.historyIndex <= 0) return undefined;
|
|
164
|
+
const prevName = state.history[state.historyIndex - 1]?.passage;
|
|
165
|
+
return prevName ? state.storyData?.passages.get(prevName) : undefined;
|
|
166
|
+
},
|
|
167
|
+
|
|
136
168
|
get title(): string {
|
|
137
169
|
return useStoryStore.getState().storyData?.name || '';
|
|
138
170
|
},
|
|
@@ -218,6 +250,37 @@ function createStoryAPI(): StoryAPI {
|
|
|
218
250
|
});
|
|
219
251
|
});
|
|
220
252
|
},
|
|
253
|
+
|
|
254
|
+
random(): number {
|
|
255
|
+
return random();
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
randomInt(min: number, max: number): number {
|
|
259
|
+
return randomInt(min, max);
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
prng: {
|
|
263
|
+
init(seed?: string, useEntropy?: boolean): void {
|
|
264
|
+
initPRNG(seed, useEntropy);
|
|
265
|
+
// Update current history moment's snapshot via immer
|
|
266
|
+
const { historyIndex } = useStoryStore.getState();
|
|
267
|
+
useStoryStore.setState((state) => {
|
|
268
|
+
const moment = state.history[historyIndex];
|
|
269
|
+
if (moment) {
|
|
270
|
+
moment.prng = snapshotPRNG();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
isEnabled(): boolean {
|
|
275
|
+
return isPRNGEnabled();
|
|
276
|
+
},
|
|
277
|
+
get seed(): string {
|
|
278
|
+
return getPRNGSeed();
|
|
279
|
+
},
|
|
280
|
+
get pull(): number {
|
|
281
|
+
return getPRNGPull();
|
|
282
|
+
},
|
|
283
|
+
},
|
|
221
284
|
};
|
|
222
285
|
}
|
|
223
286
|
|
package/src/story-variables.ts
CHANGED
|
@@ -16,6 +16,8 @@ const DECLARATION_RE = /^\$(\w+)\s*=\s*(.+)$/;
|
|
|
16
16
|
const VAR_REF_RE = /\$(\w+(?:\.\w+)*)/g;
|
|
17
17
|
const FOR_LOCAL_RE = /\{for\s+(\$\w+)(?:\s*,\s*(\$\w+))?\s+of\b/g;
|
|
18
18
|
|
|
19
|
+
const VALID_VAR_TYPES = new Set<string>(['number', 'string', 'boolean']);
|
|
20
|
+
|
|
19
21
|
function inferSchema(value: unknown): FieldSchema {
|
|
20
22
|
if (Array.isArray(value)) {
|
|
21
23
|
return { type: 'array' };
|
|
@@ -27,7 +29,13 @@ function inferSchema(value: unknown): FieldSchema {
|
|
|
27
29
|
}
|
|
28
30
|
return { type: 'object', fields };
|
|
29
31
|
}
|
|
30
|
-
|
|
32
|
+
const jsType = typeof value;
|
|
33
|
+
if (!VALID_VAR_TYPES.has(jsType)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`StoryVariables: Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return { type: jsType as VarType };
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/**
|
|
@@ -50,7 +58,7 @@ export function parseStoryVariables(
|
|
|
50
58
|
);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
const [, name, expr] = match;
|
|
61
|
+
const [, name, expr] = match as [string, string, string];
|
|
54
62
|
let value: unknown;
|
|
55
63
|
try {
|
|
56
64
|
value = new Function('return (' + expr + ')')();
|
|
@@ -77,8 +85,8 @@ function extractForLocals(content: string): Set<string> {
|
|
|
77
85
|
let match: RegExpExecArray | null;
|
|
78
86
|
FOR_LOCAL_RE.lastIndex = 0;
|
|
79
87
|
while ((match = FOR_LOCAL_RE.exec(content)) !== null) {
|
|
80
|
-
locals.add(match[1]
|
|
81
|
-
if (match[2]) locals.add(match[2]
|
|
88
|
+
locals.add(match[1]!.slice(1)); // strip $
|
|
89
|
+
if (match[2]) locals.add(match[2]!.slice(1));
|
|
82
90
|
}
|
|
83
91
|
return locals;
|
|
84
92
|
}
|
|
@@ -93,7 +101,7 @@ function validateRef(
|
|
|
93
101
|
forLocals: Set<string>,
|
|
94
102
|
): string | null {
|
|
95
103
|
const parts = ref.split('.');
|
|
96
|
-
const rootName = parts[0]
|
|
104
|
+
const rootName = parts[0]!;
|
|
97
105
|
|
|
98
106
|
// Skip for-loop locals
|
|
99
107
|
if (forLocals.has(rootName)) return null;
|
|
@@ -107,12 +115,13 @@ function validateRef(
|
|
|
107
115
|
let current: FieldSchema = rootSchema;
|
|
108
116
|
for (let i = 1; i < parts.length; i++) {
|
|
109
117
|
// Arrays have a .length property
|
|
110
|
-
|
|
118
|
+
const part = parts[i] as string;
|
|
119
|
+
if (current.type === 'array' && part === 'length') return null;
|
|
111
120
|
|
|
112
121
|
if (current.type !== 'object' || !current.fields) {
|
|
113
|
-
return `Cannot access field "${
|
|
122
|
+
return `Cannot access field "${part}" on $${parts.slice(0, i).join('.')} (type: ${current.type})`;
|
|
114
123
|
}
|
|
115
|
-
const fieldSchema = current.fields.get(
|
|
124
|
+
const fieldSchema = current.fields.get(part);
|
|
116
125
|
if (!fieldSchema) {
|
|
117
126
|
// Unknown fields on objects are allowed — classes registered via
|
|
118
127
|
// Story.registerClass() can add methods/getters not in the defaults.
|
|
@@ -143,7 +152,7 @@ export function validatePassages(
|
|
|
143
152
|
let match: RegExpExecArray | null;
|
|
144
153
|
VAR_REF_RE.lastIndex = 0;
|
|
145
154
|
while ((match = VAR_REF_RE.exec(passage.content)) !== null) {
|
|
146
|
-
const ref = match[1]
|
|
155
|
+
const ref = match[1]!;
|
|
147
156
|
const error = validateRef(ref, schema, forLocals);
|
|
148
157
|
if (error) {
|
|
149
158
|
errors.push(`Passage "${name}": ${error}`);
|