@novely/core 0.38.2 → 0.39.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
@@ -49,10 +49,10 @@ var MAIN_CONTEXT_KEY = "$MAIN";
49
49
  var STACK_MAP = /* @__PURE__ */ new Map();
50
50
  var CUSTOM_ACTION_MAP = /* @__PURE__ */ new Map();
51
51
  var PRELOADED_ASSETS = /* @__PURE__ */ new Set();
52
- var RESOURCE_TYPE_CACHE = /* @__PURE__ */ new Map();
52
+ var ASSETS_TO_PRELOAD = /* @__PURE__ */ new Set();
53
53
 
54
54
  // src/utils.ts
55
- import { DEV } from "esm-env";
55
+ import { DEV as DEV2 } from "esm-env";
56
56
 
57
57
  // ../../node_modules/.pnpm/klona@2.0.6/node_modules/klona/json/index.mjs
58
58
  function klona(val) {
@@ -81,6 +81,127 @@ function klona(val) {
81
81
  return val;
82
82
  }
83
83
 
84
+ // src/utils.ts
85
+ import { default as memoize2 } from "micro-memoize";
86
+
87
+ // src/asset.ts
88
+ import { DEV } from "esm-env";
89
+
90
+ // src/audio-codecs.ts
91
+ var cut = (str2) => str2.replace(/^no$/, "");
92
+ var audio = new Audio();
93
+ var canPlay = (type) => !!cut(audio.canPlayType(type));
94
+ var canPlayMultiple = (...types) => types.some((type) => canPlay(type));
95
+ var supportsMap = {
96
+ mp3: canPlayMultiple("audio/mpeg;", "audio/mp3;"),
97
+ mpeg: canPlay("audio/mpeg;"),
98
+ opus: canPlay('audio/ogg; codecs="opus"'),
99
+ ogg: canPlay('audio/ogg; codecs="vorbis"'),
100
+ oga: canPlay('audio/ogg; codecs="vorbis"'),
101
+ wav: canPlayMultiple('audio/wav; codecs="1"', "audio/wav;"),
102
+ aac: canPlay("audio/aac;"),
103
+ caf: canPlay("audio/x-caf;"),
104
+ m4a: canPlayMultiple("audio/x-m4a;", "audio/m4a;", "audio/aac;"),
105
+ m4b: canPlayMultiple("audio/x-m4b;", "audio/m4b;", "audio/aac;"),
106
+ mp4: canPlayMultiple("audio/x-mp4;", "audio/mp4;", "audio/aac;"),
107
+ weba: canPlay('audio/webm; codecs="vorbis"'),
108
+ webm: canPlay('audio/webm; codecs="vorbis"'),
109
+ dolby: canPlay('audio/mp4; codecs="ec-3"'),
110
+ flac: canPlayMultiple("audio/x-flac;", "audio/flac;")
111
+ };
112
+
113
+ // src/image-formats.ts
114
+ var avif = "data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=";
115
+ var jxl = "data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgAAFSqjjBu8nOv58kOHxbSN6wxttW1hSaLIODZJJ3BIEkkaoCUzGM6qJAE=";
116
+ var webp = "data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA";
117
+ var supportsFormat = (source) => {
118
+ const { promise, resolve } = Promise.withResolvers();
119
+ const img = Object.assign(document.createElement("img"), {
120
+ src: source
121
+ });
122
+ img.onload = img.onerror = () => {
123
+ resolve(img.height === 2);
124
+ };
125
+ return promise;
126
+ };
127
+ var supportsMap2 = {
128
+ avif: false,
129
+ jxl: false,
130
+ webp: false
131
+ };
132
+ var formatsMap = {
133
+ avif,
134
+ jxl,
135
+ webp
136
+ };
137
+ var loadImageFormatsSupport = async () => {
138
+ const promises = [];
139
+ for (const [format, source] of Object.entries(formatsMap)) {
140
+ const promise = supportsFormat(source).then((supported) => {
141
+ supportsMap2[format] = supported;
142
+ });
143
+ promises.push(promise);
144
+ }
145
+ await Promise.all(promises);
146
+ };
147
+ loadImageFormatsSupport();
148
+
149
+ // src/asset.ts
150
+ import { default as memoize } from "micro-memoize";
151
+ var getType = memoize((extensions) => {
152
+ if (extensions.every((extension) => HOWLER_SUPPORTED_FILE_FORMATS.has(extension))) {
153
+ return "audio";
154
+ }
155
+ if (extensions.every((extension) => SUPPORTED_IMAGE_FILE_FORMATS.has(extension))) {
156
+ return "image";
157
+ }
158
+ throw extensions;
159
+ });
160
+ var SUPPORT_MAPS = {
161
+ "image": supportsMap2,
162
+ "audio": supportsMap
163
+ };
164
+ var asset = memoize((...variants) => {
165
+ if (DEV && variants.length === 0) {
166
+ throw new Error(`Attempt to use "asset" function without arguments`);
167
+ }
168
+ const map = {};
169
+ const extensions = [];
170
+ for (const v of variants) {
171
+ const e = getUrlFileExtension(v);
172
+ map[e] = v;
173
+ extensions.push(e);
174
+ }
175
+ const type = getType(extensions);
176
+ const getSource = memoize(() => {
177
+ const support = SUPPORT_MAPS[type];
178
+ for (const extension of extensions) {
179
+ if (extension in support) {
180
+ if (support[extension]) {
181
+ return map[extension];
182
+ }
183
+ } else {
184
+ return map[extension];
185
+ }
186
+ }
187
+ if (DEV) {
188
+ throw new Error(`No matching asset was found for ${variants.map((v) => `"${v}"`).join(", ")}`);
189
+ }
190
+ return "";
191
+ });
192
+ return {
193
+ get source() {
194
+ return getSource();
195
+ },
196
+ get type() {
197
+ return type;
198
+ }
199
+ };
200
+ });
201
+ var isAsset = (suspect) => {
202
+ return suspect !== null && typeof suspect === "object" && "source" in suspect && "type" in suspect;
203
+ };
204
+
84
205
  // src/utils.ts
85
206
  var matchAction = ({ getContext, onBeforeActionCall, push, forward }, values) => {
86
207
  return (action, props, { ctx, data }) => {
@@ -325,7 +446,7 @@ var createQueueProcessor = (queue, options) => {
325
446
  const processedQueue = [];
326
447
  const keep = /* @__PURE__ */ new Set();
327
448
  const characters = /* @__PURE__ */ new Set();
328
- const audio = {
449
+ const audio2 = {
329
450
  music: /* @__PURE__ */ new Set(),
330
451
  sound: /* @__PURE__ */ new Set()
331
452
  };
@@ -350,14 +471,9 @@ var createQueueProcessor = (queue, options) => {
350
471
  });
351
472
  if (notLatest) continue;
352
473
  } else if ("skipOnRestore" in fn && fn.skipOnRestore) {
353
- let getNext = () => {
354
- const nextActions = next(i);
355
- getNext = () => {
356
- return nextActions;
357
- };
358
- return nextActions;
359
- };
360
- if (fn.skipOnRestore(getNext)) continue;
474
+ if (fn.skipOnRestore(() => next(i))) {
475
+ continue;
476
+ }
361
477
  }
362
478
  }
363
479
  processedQueue.push(item);
@@ -375,9 +491,9 @@ var createQueueProcessor = (queue, options) => {
375
491
  if (action === "showCharacter") {
376
492
  characters.add(params[0]);
377
493
  } else if (action === "playMusic") {
378
- audio.music.add(params[0]);
494
+ audio2.music.add(unwrapAsset(params[0]));
379
495
  } else if (action === "playSound") {
380
- audio.sound.add(params[0]);
496
+ audio2.sound.add(unwrapAsset(params[0]));
381
497
  }
382
498
  processedQueue.push(item);
383
499
  } else if (action === "showBackground" || action === "preload") {
@@ -414,7 +530,7 @@ var createQueueProcessor = (queue, options) => {
414
530
  keep: {
415
531
  keep,
416
532
  characters,
417
- audio
533
+ audio: audio2
418
534
  }
419
535
  };
420
536
  };
@@ -461,15 +577,15 @@ var createUseStackFunction = (renderer) => {
461
577
  var mapSet = (set, fn) => {
462
578
  return [...set].map(fn);
463
579
  };
464
- var isImageAsset = (asset) => {
465
- return isString(asset) && isCSSImage(asset);
580
+ var isImageAsset = (asset2) => {
581
+ return isString(asset2) && isCSSImage(asset2);
466
582
  };
467
583
  var getUrlFileExtension = (address) => {
468
584
  try {
469
585
  const { pathname } = new URL(address, location.href);
470
586
  return pathname.split(".").at(-1).split("!")[0].split(":")[0];
471
587
  } catch (error) {
472
- if (DEV) {
588
+ if (DEV2) {
473
589
  console.error(new Error(`Could not construct URL "${address}".`, { cause: error }));
474
590
  }
475
591
  return "";
@@ -482,43 +598,41 @@ var fetchContentType = async (request, url) => {
482
598
  });
483
599
  return response.headers.get("Content-Type") || "";
484
600
  } catch (error) {
485
- if (DEV) {
601
+ if (DEV2) {
486
602
  console.error(new Error(`Failed to fetch file at "${url}"`, { cause: error }));
487
603
  }
488
604
  return "";
489
605
  }
490
606
  };
491
- var getResourseType = async (request, url) => {
492
- if (RESOURCE_TYPE_CACHE.has(url)) {
493
- return RESOURCE_TYPE_CACHE.get(url);
494
- }
495
- const encache = (value) => {
496
- RESOURCE_TYPE_CACHE.set(url, value);
497
- return value;
498
- };
499
- if (!isCSSImage(url)) {
500
- return encache("other");
501
- }
502
- const extension = getUrlFileExtension(url);
503
- if (HOWLER_SUPPORTED_FILE_FORMATS.has(extension)) {
504
- return encache("audio");
505
- }
506
- if (SUPPORTED_IMAGE_FILE_FORMATS.has(extension)) {
507
- return encache("image");
508
- }
509
- const contentType = await fetchContentType(request, url);
510
- if (contentType.includes("audio")) {
511
- return encache("audio");
512
- }
513
- if (contentType.includes("image")) {
514
- return encache("image");
607
+ var getResourseType = memoize2(
608
+ async (request, url) => {
609
+ if (!isCSSImage(url)) {
610
+ return "other";
611
+ }
612
+ const extension = getUrlFileExtension(url);
613
+ if (HOWLER_SUPPORTED_FILE_FORMATS.has(extension)) {
614
+ return "audio";
615
+ }
616
+ if (SUPPORTED_IMAGE_FILE_FORMATS.has(extension)) {
617
+ return "image";
618
+ }
619
+ const contentType = await fetchContentType(request, url);
620
+ if (contentType.includes("audio")) {
621
+ return "audio";
622
+ }
623
+ if (contentType.includes("image")) {
624
+ return "image";
625
+ }
626
+ return "other";
627
+ },
628
+ {
629
+ isPromise: true
515
630
  }
516
- return encache("other");
517
- };
631
+ );
518
632
  var capitalize = (str2) => {
519
633
  return str2[0].toUpperCase() + str2.slice(1);
520
634
  };
521
- var getIntlLanguageDisplayName = (lang) => {
635
+ var getIntlLanguageDisplayName = memoize2((lang) => {
522
636
  try {
523
637
  const intl = new Intl.DisplayNames([lang], {
524
638
  type: "language"
@@ -527,7 +641,7 @@ var getIntlLanguageDisplayName = (lang) => {
527
641
  } catch {
528
642
  return lang;
529
643
  }
530
- };
644
+ });
531
645
  var createReferFunction = (story) => {
532
646
  const refer = (path) => {
533
647
  let current = story;
@@ -671,6 +785,29 @@ var collectActionsBeforeBlockingAction = ({ path, refer }) => {
671
785
  }
672
786
  return collection;
673
787
  };
788
+ var unwrapAsset = (asset2) => {
789
+ return isAsset(asset2) ? asset2.source : asset2;
790
+ };
791
+ var handleAudioAsset = (asset2) => {
792
+ if (DEV2 && isAsset(asset2) && asset2.type !== "audio") {
793
+ throw new Error("Attempt to use non-audio asset in audio action", { cause: asset2 });
794
+ }
795
+ return unwrapAsset(asset2);
796
+ };
797
+ var handleImageAsset = (asset2) => {
798
+ if (DEV2 && isAsset(asset2) && asset2.type !== "image") {
799
+ throw new Error("Attempt to use non-image asset in action that requires image assets", { cause: asset2 });
800
+ }
801
+ return unwrapAsset(asset2);
802
+ };
803
+ var getCharactersData = (characters) => {
804
+ const entries = Object.entries(characters);
805
+ const mapped = entries.map(([key, value]) => [key, { name: value.name, emotions: Object.keys(value.emotions) }]);
806
+ return Object.fromEntries(mapped);
807
+ };
808
+ var toArray = (target) => {
809
+ return Array.isArray(target) ? target : [target];
810
+ };
674
811
 
675
812
  // src/novely.ts
676
813
  import { dequal } from "dequal/lite";
@@ -921,7 +1058,69 @@ var setupBrowserVisibilityChangeListeners = ({ onChange }) => {
921
1058
 
922
1059
  // src/novely.ts
923
1060
  import pLimit from "p-limit";
924
- import { DEV as DEV2 } from "esm-env";
1061
+ import { DEV as DEV3 } from "esm-env";
1062
+
1063
+ // src/preloading.ts
1064
+ var enqueueAssetForPreloading = (asset2) => {
1065
+ if (!PRELOADED_ASSETS.has(asset2)) {
1066
+ ASSETS_TO_PRELOAD.add(asset2);
1067
+ }
1068
+ };
1069
+ var handleAssetsPreloading = async ({ request, limiter, preloadAudioBlocking, preloadImageBlocking }) => {
1070
+ const list = mapSet(ASSETS_TO_PRELOAD, (asset2) => {
1071
+ return limiter(async () => {
1072
+ const type = await getResourseType(request, asset2);
1073
+ switch (type) {
1074
+ case "audio": {
1075
+ await preloadAudioBlocking(asset2);
1076
+ break;
1077
+ }
1078
+ case "image": {
1079
+ await preloadImageBlocking(asset2);
1080
+ break;
1081
+ }
1082
+ }
1083
+ ASSETS_TO_PRELOAD.delete(asset2);
1084
+ PRELOADED_ASSETS.add(asset2);
1085
+ });
1086
+ });
1087
+ await Promise.allSettled(list);
1088
+ ASSETS_TO_PRELOAD.clear();
1089
+ };
1090
+ var huntAssets = ({ characters, action, props, handle }) => {
1091
+ if (action === "showBackground") {
1092
+ if (isString(props[0])) {
1093
+ handle(handleAudioAsset(props[0]));
1094
+ }
1095
+ if (props[0] && typeof props[0] === "object") {
1096
+ for (const value of Object.values(props[0])) {
1097
+ if (isImageAsset(value)) {
1098
+ handle(value);
1099
+ }
1100
+ }
1101
+ }
1102
+ return;
1103
+ }
1104
+ if (isAudioAction(action) && isString(props[0])) {
1105
+ handle(handleAudioAsset(props[0]));
1106
+ return;
1107
+ }
1108
+ if (action === "showCharacter" && isString(props[0]) && isString(props[1])) {
1109
+ const images = toArray(characters[props[0]].emotions[props[1]]);
1110
+ for (const asset2 of images) {
1111
+ handle(handleImageAsset(asset2));
1112
+ }
1113
+ return;
1114
+ }
1115
+ if (action === "custom" && props[0].assets && props[0].assets.length > 0) {
1116
+ for (const asset2 of props[0].assets) {
1117
+ handle(asset2);
1118
+ }
1119
+ return;
1120
+ }
1121
+ };
1122
+
1123
+ // src/novely.ts
925
1124
  var novely = ({
926
1125
  characters,
927
1126
  characterAssetSizes = {},
@@ -950,38 +1149,12 @@ var novely = ({
950
1149
  const limitAssetsDownload = pLimit(parallelAssetsDownloadLimit);
951
1150
  const story = {};
952
1151
  const times = /* @__PURE__ */ new Set();
953
- const ASSETS_TO_PRELOAD = /* @__PURE__ */ new Set();
954
1152
  const dataLoaded = createControlledPromise();
955
1153
  let initialScreenWasShown = false;
956
1154
  let destroyed = false;
957
1155
  const intime = (value) => {
958
1156
  return times.add(value), value;
959
1157
  };
960
- const handleAssetsPreloading = async () => {
961
- const { preloadAudioBlocking, preloadImageBlocking } = renderer.misc;
962
- const list = mapSet(ASSETS_TO_PRELOAD, (asset) => {
963
- return limitAssetsDownload(async () => {
964
- const type = await getResourseType(request, asset);
965
- switch (type) {
966
- case "audio": {
967
- await preloadAudioBlocking(asset);
968
- break;
969
- }
970
- case "image": {
971
- await preloadImageBlocking(asset);
972
- break;
973
- }
974
- }
975
- });
976
- });
977
- await Promise.allSettled(list);
978
- ASSETS_TO_PRELOAD.clear();
979
- };
980
- const assetsToPreloadAdd = (asset) => {
981
- if (!PRELOADED_ASSETS.has(asset)) {
982
- ASSETS_TO_PRELOAD.add(asset);
983
- }
984
- };
985
1158
  const scriptBase = async (part) => {
986
1159
  if (destroyed) return;
987
1160
  Object.assign(story, flattenStory(part));
@@ -994,7 +1167,11 @@ var novely = ({
994
1167
  if (!loadingIsShown) {
995
1168
  renderer.ui.showLoading();
996
1169
  }
997
- await handleAssetsPreloading();
1170
+ await handleAssetsPreloading({
1171
+ ...renderer.misc,
1172
+ limiter: limitAssetsDownload,
1173
+ request
1174
+ });
998
1175
  }
999
1176
  await dataLoaded.promise;
1000
1177
  renderer.ui.hideLoading();
@@ -1010,40 +1187,6 @@ var novely = ({
1010
1187
  const script = (part) => {
1011
1188
  return limitScript(() => scriptBase(part));
1012
1189
  };
1013
- const assetPreloadingCheck = (action2, props, doAction = assetsToPreloadAdd) => {
1014
- if (action2 === "showBackground") {
1015
- if (isImageAsset(props[0])) {
1016
- doAction(props[0]);
1017
- }
1018
- if (props[0] && typeof props[0] === "object") {
1019
- for (const value of Object.values(props[0])) {
1020
- if (!isImageAsset(value)) continue;
1021
- doAction(value);
1022
- }
1023
- }
1024
- return;
1025
- }
1026
- if (isAudioAction(action2) && isString(props[0])) {
1027
- doAction(props[0]);
1028
- return;
1029
- }
1030
- if (action2 === "showCharacter" && isString(props[0]) && isString(props[1])) {
1031
- const images = characters[props[0]].emotions[props[1]];
1032
- if (Array.isArray(images)) {
1033
- for (const asset of images) {
1034
- doAction(asset);
1035
- }
1036
- } else {
1037
- doAction(images);
1038
- }
1039
- return;
1040
- }
1041
- if (action2 === "custom" && props[0].assets && props[0].assets.length > 0) {
1042
- for (const asset of props[0].assets) {
1043
- doAction(asset);
1044
- }
1045
- }
1046
- };
1047
1190
  const action = new Proxy({}, {
1048
1191
  get(_, action2) {
1049
1192
  if (action2 in renderer.actions) {
@@ -1051,7 +1194,7 @@ var novely = ({
1051
1194
  }
1052
1195
  return (...props) => {
1053
1196
  if (preloadAssets === "blocking") {
1054
- assetPreloadingCheck(action2, props);
1197
+ huntAssets({ characters, action: action2, props, handle: enqueueAssetForPreloading });
1055
1198
  }
1056
1199
  return [action2, ...props];
1057
1200
  };
@@ -1072,7 +1215,7 @@ var novely = ({
1072
1215
  if (languages.includes(language)) {
1073
1216
  return language;
1074
1217
  }
1075
- if (DEV2) {
1218
+ if (DEV3) {
1076
1219
  throw new Error(`Attempt to use unsupported language "${language}". Supported languages: ${languages.join(", ")}.`);
1077
1220
  }
1078
1221
  throw 0;
@@ -1102,7 +1245,7 @@ var novely = ({
1102
1245
  };
1103
1246
  const throttledOnStorageDataChange = throttle(onStorageDataChange, throttleTimeout);
1104
1247
  const throttledEmergencyOnStorageDataChange = throttle(() => {
1105
- if (saveOnUnload === true || saveOnUnload === "prod" && !DEV2) {
1248
+ if (saveOnUnload === true || saveOnUnload === "prod" && !DEV3) {
1106
1249
  onStorageDataChange(storageData.get());
1107
1250
  }
1108
1251
  }, 10);
@@ -1111,7 +1254,7 @@ var novely = ({
1111
1254
  let stored = await storage.get();
1112
1255
  for (const migration of migrations) {
1113
1256
  stored = migration(stored);
1114
- if (DEV2 && !stored) {
1257
+ if (DEV3 && !stored) {
1115
1258
  throw new Error("Migrations should return a value.");
1116
1259
  }
1117
1260
  }
@@ -1188,7 +1331,7 @@ var novely = ({
1188
1331
  let interacted = 0;
1189
1332
  const restore = async (save2) => {
1190
1333
  if (isEmpty(story)) {
1191
- if (DEV2) {
1334
+ if (DEV3) {
1192
1335
  throw new Error("Story is empty. You should call an `enine.script` function [https://novely.pages.dev/guide/story.html]");
1193
1336
  }
1194
1337
  return;
@@ -1209,7 +1352,7 @@ var novely = ({
1209
1352
  const [path] = stack.value = latest;
1210
1353
  renderer.ui.showScreen("game");
1211
1354
  const { queue, skip, skipPreserve } = getActionsFromPath(story, path, false);
1212
- const { run, keep: { keep, characters: characters2, audio } } = createQueueProcessor(queue, {
1355
+ const { run, keep: { keep, characters: characters2, audio: audio2 } } = createQueueProcessor(queue, {
1213
1356
  skip,
1214
1357
  skipPreserve
1215
1358
  });
@@ -1227,7 +1370,7 @@ var novely = ({
1227
1370
  }
1228
1371
  }
1229
1372
  if (context.meta.goingBack) {
1230
- match("clear", [keep, characters2, audio], {
1373
+ match("clear", [keep, characters2, audio2], {
1231
1374
  ctx: context,
1232
1375
  data: latest[1]
1233
1376
  });
@@ -1314,7 +1457,7 @@ var novely = ({
1314
1457
  if (isAudioAction(action2)) return;
1315
1458
  if (action2 === "vibrate") return;
1316
1459
  if (action2 === "end") return;
1317
- assetPreloadingCheck(action2, props, assets.push.bind(assets));
1460
+ huntAssets({ characters, action: action2, props, handle: assets.push.bind(assets) });
1318
1461
  return match(action2, props, {
1319
1462
  ctx,
1320
1463
  data: data2
@@ -1346,7 +1489,7 @@ var novely = ({
1346
1489
  };
1347
1490
  const getLanguageDisplayName = (lang) => {
1348
1491
  const language = translation[lang];
1349
- if (DEV2 && !language) {
1492
+ if (DEV3 && !language) {
1350
1493
  throw new Error(`Attempt to use unsupported language "${language}". Supported languages: ${languages.join(", ")}.`);
1351
1494
  }
1352
1495
  return capitalize(language.nameOverride || getIntlLanguageDisplayName(lang));
@@ -1357,9 +1500,15 @@ var novely = ({
1357
1500
  const getResourseTypeForRenderer = (url) => {
1358
1501
  return getResourseType(request, url);
1359
1502
  };
1503
+ const getCharacterColor = (c) => {
1504
+ return c in characters ? characters[c].color : "#000000";
1505
+ };
1506
+ const getCharacterAssets = (character, emotion) => {
1507
+ return toArray(characters[character].emotions[emotion]).map(handleImageAsset);
1508
+ };
1360
1509
  const renderer = createRenderer({
1361
1510
  mainContextKey: MAIN_CONTEXT_KEY,
1362
- characters,
1511
+ characters: getCharactersData(characters),
1363
1512
  characterAssetSizes,
1364
1513
  set,
1365
1514
  restore,
@@ -1376,6 +1525,8 @@ var novely = ({
1376
1525
  storageData,
1377
1526
  coreData,
1378
1527
  getLanguageDisplayName,
1528
+ getCharacterColor,
1529
+ getCharacterAssets,
1379
1530
  getResourseType: getResourseTypeForRenderer
1380
1531
  });
1381
1532
  const useStack = createUseStackFunction(renderer);
@@ -1416,26 +1567,12 @@ var novely = ({
1416
1567
  refer
1417
1568
  });
1418
1569
  for (const [action3, ...props2] of collection) {
1419
- assetPreloadingCheck(action3, props2);
1570
+ huntAssets({ characters, action: action3, props: props2, handle: enqueueAssetForPreloading });
1420
1571
  }
1421
- const { preloadAudioBlocking, preloadImageBlocking } = renderer.misc;
1422
- ASSETS_TO_PRELOAD.forEach(async (asset) => {
1423
- ASSETS_TO_PRELOAD.delete(asset);
1424
- const type = await getResourseType(request, asset);
1425
- switch (type) {
1426
- case "audio": {
1427
- preloadAudioBlocking(asset).then(() => {
1428
- PRELOADED_ASSETS.add(asset);
1429
- });
1430
- break;
1431
- }
1432
- case "image": {
1433
- preloadImageBlocking(asset).then(() => {
1434
- PRELOADED_ASSETS.add(asset);
1435
- });
1436
- break;
1437
- }
1438
- }
1572
+ handleAssetsPreloading({
1573
+ ...renderer.misc,
1574
+ request,
1575
+ limiter: limitAssetsDownload
1439
1576
  });
1440
1577
  } catch (cause) {
1441
1578
  console.error(cause);
@@ -1448,41 +1585,47 @@ var novely = ({
1448
1585
  setTimeout(push, isFunction(time) ? time(getStateAtCtx(ctx)) : time);
1449
1586
  },
1450
1587
  showBackground({ ctx, push }, [background]) {
1451
- ctx.background(background);
1588
+ if (isString(background) || isAsset(background)) {
1589
+ ctx.background({
1590
+ "all": handleImageAsset(background)
1591
+ });
1592
+ } else {
1593
+ ctx.background(Object.fromEntries(Object.entries(background).map(([media, asset2]) => [media, handleImageAsset(asset2)])));
1594
+ }
1452
1595
  push();
1453
1596
  },
1454
1597
  playMusic({ ctx, push }, [source]) {
1455
- ctx.audio.music(source, "music").play(true);
1598
+ ctx.audio.music(handleAudioAsset(source), "music").play(true);
1456
1599
  push();
1457
1600
  },
1458
1601
  pauseMusic({ ctx, push }, [source]) {
1459
- ctx.audio.music(source, "music").pause();
1602
+ ctx.audio.music(handleAudioAsset(source), "music").pause();
1460
1603
  push();
1461
1604
  },
1462
1605
  stopMusic({ ctx, push }, [source]) {
1463
- ctx.audio.music(source, "music").stop();
1606
+ ctx.audio.music(handleAudioAsset(source), "music").stop();
1464
1607
  push();
1465
1608
  },
1466
1609
  playSound({ ctx, push }, [source, loop]) {
1467
- ctx.audio.music(source, "sound").play(loop || false);
1610
+ ctx.audio.music(handleAudioAsset(source), "sound").play(loop || false);
1468
1611
  push();
1469
1612
  },
1470
1613
  pauseSound({ ctx, push }, [source]) {
1471
- ctx.audio.music(source, "sound").pause();
1614
+ ctx.audio.music(handleAudioAsset(source), "sound").pause();
1472
1615
  push();
1473
1616
  },
1474
1617
  stopSound({ ctx, push }, [source]) {
1475
- ctx.audio.music(source, "sound").stop();
1618
+ ctx.audio.music(handleAudioAsset(source), "sound").stop();
1476
1619
  push();
1477
1620
  },
1478
1621
  voice({ ctx, push }, [source]) {
1479
1622
  const [lang] = storageData.get().meta;
1480
- const audioSource = isString(source) ? source : source[lang];
1623
+ const audioSource = isString(source) ? source : isAsset(source) ? source : source[lang];
1481
1624
  if (!audioSource) {
1482
1625
  push();
1483
1626
  return;
1484
1627
  }
1485
- ctx.audio.voice(audioSource);
1628
+ ctx.audio.voice(handleAudioAsset(audioSource));
1486
1629
  push();
1487
1630
  },
1488
1631
  stopVoice({ ctx, push }) {
@@ -1491,11 +1634,11 @@ var novely = ({
1491
1634
  },
1492
1635
  showCharacter({ ctx, push }, [character, emotion, className, style]) {
1493
1636
  emotion ??= defaultEmotions[character];
1494
- if (DEV2 && !emotion) {
1637
+ if (DEV3 && !emotion) {
1495
1638
  throw new Error(`Attemp to show character "${character}" without emotion provided.`);
1496
1639
  }
1497
1640
  if (!emotion) return;
1498
- if (DEV2 && !characters[character].emotions[emotion]) {
1641
+ if (DEV3 && !characters[character].emotions[emotion]) {
1499
1642
  throw new Error(`Attempt to show character "${character}" with unknown emotion "${emotion}"`);
1500
1643
  }
1501
1644
  const handle = ctx.character(character);
@@ -1532,7 +1675,7 @@ var novely = ({
1532
1675
  );
1533
1676
  },
1534
1677
  say({ ctx, data: data2 }, [character, content]) {
1535
- if (DEV2 && !characters[character]) {
1678
+ if (DEV3 && !characters[character]) {
1536
1679
  throw new Error(`Attempt to call Say action with unknown character "${character}"`);
1537
1680
  }
1538
1681
  match("dialog", [character, content], {
@@ -1565,12 +1708,12 @@ var novely = ({
1565
1708
  lang: storageData.get().meta[0],
1566
1709
  state: getStateAtCtx(ctx)
1567
1710
  });
1568
- if (DEV2 && action2.length === 0 && !shown) {
1711
+ if (DEV3 && action2.length === 0 && !shown) {
1569
1712
  console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
1570
1713
  }
1571
1714
  return [templateReplace(content, data2), shown];
1572
1715
  });
1573
- if (DEV2 && transformedChoices.length === 0) {
1716
+ if (DEV3 && transformedChoices.length === 0) {
1574
1717
  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]`);
1575
1718
  }
1576
1719
  ctx.clearBlockingActions("choice");
@@ -1580,7 +1723,7 @@ var novely = ({
1580
1723
  }
1581
1724
  const stack = useStack(ctx);
1582
1725
  const offset = isWithoutQuestion ? 0 : 1;
1583
- if (DEV2 && !transformedChoices[selected]) {
1726
+ if (DEV3 && !transformedChoices[selected]) {
1584
1727
  throw new Error("Choice children is empty, either add content there or make item not selectable");
1585
1728
  }
1586
1729
  stack.value[0].push(["choice", selected + offset], [null, 0]);
@@ -1589,10 +1732,10 @@ var novely = ({
1589
1732
  });
1590
1733
  },
1591
1734
  jump({ ctx, data: data2 }, [scene]) {
1592
- if (DEV2 && !story[scene]) {
1735
+ if (DEV3 && !story[scene]) {
1593
1736
  throw new Error(`Attempt to jump to unknown scene "${scene}"`);
1594
1737
  }
1595
- if (DEV2 && story[scene].length === 0) {
1738
+ if (DEV3 && story[scene].length === 0) {
1596
1739
  throw new Error(`Attempt to jump to empty scene "${scene}"`);
1597
1740
  }
1598
1741
  const stack = useStack(ctx);
@@ -1605,25 +1748,25 @@ var novely = ({
1605
1748
  data: data2
1606
1749
  });
1607
1750
  },
1608
- clear({ ctx, push }, [keep, characters2, audio]) {
1751
+ clear({ ctx, push }, [keep, characters2, audio2]) {
1609
1752
  ctx.vibrate(0);
1610
1753
  ctx.clear(
1611
1754
  keep || EMPTY_SET,
1612
1755
  characters2 || EMPTY_SET,
1613
- audio || { music: EMPTY_SET, sounds: EMPTY_SET },
1756
+ audio2 || { music: EMPTY_SET, sounds: EMPTY_SET },
1614
1757
  push
1615
1758
  );
1616
1759
  },
1617
1760
  condition({ ctx }, [condition, variants]) {
1618
- if (DEV2 && Object.values(variants).length === 0) {
1761
+ if (DEV3 && Object.values(variants).length === 0) {
1619
1762
  throw new Error(`Attempt to use Condition action with empty variants object`);
1620
1763
  }
1621
1764
  if (!ctx.meta.restoring) {
1622
1765
  const val = String(condition(getStateAtCtx(ctx)));
1623
- if (DEV2 && !variants[val]) {
1766
+ if (DEV3 && !variants[val]) {
1624
1767
  throw new Error(`Attempt to go to unknown variant "${val}"`);
1625
1768
  }
1626
- if (DEV2 && variants[val].length === 0) {
1769
+ if (DEV3 && variants[val].length === 0) {
1627
1770
  throw new Error(`Attempt to go to empty variant "${val}"`);
1628
1771
  }
1629
1772
  const stack = useStack(MAIN_CONTEXT_KEY);
@@ -1680,7 +1823,7 @@ var novely = ({
1680
1823
  },
1681
1824
  animateCharacter({ ctx, push }, [character, className]) {
1682
1825
  const classes = className.split(" ");
1683
- if (DEV2 && classes.length === 0) {
1826
+ if (DEV3 && classes.length === 0) {
1684
1827
  throw new Error("Attempt to use AnimateCharacter without classes. Classes should be provided [https://novely.pages.dev/guide/actions/animateCharacter.html]");
1685
1828
  }
1686
1829
  if (ctx.meta.preview) return;
@@ -1689,7 +1832,7 @@ var novely = ({
1689
1832
  },
1690
1833
  text({ ctx, data: data2, forward }, text) {
1691
1834
  const string = text.map((content) => templateReplace(content, data2)).join(" ");
1692
- if (DEV2 && string.length === 0) {
1835
+ if (DEV3 && string.length === 0) {
1693
1836
  throw new Error(`Action Text was called with empty string or array`);
1694
1837
  }
1695
1838
  ctx.clearBlockingActions("text");
@@ -1714,7 +1857,7 @@ var novely = ({
1714
1857
  render(ctx);
1715
1858
  },
1716
1859
  preload({ ctx, push }, [source]) {
1717
- if (DEV2 && preloadAssets !== "lazy") {
1860
+ if (DEV3 && preloadAssets !== "lazy") {
1718
1861
  console.error(`You do not need a preload action becase "preloadAssets" strategy was set to "${preloadAssets}"`);
1719
1862
  }
1720
1863
  if (!ctx.meta.goingBack && !ctx.meta.restoring && !PRELOADED_ASSETS.has(source)) {
@@ -1723,10 +1866,10 @@ var novely = ({
1723
1866
  push();
1724
1867
  },
1725
1868
  block({ ctx }, [scene]) {
1726
- if (DEV2 && !story[scene]) {
1869
+ if (DEV3 && !story[scene]) {
1727
1870
  throw new Error(`Attempt to call Block action with unknown scene "${scene}"`);
1728
1871
  }
1729
- if (DEV2 && story[scene].length === 0) {
1872
+ if (DEV3 && story[scene].length === 0) {
1730
1873
  throw new Error(`Attempt to call Block action with empty scene "${scene}"`);
1731
1874
  }
1732
1875
  if (!ctx.meta.restoring) {
@@ -1798,8 +1941,8 @@ var novely = ({
1798
1941
  };
1799
1942
  const setStorageData = (data2) => {
1800
1943
  if (destroyed) {
1801
- if (DEV2) {
1802
- throw new Error(`function \`setStorageData\` was called after novely instance was destroyed.`);
1944
+ if (DEV3) {
1945
+ throw new Error(`function \`setStorageData\` was called after novely instance was destroyed. Data is not updater nor synced after destroy.`);
1803
1946
  }
1804
1947
  return;
1805
1948
  }
@@ -2079,6 +2222,7 @@ export {
2079
2222
  JP,
2080
2223
  KK,
2081
2224
  RU,
2225
+ asset,
2082
2226
  extendAction,
2083
2227
  localStorageStorage,
2084
2228
  novely