@novely/core 0.10.0 → 0.12.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
@@ -14,17 +14,17 @@ type Character<LanguageKeys extends string = string> = {
14
14
  };
15
15
 
16
16
  type Thenable<T> = T | Promise<T>;
17
- type PathItem = [null, number | string] | ['choice' & Record<never, never>, number] | ['condition' & Record<never, never>, string] | ['exit' & Record<never, never>];
17
+ type PathItem = [null, number | string] | ['choice', number] | ['choice:exit'] | ['condition', string] | ['condition:exit'] | ['exit'] | ['block', string] | ['block:exit'];
18
18
  type Path = PathItem[];
19
19
  type State = Record<string, any>;
20
20
  type Data = Record<string, any>;
21
21
  type SaveDate = number;
22
22
  type SaveType = 'manual' | 'auto';
23
- type SaveMeta = [SaveDate, SaveType];
24
- type Save = [Path, State, SaveMeta];
23
+ type SaveMeta = [date: SaveDate, type: SaveType];
24
+ type Save = [path: Path, state: State, meta: SaveMeta];
25
25
  type Lang = string;
26
26
  type TypewriterSpeed = 'Slow' | 'Medium' | 'Fast' | 'Auto' | (string & Record<never, never>);
27
- type StorageMeta = [Lang, TypewriterSpeed];
27
+ type StorageMeta = [lang: Lang, typewriter_speed: TypewriterSpeed];
28
28
  type Migration = (save: unknown) => unknown;
