@novely/core 0.22.1 → 0.23.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.js CHANGED
@@ -12,18 +12,60 @@ var AUDIO_ACTIONS = /* @__PURE__ */ new Set([
12
12
  ]);
13
13
  var EMPTY_SET = /* @__PURE__ */ new Set();
14
14
  var DEFAULT_TYPEWRITER_SPEED = "Medium";
15
+ var HOWLER_SUPPORTED_FILE_FORMATS = /* @__PURE__ */ new Set([
16
+ "mp3",
17
+ "mpeg",
18
+ "opus",
19
+ "ogg",
20
+ "oga",
21
+ "wav",
22
+ "aac",
23
+ "caf",
24
+ "m4a",
25
+ "m4b",
26
+ "mp4",
27
+ "weba",
28
+ "webm",
29
+ "dolby",
30
+ "flac"
31
+ ]);
32
+ var SUPPORTED_IMAGE_FILE_FORMATS = /* @__PURE__ */ new Set([
33
+ "apng",
34
+ "avif",
35
+ "gif",
36
+ "jpg",
37
+ "jpeg",
38
+ "jfif",
39
+ "pjpeg",
40
+ "pjp",
41
+ "png",
42
+ "svg",
43
+ "webp",
44
+ "bmp"
45
+ ]);
15
46
  var MAIN_CONTEXT_KEY = "$MAIN";
16
47
 
17
48
  // src/shared.ts
18
49
  var STACK_MAP = /* @__PURE__ */ new Map();
19
50
 
20
51
  // src/utils.ts
