@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.
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);
339
+ if (skip)
340
+ continue;
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
+ });
332
352
  if (skip)
333
353
  continue;
334
- processedQueue.push([action, meta]);
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(), ["@", "%"]);
@@ -657,7 +674,8 @@ var novely = ({
657
674
  askBeforeExit = true,
658
675
  preloadAssets = "lazy",
659
676
  parallelAssetsDownloadLimit = 15,
660
- fetch: request = fetch
677
+ fetch: request = fetch,
678
+ saveOnUnload = true
661
679
  }) => {
662
680
  const languages = Object.keys(translation);
663
681
  const limitScript = pLimit(1);
@@ -747,46 +765,57 @@ 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 storageData = 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(storageData.get());
816
+ }
788
817
  }, 10);
789
- $.subscribe(throttledOnStorageDataChange);
818
+ storageData.subscribe(throttledOnStorageDataChange);
790
819
  const getStoredData = async () => {
791
820
  let stored = await storage.get();
792
821
  for (const migration of migrations) {
@@ -802,9 +831,8 @@ var novely = ({
802
831
  if (isEmpty(stored.data)) {
803
832
  stored.data = defaultData;
804
833
  }
805
- $$.update((prev) => (prev.dataLoaded = true, prev));
806
834
  dataLoaded.resolve();
807
- $.set(stored);
835
+ storageData.set(stored);
808
836
  };
809
837
  storageDelay.then(getStoredData);
810
838
  const initial = getDefaultSave(klona(defaultState));
@@ -816,13 +844,13 @@ var novely = ({
816
844
  addEventListener("visibilitychange", onVisibilityChange);
817
845
  addEventListener("beforeunload", throttledEmergencyOnStorageDataChange);
818
846
  const save = (override = false, type = override ? "auto" : "manual") => {
819
- if (!$$.get().dataLoaded)
847
+ if (!coreData.get().dataLoaded)
820
848
  return;
821
849
  if (!autosaves && type === "auto")
822
850
  return;
823
851
  const stack = useStack(MAIN_CONTEXT_KEY);
824
852
  const current = klona(stack.value);
825
- $.update((prev) => {
853
+ storageData.update((prev) => {
826
854
  const isLatest = findLastIndex(prev.saves, (value) => times.has(value[2][0])) === prev.saves.length - 1;
827
855
  current[2][0] = intime(Date.now());
828
856
  current[2][1] = type;
@@ -840,29 +868,29 @@ var novely = ({
840
868
  });
841
869
  };
842
870
  const newGame = () => {
843
- if (!$$.get().dataLoaded)
871
+ if (!coreData.get().dataLoaded)
844
872
  return;
845
873
  const save2 = getDefaultSave(klona(defaultState));
846
874
  if (autosaves) {
847
- $.update((prev) => {
875
+ storageData.update((prev) => {
848
876
  return prev.saves.push(save2), prev;
849
877
  });
850
878
  }
851
879
  restore(save2);
852
880
  };
853
881
  const set = (save2, ctx) => {
854
- const stack = useStack(ctx || renderer.getContext(MAIN_CONTEXT_KEY));
882
+ const stack = useStack(ctx || MAIN_CONTEXT_KEY);
855
883
  stack.value = save2;
856
884
  return restore(save2);
857
885
  };
858
886
  let interacted = 0;
859
887
  const restore = async (save2) => {
860
- if (!$$.get().dataLoaded)
888
+ if (!coreData.get().dataLoaded)
861
889
  return;
862
- let latest = save2 || $.get().saves.at(-1);
890
+ let latest = save2 || storageData.get().saves.at(-1);
863
891
  if (!latest) {
864
892
  latest = klona(initial);
865
- $.update((prev) => {
893
+ storageData.update((prev) => {
866
894
  prev.saves.push(latest);
867
895
  return prev;
868
896
  });
@@ -873,19 +901,22 @@ var novely = ({
873
901
  const previous = stack.previous;
874
902
  const [path] = stack.value = latest;
875
903
  renderer.ui.showScreen("game");
876
- const queue = getActionsFromPath(story, path);
877
- const processor = createQueueProcessor(queue);
878
- 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;
879
910
  if (previous) {
880
- const prevQueue = getActionsFromPath(story, previous[0], true);
881
- const currQueue = getActionsFromPath(story, path, true);
882
- 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--) {
883
913
  const element = prevQueue[i];
884
- if (isAction(element)) {
885
- const [action2, props] = element;
886
- if (action2 === "custom") {
887
- context.clearCustom(props[0]);
888
- }
914
+ if (!isAction(element)) {
915
+ continue;
916
+ }
917
+ const [action2, fn] = element;
918
+ if (action2 === "custom") {
919
+ context.clearCustom(fn);
889
920
  }
890
921
  }
891
922
  }
@@ -905,9 +936,6 @@ var novely = ({
905
936
  render(context);
906
937
  };
907
938
  const refer = (path) => {
908
- if (!path) {
909
- path = useStack(MAIN_CONTEXT_KEY).value[0];
910
- }
911
939
  let current = story;
912
940
  let precurrent = story;
913
941
  const blocks = [];
@@ -946,7 +974,7 @@ var novely = ({
946
974
  ctx.audio.destroy();
947
975
  const [time, type] = current[2];
948
976
  if (type === "auto" && interacted <= 1 && times.has(time)) {
949
- $.update((prev) => {
977
+ storageData.update((prev) => {
950
978
  prev.saves = prev.saves.filter((save2) => save2 !== current);
951
979
  return prev;
952
980
  });
@@ -963,11 +991,13 @@ var novely = ({
963
991
  return translation[lang].internal[key];
964
992
  };
965
993
  const preview = async ([path, data2], name) => {
966
- const queue = getActionsFromPath(story, path);
994
+ const { queue } = getActionsFromPath(story, path, true);
967
995
  const ctx = renderer.getContext(name);
968
996
  ctx.meta.restoring = true;
969
997
  ctx.meta.preview = true;
970
- const processor = createQueueProcessor(queue);
998
+ const processor = createQueueProcessor(queue, {
999
+ skip: /* @__PURE__ */ new Set()
1000
+ });
971
1001
  await processor.run((action2, props) => {
972
1002
  if (isAudioAction(action2))
973
1003
  return;
@@ -984,6 +1014,23 @@ var novely = ({
984
1014
  const removeContext = (name) => {
985
1015
  STACK_MAP.delete(name);
986
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
+ };
987
1034
  const renderer = createRenderer({
988
1035
  mainContextKey: MAIN_CONTEXT_KEY,
989
1036
  characters,
@@ -996,9 +1043,10 @@ var novely = ({
996
1043
  t,
997
1044
  preview,
998
1045
  removeContext,
1046
+ getStateFunction,
999
1047
  languages,
1000
- $,
1001
- $$
1048
+ storageData,
1049
+ coreData
1002
1050
  });
1003
1051
  const useStack = createUseStackFunction(renderer);
1004
1052
  useStack(MAIN_CONTEXT_KEY).push(initial);
@@ -1012,15 +1060,19 @@ var novely = ({
1012
1060
  stack.push(current);
1013
1061
  save(true, "auto");
1014
1062
  };
1015
- const next = (ctx) => {
1016
- const stack = useStack(ctx);
1017
- const path = stack.value[0];
1063
+ const nextPath = (path) => {
1018
1064
  const last = path.at(-1);
1019
1065
  if (last && (isNull(last[0]) || last[0] === "jump") && isNumber(last[1])) {
1020
1066
  last[1]++;
1021
1067
  } else {
1022
1068
  path.push([null, 0]);
1023
1069
  }
1070
+ return path;
1071
+ };
1072
+ const next = (ctx) => {
1073
+ const stack = useStack(ctx);
1074
+ const path = stack.value[0];
1075
+ nextPath(path);
1024
1076
  };
1025
1077
  const matchActionInit = {
1026
1078
  getContext: renderer.getContext,
@@ -1042,7 +1094,7 @@ var novely = ({
1042
1094
  wait({ ctx, push }, [time]) {
1043
1095
  if (ctx.meta.restoring)
1044
1096
  return;
1045
- setTimeout(push, isFunction(time) ? time() : time);
1097
+ setTimeout(push, isFunction(time) ? time(getStateAtCtx(ctx)) : time);
1046
1098
  },
1047
1099
  showBackground({ ctx, push }, [background]) {
1048
1100
  ctx.background(background);
@@ -1088,7 +1140,7 @@ var novely = ({
1088
1140
  const name = (() => {
1089
1141
  const c = character;
1090
1142
  const cs = characters;
1091
- const [lang] = $.get().meta;
1143
+ const [lang] = storageData.get().meta;
1092
1144
  if (c && c in cs) {
1093
1145
  const block = cs[c].name;
1094
1146
  if (typeof block === "string") {
@@ -1101,15 +1153,31 @@ var novely = ({
1101
1153
  return c || "";
1102
1154
  })();
1103
1155
  ctx.dialog(
1104
- unwrap2(content, data2),
1105
- unwrap2(name, data2),
1156
+ templateReplace(content, data2),
1157
+ templateReplace(name, data2),
1106
1158
  character,
1107
1159
  emotion,
1108
1160
  forward
1109
1161
  );
1110
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
+ },
1111
1172
  function({ ctx, push }, [fn]) {
1112
- 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: storageData.get().meta[0],
1176
+ goingBack,
1177
+ restoring,
1178
+ preview: preview2,
1179
+ state: getStateFunction(ctx)
1180
+ });
1113
1181
  if (!ctx.meta.restoring) {
1114
1182
  result ? result.then(push) : push();
1115
1183
  }
@@ -1121,22 +1189,26 @@ var novely = ({
1121
1189
  choices.unshift(question);
1122
1190
  question = "";
1123
1191
  }
1124
- const unwrappedChoices = choices.map(([content, action2, visible]) => {
1125
- if (DEV2 && action2.length === 0 && (!visible || visible())) {
1192
+ const transformedChoices = choices.map(([content, action2, visible]) => {
1193
+ const shown = !visible || visible({
1194
+ lang: storageData.get().meta[0],
1195
+ state: getStateAtCtx(ctx)
1196
+ });
1197
+ if (DEV2 && action2.length === 0 && !shown) {
1126
1198
  console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
1127
1199
  }
1128
- return [unwrap2(content, data2), action2, visible];
1200
+ return [templateReplace(content, data2), shown];
1129
1201
  });
1130
- if (DEV2 && unwrappedChoices.length === 0) {
1202
+ if (DEV2 && transformedChoices.length === 0) {
1131
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]`);
1132
1204
  }
1133
- ctx.choices(unwrap2(question, data2), unwrappedChoices, (selected) => {
1205
+ ctx.choices(templateReplace(question, data2), transformedChoices, (selected) => {
1134
1206
  if (!ctx.meta.preview) {
1135
1207
  enmemory(ctx);
1136
1208
  }
1137
1209
  const stack = useStack(ctx);
1138
1210
  const offset = isWithoutQuestion ? 0 : 1;
1139
- if (DEV2 && !unwrappedChoices[selected]) {
1211
+ if (DEV2 && !transformedChoices[selected]) {
1140
1212
  throw new Error("Choice children is empty, either add content there or make item not selectable");
1141
1213
  }
1142
1214
  stack.value[0].push(["choice", selected + offset], [null, 0]);
@@ -1175,7 +1247,7 @@ var novely = ({
1175
1247
  throw new Error(`Attempt to use Condition action with empty variants object`);
1176
1248
  }
1177
1249
  if (!ctx.meta.restoring) {
1178
- const val = String(condition());
1250
+ const val = String(condition(getStateAtCtx(ctx)));
1179
1251
  if (DEV2 && !variants[val]) {
1180
1252
  throw new Error(`Attempt to go to unknown variant "${val}"`);
1181
1253
  }
@@ -1198,7 +1270,7 @@ var novely = ({
1198
1270
  },
1199
1271
  input({ ctx, data: data2, forward }, [question, onInput, setup]) {
1200
1272
  ctx.input(
1201
- unwrap2(question, data2),
1273
+ templateReplace(question, data2),
1202
1274
  onInput,
1203
1275
  setup || noop,
1204
1276
  forward
@@ -1236,7 +1308,7 @@ var novely = ({
1236
1308
  push();
1237
1309
  },
1238
1310
  text({ ctx, data: data2, forward }, text) {
1239
- const string = text.map((content) => unwrap2(content, data2)).join(" ");
1311
+ const string = text.map((content) => templateReplace(content, data2)).join(" ");
1240
1312
  if (DEV2 && string.length === 0) {
1241
1313
  throw new Error(`Action Text was called with empty string or array`);
1242
1314
  }
@@ -1328,52 +1400,106 @@ var novely = ({
1328
1400
  const interactivity = (value = false) => {
1329
1401
  interacted = value ? interacted + 1 : 0;
1330
1402
  };
1331
- const unwrap2 = (content, values) => {
1403
+ const templateReplace = (content, values) => {
1332
1404
  const {
1333
1405
  data: data2,
1334
1406
  meta: [lang]
1335
- } = $.get();
1336
- const obj = values ? values : data2;
1337
- const cnt = isFunction(content) ? content() : typeof content === "string" ? content : content[lang];
1338
- const str2 = isFunction(cnt) ? cnt() : cnt;
1339
- const trans = translation[lang];
1340
- if (trans.actions || trans.plural) {
1341
- return replace(str2, obj, trans.plural, trans.actions, new Intl.PluralRules(trans.tag || lang));
1342
- }
1343
- return replace(str2, obj);
1407
+ } = storageData.get();
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
+ );
1344
1423
  };
1345
- function data(value) {
1424
+ const data = (value) => {
1425
+ const _data = storageData.get().data;
1346
1426
  if (!value)
1347
- return $.get().data;
1348
- const prev = $.get().data;
1349
- const val = isFunction(value) ? value(prev) : deepmerge(prev, value);
1350
- $.update((prev2) => {
1351
- prev2.data = val;
1352
- return prev2;
1427
+ return _data;
1428
+ const val = isFunction(value) ? value(_data) : deepmerge(_data, value);
1429
+ storageData.update((prev) => {
1430
+ prev.data = val;
1431
+ return prev;
1353
1432
  });
1354
- }
1433
+ return void 0;
1434
+ };
1355
1435
  return {
1356
1436
  /**
1357
1437
  * Function to set game script
1438
+ *
1439
+ * @example
1440
+ * ```ts
1441
+ * engine.script({
1442
+ * start: [
1443
+ * action.function(() => {})
1444
+ * ]
1445
+ * })
1446
+ * ```
1358
1447
  */
1359
1448
  script,
1360
1449
  /**
1361
- * Function to get actions
1450
+ * Get actions
1451
+ *
1452
+ * @example
1453
+ * ```ts
1454
+ * engine.script({
1455
+ * start: [
1456
+ * action.function(() => {})
1457
+ * ]
1458
+ * })
1459
+ * ```
1362
1460
  */
1363
1461
  action,
1364
1462
  /**
1365
- * State that belongs to games
1463
+ * @deprecated Will be removed BUT replaced with state passed into actions as a parameter
1366
1464
  */
1367
- state,
1465
+ state: getStateFunction(MAIN_CONTEXT_KEY),
1368
1466
  /**
1369
- * 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
+ * ```
1370
1481
  */
1371
1482
  data,
1372
1483
  /**
1373
- * Unwraps translatable content to a string value
1484
+ * @deprecated Renamed into `templateReplace`
1374
1485
  */
1375
1486
  unwrap(content) {
1376
- 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);
1377
1503
  },
1378
1504
  /**
1379
1505
  * Cancel data loading, hide UI, ignore page change events