@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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/constants.ts
2
- var SKIPPED_DURING_RESTORE = /* @__PURE__ */ new Set(["dialog", "choice", "input", "vibrate", "text"]);
2
+ var SKIPPED_DURING_RESTORE = /* @__PURE__ */ new Set(["dialog", "say", "choice", "input", "vibrate", "text"]);
3
3
  var BLOCK_EXIT_STATEMENTS = /* @__PURE__ */ new Set(["choice:exit", "condition:exit", "block:exit"]);
4
4
  var BLOCK_STATEMENTS = /* @__PURE__ */ new Set(["choice", "condition", "block"]);
5
5
  var AUDIO_ACTIONS = /* @__PURE__ */ new Set([
@@ -92,8 +92,8 @@ var isCSSImage = (str2) => {
92
92
  return startsWith("http") || startsWith("/") || startsWith(".") || startsWith("data");
93
93
  };
94
94
  var str = String;
95
- var isUserRequiredAction = (action, meta) => {
96
- return action === "custom" && meta[0] && meta[0].requireUserAction;
95
+ var isUserRequiredAction = ([action, ...meta]) => {
96
+ return Boolean(action === "custom" && meta[0] && meta[0].requireUserAction);
97
97
  };
98
98
  var getLanguage = (languages) => {
99
99
  let { language } = navigator;
@@ -214,11 +214,13 @@ var getOppositeAction = (action) => {
214
214
  };
215
215
  return MAP[action];
216
216
  };
217
- var getActionsFromPath = (story, path, raw = false) => {
217
+ var getActionsFromPath = (story, path, filter) => {
218
218
  let current = story;
219
219
  let precurrent;
220
220
  let ignoreNested = false;
221
221
  let index = 0;
222
+ let skipPreserve = void 0;
223
+ const skip = /* @__PURE__ */ new Set();
222
224
  const max = path.reduce((acc, [type, val]) => {
223
225
  if (isNull(type) && isNumber(val)) {
224
226
  return acc + 1;
@@ -247,21 +249,19 @@ var getActionsFromPath = (story, path, raw = false) => {
247
249
  const item = current[i];
248
250
  if (!isAction(item))
249
251
  continue;
250
- const [action, ...meta] = item;
251
- const push = () => {
252
- queue.push([action, meta]);
253
- };
254
- if (raw) {
255
- push();
256
- continue;
252
+ const [action] = item;
253
+ const last = index === max && i === val;
254
+ const shouldSkip = isSkippedDuringRestore(action) || isUserRequiredAction(item);
255
+ if (shouldSkip) {
256
+ skip.add(item);
257
257
  }
258
- if (isSkippedDuringRestore(action) || isUserRequiredAction(action, meta)) {
259
- if (index === max && i === val) {
260
- push();
261
- }
258
+ if (shouldSkip && last) {
259
+ skipPreserve = item;
260
+ }
261
+ if (filter && shouldSkip && !last) {
262
262
  continue;
263
263
  } else {
264
- push();
264
+ queue.push(item);
265
265
  }
266
266
  }
267
267
  }
@@ -280,9 +280,13 @@ var getActionsFromPath = (story, path, raw = false) => {
280
280
  ignoreNested = true;
281
281
  }
282
282
  }
283
- return queue;
283
+ return {
284
+ queue,
285
+ skip,
286
+ skipPreserve
287
+ };
284
288
  };
285
- var createQueueProcessor = (queue) => {
289
+ var createQueueProcessor = (queue, options) => {
286
290
  const processedQueue = [];
287
291
  const keep = /* @__PURE__ */ new Set();
288
292
  const characters = /* @__PURE__ */ new Set();
@@ -291,70 +295,83 @@ var createQueueProcessor = (queue) => {
291
295
  sound: /* @__PURE__ */ new Set()
292
296
  };
293
297
  const next = (i) => queue.slice(i + 1);
294
- for (const [i, [action, meta]] of queue.entries()) {
298
+ for (const [i, item] of queue.entries()) {
299
+ const [action, ...params] = item;
295
300
  keep.add(action);
301
+ if (options.skip.has(item) && item !== options.skipPreserve) {
302
+ continue;
303
+ }
296
304
  if (action === "function" || action === "custom") {
297
- if (action === "custom" && meta[0].callOnlyLatest) {
298
- const notLatest = next(i).some(([, _meta]) => {
299
- if (!_meta || !meta)
305
+ if (action === "custom" && params[0].callOnlyLatest) {
306
+ const notLatest = next(i).some(([, func]) => {
307
+ if (!isFunction(func))
300
308
  return false;
301
- const c0 = _meta[0];
302
- const c1 = meta[0];
303
- const isIdenticalID = c0.id && c1.id && c0.id === c1.id;
309
+ const c0 = func;
310
+ const c1 = params[0];
311
+ const isIdenticalID = Boolean(c0.id && c1.id && c0.id === c1.id);
304
312
  const isIdenticalByReference = c0 === c1;
305
313
  return isIdenticalID || isIdenticalByReference || str(c0) === str(c1);
306
314
  });
307
315
  if (notLatest)
308
316
  continue;
309
317
  }
310
- processedQueue.push([action, meta]);
318
+ processedQueue.push(item);
311
319
  } else if (action === "showCharacter" || action === "playSound" || action === "playMusic" || action === "voice") {
312
320
  const closing = getOppositeAction(action);
313
- const skip = next(i).some(([_action, _meta]) => {
314
- if (!_meta || !meta)
315
- return false;
316
- if (_meta[0] !== meta[0])
321
+ const skip = next(i).some(([_action, target]) => {
322
+ if (target !== params[0]) {
317
323
  return false;
324
+ }
318
325
  return _action === closing || _action === action;
319
326
  });
320
327
  if (skip)
321
328
  continue;
322
329
  if (action === "showCharacter") {
323
- characters.add(meta[0]);
330
+ characters.add(params[0]);
324
331
  } else if (action === "playMusic") {
325
- audio.music.add(meta[0]);
332
+ audio.music.add(params[0]);
326
333
  } else if (action === "playSound") {
327
- audio.sound.add(meta[0]);
334
+ audio.sound.add(params[0]);
328
335
  }
329
- processedQueue.push([action, meta]);
330
- } else if (action === "showBackground" || action === "animateCharacter" || action === "preload") {
331
- const skip = next(i).some(([_action], i2, array) => action === _action);
336
+ processedQueue.push(item);
337
+ } else if (action === "showBackground" || action === "preload") {
338
+ const skip = next(i).some(([_action]) => action === _action);
332
339
  if (skip)
333
340
  continue;
334
- processedQueue.push([action, meta]);
341
+ processedQueue.push(item);
342
+ } else if (action === "animateCharacter") {
343
+ const skip = next(i).some(([_action, character], j, array) => {
344
+ if (action === _action && character === params[0]) {
345
+ return true;
346
+ }
347
+ const next2 = array.slice(j);
348
+ const characterWillAnimate = next2.some(([__action, __character]) => action === __action);
349
+ const hasBlockingActions = next2.some((item2) => options.skip.has(item2));
350
+ return characterWillAnimate && hasBlockingActions;
351
+ });
352
+ if (skip)
353
+ continue;
354
+ processedQueue.push(item);
335
355
  } else {
336
- processedQueue.push([action, meta]);
356
+ processedQueue.push(item);
337
357
  }
338
358
  }
339
359
  const run = async (match) => {
340
- for await (const [action, meta] of processedQueue) {
341
- const result = match(action, meta);
360
+ for await (const [action, ...params] of processedQueue) {
361
+ const result = match(action, params);
342
362
  if (isPromise(result)) {
343
363
  await result;
344
364
  }
345
365
  }
346
366
  processedQueue.length = 0;
347
367
  };
348
- const getKeep = () => {
349
- return {
368
+ return {
369
+ run,
370
+ keep: {
350
371
  keep,
351
372
  characters,
352
373
  audio
353
- };
354
- };
355
- return {
356
- run,
357
- getKeep
374
+ }
358
375
  };
359
376
  };
360
377
  var getStack = (ctx) => {
@@ -586,17 +603,17 @@ var split = (input, delimeters) => {
586
603
  output.push(input);
587
604
  return output;
588
605
  };
589
- var unwrap = (c) => {
606
+ var flattenAllowedContent = (c, state) => {
590
607
  if (Array.isArray(c)) {
591
- return c.map((item) => unwrap(item)).join("<br>");
608
+ return c.map((item) => flattenAllowedContent(item, state)).join("<br>");
592
609
  }
593
610
  if (typeof c === "function") {
594
- return unwrap(c());
611
+ return flattenAllowedContent(c(state), state);
595
612
  }
596
613
  return c;
597
614
  };
598
615
  var replace = (str2, obj, pluralization, actions, pr) => {
599
- return unwrap(str2).replaceAll(RGX, (x, key, y) => {
616
+ return str2.replaceAll(RGX, (x, key, y) => {
600
617
  x = 0;
601
618
  y = obj;
602
619
  const [pathstr, plural, action] = split(key.trim(), ["@", "%"]);
@@ -647,7 +664,6 @@ var novely = ({
647
664
  renderer: createRenderer,
648
665
  initialScreen = "mainmenu",
649
666
  translation,
650
- languages,
651
667
  state: defaultState,
652
668
  data: defaultData,
653
669
  autosaves = true,
@@ -658,8 +674,10 @@ var novely = ({
658
674
  askBeforeExit = true,
659
675
  preloadAssets = "lazy",
660
676
  parallelAssetsDownloadLimit = 15,
661
- fetch: request = fetch
677
+ fetch: request = fetch,
678
+ saveOnUnload = true
662
679
  }) => {
680
+ const languages = Object.keys(translation);
663
681
  const limitScript = pLimit(1);
664
682
  const limitAssetsDownload = pLimit(parallelAssetsDownloadLimit);
665
683
  const story = {};
@@ -747,44 +765,55 @@ var novely = ({
747
765
  };
748
766
  }
749
767
  });
750
- function state(value) {
751
- const stack = useStack(MAIN_CONTEXT_KEY);
752
- if (!value)
753
- return stack.value[1];
754
- const prev = stack.value[1];
755
- const val = isFunction(value) ? value(prev) : deepmerge(prev, value);
756
- stack.value[1] = val;
757
- }
758
- const getDefaultSave = (state2 = {}) => {
768
+ const getDefaultSave = (state = {}) => {
759
769
  return [
760
770
  [
761
771
  ["jump", "start"],
762
772
  [null, 0]
763
773
  ],
764
- state2,
774
+ state,
765
775
  [intime(Date.now()), "auto"]
766
776
  ];
767
777
  };
768
778
  const getLanguageWithoutParameters = () => {
769
- return getLanguage2(languages, getLanguage);
779
+ const language = getLanguage2(languages, getLanguage);
780
+ if (languages.includes(language)) {
781
+ return language;
782
+ }
783
+ if (DEV2) {
784
+ throw new Error(`Attempt to use unsupported language "${language}". Supported languages: ${languages.join(", ")}.`);
785
+ }
786
+ throw 0;
770
787
  };
771
788
  const initialData = {
772
789
  saves: [],
773
790
  data: klona(defaultData),
774
791
  meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED, 1, 1, 1]
775
792
  };
776
- const coreData = {
793
+ const $ = store(initialData);
794
+ const coreData = store({
777
795
  dataLoaded: false
796
+ });
797
+ const onDataLoadedPromise = ({ cancelled }) => {
798
+ if (cancelled) {
799
+ dataLoaded.promise.then(onDataLoadedPromise);
800
+ return;
801
+ }
802
+ coreData.update((data2) => {
803
+ data2.dataLoaded = true;
804
+ return data2;
805
+ });
778
806
  };
779
- const $ = store(initialData);
780
- const $$ = store(coreData);
807
+ dataLoaded.promise.then(onDataLoadedPromise);
781
808
  const onStorageDataChange = (value) => {
782
- if ($$.get().dataLoaded)
809
+ if (coreData.get().dataLoaded)
783
810
  storage.set(value);
784
811
  };
785
812
  const throttledOnStorageDataChange = throttle(onStorageDataChange, throttleTimeout);
786
813
  const throttledEmergencyOnStorageDataChange = throttle(() => {
787
- onStorageDataChange($.get());
814
+ if (saveOnUnload === true || saveOnUnload === "prod" && !DEV2) {
815
+ onStorageDataChange($.get());
816
+ }
788
817
  }, 10);
789
818
  $.subscribe(throttledOnStorageDataChange);
790
819
  const getStoredData = async () => {
@@ -792,19 +821,16 @@ var novely = ({
792
821
  for (const migration of migrations) {
793
822
  stored = migration(stored);
794
823
  }
795
- stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
796
- if (overrideLanguage) {
824
+ if (overrideLanguage || !stored.meta[0]) {
797
825
  stored.meta[0] = getLanguageWithoutParameters();
798
- } else {
799
- stored.meta[0] ||= getLanguageWithoutParameters();
800
826
  }
827
+ stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
801
828
  stored.meta[2] ??= 1;
802
829
  stored.meta[3] ??= 1;
803
830
  stored.meta[4] ??= 1;
804
831
  if (isEmpty(stored.data)) {
805
832
  stored.data = defaultData;
806
833
  }
807
- $$.update((prev) => (prev.dataLoaded = true, prev));
808
834
  dataLoaded.resolve();
809
835
  $.set(stored);
810
836
  };
@@ -818,7 +844,7 @@ var novely = ({
818
844
  addEventListener("visibilitychange", onVisibilityChange);
819
845
  addEventListener("beforeunload", throttledEmergencyOnStorageDataChange);
820
846
  const save = (override = false, type = override ? "auto" : "manual") => {
821
- if (!$$.get().dataLoaded)
847
+ if (!coreData.get().dataLoaded)
822
848
  return;
823
849
  if (!autosaves && type === "auto")
824
850
  return;
@@ -842,7 +868,7 @@ var novely = ({
842
868
  });
843
869
  };
844
870
  const newGame = () => {
845
- if (!$$.get().dataLoaded)
871
+ if (!coreData.get().dataLoaded)
846
872
  return;
847
873
  const save2 = getDefaultSave(klona(defaultState));
848
874
  if (autosaves) {
@@ -853,13 +879,13 @@ var novely = ({
853
879
  restore(save2);
854
880
  };
855
881
  const set = (save2, ctx) => {
856
- const stack = useStack(ctx || renderer.getContext(MAIN_CONTEXT_KEY));
882
+ const stack = useStack(ctx || MAIN_CONTEXT_KEY);
857
883
  stack.value = save2;
858
884
  return restore(save2);
859
885
  };
860
886
  let interacted = 0;
861
887
  const restore = async (save2) => {
862
- if (!$$.get().dataLoaded)
888
+ if (!coreData.get().dataLoaded)
863
889
  return;
864
890
  let latest = save2 || $.get().saves.at(-1);
865
891
  if (!latest) {
@@ -875,19 +901,22 @@ var novely = ({
875
901
  const previous = stack.previous;
876
902
  const [path] = stack.value = latest;
877
903
  renderer.ui.showScreen("game");
878
- const queue = getActionsFromPath(story, path);
879
- const processor = createQueueProcessor(queue);
880
- const { keep, characters: characters2, audio } = processor.getKeep();
904
+ const { queue, skip, skipPreserve } = getActionsFromPath(story, path, false);
905
+ const processor = createQueueProcessor(queue, {
906
+ skip,
907
+ skipPreserve
908
+ });
909
+ const { keep, characters: characters2, audio } = processor.keep;
881
910
  if (previous) {
882
- const prevQueue = getActionsFromPath(story, previous[0], true);
883
- const currQueue = getActionsFromPath(story, path, true);
884
- for (let i = prevQueue.length - 1; i > currQueue.length; i--) {
911
+ const { queue: prevQueue } = getActionsFromPath(story, previous[0], false);
912
+ for (let i = prevQueue.length - 1; i > queue.length; i--) {
885
913
  const element = prevQueue[i];
886
- if (isAction(element)) {
887
- const [action2, props] = element;
888
- if (action2 === "custom") {
889
- context.clearCustom(props[0]);
890
- }
914
+ if (!isAction(element)) {
915
+ continue;
916
+ }
917
+ const [action2, fn] = element;
918
+ if (action2 === "custom") {
919
+ context.clearCustom(fn);
891
920
  }
892
921
  }
893
922
  }
@@ -907,9 +936,6 @@ var novely = ({
907
936
  render(context);
908
937
  };
909
938
  const refer = (path) => {
910
- if (!path) {
911
- path = useStack(MAIN_CONTEXT_KEY).value[0];
912
- }
913
939
  let current = story;
914
940
  let precurrent = story;
915
941
  const blocks = [];
@@ -965,11 +991,13 @@ var novely = ({
965
991
  return translation[lang].internal[key];
966
992
  };
967
993
  const preview = async ([path, data2], name) => {
968
- const queue = getActionsFromPath(story, path);
994
+ const { queue } = getActionsFromPath(story, path, true);
969
995
  const ctx = renderer.getContext(name);
970
996
  ctx.meta.restoring = true;
971
997
  ctx.meta.preview = true;
972
- const processor = createQueueProcessor(queue);
998
+ const processor = createQueueProcessor(queue, {
999
+ skip: /* @__PURE__ */ new Set()
1000
+ });
973
1001
  await processor.run((action2, props) => {
974
1002
  if (isAudioAction(action2))
975
1003
  return;
@@ -986,6 +1014,23 @@ var novely = ({
986
1014
  const removeContext = (name) => {
987
1015
  STACK_MAP.delete(name);
988
1016
  };
1017
+ const getStateAtCtx = (context) => {
1018
+ return useStack(context).value[1];
1019
+ };
1020
+ const getStateFunction = (context) => {
1021
+ const stack = useStack(context);
1022
+ const state = (value) => {
1023
+ const _state = getStateAtCtx(context);
1024
+ if (!value) {
1025
+ return _state;
1026
+ }
1027
+ const prev = _state;
1028
+ const val = isFunction(value) ? value(prev) : deepmerge(prev, value);
1029
+ stack.value[1] = val;
1030
+ return void 0;
1031
+ };
1032
+ return state;
1033
+ };
989
1034
  const renderer = createRenderer({
990
1035
  mainContextKey: MAIN_CONTEXT_KEY,
991
1036
  characters,
@@ -998,9 +1043,10 @@ var novely = ({
998
1043
  t,
999
1044
  preview,
1000
1045
  removeContext,
1046
+ getStateFunction,
1001
1047
  languages,
1002
1048
  $,
1003
- $$
1049
+ $$: coreData
1004
1050
  });
1005
1051
  const useStack = createUseStackFunction(renderer);
1006
1052
  useStack(MAIN_CONTEXT_KEY).push(initial);
@@ -1014,15 +1060,19 @@ var novely = ({
1014
1060
  stack.push(current);
1015
1061
  save(true, "auto");
1016
1062
  };
1017
- const next = (ctx) => {
1018
- const stack = useStack(ctx);
1019
- const path = stack.value[0];
1063
+ const nextPath = (path) => {
1020
1064
  const last = path.at(-1);
1021
1065
  if (last && (isNull(last[0]) || last[0] === "jump") && isNumber(last[1])) {
1022
1066
  last[1]++;
1023
1067
  } else {
1024
1068
  path.push([null, 0]);
1025
1069
  }
1070
+ return path;
1071
+ };
1072
+ const next = (ctx) => {
1073
+ const stack = useStack(ctx);
1074
+ const path = stack.value[0];
1075
+ nextPath(path);
1026
1076
  };
1027
1077
  const matchActionInit = {
1028
1078
  getContext: renderer.getContext,
@@ -1044,7 +1094,7 @@ var novely = ({
1044
1094
  wait({ ctx, push }, [time]) {
1045
1095
  if (ctx.meta.restoring)
1046
1096
  return;
1047
- setTimeout(push, isFunction(time) ? time() : time);
1097
+ setTimeout(push, isFunction(time) ? time(getStateAtCtx(ctx)) : time);
1048
1098
  },
1049
1099
  showBackground({ ctx, push }, [background]) {
1050
1100
  ctx.background(background);
@@ -1103,15 +1153,31 @@ var novely = ({
1103
1153
  return c || "";
1104
1154
  })();
1105
1155
  ctx.dialog(
1106
- unwrap2(content, data2),
1107
- unwrap2(name, data2),
1156
+ templateReplace(content, data2),
1157
+ templateReplace(name, data2),
1108
1158
  character,
1109
1159
  emotion,
1110
1160
  forward
1111
1161
  );
1112
1162
  },
1163
+ say({ ctx, data: data2 }, [character, content]) {
1164
+ if (DEV2 && !characters[character]) {
1165
+ throw new Error(`Attempt to call Say action with unknown character "${character}"`);
1166
+ }
1167
+ match("dialog", [character, content], {
1168
+ ctx,
1169
+ data: data2
1170
+ });
1171
+ },
1113
1172
  function({ ctx, push }, [fn]) {
1114
- const result = fn(ctx.meta.restoring, ctx.meta.goingBack, ctx.meta.preview);
1173
+ const { restoring, goingBack, preview: preview2 } = ctx.meta;
1174
+ const result = fn({
1175
+ lang: $.get().meta[0],
1176
+ goingBack,
1177
+ restoring,
1178
+ preview: preview2,
1179
+ state: getStateFunction(ctx)
1180
+ });
1115
1181
  if (!ctx.meta.restoring) {
1116
1182
  result ? result.then(push) : push();
1117
1183
  }
@@ -1123,22 +1189,26 @@ var novely = ({
1123
1189
  choices.unshift(question);
1124
1190
  question = "";
1125
1191
  }
1126
- const unwrappedChoices = choices.map(([content, action2, visible]) => {
1127
- if (DEV2 && action2.length === 0 && (!visible || visible())) {
1192
+ const transformedChoices = choices.map(([content, action2, visible]) => {
1193
+ const shown = !visible || visible({
1194
+ lang: $.get().meta[0],
1195
+ state: getStateAtCtx(ctx)
1196
+ });
1197
+ if (DEV2 && action2.length === 0 && !shown) {
1128
1198
  console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
1129
1199
  }
1130
- return [unwrap2(content, data2), action2, visible];
1200
+ return [templateReplace(content, data2), action2, shown];
1131
1201
  });
1132
- if (DEV2 && unwrappedChoices.length === 0) {
1202
+ if (DEV2 && transformedChoices.length === 0) {
1133
1203
  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]`);
1134
1204
  }
1135
- ctx.choices(unwrap2(question, data2), unwrappedChoices, (selected) => {
1205
+ ctx.choices(templateReplace(question, data2), transformedChoices, (selected) => {
1136
1206
  if (!ctx.meta.preview) {
1137
1207
  enmemory(ctx);
1138
1208
  }
1139
1209
  const stack = useStack(ctx);
1140
1210
  const offset = isWithoutQuestion ? 0 : 1;
1141
- if (DEV2 && !unwrappedChoices[selected]) {
1211
+ if (DEV2 && !transformedChoices[selected]) {
1142
1212
  throw new Error("Choice children is empty, either add content there or make item not selectable");
1143
1213
  }
1144
1214
  stack.value[0].push(["choice", selected + offset], [null, 0]);
@@ -1177,7 +1247,7 @@ var novely = ({
1177
1247
  throw new Error(`Attempt to use Condition action with empty variants object`);
1178
1248
  }
1179
1249
  if (!ctx.meta.restoring) {
1180
- const val = String(condition());
1250
+ const val = String(condition(getStateAtCtx(ctx)));
1181
1251
  if (DEV2 && !variants[val]) {
1182
1252
  throw new Error(`Attempt to go to unknown variant "${val}"`);
1183
1253
  }
@@ -1200,7 +1270,7 @@ var novely = ({
1200
1270
  },
1201
1271
  input({ ctx, data: data2, forward }, [question, onInput, setup]) {
1202
1272
  ctx.input(
1203
- unwrap2(question, data2),
1273
+ templateReplace(question, data2),
1204
1274
  onInput,
1205
1275
  setup || noop,
1206
1276
  forward
@@ -1238,7 +1308,7 @@ var novely = ({
1238
1308
  push();
1239
1309
  },
1240
1310
  text({ ctx, data: data2, forward }, text) {
1241
- const string = text.map((content) => unwrap2(content, data2)).join(" ");
1311
+ const string = text.map((content) => templateReplace(content, data2)).join(" ");
1242
1312
  if (DEV2 && string.length === 0) {
1243
1313
  throw new Error(`Action Text was called with empty string or array`);
1244
1314
  }
@@ -1330,52 +1400,106 @@ var novely = ({
1330
1400
  const interactivity = (value = false) => {
1331
1401
  interacted = value ? interacted + 1 : 0;
1332
1402
  };
1333
- const unwrap2 = (content, values) => {
1403
+ const templateReplace = (content, values) => {
1334
1404
  const {
1335
1405
  data: data2,
1336
1406
  meta: [lang]
1337
1407
  } = $.get();
1338
- const obj = values ? values : data2;
1339
- const cnt = isFunction(content) ? content() : typeof content === "string" ? content : content[lang];
1340
- const str2 = isFunction(cnt) ? cnt() : cnt;
1341
- const trans = translation[lang];
1342
- if (trans.actions || trans.plural) {
1343
- return replace(str2, obj, trans.plural, trans.actions, new Intl.PluralRules(trans.tag || lang));
1344
- }
1345
- return replace(str2, obj);
1408
+ const obj = values || data2;
1409
+ const cnt = isFunction(content) ? content(obj) : typeof content === "string" ? content : content[lang];
1410
+ const str2 = flattenAllowedContent(
1411
+ isFunction(cnt) ? cnt(obj) : cnt,
1412
+ obj
1413
+ );
1414
+ const t2 = translation[lang];
1415
+ const pluralRules = (t2.plural || t2.actions) && new Intl.PluralRules(t2.tag || lang);
1416
+ return replace(
1417
+ str2,
1418
+ obj,
1419
+ t2.plural,
1420
+ t2.actions,
1421
+ pluralRules
1422
+ );
1346
1423
  };
1347
- function data(value) {
1424
+ const data = (value) => {
1425
+ const _data = $.get().data;
1348
1426
  if (!value)
1349
- return $.get().data;
1350
- const prev = $.get().data;
1351
- const val = isFunction(value) ? value(prev) : deepmerge(prev, value);
1352
- $.update((prev2) => {
1353
- prev2.data = val;
1354
- return prev2;
1427
+ return _data;
1428
+ const val = isFunction(value) ? value(_data) : deepmerge(_data, value);
1429
+ $.update((prev) => {
1430
+ prev.data = val;
1431
+ return prev;
1355
1432
  });
1356
- }
1433
+ return void 0;
1434
+ };
1357
1435
  return {
1358
1436
  /**
1359
1437
  * Function to set game script
1438
+ *
1439
+ * @example
1440
+ * ```ts
1441
+ * engine.script({
1442
+ * start: [
1443
+ * action.function(() => {})
1444
+ * ]
1445
+ * })
1446
+ * ```
1360
1447
  */
1361
1448
  script,
1362
1449
  /**
1363
- * Function to get actions
1450
+ * Get actions
1451
+ *
1452
+ * @example
1453
+ * ```ts
1454
+ * engine.script({
1455
+ * start: [
1456
+ * action.function(() => {})
1457
+ * ]
1458
+ * })
1459
+ * ```
1364
1460
  */
1365
1461
  action,
1366
1462
  /**
1367
- * State that belongs to games
1463
+ * @deprecated Will be removed BUT replaced with state passed into actions as a parameter
1368
1464
  */
1369
- state,
1465
+ state: getStateFunction(MAIN_CONTEXT_KEY),
1370
1466
  /**
1371
- * Unlike `state`, stored at global scope instead and shared between games
1467
+ * Store data between games
1468
+ *
1469
+ * @example
1470
+ * ```ts
1471
+ * engine.script({
1472
+ * start: [
1473
+ * action.function(() => {
1474
+ * // Paid content should be purchased only once
1475
+ * // So it will be available in any save
1476
+ * data({ paid_content_purchased: true })
1477
+ * })
1478
+ * ]
1479
+ * })
1480
+ * ```
1372
1481
  */
1373
1482
  data,
1374
1483
  /**
1375
- * Unwraps translatable content to a string value
1484
+ * @deprecated Renamed into `templateReplace`
1376
1485
  */
1377
1486
  unwrap(content) {
1378
- return unwrap2(content, $.get().data);
1487
+ return templateReplace(content);
1488
+ },
1489
+ /**
1490
+ * Replaces content inside {{braces}} with using global data
1491
+ * @example
1492
+ * ```ts
1493
+ * data({ name: 'Alexei' })
1494
+ *
1495
+ * templateReplace('{{name}} is our hero')
1496
+ * templateReplace({
1497
+ * en: (data) => 'Hello, ' + data.name
1498
+ * })
1499
+ * ```
1500
+ */
1501
+ templateReplace(content) {
1502
+ return templateReplace(content);
1379
1503
  },
1380
1504
  /**
1381
1505
  * Cancel data loading, hide UI, ignore page change events