@novely/core 0.22.2 → 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,12 +12,44 @@ 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
52
+ import { DEV } from "esm-env";
21
53
  var matchAction = ({ getContext, push, forward }, values) => {
22
54
  return (action, props, { ctx, data }) => {
23
55
  const context = typeof ctx === "string" ? getContext(ctx) : ctx;
@@ -25,9 +57,13 @@ var matchAction = ({ getContext, push, forward }, values) => {
25
57
  ctx: context,
26
58
  data,
27
59
  push() {
60
+ if (context.meta.preview)
61
+ return;
28
62
  push(context);
29
63
  },
30
64
  forward() {
65
+ if (context.meta.preview)
66
+ return;
31
67
  forward(context);
32
68
  }
33
69
  }, props);
@@ -136,6 +172,9 @@ var isBlockExitStatement = (statement) => {
136
172
  var isSkippedDuringRestore = (item) => {
137
173
  return SKIPPED_DURING_RESTORE.has(item);
138
174
  };
175
+ var isAudioAction = (action) => {
176
+ return AUDIO_ACTIONS.has(action);
177
+ };
139
178
  var noop = () => {
140
179
  };
141
180
  var isAction = (element) => {
@@ -358,6 +397,53 @@ var createUseStackFunction = (renderer) => {
358
397
  };
359
398
  return useStack;
360
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
+ };
361
447
 
362
448
  // src/global.ts
363
449
  var PRELOADED_ASSETS = /* @__PURE__ */ new Set();
@@ -553,7 +639,7 @@ var localStorageStorage = (options) => {
553
639
 
554
640
  // src/novely.ts
555
641
  import pLimit from "p-limit";
556
- import { DEV } from "esm-env";
642
+ import { DEV as DEV2 } from "esm-env";
557
643
  var novely = ({
558
644
  characters,
559
645
  storage = localStorageStorage({ key: "novely-game-storage" }),
@@ -570,9 +656,12 @@ var novely = ({
570
656
  getLanguage: getLanguage2 = getLanguage,
571
657
  overrideLanguage = false,
572
658
  askBeforeExit = true,
573
- preloadAssets = "lazy"
659
+ preloadAssets = "lazy",
660
+ parallelAssetsDownloadLimit = 15,
661
+ fetch: request = fetch
574
662
  }) => {
575
663
  const limitScript = pLimit(1);
664
+ const limitAssetsDownload = pLimit(parallelAssetsDownloadLimit);
576
665
  const story = {};
577
666
  const times = /* @__PURE__ */ new Set();
578
667
  const ASSETS_TO_PRELOAD = /* @__PURE__ */ new Set();
@@ -588,7 +677,23 @@ var novely = ({
588
677
  Object.assign(story, flattenStory(part));
589
678
  if (preloadAssets === "blocking" && ASSETS_TO_PRELOAD.size > 0) {
590
679
  renderer.ui.showScreen("loading");
591
- 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);
592
697
  }
593
698
  const screen = renderer.ui.getScreen();
594
699
  const nextScreen = scriptCalled ? screen : initialScreen;
@@ -609,13 +714,25 @@ var novely = ({
609
714
  return limitScript(() => scriptBase(part));
610
715
  };
611
716
  const action = new Proxy({}, {
612
- get(_, prop) {
717
+ get(_, action2) {
613
718
  return (...props) => {
614
719
  if (preloadAssets === "blocking") {
615
- 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])) {
616
733
  ASSETS_TO_PRELOAD.add(props[0]);
617
734
  }
618
- if (prop === "showCharacter" && typeof props[0] === "string" && typeof props[1] === "string") {
735
+ if (action2 === "showCharacter" && isString(props[0]) && isString(props[1])) {
619
736
  const images = characters[props[0]].emotions[props[1]];
620
737
  if (Array.isArray(images)) {
621
738
  for (const asset of images) {
@@ -626,7 +743,7 @@ var novely = ({
626
743
  }
627
744
  }
628
745
  }
629
- return [prop, ...props];
746
+ return [action2, ...props];
630
747
  };
631
748
  }
632
749
  });
@@ -746,12 +863,11 @@ var novely = ({
746
863
  return;
747
864
  let latest = save2 || $.get().saves.at(-1);
748
865
  if (!latest) {
749
- $.set({
750
- saves: [initial],
751
- data: klona(defaultData),
752
- meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED, 1, 1, 1]
753
- });
754
866
  latest = klona(initial);
867
+ $.update((prev) => {
868
+ prev.saves.push(latest);
869
+ return prev;
870
+ });
755
871
  }
756
872
  const context = renderer.getContext(MAIN_CONTEXT_KEY);
757
873
  const stack = useStack(context);
@@ -855,7 +971,7 @@ var novely = ({
855
971
  ctx.meta.preview = true;
856
972
  const processor = createQueueProcessor(queue);
857
973
  await processor.run((action2, props) => {
858
- if (AUDIO_ACTIONS.has(action2))
974
+ if (isAudioAction(action2))
859
975
  return;
860
976
  if (action2 === "vibrate")
861
977
  return;
@@ -959,7 +1075,7 @@ var novely = ({
959
1075
  push();
960
1076
  },
961
1077
  showCharacter({ ctx, push }, [character, emotion, className, style]) {
962
- if (DEV && !characters[character].emotions[emotion]) {
1078
+ if (DEV2 && !characters[character].emotions[emotion]) {
963
1079
  throw new Error(`Attempt to show character "${character}" with unknown emotion "${emotion}"`);
964
1080
  }
965
1081
  const handle = ctx.character(character);
@@ -1008,12 +1124,12 @@ var novely = ({
1008
1124
  question = "";
1009
1125
  }
1010
1126
  const unwrappedChoices = choices.map(([content, action2, visible]) => {
1011
- if (DEV && action2.length === 0 && (!visible || visible())) {
1127
+ if (DEV2 && action2.length === 0 && (!visible || visible())) {
1012
1128
  console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
1013
1129
  }
1014
1130
  return [unwrap2(content, data2), action2, visible];
1015
1131
  });
1016
- if (DEV && unwrappedChoices.length === 0) {
1132
+ if (DEV2 && unwrappedChoices.length === 0) {
1017
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]`);
1018
1134
  }
1019
1135
  ctx.choices(unwrap2(question, data2), unwrappedChoices, (selected) => {
@@ -1022,7 +1138,7 @@ var novely = ({
1022
1138
  }
1023
1139
  const stack = useStack(ctx);
1024
1140
  const offset = isWithoutQuestion ? 0 : 1;
1025
- if (DEV && !unwrappedChoices[selected]) {
1141
+ if (DEV2 && !unwrappedChoices[selected]) {
1026
1142
  throw new Error("Choice children is empty, either add content there or make item not selectable");
1027
1143
  }
1028
1144
  stack.value[0].push(["choice", selected + offset], [null, 0]);
@@ -1031,10 +1147,10 @@ var novely = ({
1031
1147
  });
1032
1148
  },
1033
1149
  jump({ ctx, data: data2 }, [scene]) {
1034
- if (DEV && !story[scene]) {
1150
+ if (DEV2 && !story[scene]) {
1035
1151
  throw new Error(`Attempt to jump to unknown scene "${scene}"`);
1036
1152
  }
1037
- if (DEV && story[scene].length === 0) {
1153
+ if (DEV2 && story[scene].length === 0) {
1038
1154
  throw new Error(`Attempt to jump to empty scene "${scene}"`);
1039
1155
  }
1040
1156
  const stack = useStack(ctx);
@@ -1057,15 +1173,15 @@ var novely = ({
1057
1173
  );
1058
1174
  },
1059
1175
  condition({ ctx }, [condition, variants]) {
1060
- if (DEV && Object.values(variants).length === 0) {
1176
+ if (DEV2 && Object.values(variants).length === 0) {
1061
1177
  throw new Error(`Attempt to use Condition action with empty variants object`);
1062
1178
  }
1063
1179
  if (!ctx.meta.restoring) {
1064
1180
  const val = String(condition());
1065
- if (DEV && !variants[val]) {
1181
+ if (DEV2 && !variants[val]) {
1066
1182
  throw new Error(`Attempt to go to unknown variant "${val}"`);
1067
1183
  }
1068
- if (DEV && variants[val].length === 0) {
1184
+ if (DEV2 && variants[val].length === 0) {
1069
1185
  throw new Error(`Attempt to go to empty variant "${val}"`);
1070
1186
  }
1071
1187
  const stack = useStack(MAIN_CONTEXT_KEY);
@@ -1106,46 +1222,24 @@ var novely = ({
1106
1222
  ctx.vibrate(pattern);
1107
1223
  push();
1108
1224
  },
1109
- next({ ctx, push }) {
1225
+ next({ push }) {
1110
1226
  push();
1111
1227
  },
1112
- animateCharacter({ ctx, data: data2 }, [character, timeout, ...classes]) {
1113
- if (DEV && classes.length === 0) {
1228
+ animateCharacter({ ctx, push }, [character, timeout, ...classes]) {
1229
+ if (DEV2 && classes.length === 0) {
1114
1230
  throw new Error("Attempt to use AnimateCharacter without classes. Classes should be provided [https://novely.pages.dev/guide/actions/animateCharacter.html]");
1115
1231
  }
1116
- if (DEV && (timeout <= 0 || !Number.isFinite(timeout) || Number.isNaN(timeout))) {
1232
+ if (DEV2 && (timeout <= 0 || !Number.isFinite(timeout) || Number.isNaN(timeout))) {
1117
1233
  throw new Error("Attempt to use AnimateCharacter with unacceptable timeout. It should be finite and greater than zero");
1118
1234
  }
1119
1235
  if (ctx.meta.preview)
1120
1236
  return;
1121
- const handler = ({ get }) => {
1122
- const { clear } = get(false);
1123
- const char = ctx.getCharacter(character);
1124
- if (!char)
1125
- return;
1126
- const target = char.canvas;
1127
- if (!target)
1128
- return;
1129
- const classNames = classes.filter((className) => !target.classList.contains(className));
1130
- target.classList.add(...classNames);
1131
- const removeClassNames = () => {
1132
- target.classList.remove(...classNames);
1133
- };
1134
- const timeoutId = setTimeout(removeClassNames, timeout);
1135
- clear(() => {
1136
- removeClassNames();
1137
- clearTimeout(timeoutId);
1138
- });
1139
- };
1140
- handler.key = "@@internal-animate-character";
1141
- match("custom", [handler], {
1142
- ctx,
1143
- data: data2
1144
- });
1237
+ ctx.character(character).animate(timeout, classes);
1238
+ push();
1145
1239
  },
1146
1240
  text({ ctx, data: data2, forward }, text) {
1147
1241
  const string = text.map((content) => unwrap2(content, data2)).join(" ");
1148
- if (DEV && string.length === 0) {
1242
+ if (DEV2 && string.length === 0) {
1149
1243
  throw new Error(`Action Text was called with empty string or array`);
1150
1244
  }
1151
1245
  ctx.text(string, forward);
@@ -1204,10 +1298,10 @@ var novely = ({
1204
1298
  push();
1205
1299
  },
1206
1300
  block({ ctx }, [scene]) {
1207
- if (DEV && !story[scene]) {
1301
+ if (DEV2 && !story[scene]) {
1208
1302
  throw new Error(`Attempt to call Block action with unknown scene "${scene}"`);
1209
1303
  }
1210
- if (DEV && story[scene].length === 0) {
1304
+ if (DEV2 && story[scene].length === 0) {
1211
1305
  throw new Error(`Attempt to call Block action with empty scene "${scene}"`);
1212
1306
  }
1213
1307
  if (!ctx.meta.restoring) {