@novely/core 0.23.0 → 0.25.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);
496
503
  if (skip)
497
504
  continue;
498
- processedQueue.push([action, meta]);
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
+ });
516
+ if (skip)
517
+ continue;
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(), ["@", "%"]);
@@ -810,7 +827,6 @@ var Novely = (() => {
810
827
  renderer: createRenderer,
811
828
  initialScreen = "mainmenu",
812
829
  translation,
813
- languages,
814
830
  state: defaultState,
815
831
  data: defaultData,
816
832
  autosaves = true,
@@ -821,8 +837,10 @@ var Novely = (() => {
821
837
  askBeforeExit = true,
822
838
  preloadAssets = "lazy",
823
839
  parallelAssetsDownloadLimit = 15,
824
- fetch: request = fetch
840
+ fetch: request = fetch,
841
+ saveOnUnload = true
825
842
  }) => {
843
+ const languages = Object.keys(translation);
826
844
  const limitScript = (0, import_p_limit.default)(1);
827
845
  const limitAssetsDownload = (0, import_p_limit.default)(parallelAssetsDownloadLimit);
828
846
  const story = {};
@@ -910,44 +928,55 @@ 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 $ = 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($.get());
979
+ }
951
980
  }, 10);
952
981
  $.subscribe(throttledOnStorageDataChange);
