@novely/core 0.25.0 → 0.27.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
@@ -88,6 +88,49 @@ type AudioHandle = {
88
88
  pause: () => void;
89
89
  play: () => void;
90
90
  };
91
+ type Context = {
92
+ id: string;
93
+ get root(): HTMLElement;
94
+ set root(value: HTMLElement);
95
+ character: (character: string) => CharacterHandle;
96
+ background: (background: string | BackgroundImage) => void;
97
+ dialog: (content: string, name: string, character: string | undefined, emotion: string | undefined, resolve: () => void) => void;
98
+ choices: (question: string, choices: [name: string, active: boolean][], resolve: (selected: number) => void) => void;
99
+ input: (question: string, onInput: (meta: ActionInputOnInputMeta<Lang, State>) => void, setup: ActionInputSetup, resolve: () => void) => void;
100
+ clear: (keep: Set<keyof DefaultActionProxy>, keepCharacters: Set<string>, keepAudio: {
101
+ music: Set<string>;
102
+ sounds: Set<string>;
103
+ }, resolve: () => void) => void;
104
+ custom: (fn: CustomHandler<Lang, State>, push: () => void) => Thenable<void>;
105
+ clearCustom: (fn: CustomHandler<Lang, State>) => void;
106
+ text: (str: string, resolve: () => void) => void;
107
+ vibrate: (pattern: VibratePattern) => void;
108
+ audio: {
109
+ voice: (source: string) => void;
110
+ voiceStop: () => void;
111
+ music: (source: string, method: 'music' | 'sound', loop?: boolean) => AudioHandle;
112
+ /**
113
+ * Stop all sounds
114
+ */
115
+ clear: () => void;
116
+ /**
117
+ * Destroy
118
+ */
119
+ destroy: () => void;
120
+ /**
121
+ * Initialize audio service, attach events, etc
122
+ */
123
+ start: () => void;
124
+ };
125
+ meta: {
126
+ get restoring(): boolean;
127
+ set restoring(value: boolean);
128
+ get preview(): boolean;
129
+ set preview(value: boolean);
130
+ get goingBack(): boolean;
131
+ set goingBack(value: boolean);
132
+ };
133
+ };
91
134
  type Renderer = {
92
135
  misc: {
93
136
  /**
@@ -112,11 +155,21 @@ type Renderer = {
112
155
  /**
113
156
  * Shows the screen
114
157
  */
115
- showScreen(name: NovelyScreen | 'loading'): void;
158
+ showScreen(name: NovelyScreen): void;
116
159
  /**
117
160
  * Returns current screen
118
161
  */
119
- getScreen(): NovelyScreen | 'loading' | (string & Record<never, never>);
162
+ getScreen(): NovelyScreen | (string & Record<never, never>);
163
+ /**
164
+ * Shows loading
165
+ *
166
+ * Unline `showScreen('loading')` does not change screen
167
+ */
168
+ showLoading(): void;
169
+ /**
170
+ * Hides loading
171
+ */
172
+ hideLoading(): void;
120
173
  /**
121
174
  * Shows prompt to exit
122
175
  */
@@ -131,80 +184,35 @@ type Renderer = {
131
184
  unmount(): void;
132
185
  };
133
186
  };
134
- getContext: (context: string) => {
135
- id: string;
136
- get root(): HTMLElement;
137
- set root(value: HTMLElement);
138
- character: (character: string) => CharacterHandle;
139
- background: (background: string | BackgroundImage) => void;
140
- dialog: (content: string, name: string, character: string | undefined, emotion: string | undefined, resolve: () => void) => void;
141
- choices: (question: string, choices: [name: string, actions: ValidAction[], active?: boolean][], resolve: (selected: number) => void) => void;
142
- input: (question: string, onInput: (meta: ActionInputOnInputMeta<string, State>) => void, setup: ActionInputSetup, resolve: () => void) => void;
143
- clear: (keep: Set<keyof DefaultActionProxy>, keepCharacters: Set<string>, keepAudio: {
144
- music: Set<string>;
145
- sounds: Set<string>;
146
- }, resolve: () => void) => void;
147
- custom: (fn: Parameters<DefaultActionProxy['custom']>[0], push: () => void) => Thenable<void>;
148
- clearCustom: (fn: Parameters<DefaultActionProxy['custom']>[0]) => void;
149
- text: (str: string, resolve: () => void) => void;
150
- vibrate: (pattern: VibratePattern) => void;
151
- audio: {
152
- voice: (source: string) => void;
153
- voiceStop: () => void;
154
- music: (source: string, method: 'music' | 'sound', loop?: boolean) => AudioHandle;
155
- /**
156
- * Stop all sounds
157
- */
158
- clear: () => void;
159
- /**
160
- * Destroy
161
- */
162
- destroy: () => void;
163
- /**
164
- * Initialize audio service, attach events, etc
165
- */
166
- start: () => void;
167
- };
168
- meta: {
169
- get restoring(): boolean;
170
- set restoring(value: boolean);
171
- get preview(): boolean;
172
- set preview(value: boolean);
173
- get goingBack(): boolean;
174
- set goingBack(value: boolean);
175
- };
176
- store: unknown;
177
- setStore: unknown;
178
- };
187
+ getContext: (context: string) => Context;
179
188
  removeContext: (context: string) => void;
180
189
  };