29
29
  type StorageData = {
30
30
  saves: Save[];
@@ -44,8 +44,12 @@ type NovelyScreen = 'mainmenu' | 'game' | 'saves' | 'settings';
44
44
  type DeepPartial<T> = unknown extends T ? T : T extends object ? {
45
45
  [P in keyof T]?: T[P] extends Array<infer U> ? Array<DeepPartial<U>> : T[P] extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : DeepPartial<T[P]>;
46
46
  } : T;
47
+ type NonEmptyRecord<T extends Record<PropertyKey, unknown>> = keyof T extends never ? never : T;
48
+ type CoreData = {
49
+ dataLoaded: boolean;
50
+ };
47
51
 
48
- type ValidAction = ['choice', [number]] | ['clear', [Set<keyof DefaultActionProxyProvider>?, Set<string>?]] | ['condition', [() => boolean, Record<string, ValidAction[]>]] | ['dialog', [string | undefined, Unwrappable, string | undefined]] | ['end', []] | ['showBackground', [string]] | ['playMusic', [string]] | ['stopMusic', [string]] | ['jump', [string]] | ['showCharacter', [string, keyof Character['emotions'], string?, string?]] | ['hideCharacter', [string, string?, string?, number?]] | ['animateCharacter', [string, number, ...string[]]] | ['wait', [FunctionableValue<number>]] | ['function', [() => Thenable<void>]] | ['input', [string, (meta: ActionInputOnInputMeta) => void, ActionInputSetup?]] | ['custom', [CustomHandler]] | ['vibrate', [...number[]]] | ['next', []] | ['text', [...string[]]] | ['exit'] | ['preload', [string]] | ValidAction[];
52
+ type ValidAction = ['choice', [number]] | ['clear', [Set<keyof DefaultActionProxyProvider>?, Set<string>?]] | ['condition', [() => boolean, Record<string, ValidAction[]>]] | ['dialog', [string | undefined, Unwrappable, string | undefined]] | ['end', []] | ['showBackground', [string | NonEmptyRecord<BackgroundImage>]] | ['playMusic', [string]] | ['stopMusic', [string]] | ['jump', [string]] | ['showCharacter', [string, keyof Character['emotions'], string?, string?]] | ['hideCharacter', [string, string?, string?, number?]] | ['animateCharacter', [string, number, ...string[]]] | ['wait', [FunctionableValue<number>]] | ['function', [() => Thenable<void>]] | ['input', [string, (meta: ActionInputOnInputMeta) => void, ActionInputSetup?]] | ['custom', [CustomHandler]] | ['vibrate', [...number[]]] | ['next', []] | ['text', [...string[]]] | ['exit', []] | ['preload', [string]] | ['block', [string]] | ValidAction[];
49
53
  type Story = Record<string, ValidAction[]>;
50
54
  type Unwrappable = string | ((lang: string, obj: Record<string, unknown>) => string) | Record<string, string | (() => string)>;
51
55
  type FunctionableValue<T> = T | (() => T);
@@ -100,6 +104,7 @@ interface ActionInputOnInputMeta {
100
104
  value: string;
101
105
  }
102
106
  type ActionInputSetup = (input: HTMLInputElement, cleanup: (cb: () => void) => void) => void;
107
+ type BackgroundImage = Partial<Record<"portrait" | "landscape" | "all", string>> & Record<(string), string>;
103
108
  type ActionProxyProvider<Characters extends Record<string, Character>> = {
104
109
  choice: {
105
110
  (...choices: ([Unwrappable, ValidAction[]] | [Unwrappable, ValidAction[], () => boolean])[]): ValidAction;
@@ -114,19 +119,15 @@ type ActionProxyProvider<Characters extends Record<string, Character>> = {
114
119
  (person: string, content: Unwrappable, emotion?: undefined): ValidAction;
115
120
  };
116
121
  end: () => ValidAction;
117
- showBackground: (background: string) => ValidAction;
122
+ showBackground: <T extends string | BackgroundImage>(background: T extends string ? T : T extends Record<PropertyKey, unknown> ? NonEmptyRecord<T> : never) => ValidAction;
118
123
  playMusic: (audio: string) => ValidAction;
119
124
  stopMusic: (audio: string) => ValidAction;
120
125
  jump: (scene: string) => ValidAction;
121
126
  showCharacter: {
122
127
  <C extends keyof Characters>(character: C, emotion: keyof Characters[C]['emotions'], className?: string, style?: string): ValidAction;
123
128
  };
124
- hideCharacter: {
125
- <C extends keyof Characters>(character: C, className?: string, style?: string, duration?: number): ValidAction;
126
- };
127
- animateCharacter: {
128
- <C extends keyof Characters>(character: C, timeout: number, ...classes: string[]): ValidAction;
129
- };
129
+ hideCharacter: (character: keyof Characters, className?: string, style?: string, duration?: number) => ValidAction;
130
+ animateCharacter: (character: keyof Characters, timeout: number, ...classes: string[]) => ValidAction;
130
131
  wait: (time: FunctionableValue<number>) => ValidAction;
131
132
  function: (fn: (restoring: boolean, goingBack: boolean) => Thenable<void>) => ValidAction;
132
133
  input: (question: Unwrappable, onInput: (meta: ActionInputOnInputMeta) => void, setup?: ActionInputSetup) => ValidAction;
@@ -135,6 +136,7 @@ type ActionProxyProvider<Characters extends Record<string, Character>> = {
135
136
  next: () => ValidAction;
136
137
  text: (...text: Unwrappable[]) => ValidAction;
137
138
  preload: (source: string) => ValidAction;
139
+ block: (scene: string) => ValidAction;
138
140
  };
139
141
  type DefaultActionProxyProvider = ActionProxyProvider<Record<string, Character>>;
140
142
  type GetActionParameters<T extends Capitalize<keyof DefaultActionProxyProvider>> = Parameters<DefaultActionProxyProvider[Uncapitalize<T>]>;
@@ -165,7 +167,7 @@ interface RendererStore {
165
167
  }
166
168
  type Renderer = {
167
169
  character: (character: string) => CharacterHandle;
168
- background: (background: string) => void;
170
+ background: (background: string | BackgroundImage) => void;
169
171
  dialog: (content: string, name: string, character?: string, emotion?: string) => (resolve: () => void, goingBack: boolean) => void;
170
172
  choices: (question: string, choices: ([string, ValidAction[]] | [string, ValidAction[], () => boolean])[]) => (resolve: (selected: number) => void) => void;
171
173
  input: (question: string, onInput: Parameters<DefaultActionProxyProvider['input']>[1], setup?: Parameters<DefaultActionProxyProvider['input']>[2]) => (resolve: () => void) => void;
@@ -176,13 +178,22 @@ type Renderer = {
176
178
  store: RendererStore;
177
179
  ui: {
178
180
  /**
179
- * Показывает экран, скрывает другие
181
+ * Shows the screen
180
182
  */
181
183
  showScreen(name: 'mainmenu' | 'game' | 'saves' | 'settings' | 'loading'): void;
182
184
  /**
183
185
  * Shows prompt to exit
184
186
  */
185
187
  showExitPrompt(): void;
188
+ /**
189
+ * Render the game
190
+ */
191
+ start(): {
192
+ /**
193
+ * Unmount
194
+ */
195
+ unmount(): void;
196
+ };
186
197
  };
187
198
  };
188
199
  type RendererInit = {
@@ -203,6 +214,10 @@ type RendererInit = {
203
214
  * Store that tracks data updates
204
215
  */
205
216
  $: Stored<StorageData>;
217
+ /**
218
+ * Store that used to communicate between renderer and core
219
+ */
220
+ $$: Stored<CoreData>;
206
221
  };
207
222
 
208
223
  interface LocalStorageStorageSettings {
@@ -328,4 +343,4 @@ declare const novely: <Languages extends string, Characters extends Record<strin
328
343
  t: Inter["t"];
329
344
  };
330
345
 
331
- export { ActionProxyProvider, AudioHandle, Character, CharacterHandle, CustomHandler, CustomHandlerGetResult, CustomHandlerGetResultDataFunction, DefaultActionProxyProvider, Emotions, FunctionableValue, GetActionParameters, Lang, NovelyScreen, Path, Renderer, RendererInit, RendererStore, Storage, StorageData, StorageMeta, Stored, Story, Thenable, TypewriterSpeed, Unwrappable, ValidAction, localStorageStorage, novely };
346
+ export { ActionProxyProvider, AudioHandle, Character, CharacterHandle, CoreData, CustomHandler, CustomHandlerGetResult, CustomHandlerGetResultDataFunction, DefaultActionProxyProvider, Emotions, FunctionableValue, GetActionParameters, Lang, NovelyScreen, Path, Renderer, RendererInit, RendererStore, Storage, StorageData, StorageMeta, Stored, Story, Thenable, TypewriterSpeed, Unwrappable, ValidAction, localStorageStorage, novely };
@@ -141,6 +141,13 @@ var Novely = (() => {
141
141
  novely: () => novely
142
142
  });
143
143
 
144
+ // src/constants.ts
145
+ var SKIPPED_DURING_RESTORE = /* @__PURE__ */ new Set(["dialog", "choice", "input", "vibrate", "text"]);
146
+ var BLOCK_EXIT_STATEMENTS = /* @__PURE__ */ new Set(["choice:exit", "condition:exit", "block:exit"]);
147
+ var BLOCK_STATEMENTS = /* @__PURE__ */ new Set(["choice", "condition", "block"]);
148
+ var EMPTY_SET = /* @__PURE__ */ new Set();
149
+ var DEFAULT_TYPEWRITER_SPEED = "Medium";
150
+
144
151
  // src/utils.ts
145
152
  var matchAction = (values) => {
146
153
  return (action, props) => {
@@ -213,8 +220,8 @@ var Novely = (() => {
213
220
  }
214
221
  };
215
222
  var findLastIndex = (array, fn) => {
216
- for (let i = array.length - 1; i > 0; i--) {
217
- if (fn(array[i])) {
223
+ for (let i = array.length - 1; i >= 0; i--) {
224
+ if (fn(array[i], array[i + 1])) {
218
225
  return i;
219
226
  }
220
227
  }
@@ -242,6 +249,24 @@ var Novely = (() => {
242
249
  });
243
250
  return { resolve, reject, promise };
244
251
  };
252
+ var findLastPathItemBeforeItemOfType = (path, name) => {
253
+ const index = findLastIndex(path, ([_name, _value], next) => {
254
+ return isNull(_name) && isNumber(_value) && next != null && next[0] === name;
255
+ });
256
+ return path[index];
257
+ };
258
+ var isBlockStatement = (statement) => {
259
+ return BLOCK_STATEMENTS.has(statement);
260
+ };
261
+ var isBlockExitStatement = (statement) => {
262
+ return BLOCK_EXIT_STATEMENTS.has(statement);
263
+ };
264
+ var isSkippedDurigRestore = (item) => {
265
+ return SKIPPED_DURING_RESTORE.has(item);
266
+ };
267
+ var isAction = (element) => {
268
+ return Array.isArray(element) && isString(element[0]);
269
+ };
245
270
 
246
271
  // src/global.ts
247
272
  var PRELOADED_ASSETS = /* @__PURE__ */ new Set();
@@ -298,11 +323,6 @@ var Novely = (() => {
298
323
  return val;
299
324
  }
300
325
 
301
- // src/constants.ts
302
- var SKIPPED_DURING_RESTORE = /* @__PURE__ */ new Set(["dialog", "choice", "input", "vibrate", "text"]);
303
- var EMPTY_SET = /* @__PURE__ */ new Set();
304
- var DEFAULT_TYPEWRITER_SPEED = "Medium";
305
-
306
326
  // ../t9n/dist/index.js
307
327
  var g = (e, r) => {
308
328
  let o = [];
@@ -447,41 +467,43 @@ var Novely = (() => {
447
467
  data: klona(defaultData),
448
468
  meta: [getLanguageWithoutParameters(), DEFAULT_TYPEWRITER_SPEED]
449
469
  };
470
+ const coreData = {
471
+ dataLoaded: false
472
+ };
450
473
  const $ = store(initialData);
451
- let initialDataLoaded = false;
474
+ const $$ = store(coreData);
452
475
  const onStorageDataChange = (value) => {
453
- if (initialDataLoaded)
476
+ if ($$.get().dataLoaded)
454
477
  storage.set(value);
455
478
  };
456
479
  const throttledOnStorageDataChange = throttle(onStorageDataChange, throttleTimeout);
457
480
  $.subscribe(throttledOnStorageDataChange);
458
- const getStoredData = () => {
459
- storage.get().then((stored) => {
460
- for (const migration of migrations) {
461
- stored = migration(stored);
462
- }
463
- stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
464
- if (overrideLanguage) {
465
- stored.meta[0] = getLanguageWithoutParameters();
466
- } else {
467
- stored.meta[0] ||= getLanguageWithoutParameters();
468
- }
469
- if (isEmpty(stored.data)) {
470
- stored.data = defaultData;
471
- }
472
- initialDataLoaded = true;
473
- $.update(() => stored);
474
- if (initialScreen === "game")
475
- assetsLoaded.promise.then(() => {
476
- restore();
477
- });
478
- });
481
+ const getStoredData = async () => {
482
+ let stored = await storage.get();
483
+ for (const migration of migrations) {
484
+ stored = migration(stored);
485
+ }
486
+ stored.meta[1] ||= DEFAULT_TYPEWRITER_SPEED;
487
+ if (overrideLanguage) {
488
+ stored.meta[0] = getLanguageWithoutParameters();
489
+ } else {
490
+ stored.meta[0] ||= getLanguageWithoutParameters();
491
+ }
492
+ if (isEmpty(stored.data)) {
493
+ stored.data = defaultData;
494
+ }
495
+ $$.update((prev) => (prev.dataLoaded = true, prev));
496
+ $.update(() => stored);
497
+ if (initialScreen === "game") {
498
+ await assetsLoaded.promise;
499
+ restore();
500
+ }
479
501
  };
480
502
  storageDelay.then(getStoredData);
481
503
  const initial = getDefaultSave(klona(defaultState));
482
504
  const stack = createStack(initial);
483
505
  const save = (override = false, type = override ? "auto" : "manual") => {
484
- if (!initialDataLoaded)
506
+ if (!$$.get().dataLoaded)
485
507
  return;
486
508
  if (!autosaves && type === "auto")
487
509
  return;
@@ -504,7 +526,7 @@ var Novely = (() => {
504
526
  });
505
527
  };
506
528
  const newGame = () => {
507
- if (!initialDataLoaded)
529
+ if (!$$.get().dataLoaded)
508
530
  return;
509
531
  const save2 = getDefaultSave(klona(defaultState));
510
532
  if (autosaves) {
@@ -522,7 +544,7 @@ var Novely = (() => {
522
544
  let goingBack = false;
523
545
  let interacted = 0;
524
546
  const restore = async (save2) => {
525
- if (!initialDataLoaded)
547
+ if (!$$.get().dataLoaded)
526
548
  return;
527
549
  let latest = save2 || $.get().saves.at(-1);
528
550
  if (!latest) {
@@ -534,31 +556,46 @@ var Novely = (() => {
534
556
  latest = klona(initial);
535
557
  }
536
558
  restoring = true, stack.value = latest;
559
+ const path = stack.value[0];
537
560
  let current = story;
561
+ let precurrent;
562
+ let ignoreNested = false;
538
563
  let index = 0;
539
564
  const max = stack.value[0].reduce((acc, [type, val]) => {
540
- if (isNull(type) && isNumber(val))
565
+ if (isNull(type) && isNumber(val)) {
541
566
  return acc + 1;
567
+ }
542
568
  return acc;
543
569
  }, 0);
544
570
  const queue = [];
545
571
  const keep = /* @__PURE__ */ new Set();
546
572
  const characters2 = /* @__PURE__ */ new Set();
547
- for (const [type, val] of stack.value[0]) {
573
+ const blocks = [];
574
+ for (const [type, val] of path) {
548
575
  if (type === null) {
549
- if (isString(val)) {
550
- current = current[val];
551
- } else if (isNumber(val)) {
576
+ precurrent = current;
577
+ if (isNumber(val)) {
552
578
  index++;
553
- for (let i = 0; i <= val; i++) {
554
- const [action2, ...meta] = current[i];
579
+ let startIndex = 0;
580
+ if (ignoreNested) {
581
+ const prev = findLastPathItemBeforeItemOfType(path.slice(0, index), "block");
582
+ if (prev) {
583
+ startIndex = prev[1];
584
+ ignoreNested = false;
585
+ }
586
+ }
587
+ for (let i = startIndex; i <= val; i++) {
588
+ const item = current[i];
589
+ if (!isAction(item))
590
+ continue;
591
+ const [action2, ...meta] = item;
555
592
  const push2 = () => {
556
593
  keep.add(action2);
557
594
  queue.push([action2, meta]);
558
595
  };
559
596
  if (action2 === "showCharacter")
560
597
  characters2.add(meta[0]);
561
- if (SKIPPED_DURING_RESTORE.has(action2) || isUserRequiredAction(action2, meta)) {
598
+ if (isSkippedDurigRestore(action2) || isUserRequiredAction(action2, meta)) {
562
599
  if (index === max && i === val) {
563
600
  push2();
564
601
  } else {
@@ -567,12 +604,20 @@ var Novely = (() => {
567
604
  }
568
605
  push2();
569
606
  }
570
- current = current[val];
571
607
  }
608
+ current = current[val];
572
609
  } else if (type === "choice") {
610
+ blocks.push(precurrent = current);
573
611
  current = current[val + 1][1];
574
612
  } else if (type === "condition") {
613
+ blocks.push(precurrent = current);
575
614
  current = current[2][val];
615
+ } else if (type === "block") {
616
+ blocks.push(precurrent);
617
+ current = story[val];
618
+ } else if (type === "block:exit" || type === "choice:exit" || type === "condition:exit") {
619
+ current = blocks.pop();
620
+ ignoreNested = true;
576
621
  }
577
622
  }
578
623
  renderer.ui.showScreen("game");
@@ -587,7 +632,8 @@ var Novely = (() => {
587
632
  const c0 = _meta[0];
588
633
  const c1 = meta[0];
589
634
  const isIdenticalID = c0.id && c1.id && c0.id === c1.id;
590
- return isIdenticalID || str(c0) === str(c1);
635
+ const isIdenticalByReference = c0 === c1;
636
+ return isIdenticalID || isIdenticalByReference || str(c0) === str(c1);
591
637
  });
592
638
  if (notLatest)
593
639
  continue;
@@ -618,15 +664,25 @@ var Novely = (() => {
618
664
  }
619
665
  restoring = goingBack = false, render();
620
666
  };
621
- const refer = () => {
667
+ const refer = (path = stack.value[0]) => {
622
668
  let current = story;
623
- for (const [type, val] of stack.value[0]) {
669
+ let precurrent = story;
670
+ const blocks = [];
671
+ for (const [type, val] of path) {
624
672
  if (type === null) {
673
+ precurrent = current;
625
674
  current = current[val];
626
675
  } else if (type === "choice") {
676
+ blocks.push(precurrent = current);
627
677
  current = current[val + 1][1];
628
678
  } else if (type === "condition") {
679
+ blocks.push(precurrent = current);
629
680
  current = current[2][val];
681
+ } else if (type === "block") {
682
+ blocks.push(precurrent);
683
+ current = story[val];
684
+ } else if (type === "block:exit" || type === "choice:exit" || type === "condition:exit") {
685
+ current = blocks.pop();
630
686
  }
631
687
  }
632
688
  return current;
@@ -663,8 +719,10 @@ var Novely = (() => {
663
719
  stack,
664
720
  languages,
665
721
  t: t9n.i,
666
- $
722
+ $,
723
+ $$
667
724
  });
725
+ renderer.ui.start();
668
726
  const match = matchAction({
669
727
  wait([time]) {
670
728
  if (!restoring)
@@ -749,9 +807,13 @@ var Novely = (() => {
749
807
  renderer.clear(goingBack, keep || EMPTY_SET, characters2 || EMPTY_SET)(push);
750
808
  },
751
809
  condition([condition]) {
752
- const value = condition();
753
- if (!restoring)
754
- stack.value[0].push(["condition", String(value)], [null, 0]), render();
810
+ if (!restoring) {
811
+ stack.value[0].push(
812
+ ["condition", String(condition())],
813
+ [null, 0]
814
+ );
815
+ render();
816
+ }
755
817
  },
756
818
  end() {
757
819
  match("clear", []);
@@ -803,24 +865,55 @@ var Novely = (() => {
803
865
  renderer.text(text.map((content) => unwrap(content)).join(" "), forward, goingBack);
804
866
  },
805
867
  exit() {
868
+ if (restoring)
869
+ return;
806
870
  const path = stack.value[0];
807
- let exited = false;
871
+ const last = path.at(-1);
872
+ const ignore = [];
873
+ if (!isAction(refer(path))) {
874
+ if (last && isNull(last[0]) && isNumber(last[1])) {
875
+ last[1]--;
876
+ } else {
877
+ path.pop();
878
+ }
879
+ }
808
880
  for (let i = path.length - 1; i > 0; i--) {
809
- if (path[i][0] !== "choice" && path[i][0] !== "condition")
881
+ const [name] = path[i];
882
+ if (isBlockExitStatement(name)) {
883
+ ignore.push(name);
884
+ }
885
+ if (!isBlockStatement(name))
886
+ continue;
887
+ if (ignore.at(-1)?.startsWith(name)) {
888
+ ignore.pop();
889
+ continue;
890
+ }
891
+ path.push([`${name}:exit`]);
892
+ const prev = findLastPathItemBeforeItemOfType(path.slice(0, i + 1), name);
893
+ if (prev)
894
+ path.push([null, prev[1] + 1]);
895
+ if (!isAction(refer(path))) {
896
+ path.pop();
810
897
  continue;
811
- exited = true;
812
- stack.value[0] = path.slice(0, i);
813
- next();
898
+ }
814
899
  break;
815
900
  }
816
- if (exited)
817
- render();
901
+ render();
818
902
  },
819
903
  preload([source]) {
820
904
  if (!PRELOADED_ASSETS.has(source) && !goingBack && !restoring) {
821
905
  PRELOADED_ASSETS.add(document.createElement("img").src = source);
822
906
  }
823
907
  push();
908
+ },
909
+ block([scene]) {
910
+ if (!restoring) {
911
+ stack.value[0].push(
912
+ ["block", scene],
913
+ [null, 0]
914
+ );
915
+ render();
916
+ }
824
917
  }
825
918
  });
826
919
  const enmemory = () => {
@@ -834,13 +927,11 @@ var Novely = (() => {
834
927
  const next = () => {
835
928
  const path = stack.value[0];
836
929
  const last = path.at(-1);
837
- if (!last)
838
- return;
839
- if (isNull(last[0]) && isNumber(last[1])) {
840
- last[1] = last[1] + 1;
841
- return;
930
+ if (last && isNull(last[0]) && isNumber(last[1])) {
931
+ last[1]++;
932
+ } else {
933
+ path.push([null, 0]);
842
934
  }
843
- path.push([null, 0]);
844
935
  };
845
936
  const render = () => {
846
937
  const referred = refer();