953
982
  const getStoredData = async () => {
@@ -955,19 +984,16 @@ var Novely = (() => {
955
984
  for (const migration of migrations) {
956
985
  stored = migration(stored);
957
986
  }
958
- stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
959
- if (overrideLanguage) {
987
+ if (overrideLanguage || !stored.meta[0]) {
960
988
  stored.meta[0] = getLanguageWithoutParameters();
961
- } else {
962
- stored.meta[0] ||= getLanguageWithoutParameters();
963
989
  }
990
+ stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
964
991
  stored.meta[2] ??= 1;
965
992
  stored.meta[3] ??= 1;
966
993
  stored.meta[4] ??= 1;
967
994
  if (isEmpty(stored.data)) {
968
995
  stored.data = defaultData;
969
996
  }
970
- $$.update((prev) => (prev.dataLoaded = true, prev));
971
997
  dataLoaded.resolve();
972
998
  $.set(stored);
973
999
  };
@@ -981,7 +1007,7 @@ var Novely = (() => {
981
1007
  addEventListener("visibilitychange", onVisibilityChange);
982
1008
  addEventListener("beforeunload", throttledEmergencyOnStorageDataChange);
983
1009
  const save = (override = false, type = override ? "auto" : "manual") => {
984
- if (!$$.get().dataLoaded)
1010
+ if (!coreData.get().dataLoaded)
985
1011
  return;
986
1012
  if (!autosaves && type === "auto")
987
1013
  return;
@@ -1005,7 +1031,7 @@ var Novely = (() => {
1005
1031
  });
1006
1032
  };
1007
1033
  const newGame = () => {
1008
- if (!$$.get().dataLoaded)
1034
+ if (!coreData.get().dataLoaded)
1009
1035
  return;
1010
1036
  const save2 = getDefaultSave(klona(defaultState));
1011
1037
  if (autosaves) {
@@ -1016,13 +1042,13 @@ var Novely = (() => {
1016
1042
  restore(save2);
1017
1043
  };
1018
1044
  const set = (save2, ctx) => {
1019
- const stack = useStack(ctx || renderer.getContext(MAIN_CONTEXT_KEY));
1045
+ const stack = useStack(ctx || MAIN_CONTEXT_KEY);
1020
1046
  stack.value = save2;
1021
1047
  return restore(save2);
1022
1048
  };
1023
1049
  let interacted = 0;
1024
1050
  const restore = async (save2) => {
1025
- if (!$$.get().dataLoaded)
1051
+ if (!coreData.get().dataLoaded)
1026
1052
  return;
1027
1053
  let latest = save2 || $.get().saves.at(-1);
1028
1054
  if (!latest) {
@@ -1038,19 +1064,22 @@ var Novely = (() => {
1038
1064
  const previous = stack.previous;
1039
1065
  const [path] = stack.value = latest;
1040
1066
  renderer.ui.showScreen("game");
1041
- const queue = getActionsFromPath(story, path);
1042
- const processor = createQueueProcessor(queue);
1043
- 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;
1044
1073
  if (previous) {
1045
- const prevQueue = getActionsFromPath(story, previous[0], true);
1046
- const currQueue = getActionsFromPath(story, path, true);
1047
- 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--) {
1048
1076
  const element = prevQueue[i];
1049
- if (isAction(element)) {
1050
- const [action2, props] = element;
1051
- if (action2 === "custom") {
1052
- context.clearCustom(props[0]);
1053
- }
1077
+ if (!isAction(element)) {
1078
+ continue;
1079
+ }
1080
+ const [action2, fn] = element;
1081
+ if (action2 === "custom") {
1082
+ context.clearCustom(fn);
1054
1083
  }
1055
1084
  }
1056
1085
  }
@@ -1070,9 +1099,6 @@ var Novely = (() => {
1070
1099
  render(context);
1071
1100
  };
1072
1101
  const refer = (path) => {
1073
- if (!path) {
1074
- path = useStack(MAIN_CONTEXT_KEY).value[0];
1075
- }
1076
1102
  let current = story;
1077
1103
  let precurrent = story;
1078
1104
  const blocks = [];
@@ -1128,11 +1154,13 @@ var Novely = (() => {
1128
1154
  return translation[lang].internal[key];
1129
1155
  };
1130
1156
  const preview = async ([path, data2], name) => {
1131
- const queue = getActionsFromPath(story, path);
1157
+ const { queue } = getActionsFromPath(story, path, true);
1132
1158
  const ctx = renderer.getContext(name);
1133
1159
  ctx.meta.restoring = true;
1134
1160
  ctx.meta.preview = true;
1135
- const processor = createQueueProcessor(queue);
1161
+ const processor = createQueueProcessor(queue, {
1162
+ skip: /* @__PURE__ */ new Set()
1163
+ });
1136
1164
  await processor.run((action2, props) => {
1137
1165
  if (isAudioAction(action2))
1138
1166
  return;
@@ -1149,6 +1177,23 @@ var Novely = (() => {
1149
1177
  const removeContext = (name) => {
1150
1178
  STACK_MAP.delete(name);
1151
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
+ };
1152
1197
  const renderer = createRenderer({
1153
1198
  mainContextKey: MAIN_CONTEXT_KEY,
1154
1199
  characters,
@@ -1161,9 +1206,10 @@ var Novely = (() => {
1161
1206
  t,
1162
1207
  preview,
1163
1208
  removeContext,
1209
+ getStateFunction,
1164
1210
  languages,
1165
1211
  $,
1166
- $$
1212
+ $$: coreData
1167
1213
  });
1168
1214
  const useStack = createUseStackFunction(renderer);
1169
1215
  useStack(MAIN_CONTEXT_KEY).push(initial);
@@ -1177,15 +1223,19 @@ var Novely = (() => {
1177
1223
  stack.push(current);
1178
1224
  save(true, "auto");
1179
1225
  };
1180
- const next = (ctx) => {
1181
- const stack = useStack(ctx);
1182
- const path = stack.value[0];
1226
+ const nextPath = (path) => {
1183
1227
  const last = path.at(-1);
1184
1228
  if (last && (isNull(last[0]) || last[0] === "jump") && isNumber(last[1])) {
1185
1229
  last[1]++;
1186
1230
  } else {
1187
1231
  path.push([null, 0]);
1188
1232
  }
1233
+ return path;
1234
+ };
1235
+ const next = (ctx) => {
1236
+ const stack = useStack(ctx);
1237
+ const path = stack.value[0];
1238
+ nextPath(path);
1189
1239
  };
1190
1240
  const matchActionInit = {
1191
1241
  getContext: renderer.getContext,
@@ -1207,7 +1257,7 @@ var Novely = (() => {
1207
1257
  wait({ ctx, push }, [time]) {
1208
1258
  if (ctx.meta.restoring)
1209
1259
  return;
1210
- setTimeout(push, isFunction(time) ? time() : time);
1260
+ setTimeout(push, isFunction(time) ? time(getStateAtCtx(ctx)) : time);
1211
1261
  },
1212
1262
  showBackground({ ctx, push }, [background]) {
1213
1263
  ctx.background(background);
@@ -1266,15 +1316,31 @@ var Novely = (() => {
1266
1316
  return c || "";
1267
1317
  })();
1268
1318
  ctx.dialog(
1269
- unwrap2(content, data2),
1270
- unwrap2(name, data2),
1319
+ templateReplace(content, data2),
1320
+ templateReplace(name, data2),
1271
1321
  character,
1272
1322
  emotion,
1273
1323
  forward
1274
1324
  );
1275
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
+ },
1276
1335
  function({ ctx, push }, [fn]) {
1277
- 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: $.get().meta[0],
1339
+ goingBack,
1340
+ restoring,
1341
+ preview: preview2,
1342
+ state: getStateFunction(ctx)
1343
+ });
1278
1344
  if (!ctx.meta.restoring) {
1279
1345
  result ? result.then(push) : push();
1280
1346
  }
@@ -1286,22 +1352,26 @@ var Novely = (() => {
1286
1352
  choices.unshift(question);
1287
1353
  question = "";
1288
1354
  }
1289
- const unwrappedChoices = choices.map(([content, action2, visible]) => {
1290
- if (DEV && action2.length === 0 && (!visible || visible())) {
1355
+ const transformedChoices = choices.map(([content, action2, visible]) => {
1356
+ const shown = !visible || visible({
1357
+ lang: $.get().meta[0],
1358
+ state: getStateAtCtx(ctx)
1359
+ });
1360
+ if (DEV && action2.length === 0 && !shown) {
1291
1361
  console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
1292
1362
  }
1293
- return [unwrap2(content, data2), action2, visible];
1363
+ return [templateReplace(content, data2), action2, shown];
1294
1364
  });
1295
- if (DEV && unwrappedChoices.length === 0) {
1365
+ if (DEV && transformedChoices.length === 0) {
1296
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]`);
1297
1367
  }
1298
- ctx.choices(unwrap2(question, data2), unwrappedChoices, (selected) => {
1368
+ ctx.choices(templateReplace(question, data2), transformedChoices, (selected) => {
1299
1369
  if (!ctx.meta.preview) {
1300
1370
  enmemory(ctx);
1301
1371
  }
1302
1372
  const stack = useStack(ctx);
1303
1373
  const offset = isWithoutQuestion ? 0 : 1;
1304
- if (DEV && !unwrappedChoices[selected]) {
1374
+ if (DEV && !transformedChoices[selected]) {
1305
1375
  throw new Error("Choice children is empty, either add content there or make item not selectable");
1306
1376
  }
1307
1377
  stack.value[0].push(["choice", selected + offset], [null, 0]);
@@ -1340,7 +1410,7 @@ var Novely = (() => {
1340
1410
  throw new Error(`Attempt to use Condition action with empty variants object`);
1341
1411
  }
1342
1412
  if (!ctx.meta.restoring) {
1343
- const val = String(condition());
1413
+ const val = String(condition(getStateAtCtx(ctx)));
1344
1414
  if (DEV && !variants[val]) {
1345
1415
  throw new Error(`Attempt to go to unknown variant "${val}"`);
1346
1416
  }
@@ -1363,7 +1433,7 @@ var Novely = (() => {
1363
1433
  },
1364
1434
  input({ ctx, data: data2, forward }, [question, onInput, setup]) {
1365
1435
  ctx.input(
1366
- unwrap2(question, data2),
1436
+ templateReplace(question, data2),
1367
1437
  onInput,
1368
1438
  setup || noop,
1369
1439
  forward
@@ -1401,7 +1471,7 @@ var Novely = (() => {
1401
1471
  push();
1402
1472
  },
1403
1473
  text({ ctx, data: data2, forward }, text) {
1404
- const string = text.map((content) => unwrap2(content, data2)).join(" ");
1474
+ const string = text.map((content) => templateReplace(content, data2)).join(" ");
1405
1475
  if (DEV && string.length === 0) {
1406
1476
  throw new Error(`Action Text was called with empty string or array`);
1407
1477
  }
@@ -1493,52 +1563,106 @@ var Novely = (() => {
1493
1563
  const interactivity = (value = false) => {
1494
1564
  interacted = value ? interacted + 1 : 0;
1495
1565
  };
1496
- const unwrap2 = (content, values) => {
1566
+ const templateReplace = (content, values) => {
1497
1567
  const {
1498
1568
  data: data2,
1499
1569
  meta: [lang]
1500
1570
  } = $.get();
1501
- const obj = values ? values : data2;
1502
- const cnt = isFunction(content) ? content() : typeof content === "string" ? content : content[lang];
1503
- const str2 = isFunction(cnt) ? cnt() : cnt;
1504
- const trans = translation[lang];
1505
- if (trans.actions || trans.plural) {
1506
- return replace(str2, obj, trans.plural, trans.actions, new Intl.PluralRules(trans.tag || lang));
1507
- }
1508
- return replace(str2, obj);
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
+ );
1509
1586
  };
1510
- function data(value) {
1587
+ const data = (value) => {
1588
+ const _data = $.get().data;
1511
1589
  if (!value)
1512
- return $.get().data;
1513
- const prev = $.get().data;
1514
- const val = isFunction(value) ? value(prev) : deepmerge(prev, value);
1515
- $.update((prev2) => {
1516
- prev2.data = val;
1517
- return prev2;
1590
+ return _data;
1591
+ const val = isFunction(value) ? value(_data) : deepmerge(_data, value);
1592
+ $.update((prev) => {
1593
+ prev.data = val;
1594
+ return prev;
1518
1595
  });
1519
- }
1596
+ return void 0;
1597
+ };
1520
1598
  return {
1521
1599
  /**
1522
1600
  * Function to set game script
1601
+ *
1602
+ * @example
1603
+ * ```ts
1604
+ * engine.script({
1605
+ * start: [
1606
+ * action.function(() => {})
1607
+ * ]
1608
+ * })
1609
+ * ```
1523
1610
  */
1524
1611
  script,
1525
1612
  /**
1526
- * Function to get actions
1613
+ * Get actions
1614
+ *
1615
+ * @example
1616
+ * ```ts
1617
+ * engine.script({
1618
+ * start: [
1619
+ * action.function(() => {})
1620
+ * ]
1621
+ * })
1622
+ * ```
1527
1623
  */
1528
1624
  action,
1529
1625
  /**
1530
- * State that belongs to games
1626
+ * @deprecated Will be removed BUT replaced with state passed into actions as a parameter
1531
1627
  */
1532
- state,
1628
+ state: getStateFunction(MAIN_CONTEXT_KEY),
1533
1629
  /**
1534
- * 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
+ * ```
1535
1644
  */
1536
1645
  data,
1537
1646
  /**
1538
- * Unwraps translatable content to a string value
1647
+ * @deprecated Renamed into `templateReplace`
1539
1648
  */
1540
1649
  unwrap(content) {
1541
- 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);
1542
1666
  },
1543
1667
  /**
1544
1668
  * Cancel data loading, hide UI, ignore page change events