@novely/core 0.24.0 → 0.26.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.
@@ -161,7 +161,7 @@ var Novely = (() => {
161
161
  });
162
162
 
163
163
  // src/constants.ts
164
- var SKIPPED_DURING_RESTORE = /* @__PURE__ */ new Set(["dialog", "choice", "input", "vibrate", "text"]);
164
+ var SKIPPED_DURING_RESTORE = /* @__PURE__ */ new Set(["dialog", "say", "choice", "input", "vibrate", "text"]);
165
165
  var BLOCK_EXIT_STATEMENTS = /* @__PURE__ */ new Set(["choice:exit", "condition:exit", "block:exit"]);
166
166
  var BLOCK_STATEMENTS = /* @__PURE__ */ new Set(["choice", "condition", "block"]);
167
167
  var AUDIO_ACTIONS = /* @__PURE__ */ new Set([
@@ -256,8 +256,8 @@ var Novely = (() => {
256
256
  return startsWith("http") || startsWith("/") || startsWith(".") || startsWith("data");
257
257
  };
258
258
  var str = String;
259
- var isUserRequiredAction = (action, meta) => {
260
- return action === "custom" && meta[0] && meta[0].requireUserAction;
259
+ var isUserRequiredAction = ([action, ...meta]) => {
260
+ return Boolean(action === "custom" && meta[0] && meta[0].requireUserAction);
261
261
  };
262
262
  var getLanguage = (languages) => {
263
263
  let { language } = navigator;
@@ -378,11 +378,13 @@ var Novely = (() => {
378
378
  };
379
379
  return MAP[action];
380
380
  };
381
- var getActionsFromPath = (story, path, raw = false) => {
381
+ var getActionsFromPath = (story, path, filter) => {
382
382
  let current = story;
383
383
  let precurrent;
384
384
  let ignoreNested = false;
385
385
  let index = 0;
386
+ let skipPreserve = void 0;
387
+ const skip = /* @__PURE__ */ new Set();
386
388
  const max = path.reduce((acc, [type, val]) => {
387
389
  if (isNull(type) && isNumber(val)) {
388
390
  return acc + 1;
@@ -411,21 +413,19 @@ var Novely = (() => {
411
413
  const item = current[i];
412
414
  if (!isAction(item))
413
415
  continue;
414
- const [action, ...meta] = item;
415
- const push = () => {
416
- queue.push([action, meta]);
417
- };
418
- if (raw) {
419
- push();
420
- continue;
416
+ const [action] = item;
417
+ const last = index === max && i === val;
418
+ const shouldSkip = isSkippedDuringRestore(action) || isUserRequiredAction(item);
419
+ if (shouldSkip) {
420
+ skip.add(item);
421
421
  }
422
- if (isSkippedDuringRestore(action) || isUserRequiredAction(action, meta)) {
423
- if (index === max && i === val) {
424
- push();
425
- }
422
+ if (shouldSkip && last) {
423
+ skipPreserve = item;
424
+ }
425
+ if (filter && shouldSkip && !last) {
426
426
  continue;
427
427
  } else {
428
- push();
428
+ queue.push(item);
429
429
  }
430
430
  }
431
431
  }
@@ -444,9 +444,13 @@ var Novely = (() => {
444
444
  ignoreNested = true;
445
445
  }
446
446
  }
447
- return queue;
447
+ return {
448
+ queue,
449
+ skip,
450
+ skipPreserve
451
+ };
448
452
  };
449
- var createQueueProcessor = (queue) => {
453
+ var createQueueProcessor = (queue, options) => {
450
454
  const processedQueue = [];
451
455
  const keep = /* @__PURE__ */ new Set();
452
456
  const characters = /* @__PURE__ */ new Set();
@@ -455,70 +459,83 @@ var Novely = (() => {
455
459
  sound: /* @__PURE__ */ new Set()
456
460
  };
457
461
  const next = (i) => queue.slice(i + 1);
458
- for (const [i, [action, meta]] of queue.entries()) {
462
+ for (const [i, item] of queue.entries()) {
463
+ const [action, ...params] = item;
459
464
  keep.add(action);
465
+ if (options.skip.has(item) && item !== options.skipPreserve) {
466
+ continue;
467
+ }
460
468
  if (action === "function" || action === "custom") {
461
- if (action === "custom" && meta[0].callOnlyLatest) {
462
- const notLatest = next(i).some(([, _meta]) => {
463
- if (!_meta || !meta)
469
+ if (action === "custom" && params[0].callOnlyLatest) {
470
+ const notLatest = next(i).some(([, func]) => {
471
+ if (!isFunction(func))
464
472
  return false;
465
- const c0 = _meta[0];
466
- const c1 = meta[0];
467
- const isIdenticalID = c0.id && c1.id && c0.id === c1.id;
473
+ const c0 = func;
474
+ const c1 = params[0];
475
+ const isIdenticalID = Boolean(c0.id && c1.id && c0.id === c1.id);
468
476
  const isIdenticalByReference = c0 === c1;
469
477
  return isIdenticalID || isIdenticalByReference || str(c0) === str(c1);
470
478
  });
471
479
  if (notLatest)
472
480
  continue;
473
481
  }
474
- processedQueue.push([action, meta]);
482
+ processedQueue.push(item);
475
483
  } else if (action === "showCharacter" || action === "playSound" || action === "playMusic" || action === "voice") {
476
484
  const closing = getOppositeAction(action);
477
- const skip = next(i).some(([_action, _meta]) => {
478
- if (!_meta || !meta)
479
- return false;
480
- if (_meta[0] !== meta[0])
485
+ const skip = next(i).some(([_action, target]) => {
486
+ if (target !== params[0]) {
481
487
  return false;
488
+ }
482
489
  return _action === closing || _action === action;
483
490
  });
484
491
  if (skip)
485
492
  continue;
486
493
  if (action === "showCharacter") {
487
- characters.add(meta[0]);
494
+ characters.add(params[0]);
488
495
  } else if (action === "playMusic") {
489
- audio.music.add(meta[0]);
496
+ audio.music.add(params[0]);
490
497
  } else if (action === "playSound") {
491
- audio.sound.add(meta[0]);
498
+ audio.sound.add(params[0]);
492
499
  }
493
- processedQueue.push([action, meta]);
494
- } else if (action === "showBackground" || action === "animateCharacter" || action === "preload") {
495
- const skip = next(i).some(([_action], i2, array) => action === _action);
500
+ processedQueue.push(item);
501
+ } else if (action === "showBackground" || action === "preload") {
502
+ const skip = next(i).some(([_action]) => action === _action);
503
+ if (skip)
504
+ continue;
505
+ processedQueue.push(item);
506
+ } else if (action === "animateCharacter") {
507
+ const skip = next(i).some(([_action, character], j, array) => {
508
+ if (action === _action && character === params[0]) {
509
+ return true;
510
+ }
511
+ const next2 = array.slice(j);
512
+ const characterWillAnimate = next2.some(([__action, __character]) => action === __action);
513
+ const hasBlockingActions = next2.some((item2) => options.skip.has(item2));
514
+ return characterWillAnimate && hasBlockingActions;
515
+ });
496
516
  if (skip)
497
517
  continue;
498
- processedQueue.push([action, meta]);
518
+ processedQueue.push(item);
499
519
  } else {
500
- processedQueue.push([action, meta]);
520
+ processedQueue.push(item);
501
521
  }
502
522
  }
503
523
  const run = async (match) => {
504
- for await (const [action, meta] of processedQueue) {
505
- const result = match(action, meta);
524
+ for await (const [action, ...params] of processedQueue) {
525
+ const result = match(action, params);
506
526
  if (isPromise(result)) {
507
527
  await result;
508
528
  }
509
529
  }
510
530
  processedQueue.length = 0;
511
531
  };
512
- const getKeep = () => {
513
- return {
532
+ return {
533
+ run,
534
+ keep: {
514
535
  keep,
515
536
  characters,
516
537
  audio
517
- };
518
- };
519
- return {
520
- run,
521
- getKeep
538
+ }
522
539
  };
523
540
  };
524
541
  var getStack = (ctx) => {
@@ -750,17 +767,17 @@ var Novely = (() => {
750
767
  output.push(input);
751
768
  return output;
752
769
  };
753
- var unwrap = (c) => {
770
+ var flattenAllowedContent = (c, state) => {
754
771
  if (Array.isArray(c)) {
755
- return c.map((item) => unwrap(item)).join("<br>");
772
+ return c.map((item) => flattenAllowedContent(item, state)).join("<br>");
756
773
  }
757
774
  if (typeof c === "function") {
758
- return unwrap(c());
775
+ return flattenAllowedContent(c(state), state);
759
776
  }
760
777
  return c;
761
778
  };
762
779
  var replace = (str2, obj, pluralization, actions, pr) => {
763
- return unwrap(str2).replaceAll(RGX, (x, key, y) => {
780
+ return str2.replaceAll(RGX, (x, key, y) => {
764
781
  x = 0;
765
782
  y = obj;
766
783
  const [pathstr, plural, action] = split(key.trim(), ["@", "%"]);
@@ -820,7 +837,8 @@ var Novely = (() => {
820
837
  askBeforeExit = true,
821
838
  preloadAssets = "lazy",
822
839
  parallelAssetsDownloadLimit = 15,
823
- fetch: request = fetch
840
+ fetch: request = fetch,
841
+ saveOnUnload = true
824
842
  }) => {
825
843
  const languages = Object.keys(translation);
826
844
  const limitScript = (0, import_p_limit.default)(1);
@@ -910,46 +928,57 @@ var Novely = (() => {
910
928
  };
911
929
  }
912
930
  });
913
- function state(value) {
914
- const stack = useStack(MAIN_CONTEXT_KEY);
915
- if (!value)
916
- return stack.value[1];
917
- const prev = stack.value[1];
918
- const val = isFunction(value) ? value(prev) : deepmerge(prev, value);
919
- stack.value[1] = val;
920
- }
921
- const getDefaultSave = (state2 = {}) => {
931
+ const getDefaultSave = (state = {}) => {
922
932
  return [
923
933
  [
924
934
  ["jump", "start"],
925
935
  [null, 0]
926
936
  ],
927
- state2,
937
+ state,
928
938
  [intime(Date.now()), "auto"]
929
939
  ];
930
940
  };
931
941
  const getLanguageWithoutParameters = () => {
932
- return getLanguage2(languages, getLanguage);
942
+ const language = getLanguage2(languages, getLanguage);
943
+ if (languages.includes(language)) {
944
+ return language;
945
+ }
946
+ if (DEV) {
947
+ throw new Error(`Attempt to use unsupported language "${language}". Supported languages: ${languages.join(", ")}.`);
948
+ }
949
+ throw 0;
933
950
  };
934
951
  const initialData = {
935
952
  saves: [],
936
953
  data: klona(defaultData),
937
954
  meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED, 1, 1, 1]
938
955
  };
939
- const coreData = {
956
+ const storageData = store(initialData);
957
+ const coreData = store({
940
958
  dataLoaded: false
959
+ });
960
+ const onDataLoadedPromise = ({ cancelled }) => {
961
+ if (cancelled) {
962
+ dataLoaded.promise.then(onDataLoadedPromise);
963
+ return;
964
+ }
965
+ coreData.update((data2) => {
966
+ data2.dataLoaded = true;
967
+ return data2;
968
+ });
941
969
  };
942
- const $ = store(initialData);
943
- const $$ = store(coreData);
970
+ dataLoaded.promise.then(onDataLoadedPromise);
944
971
  const onStorageDataChange = (value) => {
945
- if ($$.get().dataLoaded)
972
+ if (coreData.get().dataLoaded)
946
973
  storage.set(value);
947
974
  };
948
975
  const throttledOnStorageDataChange = throttle(onStorageDataChange, throttleTimeout);
949
976
  const throttledEmergencyOnStorageDataChange = throttle(() => {
950
- onStorageDataChange($.get());
977
+ if (saveOnUnload === true || saveOnUnload === "prod" && !DEV) {
978
+ onStorageDataChange(storageData.get());
979
+ }
951
980
  }, 10);
952
- $.subscribe(throttledOnStorageDataChange);
981
+ storageData.subscribe(throttledOnStorageDataChange);
953
982
  const getStoredData = async () => {
954
983
  let stored = await storage.get();
955
984
  for (const migration of migrations) {
@@ -965,9 +994,8 @@ var Novely = (() => {
965
994
  if (isEmpty(stored.data)) {
966
995
  stored.data = defaultData;
967
996
  }
968
- $$.update((prev) => (prev.dataLoaded = true, prev));
969
997
  dataLoaded.resolve();
970
- $.set(stored);
998
+ storageData.set(stored);
971
999
  };
972
1000
  storageDelay.then(getStoredData);
973
1001
  const initial = getDefaultSave(klona(defaultState));
@@ -979,13 +1007,13 @@ var Novely = (() => {
979
1007
  addEventListener("visibilitychange", onVisibilityChange);
980
1008
  addEventListener("beforeunload", throttledEmergencyOnStorageDataChange);
981
1009
  const save = (override = false, type = override ? "auto" : "manual") => {
982
- if (!$$.get().dataLoaded)
1010
+ if (!coreData.get().dataLoaded)
983
1011
  return;
984
1012
  if (!autosaves && type === "auto")
985
1013
  return;
986
1014
  const stack = useStack(MAIN_CONTEXT_KEY);
987
1015
  const current = klona(stack.value);
988
- $.update((prev) => {
1016
+ storageData.update((prev) => {
989
1017
  const isLatest = findLastIndex(prev.saves, (value) => times.has(value[2][0])) === prev.saves.length - 1;
990
1018
  current[2][0] = intime(Date.now());
991
1019
  current[2][1] = type;
@@ -1003,29 +1031,29 @@ var Novely = (() => {
1003
1031
  });
1004
1032
  };
1005
1033
  const newGame = () => {
1006
- if (!$$.get().dataLoaded)
1034
+ if (!coreData.get().dataLoaded)
1007
1035
  return;
1008
1036
  const save2 = getDefaultSave(klona(defaultState));
1009
1037
  if (autosaves) {
1010
- $.update((prev) => {
1038
+ storageData.update((prev) => {
1011
1039
  return prev.saves.push(save2), prev;
1012
1040
  });
1013
1041
  }
1014
1042
  restore(save2);
1015
1043
  };
1016
1044
  const set = (save2, ctx) => {
1017
- const stack = useStack(ctx || renderer.getContext(MAIN_CONTEXT_KEY));
1045
+ const stack = useStack(ctx || MAIN_CONTEXT_KEY);
1018
1046
  stack.value = save2;
1019
1047
  return restore(save2);
1020
1048
  };
1021
1049
  let interacted = 0;
1022
1050
  const restore = async (save2) => {
1023
- if (!$$.get().dataLoaded)
1051
+ if (!coreData.get().dataLoaded)
1024
1052
  return;
1025
- let latest = save2 || $.get().saves.at(-1);
1053
+ let latest = save2 || storageData.get().saves.at(-1);
1026
1054
  if (!latest) {
1027
1055
  latest = klona(initial);
1028
- $.update((prev) => {
1056
+ storageData.update((prev) => {
1029
1057
  prev.saves.push(latest);
1030
1058
  return prev;
1031
1059
  });
@@ -1036,19 +1064,22 @@ var Novely = (() => {
1036
1064
  const previous = stack.previous;
1037
1065
  const [path] = stack.value = latest;
1038
1066
  renderer.ui.showScreen("game");
1039
- const queue = getActionsFromPath(story, path);
1040
- const processor = createQueueProcessor(queue);
1041
- const { keep, characters: characters2, audio } = processor.getKeep();
1067
+ const { queue, skip, skipPreserve } = getActionsFromPath(story, path, false);
1068
+ const processor = createQueueProcessor(queue, {
1069
+ skip,
1070
+ skipPreserve
1071
+ });
1072
+ const { keep, characters: characters2, audio } = processor.keep;
1042
1073
  if (previous) {
1043
- const prevQueue = getActionsFromPath(story, previous[0], true);
1044
- const currQueue = getActionsFromPath(story, path, true);
1045
- for (let i = prevQueue.length - 1; i > currQueue.length; i--) {
1074
+ const { queue: prevQueue } = getActionsFromPath(story, previous[0], false);
1075
+ for (let i = prevQueue.length - 1; i > queue.length; i--) {
1046
1076
  const element = prevQueue[i];
1047
- if (isAction(element)) {
1048
- const [action2, props] = element;
1049
- if (action2 === "custom") {
1050
- context.clearCustom(props[0]);
1051
- }
1077
+ if (!isAction(element)) {
1078
+ continue;
1079
+ }
1080
+ const [action2, fn] = element;
1081
+ if (action2 === "custom") {
1082
+ context.clearCustom(fn);
1052
1083
  }
1053
1084
  }
1054
1085
  }
@@ -1068,9 +1099,6 @@ var Novely = (() => {
1068
1099
  render(context);
1069
1100
  };
1070
1101
  const refer = (path) => {
1071
- if (!path) {
1072
- path = useStack(MAIN_CONTEXT_KEY).value[0];
1073
- }
1074
1102
  let current = story;
1075
1103
  let precurrent = story;
1076
1104
  const blocks = [];
@@ -1109,7 +1137,7 @@ var Novely = (() => {
1109
1137
  ctx.audio.destroy();
1110
1138
  const [time, type] = current[2];
1111
1139
  if (type === "auto" && interacted <= 1 && times.has(time)) {
1112
- $.update((prev) => {
1140
+ storageData.update((prev) => {
1113
1141
  prev.saves = prev.saves.filter((save2) => save2 !== current);
1114
1142
  return prev;
1115
1143
  });
@@ -1126,11 +1154,13 @@ var Novely = (() => {
1126
1154
  return translation[lang].internal[key];
1127
1155
  };
1128
1156
  const preview = async ([path, data2], name) => {
1129
- const queue = getActionsFromPath(story, path);
1157
+ const { queue } = getActionsFromPath(story, path, true);
1130
1158
  const ctx = renderer.getContext(name);
1131
1159
  ctx.meta.restoring = true;
1132
1160
  ctx.meta.preview = true;
1133
- const processor = createQueueProcessor(queue);
1161
+ const processor = createQueueProcessor(queue, {
1162
+ skip: /* @__PURE__ */ new Set()
1163
+ });
1134
1164
  await processor.run((action2, props) => {
1135
1165
  if (isAudioAction(action2))
1136
1166
  return;
@@ -1147,6 +1177,23 @@ var Novely = (() => {
1147
1177
  const removeContext = (name) => {
1148
1178
  STACK_MAP.delete(name);
1149
1179
  };
1180
+ const getStateAtCtx = (context) => {
1181
+ return useStack(context).value[1];
1182
+ };
1183
+ const getStateFunction = (context) => {
1184
+ const stack = useStack(context);
1185
+ const state = (value) => {
1186
+ const _state = getStateAtCtx(context);
1187
+ if (!value) {
1188
+ return _state;
1189
+ }
1190
+ const prev = _state;
1191
+ const val = isFunction(value) ? value(prev) : deepmerge(prev, value);
1192
+ stack.value[1] = val;
1193
+ return void 0;
1194
+ };
1195
+ return state;
1196
+ };
1150
1197
  const renderer = createRenderer({
1151
1198
  mainContextKey: MAIN_CONTEXT_KEY,
1152
1199
  characters,
@@ -1159,9 +1206,10 @@ var Novely = (() => {
1159
1206
  t,
1160
1207
  preview,
1161
1208
  removeContext,
1209
+ getStateFunction,
1162
1210
  languages,
1163
- $,
1164
- $$
1211
+ storageData,
1212
+ coreData
1165
1213
  });
1166
1214
  const useStack = createUseStackFunction(renderer);
1167
1215
  useStack(MAIN_CONTEXT_KEY).push(initial);
@@ -1175,15 +1223,19 @@ var Novely = (() => {
1175
1223
  stack.push(current);
1176
1224
  save(true, "auto");
1177
1225
  };
1178
- const next = (ctx) => {
1179
- const stack = useStack(ctx);
1180
- const path = stack.value[0];
1226
+ const nextPath = (path) => {
1181
1227
  const last = path.at(-1);
1182
1228
  if (last && (isNull(last[0]) || last[0] === "jump") && isNumber(last[1])) {
1183
1229
  last[1]++;
1184
1230
  } else {
1185
1231
  path.push([null, 0]);
1186
1232
  }
1233
+ return path;
1234
+ };
1235
+ const next = (ctx) => {
1236
+ const stack = useStack(ctx);
1237
+ const path = stack.value[0];
1238
+ nextPath(path);
1187
1239
  };
1188
1240
  const matchActionInit = {
1189
1241
  getContext: renderer.getContext,
@@ -1205,7 +1257,7 @@ var Novely = (() => {
1205
1257
  wait({ ctx, push }, [time]) {
1206
1258
  if (ctx.meta.restoring)
1207
1259
  return;
1208
- setTimeout(push, isFunction(time) ? time() : time);
1260
+ setTimeout(push, isFunction(time) ? time(getStateAtCtx(ctx)) : time);
1209
1261
  },
1210
1262
  showBackground({ ctx, push }, [background]) {
1211
1263
  ctx.background(background);
@@ -1251,7 +1303,7 @@ var Novely = (() => {
1251
1303
  const name = (() => {
1252
1304
  const c = character;
1253
1305
  const cs = characters;
1254
- const [lang] = $.get().meta;
1306
+ const [lang] = storageData.get().meta;
1255
1307
  if (c && c in cs) {
1256
1308
  const block = cs[c].name;
1257
1309
  if (typeof block === "string") {
@@ -1264,15 +1316,31 @@ var Novely = (() => {
1264
1316
  return c || "";
1265
1317
  })();
1266
1318
  ctx.dialog(
1267
- unwrap2(content, data2),
1268
- unwrap2(name, data2),
1319
+ templateReplace(content, data2),
1320
+ templateReplace(name, data2),
1269
1321
  character,
1270
1322
  emotion,
1271
1323
  forward
1272
1324
  );
1273
1325
  },
1326
+ say({ ctx, data: data2 }, [character, content]) {
1327
+ if (DEV && !characters[character]) {
1328
+ throw new Error(`Attempt to call Say action with unknown character "${character}"`);
1329
+ }
1330
+ match("dialog", [character, content], {
1331
+ ctx,
1332
+ data: data2
1333
+ });
1334
+ },
1274
1335
  function({ ctx, push }, [fn]) {
1275
- const result = fn(ctx.meta.restoring, ctx.meta.goingBack, ctx.meta.preview);
1336
+ const { restoring, goingBack, preview: preview2 } = ctx.meta;
1337
+ const result = fn({
1338
+ lang: storageData.get().meta[0],
1339
+ goingBack,
1340
+ restoring,
1341
+ preview: preview2,
1342
+ state: getStateFunction(ctx)
1343
+ });
1276
1344
  if (!ctx.meta.restoring) {
1277
1345
  result ? result.then(push) : push();
1278
1346
  }
@@ -1284,22 +1352,26 @@ var Novely = (() => {
1284
1352
  choices.unshift(question);
1285
1353
  question = "";
1286
1354
  }
1287
- const unwrappedChoices = choices.map(([content, action2, visible]) => {
1288
- if (DEV && action2.length === 0 && (!visible || visible())) {
1355
+ const transformedChoices = choices.map(([content, action2, visible]) => {
1356
+ const shown = !visible || visible({
1357
+ lang: storageData.get().meta[0],
1358
+ state: getStateAtCtx(ctx)
1359
+ });
1360
+ if (DEV && action2.length === 0 && !shown) {
1289
1361
  console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
1290
1362
  }
1291
- return [unwrap2(content, data2), action2, visible];
1363
+ return [templateReplace(content, data2), shown];
1292
1364
  });
1293
- if (DEV && unwrappedChoices.length === 0) {
1365
+ if (DEV && transformedChoices.length === 0) {
1294
1366
  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]`);
1295
1367
  }
1296
- ctx.choices(unwrap2(question, data2), unwrappedChoices, (selected) => {
1368
+ ctx.choices(templateReplace(question, data2), transformedChoices, (selected) => {
1297
1369
  if (!ctx.meta.preview) {
1298
1370
  enmemory(ctx);
1299
1371
  }
1300
1372
  const stack = useStack(ctx);
1301
1373
  const offset = isWithoutQuestion ? 0 : 1;
1302
- if (DEV && !unwrappedChoices[selected]) {
1374
+ if (DEV && !transformedChoices[selected]) {
1303
1375
  throw new Error("Choice children is empty, either add content there or make item not selectable");
1304
1376
  }
1305
1377
  stack.value[0].push(["choice", selected + offset], [null, 0]);
@@ -1338,7 +1410,7 @@ var Novely = (() => {
1338
1410
  throw new Error(`Attempt to use Condition action with empty variants object`);
1339
1411
  }
1340
1412
  if (!ctx.meta.restoring) {
1341
- const val = String(condition());
1413
+ const val = String(condition(getStateAtCtx(ctx)));
1342
1414
  if (DEV && !variants[val]) {
1343
1415
  throw new Error(`Attempt to go to unknown variant "${val}"`);
1344
1416
  }
@@ -1361,7 +1433,7 @@ var Novely = (() => {
1361
1433
  },
1362
1434
  input({ ctx, data: data2, forward }, [question, onInput, setup]) {
1363
1435
  ctx.input(
1364
- unwrap2(question, data2),
1436
+ templateReplace(question, data2),
1365
1437
  onInput,
1366
1438
  setup || noop,
1367
1439
  forward
@@ -1399,7 +1471,7 @@ var Novely = (() => {
1399
1471
  push();
1400
1472
  },
1401
1473
  text({ ctx, data: data2, forward }, text) {
1402
- const string = text.map((content) => unwrap2(content, data2)).join(" ");
1474
+ const string = text.map((content) => templateReplace(content, data2)).join(" ");
1403
1475
  if (DEV && string.length === 0) {
1404
1476
  throw new Error(`Action Text was called with empty string or array`);
1405
1477
  }
@@ -1491,52 +1563,106 @@ var Novely = (() => {
1491
1563
  const interactivity = (value = false) => {
1492
1564
  interacted = value ? interacted + 1 : 0;
1493
1565
  };
1494
- const unwrap2 = (content, values) => {
1566
+ const templateReplace = (content, values) => {
1495
1567
  const {
1496
1568
  data: data2,
1497
1569
  meta: [lang]
1498
- } = $.get();
1499
- const obj = values ? values : data2;
1500
- const cnt = isFunction(content) ? content() : typeof content === "string" ? content : content[lang];
1501
- const str2 = isFunction(cnt) ? cnt() : cnt;
1502
- const trans = translation[lang];
1503
- if (trans.actions || trans.plural) {
1504
- return replace(str2, obj, trans.plural, trans.actions, new Intl.PluralRules(trans.tag || lang));
1505
- }
1506
- return replace(str2, obj);
1570
+ } = storageData.get();
1571
+ const obj = values || data2;
1572
+ const cnt = isFunction(content) ? content(obj) : typeof content === "string" ? content : content[lang];
1573
+ const str2 = flattenAllowedContent(
1574
+ isFunction(cnt) ? cnt(obj) : cnt,
1575
+ obj
1576
+ );
1577
+ const t2 = translation[lang];
1578
+ const pluralRules = (t2.plural || t2.actions) && new Intl.PluralRules(t2.tag || lang);
1579
+ return replace(
1580
+ str2,
1581
+ obj,
1582
+ t2.plural,
1583
+ t2.actions,
1584
+ pluralRules
1585
+ );
1507
1586
  };
1508
- function data(value) {
1587
+ const data = (value) => {
1588
+ const _data = storageData.get().data;
1509
1589
  if (!value)
1510
- return $.get().data;
1511
- const prev = $.get().data;
1512
- const val = isFunction(value) ? value(prev) : deepmerge(prev, value);
1513
- $.update((prev2) => {
1514
- prev2.data = val;
1515
- return prev2;
1590
+ return _data;
1591
+ const val = isFunction(value) ? value(_data) : deepmerge(_data, value);
1592
+ storageData.update((prev) => {
1593
+ prev.data = val;
1594
+ return prev;
1516
1595
  });
1517
- }
1596
+ return void 0;
1597
+ };
1518
1598
  return {
1519
1599
  /**
1520
1600
  * Function to set game script
1601
+ *
1602
+ * @example
1603
+ * ```ts
1604
+ * engine.script({
1605
+ * start: [
1606
+ * action.function(() => {})
1607
+ * ]
1608
+ * })
1609
+ * ```
1521
1610
  */
1522
1611
  script,
1523
1612
  /**
1524
- * Function to get actions
1613
+ * Get actions
1614
+ *
1615
+ * @example
1616
+ * ```ts
1617
+ * engine.script({
1618
+ * start: [
1619
+ * action.function(() => {})
1620
+ * ]
1621
+ * })
1622
+ * ```
1525
1623
  */
1526
1624
  action,
1527
1625
  /**
1528
- * State that belongs to games
1626
+ * @deprecated Will be removed BUT replaced with state passed into actions as a parameter
1529
1627
  */
1530
- state,
1628
+ state: getStateFunction(MAIN_CONTEXT_KEY),
1531
1629
  /**
1532
- * Unlike `state`, stored at global scope instead and shared between games
1630
+ * Store data between games
1631
+ *
1632
+ * @example
1633
+ * ```ts
1634
+ * engine.script({
1635
+ * start: [
1636
+ * action.function(() => {
1637
+ * // Paid content should be purchased only once
1638
+ * // So it will be available in any save
1639
+ * data({ paid_content_purchased: true })
1640
+ * })
1641
+ * ]
1642
+ * })
1643
+ * ```
1533
1644
  */
1534
1645
  data,
1535
1646
  /**
1536
- * Unwraps translatable content to a string value
1647
+ * @deprecated Renamed into `templateReplace`
1537
1648
  */
1538
1649
  unwrap(content) {
1539
- return unwrap2(content, $.get().data);
1650
+ return templateReplace(content);
1651
+ },
1652
+ /**
1653
+ * Replaces content inside {{braces}} with using global data
1654
+ * @example
1655
+ * ```ts
1656
+ * data({ name: 'Alexei' })
1657
+ *
1658
+ * templateReplace('{{name}} is our hero')
1659
+ * templateReplace({
1660
+ * en: (data) => 'Hello, ' + data.name
1661
+ * })
1662
+ * ```
1663
+ */
1664
+ templateReplace(content) {
1665
+ return templateReplace(content);
1540
1666
  },
1541
1667
  /**
1542
1668
  * Cancel data loading, hide UI, ignore page change events