21
- var matchAction = (getContext, values) => {
52
+ import { DEV } from "esm-env";
53
+ var matchAction = ({ getContext, push, forward }, values) => {
22
54
  return (action, props, { ctx, data }) => {
23
55
  const context = typeof ctx === "string" ? getContext(ctx) : ctx;
24
56
  return values[action]({
25
57
  ctx: context,
26
- data
58
+ data,
59
+ push() {
60
+ if (context.meta.preview)
61
+ return;
62
+ push(context);
63
+ },
64
+ forward() {
65
+ if (context.meta.preview)
66
+ return;
67
+ forward(context);
68
+ }
27
69
  }, props);
28
70
  };
29
71
  };
@@ -130,6 +172,9 @@ var isBlockExitStatement = (statement) => {
130
172
  var isSkippedDuringRestore = (item) => {
131
173
  return SKIPPED_DURING_RESTORE.has(item);
132
174
  };
175
+ var isAudioAction = (action) => {
176
+ return AUDIO_ACTIONS.has(action);
177
+ };
133
178
  var noop = () => {
134
179
  };
135
180
  var isAction = (element) => {
@@ -352,6 +397,53 @@ var createUseStackFunction = (renderer) => {
352
397
  };
353
398
  return useStack;
354
399
  };
400
+ var mapSet = (set, fn) => {
401
+ return [...set].map(fn);
402
+ };
403
+ var isImageAsset = (asset) => {
404
+ return isString(asset) && isCSSImage(asset);
405
+ };
406
+ var getUrlFileExtension = (address) => {
407
+ try {
408
+ const { pathname } = new URL(address, location.href);
409
+ return pathname.split(".").at(-1).split("!")[0].split(":")[0];
410
+ } catch (error) {
411
+ if (DEV) {
412
+ console.error(new Error(`Could not construct URL "${address}".`, { cause: error }));
413
+ }
414
+ return "";
415
+ }
416
+ };
417
+ var fetchContentType = async (request, url) => {
418
+ try {
419
+ const response = await request(url, {
420
+ method: "HEAD"
421
+ });
422
+ return response.headers.get("Content-Type") || "";
423
+ } catch (error) {
424
+ if (DEV) {
425
+ console.error(new Error(`Failed to fetch file at "${url}"`, { cause: error }));
426
+ }
427
+ return "";
428
+ }
429
+ };
430
+ var getResourseType = async (request, url) => {
431
+ const extension = getUrlFileExtension(url);
432
+ if (HOWLER_SUPPORTED_FILE_FORMATS.has(extension)) {
433
+ return "audio";
434
+ }
435
+ if (SUPPORTED_IMAGE_FILE_FORMATS.has(extension)) {
436
+ return "image";
437
+ }
438
+ const contentType = await fetchContentType(request, url);
439
+ if (contentType.includes("audio")) {
440
+ return "audio";
441
+ }
442
+ if (contentType.includes("image")) {
443
+ return "image";
444
+ }
445
+ return "other";
446
+ };
355
447
 
356
448
  // src/global.ts
357
449
  var PRELOADED_ASSETS = /* @__PURE__ */ new Set();
@@ -547,7 +639,7 @@ var localStorageStorage = (options) => {
547
639
 
548
640
  // src/novely.ts
549
641
  import pLimit from "p-limit";
550
- import { DEV } from "esm-env";
642
+ import { DEV as DEV2 } from "esm-env";
551
643
  var novely = ({
552
644
  characters,
553
645
  storage = localStorageStorage({ key: "novely-game-storage" }),
@@ -564,9 +656,12 @@ var novely = ({
564
656
  getLanguage: getLanguage2 = getLanguage,
565
657
  overrideLanguage = false,
566
658
  askBeforeExit = true,
567
- preloadAssets = "lazy"
659
+ preloadAssets = "lazy",
660
+ parallelAssetsDownloadLimit = 15,
661
+ fetch: request = fetch
568
662
  }) => {
569
663
  const limitScript = pLimit(1);
664
+ const limitAssetsDownload = pLimit(parallelAssetsDownloadLimit);
570
665
  const story = {};
571
666
  const times = /* @__PURE__ */ new Set();
572
667
  const ASSETS_TO_PRELOAD = /* @__PURE__ */ new Set();
@@ -582,7 +677,23 @@ var novely = ({
582
677
  Object.assign(story, flattenStory(part));
583
678
  if (preloadAssets === "blocking" && ASSETS_TO_PRELOAD.size > 0) {
584
679
  renderer.ui.showScreen("loading");
585
- await renderer.misc.preloadImagesBlocking(ASSETS_TO_PRELOAD);
680
+ const { preloadAudioBlocking, preloadImageBlocking } = renderer.misc;
681
+ const list = mapSet(ASSETS_TO_PRELOAD, (asset) => {
682
+ return limitAssetsDownload(async () => {
683
+ const type = await getResourseType(request, asset);
684
+ switch (type) {
685
+ case "audio": {
686
+ await preloadAudioBlocking(asset);
687
+ break;
688
+ }
689
+ case "image": {
690
+ await preloadImageBlocking(asset);
691
+ break;
692
+ }
693
+ }
694
+ });
695
+ });
696
+ await Promise.allSettled(list);
586
697
  }
587
698
  const screen = renderer.ui.getScreen();
588
699
  const nextScreen = scriptCalled ? screen : initialScreen;
@@ -603,13 +714,25 @@ var novely = ({
603
714
  return limitScript(() => scriptBase(part));
604
715
  };
605
716
  const action = new Proxy({}, {
606
- get(_, prop) {
717
+ get(_, action2) {
607
718
  return (...props) => {
608
719
  if (preloadAssets === "blocking") {
609
- if (prop === "showBackground" && typeof props[0] === "string" && isCSSImage(props[0])) {
720
+ if (action2 === "showBackground") {
721
+ if (isImageAsset(props[0])) {
722
+ ASSETS_TO_PRELOAD.add(props[0]);
723
+ }
724
+ if (props[0] && typeof props[0] === "object") {
725
+ for (const value of Object.values(props[0])) {
726
+ if (!isImageAsset(value))
727
+ continue;
728
+ ASSETS_TO_PRELOAD.add(value);
729
+ }
730
+ }
731
+ }
732
+ if (isAudioAction(action2) && isString(props[0])) {
610
733
  ASSETS_TO_PRELOAD.add(props[0]);
611
734
  }
612
- if (prop === "showCharacter" && typeof props[0] === "string" && typeof props[1] === "string") {
735
+ if (action2 === "showCharacter" && isString(props[0]) && isString(props[1])) {
613
736
  const images = characters[props[0]].emotions[props[1]];
614
737
  if (Array.isArray(images)) {
615
738
  for (const asset of images) {
@@ -620,7 +743,7 @@ var novely = ({
620
743
  }
621
744
  }
622
745
  }
623
- return [prop, ...props];
746
+ return [action2, ...props];
624
747
  };
625
748
  }
626
749
  });
@@ -687,11 +810,12 @@ var novely = ({
687
810
  };
688
811
  storageDelay.then(getStoredData);
689
812
  const initial = getDefaultSave(klona(defaultState));
690
- addEventListener("visibilitychange", () => {
813
+ const onVisibilityChange = () => {
691
814
  if (document.visibilityState === "hidden") {
692
815
  throttledEmergencyOnStorageDataChange();
693
816
  }
694
- });
817
+ };
818
+ addEventListener("visibilitychange", onVisibilityChange);
695
819
  addEventListener("beforeunload", throttledEmergencyOnStorageDataChange);
696
820
  const save = (override = false, type = override ? "auto" : "manual") => {
697
821
  if (!$$.get().dataLoaded)
@@ -739,12 +863,11 @@ var novely = ({
739
863
  return;
740
864
  let latest = save2 || $.get().saves.at(-1);
741
865
  if (!latest) {
742
- $.set({
743
- saves: [initial],
744
- data: klona(defaultData),
745
- meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED, 1, 1, 1]
746
- });
747
866
  latest = klona(initial);
867
+ $.update((prev) => {
868
+ prev.saves.push(latest);
869
+ return prev;
870
+ });
748
871
  }
749
872
  const context = renderer.getContext(MAIN_CONTEXT_KEY);
750
873
  const stack = useStack(context);
@@ -780,7 +903,7 @@ var novely = ({
780
903
  data: latest[1]
781
904
  });
782
905
  });
783
- context.meta.restoring = context.meta.restoring = false;
906
+ context.meta.restoring = context.meta.goingBack = false;
784
907
  render(context);
785
908
  };
786
909
  const refer = (path) => {
@@ -848,7 +971,7 @@ var novely = ({
848
971
  ctx.meta.preview = true;
849
972
  const processor = createQueueProcessor(queue);
850
973
  await processor.run((action2, props) => {
851
- if (AUDIO_ACTIONS.has(action2))
974
+ if (isAudioAction(action2))
852
975
  return;
853
976
  if (action2 === "vibrate")
854
977
  return;
@@ -881,55 +1004,89 @@ var novely = ({
881
1004
  });
882
1005
  const useStack = createUseStackFunction(renderer);
883
1006
  useStack(MAIN_CONTEXT_KEY).push(initial);
884
- renderer.ui.start();
885
- const match = matchAction(renderer.getContext, {
886
- wait({ ctx }, [time]) {
887
- if (!ctx.meta.restoring)
888
- setTimeout(push, isFunction(time) ? time() : time);
1007
+ const UIInstance = renderer.ui.start();
1008
+ const enmemory = (ctx) => {
1009
+ if (ctx.meta.restoring)
1010
+ return;
1011
+ const stack = useStack(ctx);
1012
+ const current = klona(stack.value);
1013
+ current[2][1] = "auto";
1014
+ stack.push(current);
1015
+ save(true, "auto");
1016
+ };
1017
+ const next = (ctx) => {
1018
+ const stack = useStack(ctx);
1019
+ const path = stack.value[0];
1020
+ const last = path.at(-1);
1021
+ if (last && (isNull(last[0]) || last[0] === "jump") && isNumber(last[1])) {
1022
+ last[1]++;
1023
+ } else {
1024
+ path.push([null, 0]);
1025
+ }
1026
+ };
1027
+ const matchActionInit = {
1028
+ getContext: renderer.getContext,
1029
+ push(ctx) {
1030
+ if (ctx.meta.restoring)
1031
+ return;
1032
+ next(ctx);
1033
+ render(ctx);
889
1034
  },
890
- showBackground({ ctx }, [background]) {
1035
+ forward(ctx) {
1036
+ if (!ctx.meta.preview)
1037
+ enmemory(ctx);
1038
+ matchActionInit.push(ctx);
1039
+ if (!ctx.meta.preview)
1040
+ interactivity(true);
1041
+ }
1042
+ };
1043
+ const match = matchAction(matchActionInit, {
1044
+ wait({ ctx, push }, [time]) {
1045
+ if (ctx.meta.restoring)
1046
+ return;
1047
+ setTimeout(push, isFunction(time) ? time() : time);
1048
+ },
1049
+ showBackground({ ctx, push }, [background]) {
891
1050
  ctx.background(background);
892
- push(ctx);
1051
+ push();
893
1052
  },
894
- playMusic({ ctx }, [source]) {
1053
+ playMusic({ ctx, push }, [source]) {
895
1054
  ctx.audio.music(source, "music", true).play();
896
- push(ctx);
1055
+ push();
897
1056
  },
898
- stopMusic({ ctx }, [source]) {
1057
+ stopMusic({ ctx, push }, [source]) {
899
1058
  ctx.audio.music(source, "music").stop();
900
- push(ctx);
1059
+ push();
901
1060
  },
902
- playSound({ ctx }, [source, loop]) {
1061
+ playSound({ ctx, push }, [source, loop]) {
903
1062
  ctx.audio.music(source, "sound", loop || false).play();
904
- push(ctx);
1063
+ push();
905
1064
  },
906
- stopSound({ ctx }, [source]) {
1065
+ stopSound({ ctx, push }, [source]) {
907
1066
  ctx.audio.music(source, "sound").stop();
908
- push(ctx);
1067
+ push();
909
1068
  },
910
- voice({ ctx }, [source]) {
1069
+ voice({ ctx, push }, [source]) {
911
1070
  ctx.audio.voice(source);
912
- push(ctx);
1071
+ push();
913
1072
  },
914
- stopVoice({ ctx }) {
1073
+ stopVoice({ ctx, push }) {
915
1074
  ctx.audio.voiceStop();
916
- push(ctx);
1075
+ push();
917
1076
  },
918
- showCharacter({ ctx }, [character, emotion, className, style]) {
919
- if (DEV && !characters[character].emotions[emotion]) {
1077
+ showCharacter({ ctx, push }, [character, emotion, className, style]) {
1078
+ if (DEV2 && !characters[character].emotions[emotion]) {
920
1079
  throw new Error(`Attempt to show character "${character}" with unknown emotion "${emotion}"`);
921
1080
  }
922
1081
  const handle = ctx.character(character);
923
1082
  handle.append(className, style, ctx.meta.restoring);
924
1083
  handle.emotion(emotion, true);
925
- push(ctx);
1084
+ push();
926
1085
  },
927
- hideCharacter({ ctx }, [character, className, style, duration]) {
928
- ctx.character(character).remove(className, style, duration, ctx.meta.restoring).then(() => {
929
- push(ctx);
930
- });
1086
+ hideCharacter({ ctx, push }, [character, className, style, duration]) {
1087
+ ctx.character(character).remove(className, style, duration, ctx.meta.restoring).then(push);
931
1088
  },
932
- dialog({ ctx, data: data2 }, [character, content, emotion]) {
1089
+ dialog({ ctx, data: data2, forward }, [character, content, emotion]) {
933
1090
  const name = (() => {
934
1091
  const c = character;
935
1092
  const cs = characters;
@@ -950,13 +1107,13 @@ var novely = ({
950
1107
  unwrap2(name, data2),
951
1108
  character,
952
1109
  emotion,
953
- () => forward(ctx)
1110
+ forward
954
1111
  );
955
1112
  },
956
- function({ ctx }, [fn]) {
1113
+ function({ ctx, push }, [fn]) {
957
1114
  const result = fn(ctx.meta.restoring, ctx.meta.goingBack, ctx.meta.preview);
958
1115
  if (!ctx.meta.restoring) {
959
- result ? result.then(() => push(ctx)) : push(ctx);
1116
+ result ? result.then(push) : push();
960
1117
  }
961
1118
  return result;
962
1119
  },
@@ -967,12 +1124,12 @@ var novely = ({
967
1124
  question = "";
968
1125
  }
969
1126
  const unwrappedChoices = choices.map(([content, action2, visible]) => {
970
- if (DEV && action2.length === 0 && (!visible || visible())) {
1127
+ if (DEV2 && action2.length === 0 && (!visible || visible())) {
971
1128
  console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
972
1129
  }
973
1130
  return [unwrap2(content, data2), action2, visible];
974
1131
  });
975
- if (DEV && unwrappedChoices.length === 0) {
1132
+ if (DEV2 && unwrappedChoices.length === 0) {
976
1133
  throw new Error(`Running choice without variants to choose from, look at how to use Choice action properly [https://novely.pages.dev/guide/actions/choice#usage]`);
977
1134
  }
978
1135
  ctx.choices(unwrap2(question, data2), unwrappedChoices, (selected) => {
@@ -981,7 +1138,7 @@ var novely = ({
981
1138
  }
982
1139
  const stack = useStack(ctx);
983
1140
  const offset = isWithoutQuestion ? 0 : 1;
984
- if (DEV && !unwrappedChoices[selected]) {
1141
+ if (DEV2 && !unwrappedChoices[selected]) {
985
1142
  throw new Error("Choice children is empty, either add content there or make item not selectable");
986
1143
  }
987
1144
  stack.value[0].push(["choice", selected + offset], [null, 0]);
@@ -990,10 +1147,10 @@ var novely = ({
990
1147
  });
991
1148
  },
992
1149
  jump({ ctx, data: data2 }, [scene]) {
993
- if (DEV && !story[scene]) {
1150
+ if (DEV2 && !story[scene]) {
994
1151
  throw new Error(`Attempt to jump to unknown scene "${scene}"`);
995
1152
  }
996
- if (DEV && story[scene].length === 0) {
1153
+ if (DEV2 && story[scene].length === 0) {
997
1154
  throw new Error(`Attempt to jump to empty scene "${scene}"`);
998
1155
  }
999
1156
  const stack = useStack(ctx);
@@ -1006,25 +1163,25 @@ var novely = ({
1006
1163
  data: data2
1007
1164
  });
1008
1165
  },
1009
- clear({ ctx }, [keep, characters2, audio]) {
1166
+ clear({ ctx, push }, [keep, characters2, audio]) {
1010
1167
  ctx.vibrate(0);
1011
1168
  ctx.clear(
1012
1169
  keep || EMPTY_SET,
1013
1170
  characters2 || EMPTY_SET,
1014
1171
  audio || { music: EMPTY_SET, sounds: EMPTY_SET },
1015
- () => push(ctx)
1172
+ push
1016
1173
  );
1017
1174
  },
1018
1175
  condition({ ctx }, [condition, variants]) {
1019
- if (DEV && Object.values(variants).length === 0) {
1176
+ if (DEV2 && Object.values(variants).length === 0) {
1020
1177
  throw new Error(`Attempt to use Condition action with empty variants object`);
1021
1178
  }
1022
1179
  if (!ctx.meta.restoring) {
1023
1180
  const val = String(condition());
1024
- if (DEV && !variants[val]) {
1181
+ if (DEV2 && !variants[val]) {
1025
1182
  throw new Error(`Attempt to go to unknown variant "${val}"`);
1026
1183
  }
1027
- if (DEV && variants[val].length === 0) {
1184
+ if (DEV2 && variants[val].length === 0) {
1028
1185
  throw new Error(`Attempt to go to empty variant "${val}"`);
1029
1186
  }
1030
1187
  const stack = useStack(MAIN_CONTEXT_KEY);
@@ -1041,15 +1198,15 @@ var novely = ({
1041
1198
  interactivity(false);
1042
1199
  times.clear();
1043
1200
  },
1044
- input({ ctx, data: data2 }, [question, onInput, setup]) {
1201
+ input({ ctx, data: data2, forward }, [question, onInput, setup]) {
1045
1202
  ctx.input(
1046
1203
  unwrap2(question, data2),
1047
1204
  onInput,
1048
1205
  setup || noop,
1049
- () => forward(ctx)
1206
+ forward
1050
1207
  );
1051
1208
  },
1052
- custom({ ctx }, [handler]) {
1209
+ custom({ ctx, push }, [handler]) {
1053
1210
  const result = ctx.custom(handler, () => {
1054
1211
  if (ctx.meta.restoring)
1055
1212
  return;
@@ -1057,56 +1214,35 @@ var novely = ({
1057
1214
  enmemory(ctx);
1058
1215
  interactivity(true);
1059
1216
  }
1060
- push(ctx);
1217
+ push();
1061
1218
  });
1062
1219
  return result;
1063
1220
  },
1064
- vibrate({ ctx }, pattern) {
1221
+ vibrate({ ctx, push }, pattern) {
1065
1222
  ctx.vibrate(pattern);
1066
- push(ctx);
1223
+ push();
1067
1224
  },
1068
- next({ ctx }) {
1069
- push(ctx);
1225
+ next({ push }) {
1226
+ push();
1070
1227
  },
1071
- animateCharacter({ ctx, data: data2 }, [character, timeout, ...classes]) {
1072
- if (DEV && classes.length === 0) {
1228
+ animateCharacter({ ctx, push }, [character, timeout, ...classes]) {
1229
+ if (DEV2 && classes.length === 0) {
1073
1230
  throw new Error("Attempt to use AnimateCharacter without classes. Classes should be provided [https://novely.pages.dev/guide/actions/animateCharacter.html]");
1074
1231
  }
1075
- if (DEV && (timeout <= 0 || !Number.isFinite(timeout) || Number.isNaN(timeout))) {
1232
+ if (DEV2 && (timeout <= 0 || !Number.isFinite(timeout) || Number.isNaN(timeout))) {
1076
1233
  throw new Error("Attempt to use AnimateCharacter with unacceptable timeout. It should be finite and greater than zero");
1077
1234
  }
1078
1235
  if (ctx.meta.preview)
1079
1236
  return;
1080
- const handler = ({ get }) => {
1081
- const { clear } = get(false);
1082
- const char = ctx.getCharacter(character);
1083
- if (!char)
1084
- return;
1085
- const target = char.canvas;
1086
- if (!target)
1087
- return;
1088
- const classNames = classes.filter((className) => !target.classList.contains(className));
1089
- target.classList.add(...classNames);
1090
- const timeoutId = setTimeout(() => {
1091
- target.classList.remove(...classNames);
1092
- }, timeout);
1093
- clear(() => {
1094
- target.classList.remove(...classNames);
1095
- clearTimeout(timeoutId);
1096
- });
1097
- };
1098
- handler.key = "@@internal-animate-character";
1099
- match("custom", [handler], {
1100
- ctx,
1101
- data: data2
1102
- });
1237
+ ctx.character(character).animate(timeout, classes);
1238
+ push();
1103
1239
  },
1104
- text({ ctx, data: data2 }, text) {
1240
+ text({ ctx, data: data2, forward }, text) {
1105
1241
  const string = text.map((content) => unwrap2(content, data2)).join(" ");
1106
- if (DEV && string.length === 0) {
1242
+ if (DEV2 && string.length === 0) {
1107
1243
  throw new Error(`Action Text was called with empty string or array`);
1108
1244
  }
1109
- ctx.text(string, () => forward(ctx));
1245
+ ctx.text(string, forward);
1110
1246
  },
1111
1247
  exit({ ctx, data: data2 }) {
1112
1248
  if (ctx.meta.restoring)
@@ -1155,17 +1291,17 @@ var novely = ({
1155
1291
  }
1156
1292
  render(ctx);
1157
1293
  },
1158
- preload({ ctx }, [source]) {
1294
+ preload({ ctx, push }, [source]) {
1159
1295
  if (!ctx.meta.goingBack && !ctx.meta.restoring && !PRELOADED_ASSETS.has(source)) {
1160
1296
  PRELOADED_ASSETS.add(renderer.misc.preloadImage(source));
1161
1297
  }
1162
- push(ctx);
1298
+ push();
1163
1299
  },
1164
1300
  block({ ctx }, [scene]) {
1165
- if (DEV && !story[scene]) {
1301
+ if (DEV2 && !story[scene]) {
1166
1302
  throw new Error(`Attempt to call Block action with unknown scene "${scene}"`);
1167
1303
  }
1168
- if (DEV && story[scene].length === 0) {
1304
+ if (DEV2 && story[scene].length === 0) {
1169
1305
  throw new Error(`Attempt to call Block action with empty scene "${scene}"`);
1170
1306
  }
1171
1307
  if (!ctx.meta.restoring) {
@@ -1175,25 +1311,6 @@ var novely = ({
1175
1311
  }
1176
1312
  }
1177
1313
  });
1178
- const enmemory = (ctx) => {
1179
- if (ctx.meta.restoring)
1180
- return;
1181
- const stack = useStack(ctx);
1182
- const current = klona(stack.value);
1183
- current[2][1] = "auto";
1184
- stack.push(current);
1185
- save(true, "auto");
1186
- };
1187
- const next = (ctx) => {
1188
- const stack = useStack(ctx);
1189
- const path = stack.value[0];
1190
- const last = path.at(-1);
1191
- if (last && (isNull(last[0]) || last[0] === "jump") && isNumber(last[1])) {
1192
- last[1]++;
1193
- } else {
1194
- path.push([null, 0]);
1195
- }
1196
- };
1197
1314
  const render = (ctx) => {
1198
1315
  const stack = useStack(ctx);
1199
1316
  const referred = refer(stack.value[0]);
@@ -1210,17 +1327,6 @@ var novely = ({
1210
1327
  });
1211
1328
  }
1212
1329
  };
1213
- const push = (ctx) => {
1214
- if (!ctx.meta.restoring)
1215
- next(ctx), render(ctx);
1216
- };
1217
- const forward = (ctx) => {
1218
- if (!ctx.meta.preview)
1219
- enmemory(ctx);
1220
- push(ctx);
1221
- if (!ctx.meta.preview)
1222
- interactivity(true);
1223
- };
1224
1330
  const interactivity = (value = false) => {
1225
1331
  interacted = value ? interacted + 1 : 0;
1226
1332
  };
@@ -1270,6 +1376,16 @@ var novely = ({
1270
1376
  */
1271
1377
  unwrap(content) {
1272
1378
  return unwrap2(content, $.get().data);
1379
+ },
1380
+ /**
1381
+ * Cancel data loading, hide UI, ignore page change events
1382
+ * Data updates still will work in case Novely already was loaded
1383
+ */
1384
+ destroy() {
1385
+ dataLoaded.cancel();
1386
+ UIInstance.unmount();
1387
+ removeEventListener("visibilitychange", onVisibilityChange);
1388
+ removeEventListener("beforeunload", throttledEmergencyOnStorageDataChange);
1273
1389
  }
1274
1390
  };
1275
1391
  };