@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.d.ts +19 -10
- package/dist/index.global.js +134 -43
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +147 -53
- package/dist/index.js.map +1 -1
- package/package.json +62 -62
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
|
-
|
|
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(_,
|
|
717
|
+
get(_, action2) {
|
|
613
718
|
return (...props) => {
|
|
614
719
|
if (preloadAssets === "blocking") {
|
|
615
|
-
if (
|
|
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 (
|
|
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 [
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
1150
|
+
if (DEV2 && !story[scene]) {
|
|
1035
1151
|
throw new Error(`Attempt to jump to unknown scene "${scene}"`);
|
|
1036
1152
|
}
|
|
1037
|
-
if (
|
|
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 (
|
|
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 (
|
|
1181
|
+
if (DEV2 && !variants[val]) {
|
|
1066
1182
|
throw new Error(`Attempt to go to unknown variant "${val}"`);
|
|
1067
1183
|
}
|
|
1068
|
-
if (
|
|
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({
|
|
1225
|
+
next({ push }) {
|
|
1110
1226
|
push();
|
|
1111
1227
|
},
|
|
1112
|
-
animateCharacter({ ctx,
|
|
1113
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
1122
|
-
|
|
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 (
|
|
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 (
|
|
1301
|
+
if (DEV2 && !story[scene]) {
|
|
1208
1302
|
throw new Error(`Attempt to call Block action with unknown scene "${scene}"`);
|
|
1209
1303
|
}
|
|
1210
|
-
if (
|
|
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) {
|