@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.
- package/dist/index.d.ts +245 -189
- package/dist/index.global.js +137 -48
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +150 -58
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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" }),
|
|
@@ -561,7 +647,6 @@ var novely = ({
|
|
|
561
647
|
renderer: createRenderer,
|
|
562
648
|
initialScreen = "mainmenu",
|
|
563
649
|
translation,
|
|
564
|
-
languages,
|
|
565
650
|
state: defaultState,
|
|
566
651
|
data: defaultData,
|
|
567
652
|
autosaves = true,
|
|
@@ -570,9 +655,13 @@ var novely = ({
|
|
|
570
655
|
getLanguage: getLanguage2 = getLanguage,
|
|
571
656
|
overrideLanguage = false,
|
|
572
657
|
askBeforeExit = true,
|
|
573
|
-
preloadAssets = "lazy"
|
|
658
|
+
preloadAssets = "lazy",
|
|
659
|
+
parallelAssetsDownloadLimit = 15,
|
|
660
|
+
fetch: request = fetch
|
|
574
661
|
}) => {
|
|
662
|
+
const languages = Object.keys(translation);
|
|
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
|
});
|
|
@@ -675,12 +792,10 @@ var novely = ({
|
|
|
675
792
|
for (const migration of migrations) {
|
|
676
793
|
stored = migration(stored);
|
|
677
794
|
}
|
|
678
|
-
stored.meta[
|
|
679
|
-
if (overrideLanguage) {
|
|
795
|
+
if (overrideLanguage || !stored.meta[0]) {
|
|
680
796
|
stored.meta[0] = getLanguageWithoutParameters();
|
|
681
|
-
} else {
|
|
682
|
-
stored.meta[0] ||= getLanguageWithoutParameters();
|
|
683
797
|
}
|
|
798
|
+
stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
|
|
684
799
|
stored.meta[2] ??= 1;
|
|
685
800
|
stored.meta[3] ??= 1;
|
|
686
801
|
stored.meta[4] ??= 1;
|
|
@@ -746,12 +861,11 @@ var novely = ({
|
|
|
746
861
|
return;
|
|
747
862
|
let latest = save2 || $.get().saves.at(-1);
|
|
748
863
|
if (!latest) {
|
|
749
|
-
$.set({
|
|
750
|
-
saves: [initial],
|
|
751
|
-
data: klona(defaultData),
|
|
752
|
-
meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED, 1, 1, 1]
|
|
753
|
-
});
|
|
754
864
|
latest = klona(initial);
|
|
865
|
+
$.update((prev) => {
|
|
866
|
+
prev.saves.push(latest);
|
|
867
|
+
return prev;
|
|
868
|
+
});
|
|
755
869
|
}
|
|
756
870
|
const context = renderer.getContext(MAIN_CONTEXT_KEY);
|
|
757
871
|
const stack = useStack(context);
|
|
@@ -855,7 +969,7 @@ var novely = ({
|
|
|
855
969
|
ctx.meta.preview = true;
|
|
856
970
|
const processor = createQueueProcessor(queue);
|
|
857
971
|
await processor.run((action2, props) => {
|
|
858
|
-
if (
|
|
972
|
+
if (isAudioAction(action2))
|
|
859
973
|
return;
|
|
860
974
|
if (action2 === "vibrate")
|
|
861
975
|
return;
|
|
@@ -959,7 +1073,7 @@ var novely = ({
|
|
|
959
1073
|
push();
|
|
960
1074
|
},
|
|
961
1075
|
showCharacter({ ctx, push }, [character, emotion, className, style]) {
|
|
962
|
-
if (
|
|
1076
|
+
if (DEV2 && !characters[character].emotions[emotion]) {
|
|
963
1077
|
throw new Error(`Attempt to show character "${character}" with unknown emotion "${emotion}"`);
|
|
964
1078
|
}
|
|
965
1079
|
const handle = ctx.character(character);
|
|
@@ -1008,12 +1122,12 @@ var novely = ({
|
|
|
1008
1122
|
question = "";
|
|
1009
1123
|
}
|
|
1010
1124
|
const unwrappedChoices = choices.map(([content, action2, visible]) => {
|
|
1011
|
-
if (
|
|
1125
|
+
if (DEV2 && action2.length === 0 && (!visible || visible())) {
|
|
1012
1126
|
console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
|
|
1013
1127
|
}
|
|
1014
1128
|
return [unwrap2(content, data2), action2, visible];
|
|
1015
1129
|
});
|
|
1016
|
-
if (
|
|
1130
|
+
if (DEV2 && unwrappedChoices.length === 0) {
|
|
1017
1131
|
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
1132
|
}
|
|
1019
1133
|
ctx.choices(unwrap2(question, data2), unwrappedChoices, (selected) => {
|
|
@@ -1022,7 +1136,7 @@ var novely = ({
|
|
|
1022
1136
|
}
|
|
1023
1137
|
const stack = useStack(ctx);
|
|
1024
1138
|
const offset = isWithoutQuestion ? 0 : 1;
|
|
1025
|
-
if (
|
|
1139
|
+
if (DEV2 && !unwrappedChoices[selected]) {
|
|
1026
1140
|
throw new Error("Choice children is empty, either add content there or make item not selectable");
|
|
1027
1141
|
}
|
|
1028
1142
|
stack.value[0].push(["choice", selected + offset], [null, 0]);
|
|
@@ -1031,10 +1145,10 @@ var novely = ({
|
|
|
1031
1145
|
});
|
|
1032
1146
|
},
|
|
1033
1147
|
jump({ ctx, data: data2 }, [scene]) {
|
|
1034
|
-
if (
|
|
1148
|
+
if (DEV2 && !story[scene]) {
|
|
1035
1149
|
throw new Error(`Attempt to jump to unknown scene "${scene}"`);
|
|
1036
1150
|
}
|
|
1037
|
-
if (
|
|
1151
|
+
if (DEV2 && story[scene].length === 0) {
|
|
1038
1152
|
throw new Error(`Attempt to jump to empty scene "${scene}"`);
|
|
1039
1153
|
}
|
|
1040
1154
|
const stack = useStack(ctx);
|
|
@@ -1057,15 +1171,15 @@ var novely = ({
|
|
|
1057
1171
|
);
|
|
1058
1172
|
},
|
|
1059
1173
|
condition({ ctx }, [condition, variants]) {
|
|
1060
|
-
if (
|
|
1174
|
+
if (DEV2 && Object.values(variants).length === 0) {
|
|
1061
1175
|
throw new Error(`Attempt to use Condition action with empty variants object`);
|
|
1062
1176
|
}
|
|
1063
1177
|
if (!ctx.meta.restoring) {
|
|
1064
1178
|
const val = String(condition());
|
|
1065
|
-
if (
|
|
1179
|
+
if (DEV2 && !variants[val]) {
|
|
1066
1180
|
throw new Error(`Attempt to go to unknown variant "${val}"`);
|
|
1067
1181
|
}
|
|
1068
|
-
if (
|
|
1182
|
+
if (DEV2 && variants[val].length === 0) {
|
|
1069
1183
|
throw new Error(`Attempt to go to empty variant "${val}"`);
|
|
1070
1184
|
}
|
|
1071
1185
|
const stack = useStack(MAIN_CONTEXT_KEY);
|
|
@@ -1106,46 +1220,24 @@ var novely = ({
|
|
|
1106
1220
|
ctx.vibrate(pattern);
|
|
1107
1221
|
push();
|
|
1108
1222
|
},
|
|
1109
|
-
next({
|
|
1223
|
+
next({ push }) {
|
|
1110
1224
|
push();
|
|
1111
1225
|
},
|
|
1112
|
-
animateCharacter({ ctx,
|
|
1113
|
-
if (
|
|
1226
|
+
animateCharacter({ ctx, push }, [character, timeout, ...classes]) {
|
|
1227
|
+
if (DEV2 && classes.length === 0) {
|
|
1114
1228
|
throw new Error("Attempt to use AnimateCharacter without classes. Classes should be provided [https://novely.pages.dev/guide/actions/animateCharacter.html]");
|
|
1115
1229
|
}
|
|
1116
|
-
if (
|
|
1230
|
+
if (DEV2 && (timeout <= 0 || !Number.isFinite(timeout) || Number.isNaN(timeout))) {
|
|
1117
1231
|
throw new Error("Attempt to use AnimateCharacter with unacceptable timeout. It should be finite and greater than zero");
|
|
1118
1232
|
}
|
|
1119
1233
|
if (ctx.meta.preview)
|
|
1120
1234
|
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
|
-
});
|
|
1235
|
+
ctx.character(character).animate(timeout, classes);
|
|
1236
|
+
push();
|
|
1145
1237
|
},
|
|
1146
1238
|
text({ ctx, data: data2, forward }, text) {
|
|
1147
1239
|
const string = text.map((content) => unwrap2(content, data2)).join(" ");
|
|
1148
|
-
if (
|
|
1240
|
+
if (DEV2 && string.length === 0) {
|
|
1149
1241
|
throw new Error(`Action Text was called with empty string or array`);
|
|
1150
1242
|
}
|
|
1151
1243
|
ctx.text(string, forward);
|
|
@@ -1204,10 +1296,10 @@ var novely = ({
|
|
|
1204
1296
|
push();
|
|
1205
1297
|
},
|
|
1206
1298
|
block({ ctx }, [scene]) {
|
|
1207
|
-
if (
|
|
1299
|
+
if (DEV2 && !story[scene]) {
|
|
1208
1300
|
throw new Error(`Attempt to call Block action with unknown scene "${scene}"`);
|
|
1209
1301
|
}
|
|
1210
|
-
if (
|
|
1302
|
+
if (DEV2 && story[scene].length === 0) {
|
|
1211
1303
|
throw new Error(`Attempt to call Block action with empty scene "${scene}"`);
|
|
1212
1304
|
}
|
|
1213
1305
|
if (!ctx.meta.restoring) {
|