@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.d.ts CHANGED
@@ -214,6 +214,7 @@ interface CharacterHandle {
214
214
  emotion: (emotion: string, render: boolean) => void;
215
215
  append: (className?: string, style?: string, restoring?: boolean) => void;
216
216
  remove: (className?: string, style?: string, duration?: number, restoring?: boolean) => Promise<void>;
217
+ animate: (timeout: number, classes: string[]) => void;
217
218
  emotions: Record<string, HTMLImageElement[]>;
218
219
  }
219
220
  type AudioHandle = {
@@ -223,23 +224,23 @@ type AudioHandle = {
223
224
  };
224
225
  type Renderer = {
225
226
  misc: {
226
- /**
227
- * Function to preload images async and await for all images to load or fail
228
- * @param images Set of images to load
229
- */
230
- preloadImagesBlocking: (images: Set<string>) => Promise<PromiseSettledResult<unknown>[]>;
231
227
  /**
232
228
  * Function to preload image sync
233
229
  * @param image Image URL
234
230
  * @returns Image URL
235
231
  */
236
232
  preloadImage: <T extends string>(image: T) => T;
233
+ /**
234
+ * Function to preload image async
235
+ * @param image Image URL
236
+ * @returns Promise
237
+ */
238
+ preloadImageBlocking: (image: string) => Promise<void>;
237
239
  /**
238
240
  * Function to preload audio
239
- * @param type kind of audio
240
241
  * @param source <url> pointing to the audio
241
242
  */
242
- preloadAudioBlocking: (type: 'music', source: string) => Promise<void>;
243
+ preloadAudioBlocking: (source: string) => Promise<void>;
243
244
  };
244
245
  ui: {
245
246
  /**
@@ -308,7 +309,6 @@ type Renderer = {
308
309
  };
309
310
  store: unknown;
310
311
  setStore: unknown;
311
- getCharacter: (character: string) => CharacterHandle | undefined;
312
312
  };
313
313
  removeContext: (context: string) => void;
314
314
  };
@@ -418,6 +418,11 @@ interface NovelyInit<Languages extends string, Characters extends Record<string,
418
418
  * @default 799
419
419
  */
420
420
  throttleTimeout?: number;
421
+ /**
422
+ * Limits how many assets can be downloaded parallelly
423
+ * @default 15
424
+ */
425
+ parallelAssetsDownloadLimit?: number;
421
426
  /**
422
427
  * Custom language detector
423
428
  * @param languages Supported languages aka `languages: []` in the config
@@ -445,8 +450,12 @@ interface NovelyInit<Languages extends string, Characters extends Record<string,
445
450
  * @default "lazy"
446
451
  */
447
452
  preloadAssets?: 'lazy' | 'blocking';
453
+ /**
454
+ * Fetching function
455
+ */
456
+ fetch?: typeof fetch;
448
457
  }
449
- declare const novely: <Languages extends string, Characters extends Record<string, Character<Languages>>, StateScheme extends State, DataScheme extends Data>({ characters, storage, storageDelay, renderer: createRenderer, initialScreen, translation, languages, state: defaultState, data: defaultData, autosaves, migrations, throttleTimeout, getLanguage, overrideLanguage, askBeforeExit, preloadAssets, }: NovelyInit<Languages, Characters, StateScheme, DataScheme>) => {
458
+ declare const novely: <Languages extends string, Characters extends Record<string, Character<Languages>>, StateScheme extends State, DataScheme extends Data>({ characters, storage, storageDelay, renderer: createRenderer, initialScreen, translation, languages, state: defaultState, data: defaultData, autosaves, migrations, throttleTimeout, getLanguage, overrideLanguage, askBeforeExit, preloadAssets, parallelAssetsDownloadLimit, fetch: request }: NovelyInit<Languages, Characters, StateScheme, DataScheme>) => {
450
459
  /**
451
460
  * Function to set game script
452
461
  */
@@ -472,7 +481,12 @@ declare const novely: <Languages extends string, Characters extends Record<strin
472
481
  /**
473
482
  * Unwraps translatable content to a string value
474
483
  */
475
- unwrap(content: string | (() => string) | Record<Languages, string> | Exclude<Record<Languages, string | (() => string)>, Record<string, string>>): string;
484
+ unwrap(content: string | (() => string) | Exclude<Record<Languages, string | (() => string)>, Record<string, string>> | Record<Languages, string>): string;
485
+ /**
486
+ * Cancel data loading, hide UI, ignore page change events
487
+ * Data updates still will work in case Novely already was loaded
488
+ */
489
+ destroy(): void;
476
490
  };
477
491
 
478
492
  export { type ActionProxyProvider, type AllowedContent, type AudioHandle, type BaseTranslationStrings, type Character, type CharacterHandle, type Context, type CoreData, type CustomHandler, type CustomHandlerFunctionGetFn, type CustomHandlerFunctionParameters, type CustomHandlerGetResult, type CustomHandlerGetResultDataFunction, type DefaultActionProxyProvider, EN, type Emotions, type FunctionableValue, type GetActionParameters, JP, KK, type Lang, type NovelyScreen, type Path, type PluralType, type Pluralization, RU, type Renderer, type RendererInit, type Save, type Storage, type StorageData, type StorageMeta, type Stored, type Story, type Thenable, type TranslationActions, type TypewriterSpeed, type Unwrappable, type ValidAction, localStorageStorage, novely };
@@ -174,18 +174,62 @@ var Novely = (() => {
174
174
  ]);
175
175
  var EMPTY_SET = /* @__PURE__ */ new Set();
176
176
  var DEFAULT_TYPEWRITER_SPEED = "Medium";
177
+ var HOWLER_SUPPORTED_FILE_FORMATS = /* @__PURE__ */ new Set([
178
+ "mp3",
179
+ "mpeg",
180
+ "opus",
181
+ "ogg",
182
+ "oga",
183
+ "wav",
184
+ "aac",
185
+ "caf",
186
+ "m4a",
187
+ "m4b",
188
+ "mp4",
189
+ "weba",
190
+ "webm",
191
+ "dolby",
192
+ "flac"
193
+ ]);
194
+ var SUPPORTED_IMAGE_FILE_FORMATS = /* @__PURE__ */ new Set([
195
+ "apng",
196
+ "avif",
197
+ "gif",
198
+ "jpg",
199
+ "jpeg",
200
+ "jfif",
201
+ "pjpeg",
202
+ "pjp",
203
+ "png",
204
+ "svg",
205
+ "webp",
206
+ "bmp"
207
+ ]);
177
208
  var MAIN_CONTEXT_KEY = "$MAIN";
178
209
 
179
210
  // src/shared.ts
180
211
  var STACK_MAP = /* @__PURE__ */ new Map();
181
212
 
213
+ // ../../node_modules/.pnpm/esm-env@1.0.0/node_modules/esm-env/prod-ssr.js
214
+ var DEV = false;
215
+
182
216
  // src/utils.ts
183
- var matchAction = (getContext, values) => {
217
+ var matchAction = ({ getContext, push, forward }, values) => {
184
218
  return (action, props, { ctx, data }) => {
185
219
  const context = typeof ctx === "string" ? getContext(ctx) : ctx;
186
220
  return values[action]({
187
221
  ctx: context,
188
- data
222
+ data,
223
+ push() {
224
+ if (context.meta.preview)
225
+ return;
226
+ push(context);
227
+ },
228
+ forward() {
229
+ if (context.meta.preview)
230
+ return;
231
+ forward(context);
232
+ }
189
233
  }, props);
190
234
  };
191
235
  };
@@ -292,6 +336,9 @@ var Novely = (() => {
292
336
  var isSkippedDuringRestore = (item) => {
293
337
  return SKIPPED_DURING_RESTORE.has(item);
294
338
  };
339
+ var isAudioAction = (action) => {
340
+ return AUDIO_ACTIONS.has(action);
341
+ };
295
342
  var noop = () => {
296
343
  };
297
344
  var isAction = (element) => {
@@ -514,6 +561,53 @@ var Novely = (() => {
514
561
  };
515
562
  return useStack;
516
563
  };
564
+ var mapSet = (set, fn) => {
565
+ return [...set].map(fn);
566
+ };
567
+ var isImageAsset = (asset) => {
568
+ return isString(asset) && isCSSImage(asset);
569
+ };
570
+ var getUrlFileExtension = (address) => {
571
+ try {
572
+ const { pathname } = new URL(address, location.href);
573
+ return pathname.split(".").at(-1).split("!")[0].split(":")[0];
574
+ } catch (error) {
575
+ if (DEV) {
576
+ console.error(new Error(`Could not construct URL "${address}".`, { cause: error }));
577
+ }
578
+ return "";
579
+ }
580
+ };
581
+ var fetchContentType = async (request, url) => {
582
+ try {
583
+ const response = await request(url, {
584
+ method: "HEAD"
585
+ });
586
+ return response.headers.get("Content-Type") || "";
587
+ } catch (error) {
588
+ if (DEV) {
589
+ console.error(new Error(`Failed to fetch file at "${url}"`, { cause: error }));
590
+ }
591
+ return "";
592
+ }
593
+ };
594
+ var getResourseType = async (request, url) => {
595
+ const extension = getUrlFileExtension(url);
596
+ if (HOWLER_SUPPORTED_FILE_FORMATS.has(extension)) {
597
+ return "audio";
598
+ }
599
+ if (SUPPORTED_IMAGE_FILE_FORMATS.has(extension)) {
600
+ return "image";
601
+ }
602
+ const contentType = await fetchContentType(request, url);
603
+ if (contentType.includes("audio")) {
604
+ return "audio";
605
+ }
606
+ if (contentType.includes("image")) {
607
+ return "image";
608
+ }
609
+ return "other";
610
+ };
517
611
 
518
612
  // src/global.ts
519
613
  var PRELOADED_ASSETS = /* @__PURE__ */ new Set();
@@ -709,11 +803,6 @@ var Novely = (() => {
709
803
 
710
804
  // src/novely.ts
711
805
  var import_p_limit = __toESM(require_p_limit(), 1);
712
-
713
- // ../../node_modules/.pnpm/esm-env@1.0.0/node_modules/esm-env/prod-ssr.js
714
- var DEV = false;
715
-
716
- // src/novely.ts
717
806
  var novely = ({
718
807
  characters,
719
808
  storage = localStorageStorage({ key: "novely-game-storage" }),
@@ -730,9 +819,12 @@ var Novely = (() => {
730
819
  getLanguage: getLanguage2 = getLanguage,
731
820
  overrideLanguage = false,
732
821
  askBeforeExit = true,
733
- preloadAssets = "lazy"
822
+ preloadAssets = "lazy",
823
+ parallelAssetsDownloadLimit = 15,
824
+ fetch: request = fetch
734
825
  }) => {
735
826
  const limitScript = (0, import_p_limit.default)(1);
827
+ const limitAssetsDownload = (0, import_p_limit.default)(parallelAssetsDownloadLimit);
736
828
  const story = {};
737
829
  const times = /* @__PURE__ */ new Set();
738
830
  const ASSETS_TO_PRELOAD = /* @__PURE__ */ new Set();
@@ -748,7 +840,23 @@ var Novely = (() => {
748
840
  Object.assign(story, flattenStory(part));
749
841
  if (preloadAssets === "blocking" && ASSETS_TO_PRELOAD.size > 0) {
750
842
  renderer.ui.showScreen("loading");
751
- await renderer.misc.preloadImagesBlocking(ASSETS_TO_PRELOAD);
843
+ const { preloadAudioBlocking, preloadImageBlocking } = renderer.misc;
844
+ const list = mapSet(ASSETS_TO_PRELOAD, (asset) => {
845
+ return limitAssetsDownload(async () => {
846
+ const type = await getResourseType(request, asset);
847
+ switch (type) {
848
+ case "audio": {
849
+ await preloadAudioBlocking(asset);
850
+ break;
851
+ }
852
+ case "image": {
853
+ await preloadImageBlocking(asset);
854
+ break;
855
+ }
856
+ }
857
+ });
858
+ });
859
+ await Promise.allSettled(list);
752
860
  }
753
861
  const screen = renderer.ui.getScreen();
754
862
  const nextScreen = scriptCalled ? screen : initialScreen;
@@ -769,13 +877,25 @@ var Novely = (() => {
769
877
  return limitScript(() => scriptBase(part));
770
878
  };
771
879
  const action = new Proxy({}, {
772
- get(_, prop) {
880
+ get(_, action2) {
773
881
  return (...props) => {
774
882
  if (preloadAssets === "blocking") {
775
- if (prop === "showBackground" && typeof props[0] === "string" && isCSSImage(props[0])) {
883
+ if (action2 === "showBackground") {
884
+ if (isImageAsset(props[0])) {
885
+ ASSETS_TO_PRELOAD.add(props[0]);
886
+ }
887
+ if (props[0] && typeof props[0] === "object") {
888
+ for (const value of Object.values(props[0])) {
889
+ if (!isImageAsset(value))
890
+ continue;
891
+ ASSETS_TO_PRELOAD.add(value);
892
+ }
893
+ }
894
+ }
895
+ if (isAudioAction(action2) && isString(props[0])) {
776
896
  ASSETS_TO_PRELOAD.add(props[0]);
777
897
  }
778
- if (prop === "showCharacter" && typeof props[0] === "string" && typeof props[1] === "string") {
898
+ if (action2 === "showCharacter" && isString(props[0]) && isString(props[1])) {
779
899
  const images = characters[props[0]].emotions[props[1]];
780
900
  if (Array.isArray(images)) {
781
901
  for (const asset of images) {
@@ -786,7 +906,7 @@ var Novely = (() => {
786
906
  }
787
907
  }
788
908
  }
789
- return [prop, ...props];
909
+ return [action2, ...props];
790
910
  };
791
911
  }
792
912
  });
@@ -853,11 +973,12 @@ var Novely = (() => {
853
973
  };
854
974
  storageDelay.then(getStoredData);
855
975
  const initial = getDefaultSave(klona(defaultState));
856
- addEventListener("visibilitychange", () => {
976
+ const onVisibilityChange = () => {
857
977
  if (document.visibilityState === "hidden") {
858
978
  throttledEmergencyOnStorageDataChange();
859
979
  }
860
- });
980
+ };
981
+ addEventListener("visibilitychange", onVisibilityChange);
861
982
  addEventListener("beforeunload", throttledEmergencyOnStorageDataChange);
862
983
  const save = (override = false, type = override ? "auto" : "manual") => {
863
984
  if (!$$.get().dataLoaded)
@@ -905,12 +1026,11 @@ var Novely = (() => {
905
1026
  return;
906
1027
  let latest = save2 || $.get().saves.at(-1);
907
1028
  if (!latest) {
908
- $.set({
909
- saves: [initial],
910
- data: klona(defaultData),
911
- meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED, 1, 1, 1]
912
- });
913
1029
  latest = klona(initial);
1030
+ $.update((prev) => {
1031
+ prev.saves.push(latest);
1032
+ return prev;
1033
+ });
914
1034
  }
915
1035
  const context = renderer.getContext(MAIN_CONTEXT_KEY);
916
1036
  const stack = useStack(context);
@@ -946,7 +1066,7 @@ var Novely = (() => {
946
1066
  data: latest[1]
947
1067
  });
948
1068
  });
949
- context.meta.restoring = context.meta.restoring = false;
1069
+ context.meta.restoring = context.meta.goingBack = false;
950
1070
  render(context);
951
1071
  };
952
1072
  const refer = (path) => {
@@ -1014,7 +1134,7 @@ var Novely = (() => {
1014
1134
  ctx.meta.preview = true;
1015
1135
  const processor = createQueueProcessor(queue);
1016
1136
  await processor.run((action2, props) => {
1017
- if (AUDIO_ACTIONS.has(action2))
1137
+ if (isAudioAction(action2))
1018
1138
  return;
1019
1139
  if (action2 === "vibrate")
1020
1140
  return;
@@ -1047,55 +1167,89 @@ var Novely = (() => {
1047
1167
  });
1048
1168
  const useStack = createUseStackFunction(renderer);
1049
1169
  useStack(MAIN_CONTEXT_KEY).push(initial);
1050
- renderer.ui.start();
1051
- const match = matchAction(renderer.getContext, {
1052
- wait({ ctx }, [time]) {
1053
- if (!ctx.meta.restoring)
1054
- setTimeout(push, isFunction(time) ? time() : time);
1170
+ const UIInstance = renderer.ui.start();
1171
+ const enmemory = (ctx) => {
1172
+ if (ctx.meta.restoring)
1173
+ return;
1174
+ const stack = useStack(ctx);
1175
+ const current = klona(stack.value);
1176
+ current[2][1] = "auto";
1177
+ stack.push(current);
1178
+ save(true, "auto");
1179
+ };
1180
+ const next = (ctx) => {
1181
+ const stack = useStack(ctx);
1182
+ const path = stack.value[0];
1183
+ const last = path.at(-1);
1184
+ if (last && (isNull(last[0]) || last[0] === "jump") && isNumber(last[1])) {
1185
+ last[1]++;
1186
+ } else {
1187
+ path.push([null, 0]);
1188
+ }
1189
+ };
1190
+ const matchActionInit = {
1191
+ getContext: renderer.getContext,
1192
+ push(ctx) {
1193
+ if (ctx.meta.restoring)
1194
+ return;
1195
+ next(ctx);
1196
+ render(ctx);
1197
+ },
1198
+ forward(ctx) {
1199
+ if (!ctx.meta.preview)
1200
+ enmemory(ctx);
1201
+ matchActionInit.push(ctx);
1202
+ if (!ctx.meta.preview)
1203
+ interactivity(true);
1204
+ }
1205
+ };
1206
+ const match = matchAction(matchActionInit, {
1207
+ wait({ ctx, push }, [time]) {
1208
+ if (ctx.meta.restoring)
1209
+ return;
1210
+ setTimeout(push, isFunction(time) ? time() : time);
1055
1211
  },
1056
- showBackground({ ctx }, [background]) {
1212
+ showBackground({ ctx, push }, [background]) {
1057
1213
  ctx.background(background);
1058
- push(ctx);
1214
+ push();
1059
1215
  },
1060
- playMusic({ ctx }, [source]) {
1216
+ playMusic({ ctx, push }, [source]) {
1061
1217
  ctx.audio.music(source, "music", true).play();
1062
- push(ctx);
1218
+ push();
1063
1219
  },
1064
- stopMusic({ ctx }, [source]) {
1220
+ stopMusic({ ctx, push }, [source]) {
1065
1221
  ctx.audio.music(source, "music").stop();
1066
- push(ctx);
1222
+ push();
1067
1223
  },
1068
- playSound({ ctx }, [source, loop]) {
1224
+ playSound({ ctx, push }, [source, loop]) {
1069
1225
  ctx.audio.music(source, "sound", loop || false).play();
1070
- push(ctx);
1226
+ push();
1071
1227
  },
1072
- stopSound({ ctx }, [source]) {
1228
+ stopSound({ ctx, push }, [source]) {
1073
1229
  ctx.audio.music(source, "sound").stop();
1074
- push(ctx);
1230
+ push();
1075
1231
  },
1076
- voice({ ctx }, [source]) {
1232
+ voice({ ctx, push }, [source]) {
1077
1233
  ctx.audio.voice(source);
1078
- push(ctx);
1234
+ push();
1079
1235
  },
1080
- stopVoice({ ctx }) {
1236
+ stopVoice({ ctx, push }) {
1081
1237
  ctx.audio.voiceStop();
1082
- push(ctx);
1238
+ push();
1083
1239
  },
1084
- showCharacter({ ctx }, [character, emotion, className, style]) {
1240
+ showCharacter({ ctx, push }, [character, emotion, className, style]) {
1085
1241
  if (DEV && !characters[character].emotions[emotion]) {
1086
1242
  throw new Error(`Attempt to show character "${character}" with unknown emotion "${emotion}"`);
1087
1243
  }
1088
1244
  const handle = ctx.character(character);
1089
1245
  handle.append(className, style, ctx.meta.restoring);
1090
1246
  handle.emotion(emotion, true);
1091
- push(ctx);
1247
+ push();
1092
1248
  },
1093
- hideCharacter({ ctx }, [character, className, style, duration]) {
1094
- ctx.character(character).remove(className, style, duration, ctx.meta.restoring).then(() => {
1095
- push(ctx);
1096
- });
1249
+ hideCharacter({ ctx, push }, [character, className, style, duration]) {
1250
+ ctx.character(character).remove(className, style, duration, ctx.meta.restoring).then(push);
1097
1251
  },
1098
- dialog({ ctx, data: data2 }, [character, content, emotion]) {
1252
+ dialog({ ctx, data: data2, forward }, [character, content, emotion]) {
1099
1253
  const name = (() => {
1100
1254
  const c = character;
1101
1255
  const cs = characters;
@@ -1116,13 +1270,13 @@ var Novely = (() => {
1116
1270
  unwrap2(name, data2),
1117
1271
  character,
1118
1272
  emotion,
1119
- () => forward(ctx)
1273
+ forward
1120
1274
  );
1121
1275
  },
1122
- function({ ctx }, [fn]) {
1276
+ function({ ctx, push }, [fn]) {
1123
1277
  const result = fn(ctx.meta.restoring, ctx.meta.goingBack, ctx.meta.preview);
1124
1278
  if (!ctx.meta.restoring) {
1125
- result ? result.then(() => push(ctx)) : push(ctx);
1279
+ result ? result.then(push) : push();
1126
1280
  }
1127
1281
  return result;
1128
1282
  },
@@ -1172,13 +1326,13 @@ var Novely = (() => {
1172
1326
  data: data2
1173
1327
  });
1174
1328
  },
1175
- clear({ ctx }, [keep, characters2, audio]) {
1329
+ clear({ ctx, push }, [keep, characters2, audio]) {
1176
1330
  ctx.vibrate(0);
1177
1331
  ctx.clear(
1178
1332
  keep || EMPTY_SET,
1179
1333
  characters2 || EMPTY_SET,
1180
1334
  audio || { music: EMPTY_SET, sounds: EMPTY_SET },
1181
- () => push(ctx)
1335
+ push
1182
1336
  );
1183
1337
  },
1184
1338
  condition({ ctx }, [condition, variants]) {
@@ -1207,15 +1361,15 @@ var Novely = (() => {
1207
1361
  interactivity(false);
1208
1362
  times.clear();
1209
1363
  },
1210
- input({ ctx, data: data2 }, [question, onInput, setup]) {
1364
+ input({ ctx, data: data2, forward }, [question, onInput, setup]) {
1211
1365
  ctx.input(
1212
1366
  unwrap2(question, data2),
1213
1367
  onInput,
1214
1368
  setup || noop,
1215
- () => forward(ctx)
1369
+ forward
1216
1370
  );
1217
1371
  },
1218
- custom({ ctx }, [handler]) {
1372
+ custom({ ctx, push }, [handler]) {
1219
1373
  const result = ctx.custom(handler, () => {
1220
1374
  if (ctx.meta.restoring)
1221
1375
  return;
@@ -1223,18 +1377,18 @@ var Novely = (() => {
1223
1377
  enmemory(ctx);
1224
1378
  interactivity(true);
1225
1379
  }
1226
- push(ctx);
1380
+ push();
1227
1381
  });
1228
1382
  return result;
1229
1383
  },
1230
- vibrate({ ctx }, pattern) {
1384
+ vibrate({ ctx, push }, pattern) {
1231
1385
  ctx.vibrate(pattern);
1232
- push(ctx);
1386
+ push();
1233
1387
  },
1234
- next({ ctx }) {
1235
- push(ctx);
1388
+ next({ push }) {
1389
+ push();
1236
1390
  },
1237
- animateCharacter({ ctx, data: data2 }, [character, timeout, ...classes]) {
1391
+ animateCharacter({ ctx, push }, [character, timeout, ...classes]) {
1238
1392
  if (DEV && classes.length === 0) {
1239
1393
  throw new Error("Attempt to use AnimateCharacter without classes. Classes should be provided [https://novely.pages.dev/guide/actions/animateCharacter.html]");
1240
1394
  }
@@ -1243,36 +1397,15 @@ var Novely = (() => {
1243
1397
  }
1244
1398
  if (ctx.meta.preview)
1245
1399
  return;
1246
- const handler = ({ get }) => {
1247
- const { clear } = get(false);
1248
- const char = ctx.getCharacter(character);
1249
- if (!char)
1250
- return;
1251
- const target = char.canvas;
1252
- if (!target)
1253
- return;
1254
- const classNames = classes.filter((className) => !target.classList.contains(className));
1255
- target.classList.add(...classNames);
1256
- const timeoutId = setTimeout(() => {
1257
- target.classList.remove(...classNames);
1258
- }, timeout);
1259
- clear(() => {
1260
- target.classList.remove(...classNames);
1261
- clearTimeout(timeoutId);
1262
- });
1263
- };
1264
- handler.key = "@@internal-animate-character";
1265
- match("custom", [handler], {
1266
- ctx,
1267
- data: data2
1268
- });
1400
+ ctx.character(character).animate(timeout, classes);
1401
+ push();
1269
1402
  },
1270
- text({ ctx, data: data2 }, text) {
1403
+ text({ ctx, data: data2, forward }, text) {
1271
1404
  const string = text.map((content) => unwrap2(content, data2)).join(" ");
1272
1405
  if (DEV && string.length === 0) {
1273
1406
  throw new Error(`Action Text was called with empty string or array`);
1274
1407
  }
1275
- ctx.text(string, () => forward(ctx));
1408
+ ctx.text(string, forward);
1276
1409
  },
1277
1410
  exit({ ctx, data: data2 }) {
1278
1411
  if (ctx.meta.restoring)
@@ -1321,11 +1454,11 @@ var Novely = (() => {
1321
1454
  }
1322
1455
  render(ctx);
1323
1456
  },
1324
- preload({ ctx }, [source]) {
1457
+ preload({ ctx, push }, [source]) {
1325
1458
  if (!ctx.meta.goingBack && !ctx.meta.restoring && !PRELOADED_ASSETS.has(source)) {
1326
1459
  PRELOADED_ASSETS.add(renderer.misc.preloadImage(source));
1327
1460
  }
1328
- push(ctx);
1461
+ push();
1329
1462
  },
1330
1463
  block({ ctx }, [scene]) {
1331
1464
  if (DEV && !story[scene]) {
@@ -1341,25 +1474,6 @@ var Novely = (() => {
1341
1474
  }
1342
1475
  }
1343
1476
  });
1344
- const enmemory = (ctx) => {
1345
- if (ctx.meta.restoring)
1346
- return;
1347
- const stack = useStack(ctx);
1348
- const current = klona(stack.value);
1349
- current[2][1] = "auto";
1350
- stack.push(current);
1351
- save(true, "auto");
1352
- };
1353
- const next = (ctx) => {
1354
- const stack = useStack(ctx);
1355
- const path = stack.value[0];
1356
- const last = path.at(-1);
1357
- if (last && (isNull(last[0]) || last[0] === "jump") && isNumber(last[1])) {
1358
- last[1]++;
1359
- } else {
1360
- path.push([null, 0]);
1361
- }
1362
- };
1363
1477
  const render = (ctx) => {
1364
1478
  const stack = useStack(ctx);
1365
1479
  const referred = refer(stack.value[0]);
@@ -1376,17 +1490,6 @@ var Novely = (() => {
1376
1490
  });
1377
1491
  }
1378
1492
  };
1379
- const push = (ctx) => {
1380
- if (!ctx.meta.restoring)
1381
- next(ctx), render(ctx);
1382
- };
1383
- const forward = (ctx) => {
1384
- if (!ctx.meta.preview)
1385
- enmemory(ctx);
1386
- push(ctx);
1387
- if (!ctx.meta.preview)
1388
- interactivity(true);
1389
- };
1390
1493
  const interactivity = (value = false) => {
1391
1494
  interacted = value ? interacted + 1 : 0;
1392
1495
  };
@@ -1436,6 +1539,16 @@ var Novely = (() => {
1436
1539
  */
1437
1540
  unwrap(content) {
1438
1541
  return unwrap2(content, $.get().data);
1542
+ },
1543
+ /**
1544
+ * Cancel data loading, hide UI, ignore page change events
1545
+ * Data updates still will work in case Novely already was loaded
1546
+ */
1547
+ destroy() {
1548
+ dataLoaded.cancel();
1549
+ UIInstance.unmount();
1550
+ removeEventListener("visibilitychange", onVisibilityChange);
1551
+ removeEventListener("beforeunload", throttledEmergencyOnStorageDataChange);
1439
1552
  }
1440
1553
  };
1441
1554
  };