narraleaf-react 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.
@@ -30,6 +30,14 @@ export declare class ActionHistoryManager {
30
30
  push<T extends Array<any> = Array<any>>(props: ActionHistoryPushOptions, onUndo?: (...args: T) => void, args?: T): {
31
31
  id: string;
32
32
  };
33
+ /**
34
+ * Whether this stack still holds the entry with the given id — i.e. whether the game can be
35
+ * stepped back to it in place, rather than by restoring its snapshot.
36
+ *
37
+ * The two go by the same name: a backlog entry's token *is* the id of the action-history entry
38
+ * pushed for the same line.
39
+ */
40
+ has(id: string): boolean;
33
41
  /**
34
42
  * Undo all actions until the given id
35
43
  *
@@ -52,7 +52,7 @@ export type NvlBlockOptions = {
52
52
  hideTransition?: Partial<TransformDefinitions.CommonTransformProps>;
53
53
  };
54
54
  export type SceneActionContentType = {
55
- [K in typeof SceneActionTypes[keyof typeof SceneActionTypes]]: K extends typeof SceneActionTypes["action"] ? Scene : K extends typeof SceneActionTypes["init"] ? [Scene] : K extends typeof SceneActionTypes["exit"] ? [] : K extends typeof SceneActionTypes["jumpTo"] ? [Scene] : K extends typeof SceneActionTypes["setBackgroundMusic"] ? [Sound | null, number?] : K extends typeof SceneActionTypes["preUnmount"] ? [] : K extends typeof SceneActionTypes["transitionToScene"] ? [ImageTransition, Scene | undefined, ImageSrc | Color | undefined] : K extends typeof SceneActionTypes["nvlBlock"] ? [LogicAction.Actions[], NvlBlockOptions] : K extends typeof SceneActionTypes["nvlShow"] ? [Partial<TransformDefinitions.CommonTransformProps>?] : K extends typeof SceneActionTypes["nvlHide"] ? [Partial<TransformDefinitions.CommonTransformProps>?] : K extends typeof SceneActionTypes["nvlEnd"] ? [NvlBlockOptions?] : any;
55
+ [K in typeof SceneActionTypes[keyof typeof SceneActionTypes]]: K extends typeof SceneActionTypes["action"] ? Scene : K extends typeof SceneActionTypes["init"] ? [Scene] : K extends typeof SceneActionTypes["exit"] ? [] : K extends typeof SceneActionTypes["jumpTo"] ? [Scene] : K extends typeof SceneActionTypes["setBackgroundMusic"] ? [Sound | null, number?] : K extends typeof SceneActionTypes["preUnmount"] ? [] : K extends typeof SceneActionTypes["transitionToScene"] ? [Transition, Scene] : K extends typeof SceneActionTypes["nvlBlock"] ? [LogicAction.Actions[], NvlBlockOptions] : K extends typeof SceneActionTypes["nvlShow"] ? [Partial<TransformDefinitions.CommonTransformProps>?] : K extends typeof SceneActionTypes["nvlHide"] ? [Partial<TransformDefinitions.CommonTransformProps>?] : K extends typeof SceneActionTypes["nvlEnd"] ? [NvlBlockOptions?] : any;
56
56
  };
57
57
  export declare const StoryActionTypes: {
58
58
  readonly action: "story:action";
@@ -7,7 +7,7 @@ import { ContentNode } from "../../action/tree/actionTree";
7
7
  import { LogicAction } from "../../action/logicAction";
8
8
  import { TypedAction } from "../../action/actions";
9
9
  import { Story } from "../../elements/story";
10
- import { ImageTransition } from "../../elements/transition/transitions/image/imageTransition";
10
+ import type { Transition } from "../../elements/transition/transition";
11
11
  import { ActionSearchOptions } from "../../types";
12
12
  import { ExposedState, ExposedStateType } from "../../../player/type";
13
13
  import type { TransformDefinitions } from "../../elements/transform/type";
@@ -46,7 +46,14 @@ export declare class SceneAction<T extends typeof SceneActionTypes[keyof typeof
46
46
  static initBackgroundMusic(scene: Scene, exposed: ExposedState[ExposedStateType.scene]): Promise<void>;
47
47
  static createSceneSnapshot(scene: Scene, state: GameState): SceneSnapshot;
48
48
  static restoreSceneSnapshot(snapshot: SceneSnapshot, state: GameState): void;
49
- applyTransition(gameState: GameState, transition: ImageTransition, injection: ActionExecutionInjection): Awaitable<CalledActionResult, CalledActionResult>;
49
+ /**
50
+ * Play `transition` across the whole stage while jumping from this scene to `target`.
51
+ *
52
+ * Both scenes are mounted at this point — `scene:init` added the incoming one and
53
+ * `scene:exit` has not yet removed this one — so the transition drives the two live scene
54
+ * subtrees rather than swapping one image's source underneath them.
55
+ */
56
+ applyStageTransition(gameState: GameState, transition: Transition, target: Scene, injection: ActionExecutionInjection): Awaitable<CalledActionResult, CalledActionResult>;
50
57
  exit(state: GameState): void;
51
58
  applyNvlVisibility(gameState: GameState, visible: boolean, options: Partial<TransformDefinitions.CommonTransformProps> | undefined, injection: ActionExecutionInjection): CalledActionResult | Awaitable<CalledActionResult, CalledActionResult>;
52
59
  executeAction(gameState: GameState, injection: ActionExecutionInjection): ExecutedActionResult;
@@ -1,2 +1,16 @@
1
1
  export declare class BaseElement {
2
+ /**
3
+ * Tell the engine this element's state has been written to, so the next save carries it.
4
+ *
5
+ * The engine marks an element itself whenever an action runs against it, which covers everything
6
+ * a story does. This is for a host that reaches past the story and writes element state
7
+ * directly — an editor moving a sprite, say. Without it the write is invisible to the save: the
8
+ * element is skipped, and loading brings back the state the script wrote, with nothing reporting
9
+ * that anything was lost. Debug builds do notice, and warn (see
10
+ * {@link Story.findUnmarkedElements}).
11
+ *
12
+ * Marking an element that turns out not to have changed is free — what decides whether an
13
+ * element reaches a save is the comparison against its authored state, not this flag.
14
+ */
15
+ markDirty(): this;
2
16
  }
@@ -36,23 +36,70 @@ export type GameHistory = GameHistoryAction & {
36
36
  snapshot?: SerializedGameState | null;
37
37
  };
38
38
  export declare class GameHistoryManager {
39
+ /**
40
+ * Every line this playthrough has reached, in order — including the ones ahead of the play head
41
+ * after a rewind. What separates the two is {@link GameHistoryManager.cursor}.
42
+ */
39
43
  private history;
44
+ /**
45
+ * Index of the line the game is on. Everything up to and including it is the backlog; everything
46
+ * after it is a future the player has already read once and can step forward into again.
47
+ * `-1` when nothing has been reached yet.
48
+ */
49
+ private cursor;
50
+ /**
51
+ * Set when the play head is moved by a rewind, cleared by the first line pushed afterwards.
52
+ *
53
+ * A line's snapshot is taken as the line is reached, so resuming from it re-runs it: the first
54
+ * push after a rewind is the current line again, not the next one. Without knowing that, a
55
+ * retrace looks like the story diverging and the lines ahead are thrown away.
56
+ */
57
+ private resumingAtCursor;
40
58
  private actionHistoryMgr;
41
59
  constructor(actionHistoryMgr: ActionHistoryManager);
42
- push(action: GameHistory): this;
60
+ /**
61
+ * Record the line the game has just reached.
62
+ *
63
+ * After a rewind the play head sits behind lines that were already read, and what happens to
64
+ * them depends on whether play is retracing its steps or leaving them behind. Reaching the same
65
+ * action again is a retrace — the lines ahead are kept, so a player who stepped back three lines
66
+ * and read forward again can still step forward through the rest. Reaching a different action
67
+ * means the story went somewhere else (the other side of a choice, most often), and a future
68
+ * that no longer follows from the present is dropped.
69
+ *
70
+ * There are two shapes of retrace, because a line's snapshot is taken as it is *reached*, before
71
+ * it runs. So resuming from a restored line re-runs that very line — the first entry pushed
72
+ * after a rewind is the current one coming round again — and only the ones after it arrive as
73
+ * the line ahead.
74
+ *
75
+ * A retraced line keeps the token it had. A caller holding one is holding a reference to a line
76
+ * of the story, and reading past it a second time should not quietly break that reference.
77
+ */
78
+ push(entry: GameHistory): this;
79
+ /**
80
+ * The backlog: everything read up to and including the current line.
81
+ *
82
+ * Lines ahead of the play head after a rewind are deliberately not here — a backlog showing the
83
+ * future would be reporting what has not happened yet. Ask {@link GameHistoryManager.getFuture}
84
+ * for those.
85
+ */
43
86
  getHistory(): GameHistory[];
44
- getByToken(token: string): GameHistory | null;
45
87
  /**
46
- * Serialize the whole backlog for persistence (save format v2).
88
+ * The lines ahead of the play head read once, rewound past, and steppable into again.
47
89
  */
48
- serialize(): SerializedGameHistory[];
90
+ getFuture(): GameHistory[];
91
+ canUndo(): boolean;
92
+ canRedo(): boolean;
93
+ /** Searches the whole timeline, so a line ahead of the play head can be named too. */
94
+ getByToken(token: string): GameHistory | null;
49
95
  /**
50
- * Serialize the backlog up to and including the entry with the given token.
96
+ * Serialize the backlog for persistence.
51
97
  *
52
- * Used by restore-to-history to trim the backlog back to the restored line.
53
- * Returns an empty array if the token is not found.
98
+ * Only up to the play head: a save written after rewinding is a save of that moment, and the
99
+ * lines the player had read beyond it are not part of it. Loading such a save therefore opens
100
+ * with nothing to step forward into, which is what saving in the past means.
54
101
  */
55
- serializeUntil(token: string): SerializedGameHistory[];
102
+ serialize(): SerializedGameHistory[];
56
103
  /**
57
104
  * Rebuild the backlog from persisted entries.
58
105
  *
@@ -29,3 +29,4 @@ export type { LayeredDefinition, LayerGroupDefinition, LayerResolver, LayerSlot,
29
29
  export type { SentenceMetadata } from "../elements/character/sentence";
30
30
  export type { TextEventAppearance, TextEventConfig, TextEventExpression, } from "../elements/character/textEvent";
31
31
  export type { CharacterPortraitConfig, DialogAvatar, DialogAvatarResolver, DialogAvatarResolverContext, DialogAvatarResolution, DialogAvatarSource, } from "../elements/character/avatar";
32
+ export type { WordConfig, WordRenderProps, WordRenderer, } from "../elements/character/word";
@@ -31,7 +31,7 @@ export type CameraDataRaw = {
31
31
  * story.camera.pan({ xalign: 0.3 }, 800), // slide the view left
32
32
  * story.camera.darken(0.6, 500), // dim the stage
33
33
  * jS`It's getting dark...`,
34
- * story.camera.reset(600), // return to the neutral pose
34
+ * story.camera.resetCamera(600), // return to the neutral pose
35
35
  * ]);
36
36
  * ```
37
37
  */
@@ -68,7 +68,18 @@ export declare class Camera extends Displayable<CameraDataRaw, Camera, Transform
68
68
  /**
69
69
  * Return the camera to its neutral pose: centred, zoom `1`, no rotation, fully opaque and no
70
70
  * filter (which also clears {@link Camera.darken}).
71
+ *
72
+ * Named `resetCamera` rather than `reset` because every element already owns an internal
73
+ * `reset()` lifecycle hook — the one the engine calls when a new game starts — and an authoring
74
+ * helper of the same name would quietly stand in for it.
71
75
  * @chainable
76
+ * @example
77
+ * ```ts
78
+ * scene.action([
79
+ * story.camera.zoom(2, 800),
80
+ * story.camera.resetCamera(600),
81
+ * ]);
82
+ * ```
72
83
  */
73
- reset(duration?: number, easing?: TransformDefinitions.EasingDefinition): Proxied<Camera, Chained<LogicAction.Actions, Camera>>;
84
+ resetCamera(duration?: number, easing?: TransformDefinitions.EasingDefinition): Proxied<Camera, Chained<LogicAction.Actions, Camera>>;
74
85
  }
@@ -2,12 +2,63 @@ import { Color, Font } from "../../types";
2
2
  import { DynamicWord } from "../../elements/character/sentence";
3
3
  import { Pausing } from "../../elements/character/pause";
4
4
  import { TextEvent } from "../../elements/character/textEvent";
5
+ import type React from "react";
6
+ /**
7
+ * What a custom word renderer receives.
8
+ *
9
+ * A custom-rendered word is still an ordinary text word — it goes into the backlog, the read-text
10
+ * record and the voice pipeline as its plain text, and it is typed out character by character like
11
+ * any other. The renderer only decides what the revealed characters look like and what happens when
12
+ * they are interacted with.
13
+ */
14
+ export type WordRenderProps<T = unknown> = {
15
+ /**
16
+ * The word's text as the engine has already laid it out — ruby, vertical writing mode and
17
+ * tate-chu-yoko are applied here. Render this rather than {@link WordRenderProps.text}, or those
18
+ * settings are silently dropped.
19
+ */
20
+ children: React.ReactNode;
21
+ /** The characters revealed so far. Equal to `fullText` once the typewriter has passed the word. */
22
+ text: string;
23
+ /** The whole word, revealed or not. */
24
+ fullText: string;
25
+ /** Whether every character of this word has been revealed. */
26
+ revealed: boolean;
27
+ /** Whether the whole line has finished revealing. */
28
+ done: boolean;
29
+ /**
30
+ * The resolved style chain for this word (engine defaults, then the dialog's text props, then
31
+ * the sentence, then the word). Already applied to the element wrapping the renderer; passed in
32
+ * so the renderer can measure against it or carry it into a portal.
33
+ */
34
+ style: Readonly<React.CSSProperties>;
35
+ /** The word's own config, as given to {@link Word}. */
36
+ config: Readonly<Partial<WordConfig>>;
37
+ /** The payload passed as `data` when the word was created. */
38
+ data: T;
39
+ };
40
+ /**
41
+ * A component to render a word with, or the id of one registered with `registerWordRenderer`.
42
+ *
43
+ * Pass a component in code; pass an id when the word comes from data (a story file, a plugin) and
44
+ * cannot carry a function. An id that resolves to nothing renders as plain text.
45
+ */
46
+ export type WordRenderer<T = any> = string | React.ComponentType<WordRenderProps<T>>;
5
47
  export type WordConfig = {
6
48
  className: string;
7
49
  ruby: string;
8
50
  color: Color;
9
51
  pause: boolean;
10
52
  cps?: number;
53
+ /**
54
+ * Renders this word's revealed characters. See {@link Word.custom}.
55
+ */
56
+ render?: WordRenderer;
57
+ /**
58
+ * Arbitrary payload handed to {@link WordConfig.render} as `data`. Never merged or cloned — the
59
+ * renderer receives the very value given here.
60
+ */
61
+ data?: unknown;
11
62
  } & Font;
12
63
  export declare class Word<T extends string | DynamicWord | Pausing | TextEvent = string | DynamicWord | Pausing | TextEvent> {
13
64
  static isWord(obj: any): obj is Word;
@@ -31,6 +82,51 @@ export declare class Word<T extends string | DynamicWord | Pausing | TextEvent =
31
82
  * @param text - The text or word to italicize.
32
83
  */
33
84
  static italic(text: string | Word): Word;
85
+ /**
86
+ * Render a word with a component of your own — an inline glossary term that opens a definition
87
+ * popup, a name that links into an in-game encyclopedia, anything the dialogue box cannot say
88
+ * with color and weight alone.
89
+ *
90
+ * The word stays a text word in every other respect: it is typed out character by character, it
91
+ * reaches the backlog and the read-text record as its plain text, and it is never serialized —
92
+ * the renderer is re-attached when the `say` action is evaluated again, so a save carries no
93
+ * trace of it.
94
+ *
95
+ * The renderer sits *inside* the element the engine styles, so the style chain (engine defaults
96
+ * → the dialog's text props → the sentence → the word) already applies to it; anything the
97
+ * renderer sets wins, being last. `render` propagates to plain strings a dynamic word returns,
98
+ * the same way `className` does.
99
+ *
100
+ * While a custom word is still being typed, a click on it advances the line as a click anywhere
101
+ * else would. Once revealed, the word takes its own clicks — the line does not advance behind
102
+ * the renderer's back.
103
+ *
104
+ * @param text - The word's text, or an existing word to re-render.
105
+ * @param render - A component, or the id of one registered with `registerWordRenderer`.
106
+ * @param config - Optional styling, plus the `data` payload handed to the renderer.
107
+ * @example
108
+ * ```tsx
109
+ * function GlossaryTerm({children, revealed, data}: WordRenderProps<{entry: string}>) {
110
+ * const [open, setOpen] = useState(false);
111
+ * return (
112
+ * <span className="underline decoration-dotted"
113
+ * onClick={() => revealed && setOpen(v => !v)}>
114
+ * {children}
115
+ * {open && <span className="absolute">{lookUp(data.entry)}</span>}
116
+ * </span>
117
+ * );
118
+ * }
119
+ *
120
+ * character.say([
121
+ * "今天的",
122
+ * Word.custom("以太浓度", GlossaryTerm, {data: {entry: "aether"}}),
123
+ * "高得反常。",
124
+ * ]);
125
+ * ```
126
+ */
127
+ static custom<T = unknown>(text: string | Word, render: WordRenderer<T>, config?: Partial<Omit<WordConfig, "render" | "data">> & {
128
+ data?: T;
129
+ }): Word;
34
130
  /**
35
131
  * Wrap raw data (string, dynamic function, or pause) into a `Word` for sentences.
36
132
  * @param text - The payload shown in dialogue, which may be static, dynamic, or a pause.
@@ -11,8 +11,15 @@ export type CharacterConfig = {
11
11
  avatar?: DialogAvatar | false;
12
12
  portraits: (Image | CharacterPortraitConfig)[];
13
13
  };
14
- export type CharacterStateData = {
15
- name: string;
14
+ /**
15
+ * What a save carries for one character.
16
+ *
17
+ * Named and shaped like every other element's save payload ({@link import("../elements/sound").SoundDataRaw},
18
+ * `LayerDataRaw`, `ImageDataRaw`) so that the section a serializer owns stays one level down and
19
+ * there is somewhere to put anything a character comes to carry besides its state.
20
+ */
21
+ export type CharacterDataRaw = {
22
+ state: Record<string, any>;
16
23
  };
17
24
  export interface Character {
18
25
  (content: string, config?: SentenceUserConfig): Proxied<Character, Chained<LogicAction.Actions>>;
@@ -20,7 +27,7 @@ export interface Character {
20
27
  (content: SentencePrompt, config?: SentenceUserConfig): Proxied<Character, Chained<LogicAction.Actions>>;
21
28
  (texts: TemplateStringsArray, ...words: SingleWord[]): Proxied<Character, Chained<LogicAction.Actions>>;
22
29
  }
23
- export declare class Character extends Actionable<CharacterStateData, Character> {
30
+ export declare class Character extends Actionable<CharacterDataRaw, Character> {
24
31
  constructor(name: string | null, config?: DeepPartial<CharacterConfig>);
25
32
  /**
26
33
  * Say something
@@ -10,6 +10,7 @@ import { NVLToken } from "./nvl";
10
10
  import type { TransformDefinitions } from "../elements/transform/type";
11
11
  import type { ActionStatements } from "../elements/type";
12
12
  import type { Persistent } from "../elements/persistent";
13
+ import { Transition } from "../elements/transition/transition";
13
14
  export interface ISceneUserConfig {
14
15
  /**
15
16
  * Background music
@@ -33,7 +34,15 @@ export interface ISceneUserConfig {
33
34
  layers: Layer[];
34
35
  }
35
36
  export type JumpConfig = {
36
- transition: ImageTransition;
37
+ /**
38
+ * Played across the whole stage while the scenes swap: the outgoing scene drives the
39
+ * transition's outgoing half and the incoming scene its incoming half, so sprites, text and
40
+ * every other layer take part rather than only the background.
41
+ *
42
+ * The dialogue box is deliberately not part of it — it is rendered outside the stage and is
43
+ * expected to be gone by the time a scene ends.
44
+ */
45
+ transition: Transition;
37
46
  };
38
47
  type ChainableAction = Proxied<LogicAction.GameElement, Chained<LogicAction.Actions>> | LogicAction.Actions;
39
48
  type ChainedScene = Proxied<Scene, Chained<LogicAction.Actions>>;
@@ -65,6 +74,9 @@ export declare class Scene extends Constructable<LogicAction.Actions, Scene> {
65
74
  * Jump to another scene and discard the current one.
66
75
  *
67
76
  * After the jump the calling scene is unloaded and any actions that follow are ignored.
77
+ *
78
+ * A `transition` plays across the whole stage rather than across the background alone; see
79
+ * {@link JumpConfig.transition}.
68
80
  * @param scene - The destination scene instance.
69
81
  * @param config - Optional transition config (or transition object).
70
82
  * @chainable
@@ -9,9 +9,23 @@ export declare abstract class ImageTransition<T extends TransitionAnimationType[
9
9
  /**@package */
10
10
  private _currentSrc;
11
11
  /**@package */
12
+ private _detached;
13
+ /**@package */
12
14
  private _prevLayers;
13
15
  /**@package */
14
16
  private _targetLayers;
17
+ /**
18
+ * Detach the transition from any image source.
19
+ *
20
+ * A detached transition drives an element that owns its own content — the stage transition
21
+ * driver points the two halves at whole scene subtrees — so there is no src for the
22
+ * resolvers to carry, and `asPrev`/`asTarget` must contribute style only. Without this the
23
+ * src injection below throws, because "no src" is otherwise a genuine authoring error.
24
+ * @package
25
+ */
26
+ _setDetached(detached: boolean): this;
27
+ /**@package */
28
+ _isDetached(): boolean;
15
29
  /**@package */
16
30
  _setPrevLayers(layers: (string | null)[]): this;
17
31
  /**@package */
@@ -90,33 +90,64 @@ export declare class LiveGame {
90
90
  */
91
91
  deserialize(savedGame: SavedGame): void;
92
92
  /**
93
- * Get the history of the game
93
+ * The backlog: every line read up to and including the one the game is on.
94
94
  *
95
- * The history is a list of element actions that have been executed
96
- * For example, when a character says something, the history will record the sentence and voice
95
+ * After stepping back, the lines beyond the play head are not here — they are a future the
96
+ * player can step into again with {@link redo}, and a backlog listing them would be showing what
97
+ * has not happened yet. {@link getFuture} returns those.
97
98
  *
98
- * You can use the id to undo the action by using `liveGame.undo(id)`
99
- *
100
- * This method is an utility method for creating a backlog
99
+ * Each entry carries a `token`, which is how {@link restoreToHistory} names a line. A token
100
+ * keeps naming its line across saves and rewinds.
101
101
  */
102
102
  getHistory(): GameHistory[];
103
103
  /**
104
- * Undo the action
104
+ * The lines ahead of the play head: read once, stepped back past, and reachable again.
105
+ *
106
+ * Empty during ordinary play, and empty right after loading a save — a save written in the past
107
+ * carries no future, because saving after stepping back saves that moment and not the lines that
108
+ * had been read beyond it.
109
+ */
110
+ getFuture(): GameHistory[];
111
+ /** Whether there is a line before this one to step back to. */
112
+ canUndo(): boolean;
113
+ /** Whether a line stepped back past is waiting ahead. */
114
+ canRedo(): boolean;
115
+ /**
116
+ * Step back one line.
117
+ *
118
+ * Backward and forward are one mechanism: each line recorded a self-contained snapshot of the
119
+ * game when it was reached, and moving in either direction restores the snapshot of the line
120
+ * being moved to. That is what lets this work after loading a save, which the undo stack of
121
+ * live closures it replaced could not — those closures cannot be written to a file, so before
122
+ * this, loading a save left the player with a backlog they could not step back into.
123
+ *
124
+ * The line stepped back from is not discarded; see {@link redo}.
125
+ *
126
+ * @returns `true` if the game moved, `false` if this is already the first line or that line
127
+ * carries no snapshot.
128
+ */
129
+ undo(): boolean;
130
+ /**
131
+ * Step forward one line, into a line stepped back past.
132
+ *
133
+ * Only reaches lines the player has already read: this replays the recorded future rather than
134
+ * running the story on. Reading forward normally after stepping back keeps that future while the
135
+ * story retraces the same lines, and drops it the moment the story goes somewhere else — a
136
+ * different branch of a choice has a different future, and the old one no longer follows.
105
137
  *
106
- * - If the id is provided, it will undo the action **by id**
107
- * - If the id is not provided, it will undo **the last action**
138
+ * @returns `true` if the game moved, `false` if there is nothing ahead or it carries no
139
+ * snapshot.
108
140
  */
109
- undo(id?: string): void;
141
+ redo(): boolean;
110
142
  /**
111
- * Restore the game to a past backlog line.
143
+ * Move the game to a recorded line, named by its token.
112
144
  *
113
- * Unlike {@link undo}, this works **after loading a save**: it does not rely on the
114
- * (non-serializable) undo stack. Every backlog entry carries a self-contained state snapshot,
115
- * so restoring re-applies that snapshot and trims the backlog back to that line.
145
+ * The same mechanism as {@link undo} and {@link redo}, and it reaches in either direction: a
146
+ * token from {@link getHistory} steps back, one from {@link getFuture} steps forward.
116
147
  *
117
- * @param token - the backlog entry token (as returned by {@link getHistory})
118
- * @returns `true` if the line was restored, `false` if the token is unknown or the entry has
119
- * no restore snapshot.
148
+ * @param token - the token of the line to move to
149
+ * @returns `true` if the line was restored, `false` if the token is unknown or the line carries
150
+ * no snapshot.
120
151
  */
121
152
  restoreToHistory(token: string): boolean;
122
153
  /**
@@ -17,8 +17,13 @@ import type { AudioBusDeclaration } from "./game/audioBus";
17
17
  * - v1 (undefined on the save): core resume state only, no backlog history.
18
18
  * - v2: adds `game.history`, a full backlog where every entry carries a self-contained
19
19
  * restore snapshot, so loading a save keeps the backlog and any past line can be restored.
20
+ * - v3: `elementStates` lists only the elements whose state differs from what the script wrote.
21
+ * Restoring resets every element first, so an absent element means "as authored" rather than
22
+ * "unchanged". Older engines read a v3 save without resetting, and would leave elements holding
23
+ * whatever the running session put in them; newer engines read v1/v2 saves unchanged, since a
24
+ * list of every element is just a list that happens to name them all.
20
25
  */
21
- export declare const SAVE_FORMAT_VERSION = 2;
26
+ export declare const SAVE_FORMAT_VERSION = 3;
22
27
  export interface SavedGameMetaData {
23
28
  /**
24
29
  * The timestamp of when the game was created
@@ -76,6 +81,15 @@ export interface SerializedGameState {
76
81
  * self-contained snapshot that restores the game to exactly this line.
77
82
  */
78
83
  export interface SerializedGameHistory {
84
+ /**
85
+ * The entry's backlog token, kept so that it survives a load.
86
+ *
87
+ * A token is how a backlog UI names a line — it is what {@link LiveGame.restoreToHistory} takes.
88
+ * Rebuilding the backlog with fresh tokens would invalidate every token a caller was holding the
89
+ * moment a save is loaded or a line restored, so restoring to the same line twice would fail.
90
+ * Absent on saves written before this was persisted; those entries are given fresh tokens.
91
+ */
92
+ token?: string;
79
93
  /**
80
94
  * Stable action id anchor (`action.getId()`). Used to re-bind the entry to the live story on
81
95
  * load; entries whose action no longer exists (script changed) are dropped from the backlog.
@@ -298,10 +312,11 @@ export type GameConfig = {
298
312
  */
299
313
  allowSkipBackgroundTransform: boolean;
300
314
  /**
301
- * If true, when you press [GameConfig.player.skipKey], the game will skip the background transition
302
- * @default false
315
+ * If true, when you press [GameConfig.player.skipKey], the game will skip the transition
316
+ * played between scenes by {@link Scene.jumpTo}
317
+ * @default true
303
318
  */
304
- allowSkipBackgroundTransition: boolean;
319
+ allowSkipSceneTransition: boolean;
305
320
  /**
306
321
  * If true, when you press [GameConfig.player.skipKey], the game will skip the text transform
307
322
  * @default true
@@ -7,6 +7,7 @@ import { Color } from "../../../../game/nlcore/types";
7
7
  import React from "react";
8
8
  import { DialogState } from "./UIDialog";
9
9
  import type { NvlDialogEntry } from "../../gameState";
10
+ import { type TateChuYoko, type TextGlyphOrientation, type TextWritingMode } from "../../lib/verticalText";
10
11
  export type TextAppearanceProps = {
11
12
  /**
12
13
  * The default color of the text
@@ -16,6 +17,25 @@ export type TextAppearanceProps = {
16
17
  fontWeight?: React.CSSProperties["fontWeight"];
17
18
  fontWeightBold?: React.CSSProperties["fontWeight"];
18
19
  fontFamily?: React.CSSProperties["fontFamily"];
20
+ /**
21
+ * Block flow of the text box. `vertical-rl` is the classic Japanese novel setting: columns
22
+ * read top to bottom and advance leftwards.
23
+ * @default "horizontal-tb"
24
+ */
25
+ writingMode?: TextWritingMode;
26
+ /**
27
+ * How glyphs sit inside a vertical column. `mixed` keeps CJK upright and lays Latin on its
28
+ * side; ignored while the box is horizontal.
29
+ * @default "mixed"
30
+ */
31
+ textOrientation?: TextGlyphOrientation;
32
+ /**
33
+ * Tate-chu-yoko (縦中横): sets a short Latin or digit run upright across the column instead of
34
+ * on its side. `true` combines runs of up to two characters; a number sets the limit.
35
+ * Ignored while the box is horizontal.
36
+ * @default true
37
+ */
38
+ tateChuYoko?: TateChuYoko;
19
39
  };
20
40
  export type BaseTextsProps = TextAppearanceProps & {
21
41
  className?: string;
@@ -88,6 +108,9 @@ export interface TextsPreviewProps extends Omit<React.HTMLAttributes<HTMLDivElem
88
108
  fontWeight?: React.CSSProperties["fontWeight"];
89
109
  fontWeightBold?: React.CSSProperties["fontWeight"];
90
110
  fontFamily?: React.CSSProperties["fontFamily"];
111
+ writingMode?: TextWritingMode;
112
+ textOrientation?: TextGlyphOrientation;
113
+ tateChuYoko?: TateChuYoko;
91
114
  onCompleted?: () => void;
92
115
  }
93
116
  export type EntryTextsProps = BaseTextsProps & {
@@ -97,7 +120,7 @@ export type EntryTextsProps = BaseTextsProps & {
97
120
  useTypeEffect: boolean;
98
121
  isActive: boolean;
99
122
  };
100
- export declare function TextsPreview({ text, sentence, words, useTypeEffect, loop, restartDelay, cps, gameSpeed, pauseDuration, defaultColor, className, style, fontSize, fontWeight, fontWeightBold, fontFamily, onCompleted, ...props }: TextsPreviewProps): React.JSX.Element;
123
+ export declare function TextsPreview({ text, sentence, words, useTypeEffect, loop, restartDelay, cps, gameSpeed, pauseDuration, defaultColor, className, style, fontSize, fontWeight, fontWeightBold, fontFamily, writingMode, textOrientation, tateChuYoko, onCompleted, ...props }: TextsPreviewProps): React.JSX.Element;
101
124
  export type RawTextsProps = BaseTextsProps;
102
125
  export declare function RawTexts(props: BaseTextsProps): React.JSX.Element;
103
126
  /**
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ /**
3
+ * A rectangle in the overlay's own coordinates — the dialog box drawn at its authored size, before
4
+ * the stage scales it to the window.
5
+ */
6
+ export type DialogOverlayRect = {
7
+ left: number;
8
+ top: number;
9
+ right: number;
10
+ bottom: number;
11
+ width: number;
12
+ height: number;
13
+ };
14
+ export type DialogOverlay = {
15
+ /**
16
+ * Renders its children above the dialog, still inside the scaled stage, so a popup keeps the
17
+ * dialog's own scale and is not clipped by the text box it belongs to.
18
+ */
19
+ Portal: React.FC<{
20
+ children?: React.ReactNode;
21
+ }>;
22
+ /**
23
+ * Where an element sits in the overlay's coordinates. Feed the result straight to `left`/`top`
24
+ * on a child of {@link DialogOverlay.Portal}.
25
+ */
26
+ measure: (element: Element | null) => DialogOverlayRect | null;
27
+ /** The overlay element, or `null` outside a dialog. */
28
+ container: HTMLElement | null;
29
+ };
30
+ /**
31
+ * Somewhere to draw things that belong to a line but must not live inside it — the definition popup
32
+ * of an inline glossary word, a tooltip on a name.
33
+ *
34
+ * The overlay covers the dialog and sits above it, inside the same scaled stage, so a popup is
35
+ * drawn at the dialog's own scale and escapes the text box's clipping. It lets clicks through
36
+ * everywhere its children do not paint; give the popup itself `pointer-events: auto`.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * const overlay = useDialogOverlay();
41
+ * const rect = overlay.measure(anchorRef.current);
42
+ *
43
+ * return rect && (
44
+ * <overlay.Portal>
45
+ * <div style={{position: "absolute", left: rect.left, top: rect.top, pointerEvents: "auto"}}>
46
+ * …
47
+ * </div>
48
+ * </overlay.Portal>
49
+ * );
50
+ * ```
51
+ */
52
+ export declare function useDialogOverlay(): DialogOverlay;