181
- type Context = ReturnType<Renderer['getContext']>;
182
190
  type RendererInit = {
183
- characters: Record<string, Character>;
184
- set: (save: Save) => Promise<void>;
185
- restore: (save?: Save) => Promise<void>;
186
- save: (override?: boolean, type?: Save[2][1]) => void;
191
+ characters: Record<string, Character<Lang>>;
192
+ set: (save: Save<State>) => Promise<void>;
193
+ restore: (save?: Save<State>) => Promise<void>;
194
+ save: (override?: boolean, type?: Save<State>[2][1]) => void;
187
195
  newGame: () => void;
188
196
  exit: (force?: boolean) => void;
189
197
  back: () => Promise<void>;
190
- languages: string[];
198
+ languages: Lang[];
191
199
  /**
192
200
  * Translation function
193
201
  */
194
- t: (key: BaseTranslationStrings, lang: string) => string;
202
+ t: (key: BaseTranslationStrings, lang: Lang) => string;
195
203
  /**
196
204
  * Store that tracks data updates
197
205
  */
198
- $: Stored<StorageData>;
206
+ storageData: Stored<StorageData<Lang, Data>>;
199
207
  /**
200
208
  * Store that used to communicate between renderer and core
201
209
  */
202
- $$: Stored<CoreData>;
210
+ coreData: Stored<CoreData>;
203
211
  /**
204
212
  * There is different context, and the main one which is used for game
205
213
  */
206
214
  mainContextKey: string;
207
- preview: (save: Save, name: string) => Promise<void>;
215
+ preview: (save: Save<State>, name: string) => Promise<void>;
208
216
  removeContext: (name: string) => void;
209
217
  getStateFunction: (context: string) => StateFunction<State>;
210
218
  };
