@novely/core 0.22.2 → 0.24.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.
@@ -174,11 +174,45 @@ 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
217
  var matchAction = ({ getContext, push, forward }, values) => {
184
218
  return (action, props, { ctx, data }) => {
@@ -187,9 +221,13 @@ var Novely = (() => {
187
221
  ctx: context,
188
222
  data,
189
223
  push() {
224
+ if (context.meta.preview)
225
+ return;
190
226
  push(context);
191
227
  },
192
228
  forward() {
229
+ if (context.meta.preview)
230
+ return;
193
231
  forward(context);
194
232
  }
195
233
  }, props);
@@ -298,6 +336,9 @@ var Novely = (() => {
298
336
  var isSkippedDuringRestore = (item) => {
299
337
  return SKIPPED_DURING_RESTORE.has(item);
300
338
  };
339
+ var isAudioAction = (action) => {
340
+ return AUDIO_ACTIONS.has(action);
341
+ };
301
342
  var noop = () => {
302
343
  };
303
344
  var isAction = (element) => {
@@ -520,6 +561,53 @@ var Novely = (() => {
520
561
  };
521
562
  return useStack;
522
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
+ };
523
611
 
524
612
  // src/global.ts
525
613
  var PRELOADED_ASSETS = /* @__PURE__ */ new Set();
@@ -715,11 +803,6 @@ var Novely = (() => {
715
803
 
716
804
  // src/novely.ts
717
805
  var import_p_limit = __toESM(require_p_limit(), 1);
718
-
719
- // ../../node_modules/.pnpm/esm-env@1.0.0/node_modules/esm-env/prod-ssr.js
720
- var DEV = false;
721
-
722
- // src/novely.ts
723
806
  var novely = ({
724
807
  characters,
725
808
  storage = localStorageStorage({ key: "novely-game-storage" }),
@@ -727,7 +810,6 @@ var Novely = (() => {
727
810
  renderer: createRenderer,
728
811
  initialScreen = "mainmenu",
729
812
  translation,
730
- languages,
731
813
  state: defaultState,
732
814
  data: defaultData,
733
815
  autosaves = true,
@@ -736,9 +818,13 @@ var Novely = (() => {
736
818
  getLanguage: getLanguage2 = getLanguage,
737
819
  overrideLanguage = false,
738
820
  askBeforeExit = true,
739
- preloadAssets = "lazy"
821
+ preloadAssets = "lazy",
822
+ parallelAssetsDownloadLimit = 15,
823
+ fetch: request = fetch
740
824
  }) => {
825
+ const languages = Object.keys(translation);
741
826
  const limitScript = (0, import_p_limit.default)(1);
827
+ const limitAssetsDownload = (0, import_p_limit.default)(parallelAssetsDownloadLimit);
742
828
  const story = {};
743
829
  const times = /* @__PURE__ */ new Set();
744
830
  const ASSETS_TO_PRELOAD = /* @__PURE__ */ new Set();
@@ -754,7 +840,23 @@ var Novely = (() => {
754
840
  Object.assign(story, flattenStory(part));
755
841
  if (preloadAssets === "blocking" && ASSETS_TO_PRELOAD.size > 0) {
756
842
  renderer.ui.showScreen("loading");
757
- 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);
758
860
  }
759
861
  const screen = renderer.ui.getScreen();
760
862
  const nextScreen = scriptCalled ? screen : initialScreen;
@@ -775,13 +877,25 @@ var Novely = (() => {
775
877
  return limitScript(() => scriptBase(part));
776
878
  };
777
879
  const action = new Proxy({}, {
778
- get(_, prop) {
880
+ get(_, action2) {
779
881
  return (...props) => {
780
882
  if (preloadAssets === "blocking") {
781
- 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])) {
782
896
  ASSETS_TO_PRELOAD.add(props[0]);
783
897
  }
784
- if (prop === "showCharacter" && typeof props[0] === "string" && typeof props[1] === "string") {
898
+ if (action2 === "showCharacter" && isString(props[0]) && isString(props[1])) {
785
899
  const images = characters[props[0]].emotions[props[1]];
786
900
  if (Array.isArray(images)) {
787
901
  for (const asset of images) {
@@ -792,7 +906,7 @@ var Novely = (() => {
792
906
  }
793
907
  }
794
908
  }
795
- return [prop, ...props];
909
+ return [action2, ...props];
796
910
  };
797
911
  }
798
912
  });
@@ -841,12 +955,10 @@ var Novely = (() => {
841
955
  for (const migration of migrations) {
842
956
  stored = migration(stored);
843
957
  }
844
- stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
845
- if (overrideLanguage) {
958
+ if (overrideLanguage || !stored.meta[0]) {
846
959
  stored.meta[0] = getLanguageWithoutParameters();
847
- } else {
848
- stored.meta[0] ||= getLanguageWithoutParameters();
849
960
  }
961
+ stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
850
962
  stored.meta[2] ??= 1;
851
963
  stored.meta[3] ??= 1;
852
964
  stored.meta[4] ??= 1;
@@ -912,12 +1024,11 @@ var Novely = (() => {
912
1024
  return;
913
1025
  let latest = save2 || $.get().saves.at(-1);
914
1026
  if (!latest) {
915
- $.set({
916
- saves: [initial],
917
- data: klona(defaultData),
918
- meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED, 1, 1, 1]
919
- });
920
1027
  latest = klona(initial);
1028
+ $.update((prev) => {
1029
+ prev.saves.push(latest);
1030
+ return prev;
1031
+ });
921
1032
  }
922
1033
  const context = renderer.getContext(MAIN_CONTEXT_KEY);
923
1034
  const stack = useStack(context);
@@ -1021,7 +1132,7 @@ var Novely = (() => {
1021
1132
  ctx.meta.preview = true;
1022
1133
  const processor = createQueueProcessor(queue);
1023
1134
  await processor.run((action2, props) => {
1024
- if (AUDIO_ACTIONS.has(action2))
1135
+ if (isAudioAction(action2))
1025
1136
  return;
1026
1137
  if (action2 === "vibrate")
1027
1138
  return;
@@ -1272,10 +1383,10 @@ var Novely = (() => {
1272
1383
  ctx.vibrate(pattern);
1273
1384
  push();
1274
1385
  },
1275
- next({ ctx, push }) {
1386
+ next({ push }) {
1276
1387
  push();
1277
1388
  },
1278
- animateCharacter({ ctx, data: data2 }, [character, timeout, ...classes]) {
1389
+ animateCharacter({ ctx, push }, [character, timeout, ...classes]) {
1279
1390
  if (DEV && classes.length === 0) {
1280
1391
  throw new Error("Attempt to use AnimateCharacter without classes. Classes should be provided [https://novely.pages.dev/guide/actions/animateCharacter.html]");
1281
1392
  }
@@ -1284,30 +1395,8 @@ var Novely = (() => {
1284
1395
  }
1285
1396
  if (ctx.meta.preview)
1286
1397
  return;
1287
- const handler = ({ get }) => {
1288
- const { clear } = get(false);
1289
- const char = ctx.getCharacter(character);
1290
- if (!char)
1291
- return;
1292
- const target = char.canvas;
1293
- if (!target)
1294
- return;
1295
- const classNames = classes.filter((className) => !target.classList.contains(className));
1296
- target.classList.add(...classNames);
1297
- const removeClassNames = () => {
1298
- target.classList.remove(...classNames);
1299
- };
1300
- const timeoutId = setTimeout(removeClassNames, timeout);
1301
- clear(() => {
1302
- removeClassNames();
1303
- clearTimeout(timeoutId);
1304
- });
1305
- };
1306
- handler.key = "@@internal-animate-character";
1307
- match("custom", [handler], {
1308
- ctx,
1309
- data: data2
1310
- });
1398
+ ctx.character(character).animate(timeout, classes);
1399
+ push();
1311
1400
  },
1312
1401
  text({ ctx, data: data2, forward }, text) {
1313
1402
  const string = text.map((content) => unwrap2(content, data2)).join(" ");