@novely/core 0.48.0 → 0.49.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
@@ -124,6 +124,7 @@ type Context = {
124
124
  */
125
125
  start: () => void;
126
126
  };
127
+ loading: (shown: boolean) => void;
127
128
  meta: {
128
129
  get restoring(): boolean;
129
130
  set restoring(value: boolean);
@@ -229,8 +230,8 @@ type RendererInit<$Language extends Lang, $Characters extends Record<string, Cha
229
230
  getLanguageDisplayName: (lang: Lang) => string;
230
231
  getCharacterColor: (character: string) => string;
231
232
  getCharacterAssets: (character: string, emotion: string) => string[];
233
+ getDialogOverview: () => Promise<DialogOverview>;
232
234
  getResourseType: (url: string) => Promise<'image' | 'audio' | 'other'>;
233
- getDialogOverview: () => DialogOverview;
234
235
  };
235
236
 
236
237
  type LocalStorageStorageSettings = {
@@ -333,6 +334,50 @@ type CharactersData<$Characters extends Record<string, Character<Lang>>> = {
333
334
  };
334
335
  type AssetsPreloading = 'lazy' | 'blocking' | 'automatic';
335
336
  type CloneFN = <T>(value: T) => T;
337
+ type StoryOptionsStatic = {
338
+ /**
339
+ * Static mode means that story is static
340
+ */
341
+ mode: 'static';
342
+ };
343
+ type StoryOptionsDynamic = {
344
+ /**
345
+ * Dynamic mode means that story parts can be loaded dynamically
346
+ */
347
+ mode: 'dynamic';
348
+ /**
349
+ * Number of saves to preload story for
350
+ * @default 4
351
+ */
352
+ preloadSaves?: number;
353
+ /**
354
+ * Function to dynamically load story parts.
355
+ *
356
+ * When engine find's unknown scene it will run this function.
357
+ * @example
358
+ * ```
359
+ * const load = async (scene: string): Promise<Story> => {
360
+ * if (['part_2__act_1', 'part_2__act_2', 'part_2__act_3'].includes(scene)) {
361
+ * const { getStory } = await import('./part-2.ts');
362
+ *
363
+ * return getStory(engine.action);
364
+ * }
365
+ *
366
+ * if (scene === 'end') {
367
+ * return {
368
+ * 'end': [
369
+ * engine.action.text('The End')
370
+ * ]
371
+ * }
372
+ * }
373
+ *
374
+ * throw new Error(`Unknown scene: ${scene}`);
375
+ * }
376
+ * ```
377
+ */
378
+ load: (scene: string) => Promise<Story>;
379
+ };
380
+ type StoryOptions = StoryOptionsStatic | StoryOptionsDynamic;
336
381
  interface NovelyInit<$Language extends Lang, $Characters extends Record<string, Character<NoInfer<$Language>>>, $State extends State, $Data extends Data, $Actions extends Record<string, (...args: any[]) => ValidAction>> {
337
382
  /**
338
383
  * An object containing the characters in the game.
@@ -548,6 +593,10 @@ interface NovelyInit<$Language extends Lang, $Characters extends Record<string,
548
593
  * Typewriter speed set by default
549
594
  */
550
595
  defaultTypewriterSpeed?: TypewriterSpeed;
596
+ /**
597
+ *
598
+ */
599
+ storyOptions?: StoryOptions;
551
600
  }
552
601
  type StateFunction<S extends State> = {
553
602
  (value: DeepPartial<S> | ((prev: S) => S)): void;
@@ -858,7 +907,7 @@ type ChoiceParams<T> = T extends TypeEssentials<infer $Lang, infer $State, any,
858
907
  type FunctionParams<T> = T extends TypeEssentials<infer $Lang, infer $State, any, any> ? FunctionActionProps<$Lang, $State> : never;
859
908
  type InputHandler<T> = T extends TypeEssentials<infer $Lang, infer $State, any, any> ? ActionInputOnInputMeta<$Lang, $State> : never;
860
909
 
861
- declare const novely: <$Language extends string, $Characters extends Record<string, Character<$Language>>, $State extends State, $Data extends Data, $Actions extends Record<string, (...args: any[]) => ValidAction>>({ characters, characterAssetSizes, defaultEmotions, storage, storageDelay, renderer: createRenderer, initialScreen, translation, state: defaultState, data: defaultData, autosaves, migrations, throttleTimeout, getLanguage, overrideLanguage, askBeforeExit, preloadAssets, parallelAssetsDownloadLimit, fetch: request, cloneFunction: clone, saveOnUnload, startKey, defaultTypewriterSpeed, }: NovelyInit<$Language, $Characters, $State, $Data, $Actions>) => {
910
+ declare const novely: <$Language extends string, $Characters extends Record<string, Character<$Language>>, $State extends State, $Data extends Data, $Actions extends Record<string, (...args: any[]) => ValidAction>>({ characters, characterAssetSizes, defaultEmotions, storage, storageDelay, renderer: createRenderer, initialScreen, translation, state: defaultState, data: defaultData, autosaves, migrations, throttleTimeout, getLanguage, overrideLanguage, askBeforeExit, preloadAssets, parallelAssetsDownloadLimit, fetch: request, cloneFunction: clone, saveOnUnload, startKey, defaultTypewriterSpeed, storyOptions, }: NovelyInit<$Language, $Characters, $State, $Data, $Actions>) => {
862
911
  /**
863
912
  * Function to set game script
864
913
  *
@@ -600,40 +600,66 @@ var Novely = (() => {
600
600
  }
601
601
  return !blockExitStatements.every(([name], i) => name && name.startsWith(blockStatements[i][0]));
602
602
  };
603
- var createReferFunction = (story) => {
604
- const refer = (path) => {
603
+ var createReferFunction = ({ story, onUnknownSceneHit }) => {
604
+ const refer = async (path) => {
605
+ const { promise: ready, resolve: setReady } = Promise.withResolvers();
605
606
  let current = story;
606
607
  let precurrent = story;
607
608
  const blocks = [];
608
- for (const [type, val] of path) {
609
- if (type === "jump") {
610
- precurrent = story;
611
- current = current[val];
612
- } else if (type === null) {
613
- precurrent = current;
614
- current = current[val];
615
- } else if (type === "choice") {
616
- blocks.push(precurrent);
617
- current = current[val + 1][1];
618
- } else if (type === "condition") {
619
- blocks.push(precurrent);
620
- current = current[2][val];
621
- } else if (type === "block") {
622
- blocks.push(precurrent);
623
- current = story[val];
624
- } else if (type === "block:exit" || type === "choice:exit" || type === "condition:exit") {
625
- current = blocks.pop();
609
+ const refer2 = async () => {
610
+ for (const [type, val] of path) {
611
+ if (type === "jump") {
612
+ if (!current[val]) {
613
+ setReady(true);
614
+ await onUnknownSceneHit(val);
615
+ }
616
+ if (DEV && !story[val]) {
617
+ throw new Error(`Attempt to jump to unknown scene "${val}"`);
618
+ }
619
+ if (DEV && story[val].length === 0) {
620
+ throw new Error(`Attempt to jump to empty scene "${val}"`);
621
+ }
622
+ precurrent = story;
623
+ current = current[val];
624
+ } else if (type === null) {
625
+ precurrent = current;
626
+ current = current[val];
627
+ } else if (type === "choice") {
628
+ blocks.push(precurrent);
629
+ current = current[val + 1][1];
630
+ } else if (type === "condition") {
631
+ blocks.push(precurrent);
632
+ current = current[2][val];
633
+ } else if (type === "block") {
634
+ blocks.push(precurrent);
635
+ current = story[val];
636
+ } else if (type === "block:exit" || type === "choice:exit" || type === "condition:exit") {
637
+ current = blocks.pop();
638
+ }
626
639
  }
627
- }
628
- return current;
640
+ setReady(false);
641
+ return current;
642
+ };
643
+ const value = refer2();
644
+ const found = await ready;
645
+ return {
646
+ found,
647
+ value
648
+ };
649
+ };
650
+ const referGuarded = async (path) => {
651
+ return await (await refer(path)).value;
652
+ };
653
+ return {
654
+ refer,
655
+ referGuarded
629
656
  };
630
- return refer;
631
657
  };
632
- var exitPath = ({ path, refer, onExitImpossible }) => {
658
+ var exitPath = async ({ path, refer, onExitImpossible }) => {
633
659
  const last = path.at(-1);
634
660
  const ignore = [];
635
661
  let wasExitImpossible = false;
636
- if (!isAction(refer(path))) {
662
+ if (!isAction(await refer(path))) {
637
663
  if (last && isNull(last[0]) && isNumber(last[1])) {
638
664
  last[1]--;
639
665
  } else {
@@ -641,7 +667,7 @@ var Novely = (() => {
641
667
  }
642
668
  }
643
669
  if (isExitImpossible(path)) {
644
- const referred = refer(path);
670
+ const referred = await refer(path);
645
671
  if (isAction(referred) && isSkippedDuringRestore(referred[0])) {
646
672
  onExitImpossible?.();
647
673
  }
@@ -663,7 +689,7 @@ var Novely = (() => {
663
689
  path.push([`${name}:exit`]);
664
690
  const prev = findLastPathItemBeforeItemOfType(path.slice(0, i + 1), name);
665
691
  if (prev) path.push([null, prev[1] + 1]);
666
- if (!isAction(refer(path))) {
692
+ if (!isAction(await refer(path))) {
667
693
  path.pop();
668
694
  continue;
669
695
  }
@@ -682,12 +708,16 @@ var Novely = (() => {
682
708
  }
683
709
  return path;
684
710
  };
685
- var collectActionsBeforeBlockingAction = ({ path, refer, clone }) => {
711
+ var collectActionsBeforeBlockingAction = async ({
712
+ path,
713
+ refer,
714
+ clone
715
+ }) => {
686
716
  const collection = [];
687
- let action = refer(path);
717
+ let action = await refer(path);
688
718
  while (true) {
689
719
  if (action == void 0) {
690
- const { exitImpossible } = exitPath({
720
+ const { exitImpossible } = await exitPath({
691
721
  path,
692
722
  refer
693
723
  });
@@ -707,7 +737,7 @@ var Novely = (() => {
707
737
  if (!Array.isArray(branchContent)) continue;
708
738
  const virtualPath = clone(path);
709
739
  virtualPath.push(["choice", i], [null, 0]);
710
- const innerActions = collectActionsBeforeBlockingAction({
740
+ const innerActions = await collectActionsBeforeBlockingAction({
711
741
  path: virtualPath,
712
742
  refer,
713
743
  clone
@@ -720,7 +750,7 @@ var Novely = (() => {
720
750
  for (const condition of conditions) {
721
751
  const virtualPath = clone(path);
722
752
  virtualPath.push(["condition", condition], [null, 0]);
723
- const innerActions = collectActionsBeforeBlockingAction({
753
+ const innerActions = await collectActionsBeforeBlockingAction({
724
754
  path: virtualPath,
725
755
  refer,
726
756
  clone
@@ -741,7 +771,7 @@ var Novely = (() => {
741
771
  } else {
742
772
  nextPath(path);
743
773
  }
744
- action = refer(path);
774
+ action = await refer(path);
745
775
  }
746
776
  return collection;
747
777
  };
@@ -761,7 +791,7 @@ var Novely = (() => {
761
791
  };
762
792
  return MAP[action];
763
793
  };
764
- var getActionsFromPath = (story, path, filter) => {
794
+ var getActionsFromPath = async ({ story, path, filter, referGuarded }) => {
765
795
  let current = story;
766
796
  let precurrent;
767
797
  let ignoreNestedBefore = null;
@@ -776,6 +806,7 @@ var Novely = (() => {
776
806
  }, 0);
777
807
  const queue = [];
778
808
  const blocks = [];
809
+ await referGuarded(path);
779
810
  for (const [type, val] of path) {
780
811
  if (type === "jump") {
781
812
  precurrent = story;
@@ -919,7 +950,7 @@ var Novely = (() => {
919
950
  }
920
951
  }
921
952
  const run = async (match) => {
922
- for await (const item of processedQueue) {
953
+ for (const item of processedQueue) {
923
954
  const result = match(item);
924
955
  if (isPromise(result)) {
925
956
  await result;
@@ -1456,7 +1487,8 @@ var Novely = (() => {
1456
1487
  cloneFunction: clone = klona,
1457
1488
  saveOnUnload = true,
1458
1489
  startKey = "start",
1459
- defaultTypewriterSpeed = DEFAULT_TYPEWRITER_SPEED
1490
+ defaultTypewriterSpeed = DEFAULT_TYPEWRITER_SPEED,
1491
+ storyOptions = { mode: "static" }
1460
1492
  }) => {
1461
1493
  const languages = Object.keys(translation);
1462
1494
  const limitScript = pLimit(1);
@@ -1466,21 +1498,27 @@ var Novely = (() => {
1466
1498
  const dataLoaded = createControlledPromise();
1467
1499
  let initialScreenWasShown = false;
1468
1500
  let destroyed = false;
1501
+ if (storyOptions.mode === "dynamic") {
1502
+ storyOptions.preloadSaves ??= 4;
1503
+ }
1504
+ const storyLoad = storyOptions.mode === "static" ? noop : storyOptions.load;
1505
+ const onUnknownSceneHit = memoize(async (scene) => {
1506
+ const part = await storyLoad(scene);
1507
+ if (part) {
1508
+ await script(part);
1509
+ }
1510
+ });
1469
1511
  const intime = (value) => {
1470
1512
  return times.add(value), value;
1471
1513
  };
1472
1514
  const scriptBase = async (part) => {
1473
1515
  if (destroyed) return;
1474
1516
  Object.assign(story, flatStory(part));
1475
- let loadingIsShown = false;
1476
1517
  if (!initialScreenWasShown) {
1477
1518
  renderer.ui.showLoading();
1478
- loadingIsShown = true;
1479
1519
  }
1480
1520
  if (preloadAssets === "blocking" && ASSETS_TO_PRELOAD.size > 0) {
1481
- if (!loadingIsShown) {
1482
- renderer.ui.showLoading();
1483
- }
1521
+ renderer.ui.showLoading();
1484
1522
  await handleAssetsPreloading({
1485
1523
  ...renderer.misc,
1486
1524
  limiter: limitAssetsDownload,
@@ -1533,11 +1571,23 @@ var Novely = (() => {
1533
1571
  const coreData = store({
1534
1572
  dataLoaded: false
1535
1573
  });
1536
- const onDataLoadedPromise = ({ cancelled }) => {
1574
+ const onDataLoadedPromise = async ({ cancelled }) => {
1537
1575
  if (cancelled) {
1538
1576
  dataLoaded.promise.then(onDataLoadedPromise);
1539
1577
  return;
1540
1578
  }
1579
+ const preload = () => {
1580
+ const saves = [...storageData.get().saves].reverse();
1581
+ const sliced = saves.slice(0, storyOptions.mode === "dynamic" ? storyOptions.preloadSaves : 0);
1582
+ for (const [path] of sliced) {
1583
+ referGuarded(path);
1584
+ }
1585
+ };
1586
+ if (preloadAssets === "blocking") {
1587
+ await preload();
1588
+ } else {
1589
+ void preload();
1590
+ }
1541
1591
  coreData.update((data2) => {
1542
1592
  data2.dataLoaded = true;
1543
1593
  return data2;
@@ -1663,7 +1713,14 @@ var Novely = (() => {
1663
1713
  const previous = stack.previous;
1664
1714
  const [path] = stack.value = latest;
1665
1715
  renderer.ui.showScreen("game");
1666
- const { queue, skip, skipPreserve } = getActionsFromPath(story, path, false);
1716
+ const { found } = await refer(path);
1717
+ if (found) context.loading(true);
1718
+ const { queue, skip, skipPreserve } = await getActionsFromPath({
1719
+ story,
1720
+ path,
1721
+ filter: false,
1722
+ referGuarded
1723
+ });
1667
1724
  const {
1668
1725
  run,
1669
1726
  keep: { keep, characters: characters2, audio: audio2 }
@@ -1672,7 +1729,12 @@ var Novely = (() => {
1672
1729
  skipPreserve
1673
1730
  });
1674
1731
  if (previous) {
1675
- const { queue: prevQueue } = getActionsFromPath(story, previous[0], false);
1732
+ const { queue: prevQueue } = await getActionsFromPath({
1733
+ story,
1734
+ path: previous[0],
1735
+ filter: false,
1736
+ referGuarded
1737
+ });
1676
1738
  for (let i = prevQueue.length - 1; i > queue.length - 1; i--) {
1677
1739
  const element = prevQueue[i];
1678
1740
  if (!isAction(element)) {
@@ -1690,6 +1752,7 @@ var Novely = (() => {
1690
1752
  data: latest[1]
1691
1753
  });
1692
1754
  }
1755
+ context.loading(false);
1693
1756
  const lastQueueItem = queue.at(-1);
1694
1757
  const lastQueueItemRequiresUserAction = lastQueueItem && isBlockingAction(lastQueueItem);
1695
1758
  await run((item) => {
@@ -1706,10 +1769,13 @@ var Novely = (() => {
1706
1769
  if (!context.meta.goingBack) {
1707
1770
  context.meta.restoring = false;
1708
1771
  }
1709
- render(context);
1772
+ await render(context);
1710
1773
  context.meta.restoring = context.meta.goingBack = false;
1711
1774
  };
1712
- const refer = createReferFunction(story);
1775
+ const { refer, referGuarded } = createReferFunction({
1776
+ story,
1777
+ onUnknownSceneHit
1778
+ });
1713
1779
  const exit = (force = false, saving = true) => {
1714
1780
  const ctx = renderer.getContext(MAIN_CONTEXT_KEY);
1715
1781
  const stack = useStack(ctx);
@@ -1761,8 +1827,16 @@ var Novely = (() => {
1761
1827
  });
1762
1828
  }
1763
1829
  const [path, data2] = save2;
1764
- const { queue } = getActionsFromPath(story, path, true);
1765
1830
  const ctx = renderer.getContext(name);
1831
+ const { found } = await refer(path);
1832
+ if (found) ctx.loading(true);
1833
+ const { queue } = await getActionsFromPath({
1834
+ story,
1835
+ path,
1836
+ filter: true,
1837
+ referGuarded
1838
+ });
1839
+ ctx.loading(false);
1766
1840
  ctx.meta.restoring = true;
1767
1841
  ctx.meta.preview = true;
1768
1842
  const processor = createQueueProcessor(queue, {
@@ -1851,21 +1925,26 @@ var Novely = (() => {
1851
1925
  }
1852
1926
  return String(c) || "";
1853
1927
  };
1854
- const getDialogOverview = () => {
1928
+ const getDialogOverview = async () => {
1855
1929
  const { value: save2 } = useStack(MAIN_CONTEXT_KEY);
1856
1930
  const stateSnapshots = save2[3];
1857
1931
  if (stateSnapshots.length == 0) {
1858
1932
  return [];
1859
1933
  }
1860
- const { queue } = getActionsFromPath(story, save2[0], false);
1934
+ const { queue } = await getActionsFromPath({
1935
+ story,
1936
+ path: save2[0],
1937
+ filter: false,
1938
+ referGuarded
1939
+ });
1861
1940
  const [lang] = storageData.get().meta;
1862
1941
  const dialogItems = [];
1863
- for (let p = 0, a = stateSnapshots.length, i = queue.length - 1; a > 0; i--) {
1942
+ for (let p = 0, a = stateSnapshots.length, i = queue.length - 1; a > 0 && i > 0; i--) {
1864
1943
  const action2 = queue[i];
1865
1944
  if (action2[0] === "dialog") {
1866
1945
  const [_, name, text] = action2;
1867
1946
  let voice = void 0;
1868
- for (let j = i - 1; j > p; j--) {
1947
+ for (let j = i - 1; j > p && j > 0; j--) {
1869
1948
  const action3 = queue[j];
1870
1949
  if (isUserRequiredAction(action3) || isSkippedDuringRestore(action3[0])) break;
1871
1950
  if (action3[0] === "stopVoice") break;
@@ -1947,14 +2026,14 @@ var Novely = (() => {
1947
2026
  matchActionOptions.push(ctx);
1948
2027
  if (!ctx.meta.preview) interactivity(true);
1949
2028
  },
1950
- onBeforeActionCall({ action: action2, props, ctx }) {
2029
+ async onBeforeActionCall({ action: action2, props, ctx }) {
1951
2030
  if (preloadAssets !== "automatic") return;
1952
2031
  if (ctx.meta.preview || ctx.meta.restoring) return;
1953
2032
  if (!isBlockingAction([action2, ...props])) return;
1954
2033
  try {
1955
- const collection = collectActionsBeforeBlockingAction({
2034
+ const collection = await collectActionsBeforeBlockingAction({
1956
2035
  path: nextPath(clone(useStack(ctx).value[0])),
1957
- refer,
2036
+ refer: referGuarded,
1958
2037
  clone
1959
2038
  });
1960
2039
  for (const [action3, ...props2] of collection) {
@@ -2127,12 +2206,6 @@ var Novely = (() => {
2127
2206
  });
2128
2207
  },
2129
2208
  jump({ ctx, data: data2 }, [scene]) {
2130
- if (DEV && !story[scene]) {
2131
- throw new Error(`Attempt to jump to unknown scene "${scene}"`);
2132
- }
2133
- if (DEV && story[scene].length === 0) {
2134
- throw new Error(`Attempt to jump to empty scene "${scene}"`);
2135
- }
2136
2209
  const stack = useStack(ctx);
2137
2210
  stack.value[0] = [
2138
2211
  ["jump", scene],
@@ -2233,11 +2306,11 @@ var Novely = (() => {
2233
2306
  ctx.clearBlockingActions("text");
2234
2307
  ctx.text(string, forward);
2235
2308
  },
2236
- exit({ ctx, data: data2 }) {
2309
+ async exit({ ctx, data: data2 }) {
2237
2310
  if (ctx.meta.restoring) return;
2238
- const { exitImpossible } = exitPath({
2311
+ const { exitImpossible } = await exitPath({
2239
2312
  path: useStack(ctx).value[0],
2240
- refer,
2313
+ refer: referGuarded,
2241
2314
  onExitImpossible: () => {
2242
2315
  match("end", [], {
2243
2316
  ctx,
@@ -2283,10 +2356,17 @@ var Novely = (() => {
2283
2356
  preloadAssets,
2284
2357
  storageData
2285
2358
  });
2286
- const render = (ctx) => {
2359
+ const render = async (ctx) => {
2287
2360
  const stack = useStack(ctx);
2288
2361
  const [path, state] = stack.value;
2289
- const referred = refer(path);
2362
+ const { found, value } = await refer(path);
2363
+ if (found) {
2364
+ ctx.loading(true);
2365
+ }
2366
+ const referred = await value;
2367
+ if (found) {
2368
+ ctx.loading(false);
2369
+ }
2290
2370
  if (isAction(referred)) {
2291
2371
  const [action2, ...props] = referred;
2292
2372
  match(action2, props, {