@@ -212,6 +220,7 @@ type RendererInit = {
212
220
  declare const getLanguage: (languages: string[]) => string;
213
221
 
214
222
  type Thenable<T> = T | Promise<T>;
223
+ type NoInfer<T> = [T][T extends any ? 0 : never];
215
224
  type PathItem = [null, number | string] | ['jump', string] | ['choice', number] | ['choice:exit'] | ['condition', string] | ['condition:exit'] | ['exit'] | ['block', string] | ['block:exit'];
216
225
  type Path = PathItem[];
217
226
  type State = Record<string, any>;
@@ -225,7 +234,7 @@ type Save<S extends State = State> = [
225
234
  meta: SaveMeta
226
235
  ];
227
236
  type Lang = string;
228
- type TypewriterSpeed = 'Slow' | 'Medium' | 'Fast' | 'Auto' | (string & Record<never, never>);
237
+ type TypewriterSpeed = 'Slow' | 'Medium' | 'Fast' | 'Auto';
229
238
  type SoundVolume = number;
230
239
  type StorageMeta<L extends string = string> = [
231
240
  lang: L,
@@ -269,7 +278,7 @@ type TranslationDescription = {
269
278
  plural?: Record<string, Pluralization>;
270
279
  actions?: TranslationActions;
271
280
  };
272
- interface NovelyInit<Languages extends string, Characters extends Record<string, Character<Languages>>, StateScheme extends State, DataScheme extends Data> {
281
+ interface NovelyInit<$Language extends Lang, Characters extends Record<string, Character<NoInfer<$Language>>>, StateScheme extends State, DataScheme extends Data> {
273
282
  /**
274
283
  * An object containing the characters in the game.
275
284
  * @example
@@ -301,7 +310,7 @@ interface NovelyInit<Languages extends string, Characters extends Record<string,
301
310
  /**
302
311
  * A function that returns a Renderer object used to display the game's content
303
312
  */
304
- renderer: (characters: RendererInit) => Renderer;
313
+ renderer: (initializationData: RendererInit) => Renderer;
305
314
  /**
306
315
  * An optional property that specifies the initial screen to display when the game starts
307
316
  */
@@ -328,7 +337,7 @@ interface NovelyInit<Languages extends string, Characters extends Record<string,
328
337
  * })
329
338
  * ```
330
339
  */
331
- translation: Record<Languages, TranslationDescription>;
340
+ translation: Record<$Language, TranslationDescription>;
332
341
  /**
333
342
  * Initial state value
334
343
  *
@@ -375,7 +384,7 @@ interface NovelyInit<Languages extends string, Characters extends Record<string,
375
384
  * })
376
385
  * ```
377
386
  */
378
- getLanguage?: (languages: string[], original: typeof getLanguage) => string;
387
+ getLanguage?: (languages: NoInfer<$Language>[], original: typeof getLanguage) => $Language | (string & Record<never, never>);
379
388
  /**
380
389
  * Ignores saved language, and uses `getLanguage` to get it on every engine start
381
390
  * @default false
@@ -631,4 +640,4 @@ declare const novely: <Languages extends string, Characters extends Record<strin
631
640
  destroy(): void;
632
641
  };
633
642
 
634
- export { type ActionProxy, type AllowedContent, type AudioHandle, type BaseTranslationStrings, type Character, type CharacterHandle, type Context, type CoreData, type CustomHandler, type CustomHandlerFunctionGetFn, type CustomHandlerFunctionParameters, type CustomHandlerGetResult, type CustomHandlerGetResultDataFunction, type DefaultActionProxy, EN, type Emotions, type FunctionableValue, type GetActionParameters, JP, KK, type Lang, type NovelyInit, type NovelyScreen, type Path, type PluralType, type Pluralization, RU, type Renderer, type RendererInit, type Save, type Stack, type StackHolder, type Storage, type StorageData, type StorageMeta, type Stored, type Story, type TextContent, type Thenable, type TranslationActions, type TypewriterSpeed, type ValidAction, localStorageStorage, novely };
643
+ export { type ActionInputOnInputMeta, type ActionInputSetup, type ActionProxy, type AllowedContent, type AudioHandle, type BackgroundImage, type BaseTranslationStrings, type Character, type CharacterHandle, type Context, type CoreData, type CustomHandler, type CustomHandlerFunctionGetFn, type CustomHandlerFunctionParameters, type CustomHandlerGetResult, type CustomHandlerGetResultDataFunction, type Data, type DeepPartial, type DefaultActionProxy, EN, type Emotions, type FunctionableValue, type GetActionParameters, JP, KK, type Lang, type NovelyInit, type NovelyScreen, type Path, type PluralType, type Pluralization, RU, type Renderer, type RendererInit, type Save, type Stack, type StackHolder, type State, type StateFunction, type Storage, type StorageData, type StorageMeta, type Stored, type Story, type TextContent, type Thenable, type TranslationActions, type TypewriterSpeed, type ValidAction, localStorageStorage, novely };
@@ -846,9 +846,8 @@ var Novely = (() => {
846
846
  const story = {};
847
847
  const times = /* @__PURE__ */ new Set();
848
848
  const ASSETS_TO_PRELOAD = /* @__PURE__ */ new Set();
849
- const assetsLoaded = createControlledPromise();
850
849
  const dataLoaded = createControlledPromise();
851
- let scriptCalled = false;
850
+ let initialScreenWasShown = false;
852
851
  defaultData ||= {};
853
852
  defaultState ||= {};
854
853
  const intime = (value) => {
@@ -856,8 +855,15 @@ var Novely = (() => {
856
855
  };
857
856
  const scriptBase = async (part) => {
858
857
  Object.assign(story, flattenStory(part));
858
+ let loadingIsShown = false;
859
+ if (!initialScreenWasShown) {
860
+ renderer.ui.showLoading();
861
+ loadingIsShown = true;
862
+ }
859
863
  if (preloadAssets === "blocking" && ASSETS_TO_PRELOAD.size > 0) {
860
- renderer.ui.showScreen("loading");
864
+ if (!loadingIsShown) {
865
+ renderer.ui.showLoading();
866
+ }
861
867
  const { preloadAudioBlocking, preloadImageBlocking } = renderer.misc;
862
868
  const list = mapSet(ASSETS_TO_PRELOAD, (asset) => {
863
869
  return limitAssetsDownload(async () => {
@@ -876,20 +882,17 @@ var Novely = (() => {
876
882
  });
877
883
  await Promise.allSettled(list);
878
884
  }
879
- const screen = renderer.ui.getScreen();
880
- const nextScreen = scriptCalled ? screen : initialScreen;
881
885
  ASSETS_TO_PRELOAD.clear();
882
- assetsLoaded.resolve();
883
- if (nextScreen === "game") {
884
- await assetsLoaded.promise;
885
- await dataLoaded.promise;
886
- if (!scriptCalled) {
886
+ await dataLoaded.promise;
887
+ renderer.ui.hideLoading();
888
+ if (!initialScreenWasShown) {
889
+ initialScreenWasShown = true;
890
+ if (initialScreen === "game") {
887
891
  restore();
892
+ } else {
893
+ renderer.ui.showScreen(initialScreen);
888
894
  }
889
- } else {
890
- renderer.ui.showScreen(nextScreen);
891
895
  }
892
- scriptCalled = true;
893
896
  };
894
897
  const script = (part) => {
895
898
  return limitScript(() => scriptBase(part));
@@ -953,7 +956,7 @@ var Novely = (() => {
953
956
  data: klona(defaultData),
954
957
  meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED, 1, 1, 1]
955
958
  };
956
- const $ = store(initialData);
959
+ const storageData = store(initialData);
957
960
  const coreData = store({
958
961
  dataLoaded: false
959
962
  });
@@ -975,10 +978,10 @@ var Novely = (() => {
975
978
  const throttledOnStorageDataChange = throttle(onStorageDataChange, throttleTimeout);
976
979
  const throttledEmergencyOnStorageDataChange = throttle(() => {
977
980
  if (saveOnUnload === true || saveOnUnload === "prod" && !DEV) {
978
- onStorageDataChange($.get());
981
+ onStorageDataChange(storageData.get());
979
982
  }
980
983
  }, 10);
981
- $.subscribe(throttledOnStorageDataChange);
984
+ storageData.subscribe(throttledOnStorageDataChange);
982
985
  const getStoredData = async () => {
983
986
  let stored = await storage.get();
984
987
  for (const migration of migrations) {
@@ -995,7 +998,7 @@ var Novely = (() => {
995
998
  stored.data = defaultData;
996
999
  }
997
1000
  dataLoaded.resolve();
998
- $.set(stored);
1001
+ storageData.set(stored);
999
1002
  };
1000
1003
  storageDelay.then(getStoredData);
1001
1004
  const initial = getDefaultSave(klona(defaultState));
@@ -1013,7 +1016,7 @@ var Novely = (() => {
1013
1016
  return;
1014
1017
  const stack = useStack(MAIN_CONTEXT_KEY);
1015
1018
  const current = klona(stack.value);
1016
- $.update((prev) => {
1019
+ storageData.update((prev) => {
1017
1020
  const isLatest = findLastIndex(prev.saves, (value) => times.has(value[2][0])) === prev.saves.length - 1;
1018
1021
  current[2][0] = intime(Date.now());
1019
1022
  current[2][1] = type;
@@ -1035,7 +1038,7 @@ var Novely = (() => {
1035
1038
  return;
1036
1039
  const save2 = getDefaultSave(klona(defaultState));
1037
1040
  if (autosaves) {
1038
- $.update((prev) => {
1041
+ storageData.update((prev) => {
1039
1042
  return prev.saves.push(save2), prev;
1040
1043
  });
1041
1044
  }
@@ -1050,10 +1053,10 @@ var Novely = (() => {
1050
1053
  const restore = async (save2) => {
1051
1054
  if (!coreData.get().dataLoaded)
1052
1055
  return;
1053
- let latest = save2 || $.get().saves.at(-1);
1056
+ let latest = save2 || storageData.get().saves.at(-1);
1054
1057
  if (!latest) {
1055
1058
  latest = klona(initial);
1056
- $.update((prev) => {
1059
+ storageData.update((prev) => {
1057
1060
  prev.saves.push(latest);
1058
1061
  return prev;
1059
1062
  });
@@ -1137,7 +1140,7 @@ var Novely = (() => {
1137
1140
  ctx.audio.destroy();
1138
1141
  const [time, type] = current[2];
1139
1142
  if (type === "auto" && interacted <= 1 && times.has(time)) {
1140
- $.update((prev) => {
1143
+ storageData.update((prev) => {
1141
1144
  prev.saves = prev.saves.filter((save2) => save2 !== current);
1142
1145
  return prev;
1143
1146
  });
@@ -1208,8 +1211,8 @@ var Novely = (() => {
1208
1211
  removeContext,
1209
1212
  getStateFunction,
1210
1213
  languages,
1211
- $,
1212
- $$: coreData
1214
+ storageData,
1215
+ coreData
1213
1216
  });
1214
1217
  const useStack = createUseStackFunction(renderer);
1215
1218
  useStack(MAIN_CONTEXT_KEY).push(initial);
@@ -1303,7 +1306,7 @@ var Novely = (() => {
1303
1306
  const name = (() => {
1304
1307
  const c = character;
1305
1308
  const cs = characters;
1306
- const [lang] = $.get().meta;
1309
+ const [lang] = storageData.get().meta;
1307
1310
  if (c && c in cs) {
1308
1311
  const block = cs[c].name;
1309
1312
  if (typeof block === "string") {
@@ -1335,7 +1338,7 @@ var Novely = (() => {
1335
1338
  function({ ctx, push }, [fn]) {
1336
1339
  const { restoring, goingBack, preview: preview2 } = ctx.meta;
1337
1340
  const result = fn({
1338
- lang: $.get().meta[0],
1341
+ lang: storageData.get().meta[0],
1339
1342
  goingBack,
1340
1343
  restoring,
1341
1344
  preview: preview2,
@@ -1354,13 +1357,13 @@ var Novely = (() => {
1354
1357
  }
1355
1358
  const transformedChoices = choices.map(([content, action2, visible]) => {
1356
1359
  const shown = !visible || visible({
1357
- lang: $.get().meta[0],
1360
+ lang: storageData.get().meta[0],
1358
1361
  state: getStateAtCtx(ctx)
1359
1362
  });
1360
1363
  if (DEV && action2.length === 0 && !shown) {
1361
1364
  console.warn(`Choice children should not be empty, either add content there or make item not selectable`);
1362
1365
  }
1363
- return [templateReplace(content, data2), action2, shown];
1366
+ return [templateReplace(content, data2), shown];
1364
1367
  });
1365
1368
  if (DEV && transformedChoices.length === 0) {
1366
1369
  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]`);
@@ -1567,7 +1570,7 @@ var Novely = (() => {
1567
1570
  const {
1568
1571
  data: data2,
1569
1572
  meta: [lang]
1570
- } = $.get();
1573
+ } = storageData.get();
1571
1574
  const obj = values || data2;
1572
1575
  const cnt = isFunction(content) ? content(obj) : typeof content === "string" ? content : content[lang];
1573
1576
  const str2 = flattenAllowedContent(
@@ -1585,11 +1588,11 @@ var Novely = (() => {
1585
1588
  );
1586
1589
  };
1587
1590
  const data = (value) => {
1588
- const _data = $.get().data;
1591
+ const _data = storageData.get().data;
1589
1592
  if (!value)
1590
1593
  return _data;
1591
1594
  const val = isFunction(value) ? value(_data) : deepmerge(_data, value);
1592
- $.update((prev) => {
1595
+ storageData.update((prev) => {
1593
1596
  prev.data = val;
1594
1597
  return prev;
1595
1598
  });