@rohal12/spindle 0.42.0 → 0.43.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/src/story-api.ts CHANGED
@@ -76,20 +76,37 @@ function ensureVariableChangedSubscription(): void {
76
76
  if (variableChangedSubActive) return;
77
77
  variableChangedSubActive = true;
78
78
  let prevVars = { ...useStoryStore.getState().variables };
79
+ let prevTrans = { ...useStoryStore.getState().transient };
79
80
  useStoryStore.subscribe((state) => {
80
81
  const changed: Record<string, { from: unknown; to: unknown }> = {};
81
82
  let hasChanges = false;
82
- const allKeys = new Set([
83
+
84
+ // Check $variables
85
+ const allVarKeys = new Set([
83
86
  ...Object.keys(prevVars),
84
87
  ...Object.keys(state.variables),
85
88
  ]);
86
- for (const key of allKeys) {
89
+ for (const key of allVarKeys) {
87
90
  if (state.variables[key] !== prevVars[key]) {
88
91
  changed[key] = { from: prevVars[key], to: state.variables[key] };
89
92
  hasChanges = true;
90
93
  }
91
94
  }
95
+
96
+ // Check %transient
97
+ const allTransKeys = new Set([
98
+ ...Object.keys(prevTrans),
99
+ ...Object.keys(state.transient),
100
+ ]);
101
+ for (const key of allTransKeys) {
102
+ if (state.transient[key] !== prevTrans[key]) {
103
+ changed[`%${key}`] = { from: prevTrans[key], to: state.transient[key] };
104
+ hasChanges = true;
105
+ }
106
+ }
107
+
92
108
  prevVars = { ...state.variables };
109
+ prevTrans = { ...state.transient };
93
110
  if (hasChanges) {
94
111
  emit('variableChanged', changed);
95
112
  }
@@ -178,16 +195,27 @@ export interface StoryAPI {
178
195
  function createStoryAPI(): StoryAPI {
179
196
  return {
180
197
  get(name: string): unknown {
198
+ if (name.startsWith('%')) {
199
+ return useStoryStore.getState().transient[name.slice(1)];
200
+ }
181
201
  return useStoryStore.getState().variables[name];
182
202
  },
183
203
 
184
204
  set(nameOrVars: string | Record<string, unknown>, value?: unknown): void {
185
205
  const state = useStoryStore.getState();
186
206
  if (typeof nameOrVars === 'string') {
187
- state.setVariable(nameOrVars, value);
207
+ if (nameOrVars.startsWith('%')) {
208
+ state.setTransient(nameOrVars.slice(1), value);
209
+ } else {
210
+ state.setVariable(nameOrVars, value);
211
+ }
188
212
  } else {
189
213
  for (const [k, v] of Object.entries(nameOrVars)) {
190
- state.setVariable(k, v);
214
+ if (k.startsWith('%')) {
215
+ state.setTransient(k.slice(1), v);
216
+ } else {
217
+ state.setVariable(k, v);
218
+ }
191
219
  }
192
220
  }
193
221
  },
@@ -12,7 +12,10 @@ export interface VariableSchema extends FieldSchema {
12
12
  default: unknown;
13
13
  }
14
14
 
15
- const DECLARATION_RE = /^\$(\w+)\s*=\s*(.+)$/;
15
+ function declarationRegex(sigil: string): RegExp {
16
+ const escaped = sigil.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
+ return new RegExp(`^${escaped}(\\w+)\\s*=\\s*(.+)$`);
18
+ }
16
19
  const VAR_REF_RE = /\$(\w+(?:\.\w+)*)/g;
17
20
  const FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
18
21
 
@@ -32,20 +35,23 @@ function inferSchema(value: unknown): FieldSchema {
32
35
  const jsType = typeof value;
33
36
  if (!VALID_VAR_TYPES.has(jsType)) {
34
37
  throw new Error(
35
- `StoryVariables: Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`,
38
+ `Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`,
36
39
  );
37
40
  }
38
41
  return { type: jsType as VarType };
39
42
  }
40
43
 
41
44
  /**
42
- * Parse a StoryVariables passage content into a schema map.
43
- * Each line: `$varName = expression`
45
+ * Parse a StoryVariables or StoryTransients passage content into a schema map.
46
+ * Each line: `$varName = expression` (or `%varName = expression` for transients)
44
47
  */
45
48
  export function parseStoryVariables(
46
49
  content: string,
50
+ sigil: '$' | '%' = '$',
47
51
  ): Map<string, VariableSchema> {
48
52
  const schema = new Map<string, VariableSchema>();
53
+ const DECLARATION_RE = declarationRegex(sigil);
54
+ const passageName = sigil === '%' ? 'StoryTransients' : 'StoryVariables';
49
55
 
50
56
  for (const rawLine of content.split('\n')) {
51
57
  const line = rawLine.trim();
@@ -54,7 +60,7 @@ export function parseStoryVariables(
54
60
  const match = line.match(DECLARATION_RE);
55
61
  if (!match) {
56
62
  throw new Error(
57
- `StoryVariables: Invalid declaration: "${line}". Expected: $name = value`,
63
+ `${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`,
58
64
  );
59
65
  }
60
66
 
@@ -64,11 +70,18 @@ export function parseStoryVariables(
64
70
  value = new Function('return (' + expr + ')')();
65
71
  } catch (err) {
66
72
  throw new Error(
67
- `StoryVariables: Failed to evaluate "$${name} = ${expr}": ${err instanceof Error ? err.message : err}`,
73
+ `${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${err instanceof Error ? err.message : err}`,
68
74
  );
69
75
  }
70
76
 
71
- const fieldSchema = inferSchema(value);
77
+ let fieldSchema: FieldSchema;
78
+ try {
79
+ fieldSchema = inferSchema(value);
80
+ } catch (err) {
81
+ throw new Error(
82
+ `${passageName}: ${err instanceof Error ? err.message : err}`,
83
+ );
84
+ }
72
85
  schema.set(name, { ...fieldSchema, name, default: value });
73
86
  }
74
87
 
@@ -146,8 +159,8 @@ export function validatePassages(
146
159
  const errors: string[] = [];
147
160
 
148
161
  for (const [name, passage] of passages) {
149
- // Don't validate the StoryVariables passage itself
150
- if (name === 'StoryVariables') continue;
162
+ // Don't validate the StoryVariables/StoryTransients passages themselves
163
+ if (name === 'StoryVariables' || name === 'StoryTransients') continue;
151
164
 
152
165
  const forLocals = extractForLocals(passage.content);
153
166
 
package/src/triggers.ts CHANGED
@@ -45,7 +45,13 @@ let dialogHostCallbacks: DialogHostCallbacks | null = null;
45
45
  function evalCondition(condition: string): boolean {
46
46
  const state = useStoryStore.getState();
47
47
  try {
48
- return !!evaluate(condition, state.variables, state.temporary);
48
+ return !!evaluate(
49
+ condition,
50
+ state.variables,
51
+ state.temporary,
52
+ {},
53
+ state.transient,
54
+ );
49
55
  } catch {
50
56
  return false;
51
57
  }
package/types/index.d.ts CHANGED
@@ -294,6 +294,7 @@ export interface MacroContext {
294
294
  Record<string, unknown>,
295
295
  Record<string, unknown>,
296
296
  Record<string, unknown>,
297
+ Record<string, unknown>,
297
298
  ];
298
299
  varName?: string;
299
300
  value?: unknown;
@@ -364,12 +365,20 @@ export interface SaveInfo {
364
365
  * @see {@link ../../src/story-api.ts} for the implementation.
365
366
  */
366
367
  export interface StoryAPI {
367
- /** Get the value of a story variable. */
368
+ /**
369
+ * Get a variable value. Use '%name' prefix for transient variables.
370
+ * @example Story.get('health') // $health
371
+ * @example Story.get('%npcList') // %npcList (transient)
372
+ */
368
373
  get(name: string): unknown;
369
374
 
370
- /** Set a single story variable. */
375
+ /**
376
+ * Set one or more variables. Use '%name' prefix for transient variables.
377
+ * @example Story.set('health', 100)
378
+ * @example Story.set('%npcList', [...])
379
+ * @example Story.set({ health: 100, '%npcList': [...] })
380
+ */
371
381
  set(name: string, value: unknown): void;
372
- /** Set multiple story variables at once. */
373
382
  set(vars: Record<string, unknown>): void;
374
383
 
375
384
  /** Navigate to a passage by name. */