@novely/core 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,10 @@ type Stored<T> = {
4
4
  set: (val: T) => void;
5
5
  get: () => T;
6
6
  };
7
+ type Derived<T> = {
8
+ subscribe: (cb: (value: T) => void) => () => void;
9
+ get: () => T;
10
+ };
7
11
 
8
12
  declare const RU: {
9
13
  NewGame: string;
@@ -100,9 +104,9 @@ type Context = {
100
104
  text: (str: string, resolve: () => void) => void;
101
105
  vibrate: (pattern: VibratePattern) => void;
102
106
  audio: {
103
- voice: (source: string) => void;
107
+ voice: (source: string, paused: Derived<boolean>) => void;
104
108
  voiceStop: () => void;
105
- music: (source: string, method: 'music' | 'sound') => AudioHandle;
109
+ music: (source: string, paused: Derived<boolean>, method: 'music' | 'sound') => AudioHandle;
106
110
  /**
107
111
  * Stop all sounds
108
112
  */
@@ -224,6 +228,7 @@ type RendererInit<$Language extends Lang, $Characters extends Record<string, Cha
224
228
  getCharacterAssets: (character: string, emotion: string) => string[];
225
229
  getDialogOverview: () => Promise<DialogOverview>;
226
230
  getResourseType: (url: string) => Promise<'image' | 'audio' | 'other'>;
231
+ setLanguage: (language: string) => void;
227
232
  };
228
233
 
229
234
  type LocalStorageStorageSettings = {
@@ -292,6 +297,8 @@ type Assign<A extends object, B extends object> = Pick<A, Exclude<keyof A, keyof
292
297
  type NonEmptyRecord<T extends Record<PropertyKey, unknown>> = keyof T extends never ? never : T;
293
298
  type CoreData = {
294
299
  dataLoaded: boolean;
300
+ paused: boolean;
301
+ focused: boolean;
295
302
  };
296
303
  type StackHolder = Save[] & {
297
304
  previous: Save | undefined;
@@ -370,6 +377,7 @@ type StoryOptionsDynamic = {
370
377
  load: (scene: string) => Promise<Story>;
371
378
  };
372
379
  type StoryOptions = StoryOptionsStatic | StoryOptionsDynamic;
380
+ type OnLanguageChange<$Lang extends Lang> = (language: $Lang) => void;
373
381
  interface NovelyInit<$Language extends Lang, $Characters extends Record<string, Character<NoInfer<$Language>>>, $State extends State, $Data extends Data, $Actions extends Record<string, (...args: any[]) => ValidAction>> {
374
382
  /**
375
383
  * An object containing the characters in the game.
@@ -590,9 +598,13 @@ interface NovelyInit<$Language extends Lang, $Characters extends Record<string,
590
598
  */
591
599
  defaultTypewriterSpeed?: TypewriterSpeed;
592
600
  /**
593
- *
601
+ * Options to control story loading behaviour
594
602
  */
595
603
  storyOptions?: StoryOptions;
604
+ /**
605
+ * Will be called ONLY when language was changed by player
606
+ */
607
+ onLanguageChange?: OnLanguageChange<NoInfer<$Language>>;
596
608
  }
597
609
  type StateFunction<S extends State> = {
598
610
  (value: DeepPartial<S> | ((prev: S) => S)): void;
@@ -715,6 +727,10 @@ type CustomHandlerFunctionParameters<L extends string, S extends State> = {
715
727
  * Function to replace template content
716
728
  */
717
729
  templateReplace: (content: TextContent<L, State>, values?: State) => string;
730
+ /**
731
+ * Is game currently paused
732
+ */
733
+ paused: Derived<boolean>;
718
734
  };
719
735
  type CustomHandlerFunction<L extends string, S extends State> = (parameters: CustomHandlerFunctionParameters<L, S>) => Thenable<void>;
720
736
  type CustomHandlerCalling = {
@@ -921,7 +937,7 @@ type TypesFromEngine<T> = T extends {
921
937
  types: EngineTypes<infer $Lang, infer $State, infer $Data, infer $Characters> | null;
922
938
  } ? EngineTypes<$Lang, $State, $Data, $Characters> : never;
923
939
 
924
- declare const novely: <$Language extends string, $Characters extends Record<string, Character<$Language>>, $State extends State, $Data extends Data, $Actions extends Record<string, (...args: any[]) => ValidAction>>({ characters, characterAssetSizes, defaultEmotions, storage, storageDelay, renderer: createRenderer, initialScreen, translation, state: defaultState, data: defaultData, autosaves, migrations, throttleTimeout, getLanguage, overrideLanguage, askBeforeExit, preloadAssets, parallelAssetsDownloadLimit, fetch: request, cloneFunction: clone, saveOnUnload, startKey, defaultTypewriterSpeed, storyOptions, }: NovelyInit<$Language, $Characters, $State, $Data, $Actions>) => {
940
+ declare const novely: <$Language extends string, $Characters extends Record<string, Character<$Language>>, $State extends State, $Data extends Data, $Actions extends Record<string, (...args: any[]) => ValidAction>>({ characters, characterAssetSizes, defaultEmotions, storage, storageDelay, renderer: createRenderer, initialScreen, translation, state: defaultState, data: defaultData, autosaves, migrations, throttleTimeout, getLanguage, overrideLanguage, askBeforeExit, preloadAssets, parallelAssetsDownloadLimit, fetch: request, cloneFunction: clone, saveOnUnload, startKey, defaultTypewriterSpeed, storyOptions, onLanguageChange, }: NovelyInit<$Language, $Characters, $State, $Data, $Actions>) => {
925
941
  /**
926
942
  * Function to set game script
927
943
  *
@@ -1045,6 +1061,33 @@ declare const novely: <$Language extends string, $Characters extends Record<stri
1045
1061
  * ```
1046
1062
  */
1047
1063
  setStorageData: (data: StorageData<$Language, $Data>) => void;
1064
+ /**
1065
+ * Function to control paused state. Custom Actions are provided with `paused` store they can subscribe to.
1066
+ * This function will notify Custom Actions. Pause state can be used when showing ads.
1067
+ * @example
1068
+ * ```ts
1069
+ * sdk.on('pause' () => engine.setPaused(true));
1070
+ * sdk.on('resume', () => engine.setPaused(false));
1071
+ * ```
1072
+ */
1073
+ setPaused: (paused: boolean) => void;
1074
+ /**
1075
+ * Function to control focused state. It will affect `paused` store passed to Custom Actions.
1076
+ * This function can be used to pause game when it's not focused.
1077
+ * @example
1078
+ * ```ts
1079
+ * import { pauseOnBlur } from '@novely/core';
1080
+ *
1081
+ * // Will subscribe to blur/focus events and call `setFocused`
1082
+ * pauseOnBlur(engine);
1083
+ *
1084
+ * // OR
1085
+ *
1086
+ * sdk.on('focus' () => engine.setFocused(true));
1087
+ * sdk.on('blur', () => engine.setFocused(false));
1088
+ * ```
1089
+ */
1090
+ setFocused: (focused: boolean) => void;
1048
1091
  };
1049
1092
 
1050
1093
  type Part = Record<string, (...args: any[]) => ValidAction>;
@@ -1093,4 +1136,10 @@ declare const asset: {
1093
1136
  audio(source: string): NovelyAsset;
1094
1137
  };
1095
1138
 
1096
- export { type ActionChoiceChoice, type ActionChoiceChoiceObject, type ActionInputOnInputMeta, type ActionInputSetup, type ActionInputSetupCleanup, type ActionProxy, type AllowedContent, type AudioHandle, type BackgroundImage, type BaseTranslationStrings, type Character, type CharacterAssetSizes, type CharacterHandle, type CharactersData, type ChoiceCheckFunction, type ChoiceCheckFunctionProps, type ChoiceOnSelectFunction, type ChoiceOnSelectFunctionProps, type ChoiceParams, type ConditionCheckFunction, type ConditionParams, type Context, type CoreData, type CustomActionHandle, type CustomHandler, type CustomHandlerFunction, type CustomHandlerFunctionGetFn, type CustomHandlerFunctionParameters, type CustomHandlerGetResult, type CustomHandlerGetResultDataFunction, type CustomHandlerInfo, type Data, type DeepPartial, type DefaultActionProxy, EN, type Emotions, type EngineTypes, type FunctionParams, type FunctionableValue, type GetActionParameters, type InputHandler, type Lang, type NovelyAsset, type NovelyInit, type NovelyScreen, type NovelyStorage, type Path, type PathItem, type PluralType, type Pluralization, RU, type Renderer, type RendererInit, type RendererInitPreviewReturn, type Save, type Stack, type StackHolder, type State, type StateFunction, type StorageData, type StorageMeta, type Stored, type Story, type TextContent, type Thenable, type TranslationActions, type TypeEssentials, type TypesFromEngine, type TypewriterSpeed, type ValidAction, asset, extendAction, localStorageStorage, novely };
1139
+ declare const pauseOnBlur: (engine: {
1140
+ setFocused: (focused: boolean) => void;
1141
+ }) => {
1142
+ unsubscribe: () => void;
1143
+ };
1144
+
1145
+ export { type ActionChoiceChoice, type ActionChoiceChoiceObject, type ActionInputOnInputMeta, type ActionInputSetup, type ActionInputSetupCleanup, type ActionProxy, type AllowedContent, type AudioHandle, type BackgroundImage, type BaseTranslationStrings, type Character, type CharacterAssetSizes, type CharacterHandle, type CharactersData, type ChoiceCheckFunction, type ChoiceCheckFunctionProps, type ChoiceOnSelectFunction, type ChoiceOnSelectFunctionProps, type ChoiceParams, type ConditionCheckFunction, type ConditionParams, type Context, type CoreData, type CustomActionHandle, type CustomHandler, type CustomHandlerFunction, type CustomHandlerFunctionGetFn, type CustomHandlerFunctionParameters, type CustomHandlerGetResult, type CustomHandlerGetResultDataFunction, type CustomHandlerInfo, type Data, type DeepPartial, type DefaultActionProxy, type Derived, EN, type Emotions, type EngineTypes, type FunctionParams, type FunctionableValue, type GetActionParameters, type InputHandler, type Lang, type NovelyAsset, type NovelyInit, type NovelyScreen, type NovelyStorage, type Path, type PathItem, type PluralType, type Pluralization, RU, type Renderer, type RendererInit, type RendererInitPreviewReturn, type Save, type Stack, type StackHolder, type State, type StateFunction, type StorageData, type StorageMeta, type Stored, type Story, type TextContent, type Thenable, type TranslationActions, type TypeEssentials, type TypesFromEngine, type TypewriterSpeed, type ValidAction, asset, extendAction, localStorageStorage, novely, pauseOnBlur };
package/dist/index.js CHANGED
@@ -951,6 +951,48 @@ var getCharactersData = (characters) => {
951
951
  return Object.fromEntries(mapped);
952
952
  };
953
953
 
954
+ // src/store.ts
955
+ var store = (current, subscribers = /* @__PURE__ */ new Set()) => {
956
+ const subscribe = (cb) => {
957
+ subscribers.add(cb), cb(current);
958
+ return () => {
959
+ subscribers.delete(cb);
960
+ };
961
+ };
962
+ const push = (value) => {
963
+ for (const cb of subscribers) cb(value);
964
+ };
965
+ const update = (fn) => {
966
+ push(current = fn(current));
967
+ };
968
+ const set2 = (val) => {
969
+ update(() => val);
970
+ };
971
+ const get = () => {
972
+ return current;
973
+ };
974
+ return { subscribe, update, set: set2, get };
975
+ };
976
+ var derive = (input, map) => {
977
+ return {
978
+ get: () => map(input.get()),
979
+ subscribe: (subscriber) => {
980
+ return input.subscribe((value) => {
981
+ return subscriber(map(value));
982
+ });
983
+ }
984
+ };
985
+ };
986
+ var immutable = (value) => {
987
+ return {
988
+ get: () => value,
989
+ subscribe: (subscriber) => {
990
+ subscriber(value);
991
+ return noop;
992
+ }
993
+ };
994
+ };
995
+
954
996
  // src/custom-action.ts
955
997
  var createCustomActionNode = (id) => {
956
998
  const div = document.createElement("div");
@@ -978,7 +1020,8 @@ var handleCustomAction = (ctx, fn, {
978
1020
  setClear,
979
1021
  remove: renderersRemove,
980
1022
  getStack: getStack2,
981
- templateReplace
1023
+ templateReplace,
1024
+ paused
982
1025
  }) => {
983
1026
  const holder = getCustomActionHolder(ctx, fn);
984
1027
  const flags = {
@@ -1034,7 +1077,8 @@ var handleCustomAction = (ctx, fn, {
1034
1077
  rendererContext: ctx,
1035
1078
  getDomNodes,
1036
1079
  getPath,
1037
- contextKey: ctx.id
1080
+ contextKey: ctx.id,
1081
+ paused: flags.preview ? immutable(false) : paused
1038
1082
  });
1039
1083
  };
1040
1084
 
@@ -1164,29 +1208,6 @@ var localStorageStorage = (options) => {
1164
1208
  };
1165
1209
  };
1166
1210
 
1167
- // src/store.ts
1168
- var store = (current, subscribers = /* @__PURE__ */ new Set()) => {
1169
- const subscribe = (cb) => {
1170
- subscribers.add(cb), cb(current);
1171
- return () => {
1172
- subscribers.delete(cb);
1173
- };
1174
- };
1175
- const push = (value) => {
1176
- for (const cb of subscribers) cb(value);
1177
- };
1178
- const update = (fn) => {
1179
- push(current = fn(current));
1180
- };
1181
- const set2 = (val) => {
1182
- update(() => val);
1183
- };
1184
- const get = () => {
1185
- return current;
1186
- };
1187
- return { subscribe, update, set: set2, get };
1188
- };
1189
-
1190
1211
  // src/translation.ts
1191
1212
  var RGX = /{{(.*?)}}/g;
1192
1213
  var split = (input, delimeters) => {
@@ -1330,6 +1351,11 @@ var getDialogOverview = async function() {
1330
1351
  return entries;
1331
1352
  };
1332
1353
 
1354
+ // src/utilities/document.ts
1355
+ var setDocumentLanguage = (language) => {
1356
+ document.documentElement.lang = language;
1357
+ };
1358
+
1333
1359
  // src/novely.ts
1334
1360
  var novely = ({
1335
1361
  characters,
@@ -1355,7 +1381,8 @@ var novely = ({
1355
1381
  saveOnUnload = true,
1356
1382
  startKey = "start",
1357
1383
  defaultTypewriterSpeed = DEFAULT_TYPEWRITER_SPEED,
1358
- storyOptions = { mode: "static" }
1384
+ storyOptions = { mode: "static" },
1385
+ onLanguageChange
1359
1386
  }) => {
1360
1387
  const languages = Object.keys(translation);
1361
1388
  const limitScript = pLimit(1);
@@ -1412,6 +1439,7 @@ var novely = ({
1412
1439
  const getLanguageWithoutParameters = () => {
1413
1440
  const language = getLanguage2(languages, getLanguage);
1414
1441
  if (languages.includes(language)) {
1442
+ setDocumentLanguage(language);
1415
1443
  return language;
1416
1444
  }
1417
1445
  if (DEV5) {
@@ -1428,8 +1456,11 @@ var novely = ({
1428
1456
  };
1429
1457
  const storageData = store(initialData);
1430
1458
  const coreData = store({
1431
- dataLoaded: false
1459
+ dataLoaded: false,
1460
+ paused: false,
1461
+ focused: document.visibilityState === "visible"
1432
1462
  });
1463
+ const paused = derive(coreData, (s) => s.paused || !s.focused);
1433
1464
  const onDataLoadedPromise = async ({ cancelled }) => {
1434
1465
  if (cancelled) {
1435
1466
  dataLoaded.promise.then(onDataLoadedPromise);
@@ -1775,6 +1806,18 @@ var novely = ({
1775
1806
  }
1776
1807
  return String(c);
1777
1808
  };
1809
+ const setLanguage = (lang) => {
1810
+ storageData.update((prev) => {
1811
+ if (languages.includes(lang)) {
1812
+ prev.meta[0] = lang;
1813
+ }
1814
+ if (lang === prev.meta[0]) {
1815
+ setDocumentLanguage(lang);
1816
+ onLanguageChange?.(lang);
1817
+ }
1818
+ return prev;
1819
+ });
1820
+ };
1778
1821
  const renderer = createRenderer({
1779
1822
  mainContextKey: MAIN_CONTEXT_KEY,
1780
1823
  characters: getCharactersData(characters),
@@ -1804,7 +1847,8 @@ var novely = ({
1804
1847
  getStack: () => useStack(MAIN_CONTEXT_KEY),
1805
1848
  templateReplace: (...args) => templateReplace(...args)
1806
1849
  }),
1807
- getResourseType: getResourseTypeWrapper
1850
+ getResourseType: getResourseTypeWrapper,
1851
+ setLanguage
1808
1852
  });
1809
1853
  const useStack = createUseStackFunction(renderer);
1810
1854
  useStack(MAIN_CONTEXT_KEY).push(initial);
@@ -1882,27 +1926,27 @@ var novely = ({
1882
1926
  push();
1883
1927
  },
1884
1928
  playMusic({ ctx, push }, [source]) {
1885
- ctx.audio.music(unwrapAudioAsset(source), "music").play(true);
1929
+ ctx.audio.music(unwrapAudioAsset(source), paused, "music").play(true);
1886
1930
  push();
1887
1931
  },
1888
1932
  pauseMusic({ ctx, push }, [source]) {
1889
- ctx.audio.music(unwrapAudioAsset(source), "music").pause();
1933
+ ctx.audio.music(unwrapAudioAsset(source), paused, "music").pause();
1890
1934
  push();
1891
1935
  },
1892
1936
  stopMusic({ ctx, push }, [source]) {
1893
- ctx.audio.music(unwrapAudioAsset(source), "music").stop();
1937
+ ctx.audio.music(unwrapAudioAsset(source), paused, "music").stop();
1894
1938
  push();
1895
1939
  },
1896
1940
  playSound({ ctx, push }, [source, loop]) {
1897
- ctx.audio.music(unwrapAudioAsset(source), "sound").play(loop || false);
1941
+ ctx.audio.music(unwrapAudioAsset(source), paused, "sound").play(loop || false);
1898
1942
  push();
1899
1943
  },
1900
1944
  pauseSound({ ctx, push }, [source]) {
1901
- ctx.audio.music(unwrapAudioAsset(source), "sound").pause();
1945
+ ctx.audio.music(unwrapAudioAsset(source), paused, "sound").pause();
1902
1946
  push();
1903
1947
  },
1904
1948
  stopSound({ ctx, push }, [source]) {
1905
- ctx.audio.music(unwrapAudioAsset(source), "sound").stop();
1949
+ ctx.audio.music(unwrapAudioAsset(source), paused, "sound").stop();
1906
1950
  push();
1907
1951
  },
1908
1952
  voice({ ctx, push }, [source]) {
@@ -1912,7 +1956,7 @@ var novely = ({
1912
1956
  push();
1913
1957
  return;
1914
1958
  }
1915
- ctx.audio.voice(unwrapAudioAsset(audioSource));
1959
+ ctx.audio.voice(unwrapAudioAsset(audioSource), paused);
1916
1960
  push();
1917
1961
  },
1918
1962
  stopVoice({ ctx, push }) {
@@ -2069,6 +2113,7 @@ var novely = ({
2069
2113
  state,
2070
2114
  lang,
2071
2115
  getStack: useStack,
2116
+ paused,
2072
2117
  templateReplace
2073
2118
  });
2074
2119
  const next2 = () => {
@@ -2347,6 +2392,7 @@ var novely = ({
2347
2392
  * Data updates still will work in case Novely already was loaded
2348
2393
  */
2349
2394
  destroy() {
2395
+ if (destroyed) return;
2350
2396
  dataLoaded.cancel();
2351
2397
  UIInstance.unmount();
2352
2398
  removeEventListener("beforeunload", throttledShortOnStorageDataChange);
@@ -2377,7 +2423,44 @@ var novely = ({
2377
2423
  * }
2378
2424
  * ```
2379
2425
  */
2380
- setStorageData
2426
+ setStorageData,
2427
+ /**
2428
+ * Function to control paused state. Custom Actions are provided with `paused` store they can subscribe to.
2429
+ * This function will notify Custom Actions. Pause state can be used when showing ads.
2430
+ * @example
2431
+ * ```ts
2432
+ * sdk.on('pause' () => engine.setPaused(true));
2433
+ * sdk.on('resume', () => engine.setPaused(false));
2434
+ * ```
2435
+ */
2436
+ setPaused: (paused2) => {
2437
+ coreData.update((prev) => {
2438
+ prev.paused = paused2;
2439
+ return prev;
2440
+ });
2441
+ },
2442
+ /**
2443
+ * Function to control focused state. It will affect `paused` store passed to Custom Actions.
2444
+ * This function can be used to pause game when it's not focused.
2445
+ * @example
2446
+ * ```ts
2447
+ * import { pauseOnBlur } from '@novely/core';
2448
+ *
2449
+ * // Will subscribe to blur/focus events and call `setFocused`
2450
+ * pauseOnBlur(engine);
2451
+ *
2452
+ * // OR
2453
+ *
2454
+ * sdk.on('focus' () => engine.setFocused(true));
2455
+ * sdk.on('blur', () => engine.setFocused(false));
2456
+ * ```
2457
+ */
2458
+ setFocused: (focused) => {
2459
+ coreData.update((prev) => {
2460
+ prev.focused = focused;
2461
+ return prev;
2462
+ });
2463
+ }
2381
2464
  };
2382
2465
  };
2383
2466
 
@@ -2466,12 +2549,53 @@ var EN = {
2466
2549
  Close: "Close",
2467
2550
  DialogOverview: "Dialog Overview"
2468
2551
  };
2552
+
2553
+ // src/browser-events.ts
2554
+ var BLUR_HANDLERS = /* @__PURE__ */ new Set();
2555
+ var FOCUS_HANDLERS = /* @__PURE__ */ new Set();
2556
+ var registerEventListeners = (listeners) => {
2557
+ BLUR_HANDLERS.add(listeners.blur);
2558
+ FOCUS_HANDLERS.add(listeners.focus);
2559
+ return () => {
2560
+ BLUR_HANDLERS.delete(listeners.blur);
2561
+ FOCUS_HANDLERS.delete(listeners.focus);
2562
+ };
2563
+ };
2564
+ addEventListener("focus", function(event) {
2565
+ for (const handler of FOCUS_HANDLERS) {
2566
+ try {
2567
+ handler.call(this.document, event);
2568
+ } catch {
2569
+ }
2570
+ }
2571
+ });
2572
+ addEventListener("blur", function(event) {
2573
+ for (const handler of BLUR_HANDLERS) {
2574
+ try {
2575
+ handler.call(this.document, event);
2576
+ } catch {
2577
+ }
2578
+ }
2579
+ });
2580
+ var pauseOnBlur = (engine) => {
2581
+ return {
2582
+ unsubscribe: registerEventListeners({
2583
+ focus: () => {
2584
+ engine.setFocused(true);
2585
+ },
2586
+ blur: () => {
2587
+ engine.setFocused(false);
2588
+ }
2589
+ })
2590
+ };
2591
+ };
2469
2592
  export {
2470
2593
  EN,
2471
2594
  RU,
2472
2595
  asset,
2473
2596
  extendAction,
2474
2597
  localStorageStorage,
2475
- novely
2598
+ novely,
2599
+ pauseOnBlur
2476
2600
  };
2477
2601
  //# sourceMappingURL=index.js.map