@novely/core 0.4.4 → 0.6.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/index.d.ts CHANGED
@@ -19,11 +19,11 @@ type Path = PathItem[];
19
19
  type State = Record<string, any>;
20
20
  type Data = Record<string, any>;
21
21
  type SaveDate = number;
22
- type SaveType = "manual" | "auto";
22
+ type SaveType = 'manual' | 'auto';
23
23
  type SaveMeta = [SaveDate, SaveType];
24
24
  type Save = [Path, State, SaveMeta];
25
25
  type Lang = string;
26
- type TypewriterSpeed = "Slow" | "Medium" | "Fast" | "Auto" | (string & {});
26
+ type TypewriterSpeed = 'Slow' | 'Medium' | 'Fast' | 'Auto' | (string & {});
27
27
  type StorageMeta = [Lang, TypewriterSpeed];
28
28
  type Migration = (save: unknown) => unknown;
29
29
  type StorageData = {
@@ -37,7 +37,7 @@ type Stack = {
37
37
  push(value: Save): void;
38
38
  clear(): void;
39
39
  };
40
- type NovelyScreen = "mainmenu" | "game" | "saves" | "settings";
40
+ type NovelyScreen = 'mainmenu' | 'game' | 'saves' | 'settings';
41
41
  /**
42
42
  * @see https://pendletonjones.com/deep-partial
43
43
  */
@@ -45,9 +45,9 @@ type DeepPartial<T> = unknown extends T ? T : T extends object ? {
45
45
  [P in keyof T]?: T[P] extends Array<infer U> ? Array<DeepPartial<U>> : T[P] extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : DeepPartial<T[P]>;
46
46
  } : T;
47
47
 
48
- type ValidAction = ['choice', [number]] | ['clear', []] | ['condition', [() => boolean, Record<string, ValidAction[]>]] | ['dialog', [string | undefined, Unwrappable, string | undefined]] | ['end', []] | ['showBackground', [string]] | ['playMusic', [string]] | ['stopMusic', [string]] | ['jump', [string]] | ['showCharacter', [string, keyof Character['emotions'], string?, string?]] | ['hideCharacter', [string, string?, string?, number?]] | ['animateCharacter', [string, number, ...string[]]] | ['wait', [FunctionableValue<number>]] | ['function', [() => Thenable<void>]] | ['input', [string, (meta: ActionInputOnInputMeta) => void, ActionInputSetup?]] | ['custom', [CustomHandler]] | ['vibrate', [...number[]]] | ['next', []] | ['text', [...string[]]] | ['exit'] | ValidAction[];
48
+ type ValidAction = ['choice', [number]] | ['clear', [Set<keyof DefaultActionProxyProvider>?, Set<string>?]] | ['condition', [() => boolean, Record<string, ValidAction[]>]] | ['dialog', [string | undefined, Unwrappable, string | undefined]] | ['end', []] | ['showBackground', [string]] | ['playMusic', [string]] | ['stopMusic', [string]] | ['jump', [string]] | ['showCharacter', [string, keyof Character['emotions'], string?, string?]] | ['hideCharacter', [string, string?, string?, number?]] | ['animateCharacter', [string, number, ...string[]]] | ['wait', [FunctionableValue<number>]] | ['function', [() => Thenable<void>]] | ['input', [string, (meta: ActionInputOnInputMeta) => void, ActionInputSetup?]] | ['custom', [CustomHandler]] | ['vibrate', [...number[]]] | ['next', []] | ['text', [...string[]]] | ['exit'] | ValidAction[];
49
49
  type Story = Record<string, ValidAction[]>;
50
- type Unwrappable = string | ((lang: string, obj: Record<string, unknown>) => string) | Record<string, string>;
50
+ type Unwrappable = string | ((lang: string, obj: Record<string, unknown>) => string) | Record<string, string | (() => string)>;
51
51
  type FunctionableValue<T> = T | (() => T);
52
52
  type CustomHandlerGetResultDataFunction = {
53
53
  (data?: Record<string, unknown>): Record<string, unknown>;
@@ -76,6 +76,7 @@ type CustomHandler = CustomHandlerFunction & {
76
76
  callOnlyLatest?: boolean;
77
77
  requireUserAction?: boolean;
78
78
  skipClearOnGoingBack?: boolean;
79
+ id?: string | symbol;
79
80
  };
80
81
  interface ActionInputOnInputMeta {
81
82
  /**
@@ -104,7 +105,7 @@ type ActionProxyProvider<Characters extends Record<string, Character>> = {
104
105
  (...choices: ([Unwrappable, ValidAction[]] | [Unwrappable, ValidAction[], () => boolean])[]): ValidAction;
105
106
  (question: Unwrappable, ...choices: ([Unwrappable, ValidAction[]] | [Unwrappable, ValidAction[], () => boolean])[]): ValidAction;
106
107
  };
107
- clear: () => ValidAction;
108
+ clear: (keep?: Set<keyof DefaultActionProxyProvider>, keepCharacters?: Set<string>) => ValidAction;
108
109
  condition: <T extends string | true | false>(condition: () => T, variants: Record<T extends true ? 'true' : T extends false ? 'false' : T, ValidAction[]>) => ValidAction;
109
110
  exit: () => ValidAction;
110
111
  dialog: {
@@ -149,7 +150,7 @@ interface CharacterHandle {
149
150
  withEmotion: (emotion: string) => () => void;
150
151
  append: (className?: string, style?: string, restoring?: boolean) => void;
151
152
  remove: (className?: string, style?: string, duration?: number) => (resolve: () => void, restoring: boolean) => void;
152
- emotions: Record<string, HTMLImageElement | Record<"head" | "left" | "right", HTMLImageElement>>;
153
+ emotions: Record<string, HTMLImageElement | Record<'head' | 'left' | 'right', HTMLImageElement>>;
153
154
  }
154
155
  interface AudioHandle {
155
156
  element: HTMLAudioElement;
@@ -159,7 +160,7 @@ interface AudioHandle {
159
160
  }
160
161
  interface RendererStore {
161
162
  characters: Record<string, CharacterHandle>;
162
- audio: Partial<Record<"music", AudioHandle>>;
163
+ audio: Partial<Record<'music', AudioHandle>>;
163
164
  }
164
165
  type Renderer = {
165
166
  character: (character: string) => CharacterHandle;
@@ -168,7 +169,7 @@ type Renderer = {
168
169
  choices: (question: string, choices: ([string, ValidAction[]] | [string, ValidAction[], () => boolean])[]) => (resolve: (selected: number) => void) => void;
169
170
  input: (question: string, onInput: Parameters<DefaultActionProxyProvider['input']>[1], setup?: Parameters<DefaultActionProxyProvider['input']>[2]) => (resolve: () => void) => void;
170
171
  music: (source: string, method: keyof RendererStore['audio']) => AudioHandle;
171
- clear: (goingBack: boolean) => (resolve: () => void) => void;
172
+ clear: (goingBack: boolean, keep: Set<keyof DefaultActionProxyProvider>, keepCharacters: Set<string>) => (resolve: () => void) => void;
172
173
  custom: (fn: Parameters<DefaultActionProxyProvider['custom']>[0], goingBack: boolean, push: () => void) => Thenable<void>;
173
174
  text: (str: string, resolve: () => void) => void;
174
175
  store: RendererStore;
@@ -176,7 +177,7 @@ type Renderer = {
176
177
  /**
177
178
  * Показывает экран, скрывает другие
178
179
  */
179
- showScreen(name: "mainmenu" | "game" | "saves" | "settings" | 'loading'): void;
180
+ showScreen(name: 'mainmenu' | 'game' | 'saves' | 'settings' | 'loading'): void;
180
181
  };
181
182
  };
182
183
  type RendererInit = {
@@ -254,8 +255,25 @@ interface NovelyInit<Languages extends string, Characters extends Record<string,
254
255
  * Migration from old saves to newer
255
256
  */
256
257
  migrations?: Migration[];
258
+ /**
259
+ * For saves Novely uses `throttle` function. This might be needed if you want to control frequency of saves to the storage
260
+ * @default 799
261
+ */
262
+ throttleTimeout?: number;
263
+ /**
264
+ * Custom language detector
265
+ * @param languages Supported languages aka `languages: []` in the config
266
+ * @example ```ts
267
+ * novely({
268
+ * getLanguage(languages) {
269
+ * return sdk.environment.i18n.lang // i.e. custom language from some sdk
270
+ * }
271
+ * })
272
+ * ```
273
+ */
274
+ getLanguage?: (languages: string[]) => string;
257
275
  }
258
- declare const novely: <Languages extends string, Characters extends Record<string, Character<Languages>>, Inter extends _novely_t9n.T9N<Languages, string>, StateScheme extends State, DataScheme extends Data>({ characters, storage, storageDelay, renderer: createRenderer, initialScreen, t9n, languages, state: defaultState, data: defaultData, autosaves, migrations }: NovelyInit<Languages, Characters, Inter, StateScheme, DataScheme>) => {
276
+ declare const novely: <Languages extends string, Characters extends Record<string, Character<Languages>>, Inter extends _novely_t9n.T9N<Languages, string>, StateScheme extends State, DataScheme extends Data>({ characters, storage, storageDelay, renderer: createRenderer, initialScreen, t9n, languages, state: defaultState, data: defaultData, autosaves, migrations, throttleTimeout, getLanguage }: NovelyInit<Languages, Characters, Inter, StateScheme, DataScheme>) => {
259
277
  /**
260
278
  * Function to set story
261
279
  */
@@ -281,7 +299,7 @@ declare const novely: <Languages extends string, Characters extends Record<strin
281
299
  /**
282
300
  * Unwraps translatable content to a string value
283
301
  */
284
- unwrap(content: string | ((lang: string, obj: Record<string, unknown>) => string) | Record<Languages, string>): string;
302
+ unwrap(content: Unwrappable | Record<Languages, string>): string;
285
303
  /**
286
304
  * Function that is used for translation
287
305
  */
@@ -159,6 +159,9 @@ var Novely = (() => {
159
159
  var isFunction = (val) => {
160
160
  return typeof val === "function";
161
161
  };
162
+ var isPromise = (val) => {
163
+ return Boolean(val) && (typeof val === "object" || isFunction(val)) && isFunction(val.then);
164
+ };
162
165
  var isEmpty = (val) => {
163
166
  return typeof val === "object" && !isNull(val) && Object.keys(val).length === 0;
164
167
  };
@@ -270,34 +273,30 @@ var Novely = (() => {
270
273
  }
271
274
 
272
275
  // src/constants.ts
273
- var SKIPPED_DURING_RESTORE = /* @__PURE__ */ new Set([
274
- "dialog",
275
- "choice",
276
- "input",
277
- "vibrate",
278
- "text"
279
- ]);
276
+ var SKIPPED_DURING_RESTORE = /* @__PURE__ */ new Set(["dialog", "choice", "input", "vibrate", "text"]);
277
+ var EMPTY_SET = /* @__PURE__ */ new Set();
280
278
 
281
279
  // ../t9n/dist/index.js
282
- var p = (t, i) => {
280
+ var d = (e, o) => {
283
281
  let r = [];
284
- for (let a of i) {
285
- if (!t)
282
+ for (let a of o) {
283
+ if (!e)
286
284
  break;
287
- let [e, s] = t.split(a, 2);
288
- r.push(e), t = s;
285
+ let [t, i] = e.split(a, 2);
286
+ r.push(t), e = i;
289
287
  }
290
- return r.push(t), r;
288
+ return r.push(e), r;
291
289
  };
292
- var T = /{{(.*?)}}/g;
293
- var c = (t, i, r, a, e) => t.replace(T, (s, o, n) => {
294
- s = 0, n = i;
295
- let [d, g, l] = p(o.trim(), ["@", "%"]), u = d.split(".");
296
- for (; n && s < u.length; )
297
- n = n[u[s++]];
298
- g && r && n && e && (n = r[g][e.select(n)]);
299
- let S = a && l && a[l];
300
- return S && (n = S(n)), n ?? "";
290
+ var m = /{{(.*?)}}/g;
291
+ var l = (e) => Array.isArray(e) ? e.map((o) => l(o)).join("<br>") : typeof e == "function" ? l(e()) : e;
292
+ var T = (e, o, r, a, t) => l(e).replace(m, (i, s, n) => {
293
+ i = 0, n = o;
294
+ let [c, g, u] = d(s.trim(), ["@", "%"]), S = c.split(".");
295
+ for (; n && i < S.length; )
296
+ n = n[S[i++]];
297
+ g && r && n && t && (n = r[g][t.select(n)]);
298
+ let p = a && u && a[u];
299
+ return p && (n = p(n)), n ?? "";
301
300
  });
302
301
 
303
302
  // src/novely.ts
@@ -312,7 +311,9 @@ var Novely = (() => {
312
311
  state: defaultState,
313
312
  data: defaultData,
314
313
  autosaves = true,
315
- migrations = []
314
+ migrations = [],
315
+ throttleTimeout = 799,
316
+ getLanguage: getLanguage2 = getLanguage
316
317
  }) => {
317
318
  let story;
318
319
  let times = /* @__PURE__ */ new Set();
@@ -322,17 +323,19 @@ var Novely = (() => {
322
323
  return times.add(value), value;
323
324
  };
324
325
  const withStory = (s) => {
325
- story = Object.fromEntries(Object.entries(s).map(([name, items]) => {
326
- const flat = (item) => {
327
- return item.flatMap((data2) => {
328
- const type = data2[0];
329
- if (Array.isArray(type))
330
- return flat(data2);
331
- return [data2];
332
- });
333
- };
334
- return [name, flat(items)];
335
- }));
326
+ story = Object.fromEntries(
327
+ Object.entries(s).map(([name, items]) => {
328
+ const flat = (item) => {
329
+ return item.flatMap((data2) => {
330
+ const type = data2[0];
331
+ if (Array.isArray(type))
332
+ return flat(data2);
333
+ return [data2];
334
+ });
335
+ };
336
+ return [name, flat(items)];
337
+ })
338
+ );
336
339
  if (initialScreen !== "game")
337
340
  renderer.ui.showScreen(initialScreen);
338
341
  };
@@ -351,7 +354,14 @@ var Novely = (() => {
351
354
  stack.value[1] = val;
352
355
  }
353
356
  const getDefaultSave = (state2 = {}) => {
354
- return [[[null, "start"], [null, 0]], state2, [intime(Date.now()), "auto"]];
357
+ return [
358
+ [
359
+ [null, "start"],
360
+ [null, 0]
361
+ ],
362
+ state2,
363
+ [intime(Date.now()), "auto"]
364
+ ];
355
365
  };
356
366
  const createStack = (current, stack2 = [current]) => {
357
367
  return {
@@ -376,7 +386,7 @@ var Novely = (() => {
376
386
  const initialData = {
377
387
  saves: [],
378
388
  data: klona(defaultData),
379
- meta: [getLanguage(languages), getTypewriterSpeed()]
389
+ meta: [getLanguage2(languages), getTypewriterSpeed()]
380
390
  };
381
391
  const $ = store(initialData);
382
392
  let initialDataLoaded = false;
@@ -384,14 +394,14 @@ var Novely = (() => {
384
394
  if (initialDataLoaded)
385
395
  storage.set(value);
386
396
  };
387
- const throttledOnStorageDataChange = throttle(onStorageDataChange, 120);
397
+ const throttledOnStorageDataChange = throttle(onStorageDataChange, throttleTimeout);
388
398
  $.subscribe(throttledOnStorageDataChange);
389
399
  const getStoredData = () => {
390
400
  storage.get().then((stored) => {
391
401
  for (const migration of migrations) {
392
402
  stored = migration(stored);
393
403
  }
394
- stored.meta[0] ||= getLanguage(languages);
404
+ stored.meta[0] ||= getLanguage2(languages);
395
405
  stored.meta[1] ||= getTypewriterSpeed();
396
406
  if (isEmpty(stored.data)) {
397
407
  stored.data = defaultData;
@@ -451,12 +461,14 @@ var Novely = (() => {
451
461
  return;
452
462
  let latest = save2 ? save2 : $.get().saves.at(-1);
453
463
  if (!latest) {
454
- $.update(() => ({ saves: [initial], data: klona(defaultData), meta: [getLanguage(languages), getTypewriterSpeed()] }));
464
+ $.update(() => ({
465
+ saves: [initial],
466
+ data: klona(defaultData),
467
+ meta: [getLanguage2(languages), getTypewriterSpeed()]
468
+ }));
455
469
  latest = klona(initial);
456
470
  }
457
471
  restoring = true, stack.value = latest;
458
- renderer.ui.showScreen("game");
459
- match("clear", [goingBack]);
460
472
  let current = story;
461
473
  let index = 0;
462
474
  const max = stack.value[0].reduce((acc, [type, val]) => {
@@ -465,22 +477,30 @@ var Novely = (() => {
465
477
  return acc;
466
478
  }, 0);
467
479
  const queue = [];
480
+ const keep = /* @__PURE__ */ new Set();
481
+ const characters2 = /* @__PURE__ */ new Set();
468
482
  for (const [type, val] of stack.value[0]) {
469
483
  if (type === null) {
470
484
  if (isString(val)) {
471
485
  current = current[val];
472
486
  } else if (isNumber(val)) {
473
487
  index++;
474
- for (let i = 0; i < val; i++) {
488
+ for (let i = 0; i <= val; i++) {
475
489
  const [action2, ...meta] = current[i];
490
+ const push2 = () => {
491
+ keep.add(action2);
492
+ queue.push([action2, meta]);
493
+ };
494
+ if (action2 === "showCharacter")
495
+ characters2.add(meta[0]);
476
496
  if (SKIPPED_DURING_RESTORE.has(action2) || isUserRequiredAction(action2, meta)) {
477
497
  if (index === max && i === val) {
478
- queue.push([action2, meta]);
498
+ push2();
479
499
  } else {
480
500
  continue;
481
501
  }
482
502
  }
483
- queue.push([action2, meta]);
503
+ push2();
484
504
  }
485
505
  current = current[val];
486
506
  }
@@ -490,21 +510,33 @@ var Novely = (() => {
490
510
  current = current[2][val];
491
511
  }
492
512
  }
493
- const indexedQueue = queue.map((value, index2) => value.concat(index2));
513
+ queue.forEach((value, index2) => {
514
+ value.push(index2);
515
+ });
516
+ const indexedQueue = queue;
517
+ renderer.ui.showScreen("game");
518
+ match("clear", [keep, characters2]);
519
+ const next2 = (i) => indexedQueue.slice(i + 1);
494
520
  for await (const [action2, meta, i] of indexedQueue) {
495
521
  if (action2 === "function" || action2 === "custom") {
496
522
  if (action2 === "custom" && meta[0].callOnlyLatest) {
497
- const next2 = indexedQueue.slice(i + 1);
498
- const notLatest = next2.some(([_action, _meta]) => _meta && meta && str(_meta[0]) === str(meta[0]));
523
+ const notLatest = next2(i).some(([_action, _meta]) => {
524
+ if (!_meta || !meta)
525
+ return false;
526
+ const c0 = _meta[0];
527
+ const c1 = meta[0];
528
+ const isIdenticalID = c0.id && c1.id && c0.id === c1.id;
529
+ return isIdenticalID || str(c0) === str(c1);
530
+ });
499
531
  if (notLatest)
500
532
  continue;
501
533
  }
502
534
  const result = match(action2, meta);
503
- if (result && "then" in result)
535
+ if (isPromise(result)) {
504
536
  await result;
537
+ }
505
538
  } else if (action2 === "showCharacter") {
506
- const next2 = indexedQueue.slice(i + 1);
507
- const skip = next2.some(([_action, _meta]) => {
539
+ const skip = next2(i).some(([_action, _meta]) => {
508
540
  if (!_meta || !meta)
509
541
  return false;
510
542
  const hidden = _action === "hideCharacter" && _meta[0] === meta[0];
@@ -514,9 +546,8 @@ var Novely = (() => {
514
546
  if (skip)
515
547
  continue;
516
548
  match(action2, meta);
517
- } else if (action2 === "showBackground") {
518
- const next2 = indexedQueue.slice(i + 1);
519
- const notLatest = next2.some(([_action]) => action2 === _action);
549
+ } else if (action2 === "showBackground" || action2 === "animateCharacter") {
550
+ const notLatest = next2(i).some(([_action]) => action2 === _action);
520
551
  if (notLatest)
521
552
  continue;
522
553
  match(action2, meta);
@@ -599,9 +630,9 @@ var Novely = (() => {
599
630
  },
600
631
  dialog([character, content, emotion]) {
601
632
  const name = (() => {
602
- const c2 = character, cs = characters;
633
+ const c = character, cs = characters;
603
634
  const lang = $.get().meta[0];
604
- return c2 ? c2 in cs ? typeof cs[c2].name === "string" ? cs[c2].name : cs[c2].name[lang] : c2 : "";
635
+ return c ? c in cs ? typeof cs[c].name === "string" ? cs[c].name : cs[c].name[lang] : c : "";
605
636
  })();
606
637
  renderer.dialog(unwrap(content), unwrap(name), character, emotion)(forward);
607
638
  },
@@ -620,18 +651,27 @@ var Novely = (() => {
620
651
  const unwrapped = choices.map(([content, action2, visible]) => {
621
652
  return [unwrap(content), action2, visible];
622
653
  });
623
- renderer.choices(unwrap(question), unwrapped)((selected) => {
654
+ renderer.choices(
655
+ unwrap(question),
656
+ unwrapped
657
+ )((selected) => {
624
658
  enmemory();
625
- stack.value[0].push(["choice", isWithoutQuestion ? selected : selected + 1], [null, 0]), render(), interactivity(true);
659
+ const offset = isWithoutQuestion ? 0 : 1;
660
+ stack.value[0].push(["choice", selected + offset], [null, 0]);
661
+ render();
662
+ interactivity(true);
626
663
  });
627
664
  },
628
665
  jump([scene]) {
629
- stack.value[0] = [[null, scene], [null, -1]];
666
+ stack.value[0] = [
667
+ [null, scene],
668
+ [null, -1]
669
+ ];
630
670
  match("clear", []);
631
671
  },
632
- clear() {
672
+ clear([keep, characters2]) {
633
673
  vibrate(0);
634
- renderer.clear(goingBack)(push);
674
+ renderer.clear(goingBack, keep || EMPTY_SET, characters2 || EMPTY_SET)(push);
635
675
  },
636
676
  condition([condition]) {
637
677
  const value = condition();
@@ -682,9 +722,7 @@ var Novely = (() => {
682
722
  clearTimeout(timeoutId);
683
723
  });
684
724
  };
685
- handler.callOnlyLatest = true;
686
- return renderer.custom(handler, goingBack, () => {
687
- }), push();
725
+ match("custom", [handler]);
688
726
  },
689
727
  text(text) {
690
728
  renderer.text(text.map((content) => unwrap(content)).join(" "), forward);
@@ -738,10 +776,14 @@ var Novely = (() => {
738
776
  interacted = value;
739
777
  };
740
778
  const unwrap = (content, global = false) => {
741
- const { data: data2, meta: [lang] } = $.get();
779
+ const {
780
+ data: data2,
781
+ meta: [lang]
782
+ } = $.get();
742
783
  const obj = global ? data2 : state();
743
- const str2 = isFunction(content) ? content(lang, obj) : typeof content === "object" ? content[lang] : content;
744
- return c(str2, obj);
784
+ const cnt = isFunction(content) ? content(lang, obj) : typeof content === "string" ? content : content[lang];
785
+ const str2 = isFunction(cnt) ? cnt() : cnt;
786
+ return T(str2, obj);
745
787
  };
746
788
  function data(value) {
747
789
  if (!value)