@novely/core 0.10.0 → 0.11.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,7 +14,7 @@ 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>;
@@ -44,8 +44,9 @@ 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;
47
48
 
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[];
49
+ 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
50
  type Story = Record<string, ValidAction[]>;
50
51
  type Unwrappable = string | ((lang: string, obj: Record<string, unknown>) => string) | Record<string, string | (() => string)>;
51
52
  type FunctionableValue<T> = T | (() => T);
@@ -100,6 +101,7 @@ interface ActionInputOnInputMeta {
100
101
  value: string;
101
102
  }
102
103
  type ActionInputSetup = (input: HTMLInputElement, cleanup: (cb: () => void) => void) => void;
104
+ type BackgroundImage = Partial<Record<"portrait" | "landscape" | "all", string>> & Record<(string), string>;
103
105
  type ActionProxyProvider<Characters extends Record<string, Character>> = {
104
106
  choice: {
105
107
  (...choices: ([Unwrappable, ValidAction[]] | [Unwrappable, ValidAction[], () => boolean])[]): ValidAction;
@@ -114,19 +116,15 @@ type ActionProxyProvider<Characters extends Record<string, Character>> = {
114
116
  (person: string, content: Unwrappable, emotion?: undefined): ValidAction;
115
117
  };
116
118
  end: () => ValidAction;
117
- showBackground: (background: string) => ValidAction;
119
+ showBackground: <T extends string | BackgroundImage>(background: T extends string ? T : T extends Record<PropertyKey, unknown> ? NonEmptyRecord<T> : never) => ValidAction;
118
120
  playMusic: (audio: string) => ValidAction;
119
121
  stopMusic: (audio: string) => ValidAction;
120
122
  jump: (scene: string) => ValidAction;
121
123
  showCharacter: {
122
124
  <C extends keyof Characters>(character: C, emotion: keyof Characters[C]['emotions'], className?: string, style?: string): ValidAction;
123
125
  };
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
- };
126
+ hideCharacter: (character: keyof Characters, className?: string, style?: string, duration?: number) => ValidAction;
127
+ animateCharacter: (character: keyof Characters, timeout: number, ...classes: string[]) => ValidAction;
130
128
  wait: (time: FunctionableValue<number>) => ValidAction;
131
129
  function: (fn: (restoring: boolean, goingBack: boolean) => Thenable<void>) => ValidAction;
132
130
  input: (question: Unwrappable, onInput: (meta: ActionInputOnInputMeta) => void, setup?: ActionInputSetup) => ValidAction;
@@ -135,6 +133,7 @@ type ActionProxyProvider<Characters extends Record<string, Character>> = {
135
133
  next: () => ValidAction;
136
134
  text: (...text: Unwrappable[]) => ValidAction;
137
135
  preload: (source: string) => ValidAction;
136
+ block: (scene: string) => ValidAction;
138
137
  };
139
138
  type DefaultActionProxyProvider = ActionProxyProvider<Record<string, Character>>;
140
139
  type GetActionParameters<T extends Capitalize<keyof DefaultActionProxyProvider>> = Parameters<DefaultActionProxyProvider[Uncapitalize<T>]>;
@@ -165,7 +164,7 @@ interface RendererStore {
165
164
  }
166
165
  type Renderer = {
167
166
  character: (character: string) => CharacterHandle;
168
- background: (background: string) => void;
167
+ background: (background: string | BackgroundImage) => void;
169
168
  dialog: (content: string, name: string, character?: string, emotion?: string) => (resolve: () => void, goingBack: boolean) => void;
170
169
  choices: (question: string, choices: ([string, ValidAction[]] | [string, ValidAction[], () => boolean])[]) => (resolve: (selected: number) => void) => void;
171
170
  input: (question: string, onInput: Parameters<DefaultActionProxyProvider['input']>[1], setup?: Parameters<DefaultActionProxyProvider['input']>[2]) => (resolve: () => void) => void;
@@ -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 = [];
@@ -534,31 +554,46 @@ var Novely = (() => {
534
554
  latest = klona(initial);
535
555
  }
536
556
  restoring = true, stack.value = latest;
557
+ const path = stack.value[0];
537
558
  let current = story;
559
+ let precurrent;
560
+ let ignoreNested = false;
538
561
  let index = 0;
539
562
  const max = stack.value[0].reduce((acc, [type, val]) => {
540
- if (isNull(type) && isNumber(val))
563
+ if (isNull(type) && isNumber(val)) {
541
564
  return acc + 1;
565
+ }
542
566
  return acc;
543
567
  }, 0);
544
568
  const queue = [];
545
569
  const keep = /* @__PURE__ */ new Set();
546
570
  const characters2 = /* @__PURE__ */ new Set();
547
- for (const [type, val] of stack.value[0]) {
571
+ const blocks = [];
572
+ for (const [type, val] of path) {
548
573
  if (type === null) {
549
- if (isString(val)) {
550
- current = current[val];
551
- } else if (isNumber(val)) {
574
+ precurrent = current;
575
+ if (isNumber(val)) {
552
576
  index++;
553
- for (let i = 0; i <= val; i++) {
554
- const [action2, ...meta] = current[i];
577
+ let startIndex = 0;
578
+ if (ignoreNested) {
579
+ const prev = findLastPathItemBeforeItemOfType(path.slice(0, index), "block");
580
+ if (prev) {
581
+ startIndex = prev[1];
582
+ ignoreNested = false;
583
+ }
584
+ }
585
+ for (let i = startIndex; i <= val; i++) {
586
+ const item = current[i];
587
+ if (!isAction(item))
588
+ continue;
589
+ const [action2, ...meta] = item;
555
590
  const push2 = () => {
556
591
  keep.add(action2);
557
592
  queue.push([action2, meta]);
558
593
  };
559
594
  if (action2 === "showCharacter")
560
595
  characters2.add(meta[0]);
561
- if (SKIPPED_DURING_RESTORE.has(action2) || isUserRequiredAction(action2, meta)) {
596
+ if (isSkippedDurigRestore(action2) || isUserRequiredAction(action2, meta)) {
562
597
  if (index === max && i === val) {
563
598
  push2();
564
599
  } else {
@@ -567,12 +602,20 @@ var Novely = (() => {
567
602
  }
568
603
  push2();
569
604
  }
570
- current = current[val];
571
605
  }
606
+ current = current[val];
572
607
  } else if (type === "choice") {
608
+ blocks.push(precurrent = current);
573
609
  current = current[val + 1][1];
574
610
  } else if (type === "condition") {
611
+ blocks.push(precurrent = current);
575
612
  current = current[2][val];
613
+ } else if (type === "block") {
614
+ blocks.push(precurrent);
615
+ current = story[val];
616
+ } else if (type === "block:exit" || type === "choice:exit" || type === "condition:exit") {
617
+ current = blocks.pop();
618
+ ignoreNested = true;
576
619
  }
577
620
  }
578
621
  renderer.ui.showScreen("game");
@@ -587,7 +630,8 @@ var Novely = (() => {
587
630
  const c0 = _meta[0];
588
631
  const c1 = meta[0];
589
632
  const isIdenticalID = c0.id && c1.id && c0.id === c1.id;
590
- return isIdenticalID || str(c0) === str(c1);
633
+ const isIdenticalByReference = c0 === c1;
634
+ return isIdenticalID || isIdenticalByReference || str(c0) === str(c1);
591
635
  });
592
636
  if (notLatest)
593
637
  continue;
@@ -618,15 +662,25 @@ var Novely = (() => {
618
662
  }
619
663
  restoring = goingBack = false, render();
620
664
  };
621
- const refer = () => {
665
+ const refer = (path = stack.value[0]) => {
622
666
  let current = story;
623
- for (const [type, val] of stack.value[0]) {
667
+ let precurrent = story;
668
+ const blocks = [];
669
+ for (const [type, val] of path) {
624
670
  if (type === null) {
671
+ precurrent = current;
625
672
  current = current[val];
626
673
  } else if (type === "choice") {
674
+ blocks.push(precurrent = current);
627
675
  current = current[val + 1][1];
628
676
  } else if (type === "condition") {
677
+ blocks.push(precurrent = current);
629
678
  current = current[2][val];
679
+ } else if (type === "block") {
680
+ blocks.push(precurrent);
681
+ current = story[val];
682
+ } else if (type === "block:exit" || type === "choice:exit" || type === "condition:exit") {
683
+ current = blocks.pop();
630
684
  }
631
685
  }
632
686
  return current;
@@ -749,9 +803,13 @@ var Novely = (() => {
749
803
  renderer.clear(goingBack, keep || EMPTY_SET, characters2 || EMPTY_SET)(push);
750
804
  },
751
805
  condition([condition]) {
752
- const value = condition();
753
- if (!restoring)
754
- stack.value[0].push(["condition", String(value)], [null, 0]), render();
806
+ if (!restoring) {
807
+ stack.value[0].push(
808
+ ["condition", String(condition())],
809
+ [null, 0]
810
+ );
811
+ render();
812
+ }
755
813
  },
756
814
  end() {
757
815
  match("clear", []);
@@ -803,24 +861,55 @@ var Novely = (() => {
803
861
  renderer.text(text.map((content) => unwrap(content)).join(" "), forward, goingBack);
804
862
  },
805
863
  exit() {
864
+ if (restoring)
865
+ return;
806
866
  const path = stack.value[0];
807
- let exited = false;
867
+ const last = path.at(-1);
868
+ const ignore = [];
869
+ if (!isAction(refer(path))) {
870
+ if (last && isNull(last[0]) && isNumber(last[1])) {
871
+ last[1]--;
872
+ } else {
873
+ path.pop();
874
+ }
875
+ }
808
876
  for (let i = path.length - 1; i > 0; i--) {
809
- if (path[i][0] !== "choice" && path[i][0] !== "condition")
877
+ const [name] = path[i];
878
+ if (isBlockExitStatement(name)) {
879
+ ignore.push(name);
880
+ }
881
+ if (!isBlockStatement(name))
810
882
  continue;
811
- exited = true;
812
- stack.value[0] = path.slice(0, i);
813
- next();
883
+ if (ignore.at(-1)?.startsWith(name)) {
884
+ ignore.pop();
885
+ continue;
886
+ }
887
+ path.push([`${name}:exit`]);
888
+ const prev = findLastPathItemBeforeItemOfType(path.slice(0, i + 1), name);
889
+ if (prev)
890
+ path.push([null, prev[1] + 1]);
891
+ if (!isAction(refer(path))) {
892
+ path.pop();
893
+ continue;
894
+ }
814
895
  break;
815
896
  }
816
- if (exited)
817
- render();
897
+ render();
818
898
  },
819
899
  preload([source]) {
820
900
  if (!PRELOADED_ASSETS.has(source) && !goingBack && !restoring) {
821
901
  PRELOADED_ASSETS.add(document.createElement("img").src = source);
822
902
  }
823
903
  push();
904
+ },
905
+ block([scene]) {
906
+ if (!restoring) {
907
+ stack.value[0].push(
908
+ ["block", scene],
909
+ [null, 0]
910
+ );
911
+ render();
912
+ }
824
913
  }
825
914
  });
826
915
  const enmemory = () => {
@@ -834,13 +923,11 @@ var Novely = (() => {
834
923
  const next = () => {
835
924
  const path = stack.value[0];
836
925
  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;
926
+ if (last && isNull(last[0]) && isNumber(last[1])) {
927
+ last[1]++;
928
+ } else {
929
+ path.push([null, 0]);
842
930
  }
843
- path.push([null, 0]);
844
931
  };
845
932
  const render = () => {
846
933
  const referred = refer();