narraleaf-react 0.25.0 → 0.26.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
  *
@@ -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
  *
@@ -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
  }
@@ -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
@@ -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.
@@ -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
  /**
@@ -366,6 +366,18 @@ export declare class GameState {
366
366
  createElementSnapshot(element: PlayerStateElement): PlayerStateElementSnapshot;
367
367
  fromElementSnapshot(snapshot: PlayerStateElementSnapshot): PlayerStateElement;
368
368
  private removeElements;
369
+ /**
370
+ * Leaving a scene returns everything the scene put on stage to the pose its constructor config
371
+ * describes — the displayables on each layer, and the layers themselves, which are mutable at
372
+ * runtime and would otherwise carry a slide or a fade into the next scene.
373
+ *
374
+ * The story camera is the one displayable that deliberately outlives a scene (a story owns
375
+ * exactly one, and it frames the whole stage across scene changes), so it is skipped here.
376
+ * Nothing in the engine's own pipeline can put it on a layer — only images, texts and puppets
377
+ * emit `displayable:init`, which is what registers an element into the layer map — but a host
378
+ * may register any displayable by hand through `DevTools.registerDisplayable`, so the camera is
379
+ * excluded explicitly rather than by trusting that route to stay closed.
380
+ */
369
381
  private resetLayers;
370
382
  private syncNvlDerivedState;
371
383
  private emitNvlStateChange;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Vertical typesetting helpers shared by the typewriter and the preview renderer.
3
+ *
4
+ * A vertical box is set the way a Japanese novel is: glyphs stand upright in a column that reads
5
+ * top to bottom, and the next column starts to the left. Two things in that layout are not free,
6
+ * and both are about text that is not Japanese:
7
+ *
8
+ * - A Latin word must stay whole. `word-break: break-all`, which the horizontal renderer wants so
9
+ * that CJK wraps anywhere, will otherwise split "Prologue" across two columns, one glyph at a
10
+ * time, sideways.
11
+ * - A short run - a two-digit number, an initialism - reads better set upright across the column
12
+ * than laid on its side. That is tate-chu-yoko (縦中横), and CSS spells it
13
+ * `text-combine-upright: all` on a wrapper around exactly that run.
14
+ */
15
+ import React from "react";
16
+ export type TextWritingMode = "horizontal-tb" | "vertical-rl" | "vertical-lr";
17
+ export type TextGlyphOrientation = "mixed" | "upright" | "sideways";
18
+ /**
19
+ * Tate-chu-yoko setting: `true` uses the typographic default of two characters, a number sets the
20
+ * longest run to combine, and `false` turns it off.
21
+ */
22
+ export type TateChuYoko = boolean | number;
23
+ export declare const DEFAULT_TATE_CHU_YOKO_MAX_LENGTH = 2;
24
+ export declare function isVerticalWritingMode(mode: TextWritingMode | undefined): boolean;
25
+ /** The longest run to combine, or 0 when tate-chu-yoko is off. */
26
+ export declare function resolveTateChuYokoMaxLength(setting: TateChuYoko | undefined): number;
27
+ /**
28
+ * The writing-mode half of the text container's style.
29
+ *
30
+ * `text-orientation` is only written while vertical, where it means something; in a horizontal box
31
+ * it would sit in the inline style doing nothing.
32
+ */
33
+ export declare function verticalContainerStyle(mode: TextWritingMode | undefined, orientation: TextGlyphOrientation | undefined): React.CSSProperties;
34
+ export type VerticalTextSegment = {
35
+ text: string;
36
+ combineUpright: boolean;
37
+ };
38
+ /**
39
+ * Splits text into the runs tate-chu-yoko combines and the text around them.
40
+ *
41
+ * A run longer than `maxLength` is left in the surrounding segment rather than cut down to fit:
42
+ * a long English word belongs on its side, which is what the writing mode does with it anyway.
43
+ */
44
+ export declare function segmentVerticalText(text: string, maxLength: number): VerticalTextSegment[];
45
+ /**
46
+ * How a word's own box breaks.
47
+ *
48
+ * Vertical text keeps `word-break: normal`, which still breaks between CJK characters - that rule
49
+ * has never needed `break-all` - while leaving Latin words whole.
50
+ */
51
+ export declare function wordBreakStyleFor(vertical: boolean): React.CSSProperties;
52
+ /**
53
+ * Renders a word's text with its short runs set upright.
54
+ *
55
+ * Returns the string itself when nothing would be combined, so horizontal text - and vertical text
56
+ * with no Latin in it - is one text node, exactly as before.
57
+ */
58
+ export declare function renderWordText(text: string, vertical: boolean, tateChuYoko: TateChuYoko | undefined): React.ReactNode;
@@ -23,6 +23,14 @@ import { NvlDialogList, DefaultNvlDialogItem } from "./elements/nvl/NvlDialogLis
23
23
  import { NvlProvider, useNvl, useNvlDialogs, useIsNvlMode, useIsNvlVisible } from "./elements/nvl/NvlContext";
24
24
  export type { DialogAvatarContext } from "./elements/say/Avatar";
25
25
  export type { BaseTextsProps, EntryTextsProps, RawTextsProps, TextAppearanceProps, TextsPreviewInput, TextsPreviewLoop, TextsPreviewProps, TextsProps, } from "./elements/say/Sentence";
26
+ /**
27
+ * The vocabulary of the vertical-text props on `TextAppearanceProps`.
28
+ *
29
+ * Exported because a value has to be named somewhere other than the JSX attribute: an application
30
+ * that keeps its typography in a settings object, or hands the mode down through its own props,
31
+ * had no way to type either without restating the unions.
32
+ */
33
+ export type { TateChuYoko, TextGlyphOrientation, TextWritingMode, } from "./lib/verticalText";
26
34
  export type { NametagProps } from "./elements/say/Nametag";
27
35
  export type { ItemProps } from "./elements/menu/UIMenu/Item";
28
36
  export type { ChoiceEvaluated } from "./elements/menu/type";