narraleaf-react 0.39.1 → 0.40.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.
@@ -22,6 +22,17 @@ export declare class Action<ContentNodeType = any, Callee = LogicAction.GameElem
22
22
  readonly __stack: string;
23
23
  constructor(callee: Callee, type: Type, contentNode: ContentNode<ContentNodeType>);
24
24
  executeAction(_state: GameState, _injection: ActionExecutionInjection): ExecutedActionResult;
25
+ /**
26
+ * Give up whatever this action was still holding, without running it and without going on to
27
+ * the next one.
28
+ *
29
+ * Called on an action that is dropped from an execution stack rather than executed - today
30
+ * only when a concurrent branch is abandoned (see {@link StackModel.abandon}). Almost every
31
+ * action holds nothing outside its own stack and so does nothing here; the exception is a
32
+ * scene call's return address, which is a promise made to a scene that is sitting suspended on
33
+ * the stage waiting for it.
34
+ */
35
+ abandon(_state: GameState): void;
25
36
  getId(): string;
26
37
  setId(id: string): void;
27
38
  getStaticId(): string | null;
@@ -84,10 +84,24 @@ export declare class SceneAction<T extends typeof SceneActionTypes[keyof typeof
84
84
  /** Put back what {@link SceneAction.unwindCallStack} took away, in the order it took it. */
85
85
  static rewindCallStack(unwound: [scene: Scene, snapshot: SceneSnapshot][], state: GameState): void;
86
86
  exit(state: GameState): void;
87
+ /**
88
+ * Give up the call this return address stands for.
89
+ *
90
+ * Reached when the stack holding a `scene:resume` is thrown away rather than run to it - a
91
+ * `Control.any` branch that lost, today. The stage half of `scene:resume` and nothing else: the
92
+ * scene the call entered leaves, and the scene the call had suspended is running again. What is
93
+ * deliberately missing is the rest of it - a return address nobody is coming back to has no
94
+ * next action, and no history entry either, because the branch it was on is not a place a step
95
+ * back can land.
96
+ *
97
+ * Every other scene action holds nothing once it is off the stack, so this is the only override.
98
+ */
99
+ abandon(state: GameState): void;
87
100
  applyNvlVisibility(gameState: GameState, visible: boolean, options: Partial<TransformDefinitions.CommonTransformProps> | undefined, injection: ActionExecutionInjection): CalledActionResult | Awaitable<CalledActionResult, CalledActionResult>;
88
101
  executeAction(gameState: GameState, injection: ActionExecutionInjection): ExecutedActionResult;
89
102
  getFutureActions(story: Story, searchOptions?: ActionSearchOptions): LogicAction.Actions[];
90
103
  _sceneNotFoundError(sceneId: string): Error;
104
+ _callerAlreadyParkedError(target: Scene | string): Error;
91
105
  getSceneName(scene: Scene | string): string;
92
106
  stringify(story: Story, seen: Set<LogicAction.Actions>, _strict: boolean): string;
93
107
  }
@@ -114,6 +114,14 @@ export declare class StackModel {
114
114
  static isStackModelsAwaiting(type: StackModelWaiting["type"], stackModels: StackModel[]): boolean;
115
115
  private stack;
116
116
  private waitingAction;
117
+ /**
118
+ * True only for as long as {@link executeActions} is running {@link waitingAction}.
119
+ *
120
+ * `waitingAction` is the last action popped off the stack, and it stays set after that action
121
+ * has run to completion - so on its own it cannot say whether the action is still the one the
122
+ * model is on. {@link serialize} needs that distinction and nothing else does.
123
+ */
124
+ private executingWaitingAction;
117
125
  private loopConfig;
118
126
  private loopBodyActions;
119
127
  private loopCondition;
@@ -192,9 +200,38 @@ export declare class StackModel {
192
200
  * @returns Snapshot that can be passed to StackModel.deserialize.
193
201
  */
194
202
  serialize(frozen?: boolean): StackModelRawData;
203
+ /**
204
+ * Give up everything this stack still holds, unwinding the scene calls it opened on the way.
205
+ *
206
+ * {@link reset} clears stacks; it does not put the stage back. A branch cut mid-call is holding
207
+ * a `scene:resume` - the promise to come back to the scene it suspended - and dropping that
208
+ * promise without keeping it leaves the caller parked on the stage with nothing able to return
209
+ * to it, and the called scene mounted with nothing pointing at it. Both are permanent: no later
210
+ * action names either scene. So the call is given up, innermost first, which is what
211
+ * `SceneAction.unwindCallStack` does when a plain jump walks away from a call stack.
212
+ *
213
+ * Only the call frames are unwound. A scene the branch merely *entered* - what a plain jump
214
+ * does - belongs to the main stack from the moment the jump re-pointed it at that scene, so it
215
+ * is the story's scene by then rather than the branch's, and taking it off the stage here would
216
+ * unload the scene the story is now in.
217
+ */
218
+ abandon(): this;
219
+ /**
220
+ * Every scene-call return address this stack is holding, innermost first.
221
+ *
222
+ * Nested groups are searched too: a branch can itself be running a `Control.all` whose own
223
+ * branch opened a call, and that call is held just as firmly.
224
+ */
225
+ private collectCallFrames;
195
226
  reset(): void;
196
227
  deserialize(data: StackModelRawData, actionMap: Map<string, LogicAction.Actions>): this;
197
228
  isEmpty(): boolean;
229
+ /**
230
+ * Whether the top of the stack is a `Control.all` / `Control.any` link whose branches have not
231
+ * all drained - the one state in which the stack accepts nothing on top of it, because the
232
+ * group has to finish before whatever queued behind it can run.
233
+ */
234
+ private topIsGroupStillRunning;
198
235
  /**
199
236
  * Drop everything above the innermost scene-call return address, or the whole stack if there
200
237
  * is none.
@@ -11,6 +11,16 @@ export declare class RGBColor {
11
11
  toString(): string;
12
12
  toHex(): string;
13
13
  }
14
+ /**
15
+ * The action a {@link RuntimeScriptError} is about, reduced to what a host can put on screen:
16
+ * which action it was, and what kind of action it was.
17
+ */
18
+ export type RuntimeScriptErrorAction = {
19
+ /** The action's id, as {@link Action.getId} reports it. */
20
+ id: string;
21
+ /** The action's type, e.g. `scene:preSuspend`. */
22
+ type: string;
23
+ };
14
24
  /**
15
25
  * Alias for {@link Word.color}
16
26
  */
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What a click that landed on the stage is asking for.
3
+ *
4
+ * The stage announcer decides *whether* a click belongs to the stage at all - it is over the player,
5
+ * it is not inside a menu, a notification, a page or a word that takes its own clicks. This decides
6
+ * what the ones that survive that walk actually mean, and it is kept apart from the walk because the
7
+ * walk is DOM and this is not: the rules below are the ones worth pinning, and pinning them needs no
8
+ * document.
9
+ *
10
+ * Comments in English per project convention.
11
+ */
12
+ /**
13
+ * The three things a click on the stage can mean.
14
+ *
15
+ * - `advance` - settle the line, the ordinary case.
16
+ * - `restoreDialog` - bring the dialog box back, because it has been put away.
17
+ * - `ignore` - the click reached nothing that wants it.
18
+ */
19
+ export type StageClickIntent = "advance" | "restoreDialog" | "ignore";
20
+ export type StageClickInput = {
21
+ /**
22
+ * Whether the announcer's DOM walk decided this click belongs to the stage rather than to
23
+ * something drawn on it.
24
+ */
25
+ onStage: boolean;
26
+ /**
27
+ * The `showDialog` preference: `false` means the player has put the box away to look at the
28
+ * picture behind it.
29
+ */
30
+ dialogShown: boolean;
31
+ /** Whether anything is currently holding the line - see `GameState.suspendAdvance`. */
32
+ advanceSuspended: boolean;
33
+ };
34
+ /**
35
+ * Decide what one click on the stage means.
36
+ *
37
+ * Two rules, in the order they are asked:
38
+ *
39
+ * - **A click with the box put away brings it back; it does not spend a line.** The box is the thing
40
+ * a click on the stage acts on, so with the box gone there is nothing on screen the click could
41
+ * have been aimed at - and the line it would have settled is one the player never saw. Every
42
+ * visual novel treats the next click after a hide as the one that undoes the hide, which is also
43
+ * the only reading that cannot lose text.
44
+ * - **That outranks a hold on the line.** A suspension is a hold on *advancing*, taken by something
45
+ * drawn over a line that wants the player's attention first - a definition popup on an inline
46
+ * word. Everything that takes one is drawn inside the box, so while the box is away the hold is
47
+ * invisible; leaving it in charge would mean a player who put the box away had no way to bring it
48
+ * back and no way to reach the thing holding the line either. Restoring the box does not settle
49
+ * the line, so it takes nothing away from whatever holds it: the hold is still there, and still
50
+ * in charge, the moment the box is back.
51
+ */
52
+ export declare function resolveStageClickIntent({ onStage, dialogShown, advanceSuspended, }: StageClickInput): StageClickIntent;
@@ -50,6 +50,21 @@ export declare class DialogState {
50
50
  isIdle(): boolean;
51
51
  setIdle(idle: boolean): void;
52
52
  isActive(): boolean;
53
+ /**
54
+ * Hand the line to a box, or take it away from one.
55
+ *
56
+ * A box that becomes active over a line whose text has already finished revealing is idle at
57
+ * once. `_idle` is the latch between "there is nothing left to reveal" and "the next advance
58
+ * settles the line", and the `complete` event that used to be its only source is answered only
59
+ * by a box that was active at the instant the text finished. A line that finished while its box
60
+ * was displaced - another scene's dialog on top of it, a panel over the stage, the moment of
61
+ * retention after the line before it - therefore came back with the latch down, and the first
62
+ * advance the player spent went on raising it instead of settling the line. The latch now
63
+ * follows the fact rather than who was watching when it happened.
64
+ *
65
+ * A line that has *not* finished revealing is untouched: its box coming back must still reveal
66
+ * the rest of it before a click can settle it.
67
+ */
53
68
  setActive(active: boolean): this;
54
69
  /**
55
70
  * Only for dialog component to call
@@ -0,0 +1,42 @@
1
+ /**
2
+ * How a dialog box that has been put away is hidden.
3
+ *
4
+ * Hiding the box is how a player looks at the picture behind it; it is not the box going away. The
5
+ * box keeps its line, its typing task, its animations and - the part this file exists for - its
6
+ * place in hit testing, because whatever a game has drawn inside the box is still a thing the player
7
+ * can be reaching for. The engine's own UI has nothing in there, but a host that renders its own
8
+ * dialog into this box does, and that content is the only route it has to the pointer.
9
+ *
10
+ * Comments in English per project convention.
11
+ */
12
+ export type DialogVisibility = {
13
+ /** The class the box carries while in this state, or an empty string for none. */
14
+ className: string;
15
+ /** Whether the box should be hidden from assistive technology. */
16
+ ariaHidden: boolean;
17
+ };
18
+ /**
19
+ * Decide how a box in the given `showDialog` state is presented.
20
+ *
21
+ * **Not `visibility: hidden`, and not `display: none`.** Both take the whole subtree out of hit
22
+ * testing, which is a different statement from "do not draw this": with either of them the box is
23
+ * not merely invisible, it is unreachable, and so is everything a host has rendered inside it. The
24
+ * box used to be hidden that way while also asking for `pointer-events: auto` on the same element -
25
+ * two instructions that cannot both be followed, and the one that lost is the one that was meant.
26
+ * What it cost: a panel inside the box could not be scrolled, tapped or dismissed while the box was
27
+ * away, and nothing said why, because the elements were all still there and all still styled to
28
+ * receive the pointer.
29
+ *
30
+ * Transparency says only "do not draw this", which is what hiding the box means. It costs a stacking
31
+ * context, which this element already has wherever the stage is scaled to fit, and it leaves the box
32
+ * in the accessibility tree - so a hidden box is marked `aria-hidden` here rather than relying on a
33
+ * side effect of how it is drawn.
34
+ *
35
+ * **No `pointer-events` of its own, in either state.** Whether a box is reachable at all is its
36
+ * layer's to say, not the box's: a scene parked behind a returnable jump keeps a layer that covers
37
+ * the stage with nothing in it, and that layer turns the pointer off for everything inside it. A box
38
+ * asserting `pointer-events: auto` would overrule that and go back to swallowing the clicks meant
39
+ * for the scene in front of it. The old hidden state did assert it, which was harmless only because
40
+ * it was paired with a rule that removed the box from hit testing anyway.
41
+ */
42
+ export declare function resolveDialogVisibility(shown: boolean): DialogVisibility;
@@ -0,0 +1,101 @@
1
+ import { PlayerStateElement } from "../../gameState";
2
+ import { DialogAction } from "../say/type";
3
+ /**
4
+ * How long a dialog box is kept on screen after the line in it has been settled.
5
+ *
6
+ * A line is usually replaced by the next one within the same tick, and the grace is what lets that
7
+ * happen in the same box rather than as an exit followed by an entrance. It is a hold for a line
8
+ * that is *finished*; see {@link resolveDialogPresentation} for why a line that is still waiting can
9
+ * never be held this way.
10
+ */
11
+ export declare const DIRECT_DIALOG_REPLACEMENT_GRACE_MS = 120;
12
+ /**
13
+ * The presence bookkeeping one scene's dialog layer carries between renders.
14
+ *
15
+ * `slotKeys` maps a slot - the index a line has in the scene's `texts` - to the React key of the
16
+ * box showing it, so a line replaced in place keeps its box. `exitingKeys` are the keys
17
+ * `AnimatePresence` is still animating out; a slot that is handed one of those has to be given a
18
+ * fresh key instead, or the new line would be mounted into a box that is on its way off screen.
19
+ */
20
+ export type DialogPresenceState = {
21
+ slotKeys: Map<number, string>;
22
+ exitingKeys: Set<string>;
23
+ menuPromptIds: WeakMap<PlayerStateElement["menus"][number], string>;
24
+ nextKey: number;
25
+ };
26
+ export type DialogRenderItem = {
27
+ action: DialogAction;
28
+ onFinished?: (skiped?: boolean) => void;
29
+ useTypeEffect: boolean;
30
+ presenceKey: string;
31
+ slot: number;
32
+ /**
33
+ * Whether this box is showing a line that is still waiting for the player.
34
+ *
35
+ * An inactive box ignores clicks, the advance key and auto-forward: it is a picture of a line
36
+ * that is already over. Only a retained item is ever inactive.
37
+ */
38
+ active: boolean;
39
+ };
40
+ /** One line (or one menu prompt) the scene wants a box for, before a box has been assigned. */
41
+ export type DialogSource = {
42
+ action: DialogAction;
43
+ onFinished?: (skiped?: boolean) => void;
44
+ useTypeEffect: boolean;
45
+ slot: number;
46
+ };
47
+ export type DialogPresentationInput = {
48
+ /**
49
+ * Every line the scene is still waiting on, in slot order - or, when it is waiting on none, the
50
+ * prompt of a menu it is showing. Empty means the scene has nothing of its own to say.
51
+ */
52
+ sources: DialogSource[];
53
+ /** How many menus the scene is showing. A menu is interactive whether or not it has a prompt. */
54
+ menuCount: number;
55
+ /** Mutated: the slot/key bookkeeping this layer carries between renders. */
56
+ presence: DialogPresenceState;
57
+ /** The snapshot currently being held on screen, if the grace is running. */
58
+ retained: DialogRenderItem[] | null;
59
+ /** The items the last render with something to say produced. */
60
+ lastActive: DialogRenderItem[];
61
+ sceneId: string;
62
+ };
63
+ export type DialogPresentation = {
64
+ /** What to render, in order. */
65
+ items: DialogRenderItem[];
66
+ /** The snapshot to keep holding, or null to stop holding one. */
67
+ retained: DialogRenderItem[] | null;
68
+ /** The snapshot to remember as the last live one. */
69
+ lastActive: DialogRenderItem[];
70
+ /** Whether the retention grace should be counting after this render. */
71
+ retaining: boolean;
72
+ /**
73
+ * Whether this scene's dialog layer should take pointer events.
74
+ *
75
+ * A scene's dialog layer covers the whole stage, and every scene on the stage has one -
76
+ * including a caller parked behind a returnable jump, which has nothing to show at all. The
77
+ * layers are stacked in the order the scenes are held, so a parked caller's empty layer is
78
+ * drawn over the box of the scene the story is actually in. A layer with nothing live in it
79
+ * that still took pointer events therefore swallowed every click aimed at the line underneath:
80
+ * the box's own click handler, an inline word's, anything a line had drawn over itself.
81
+ */
82
+ interactive: boolean;
83
+ };
84
+ /**
85
+ * Decide what one scene's dialog layer renders this frame.
86
+ *
87
+ * Deterministic given its input; it starts no timers and touches no React. `presence` is the one
88
+ * thing it writes to, because slot/key assignment is bookkeeping that has to survive the render
89
+ * that made it.
90
+ *
91
+ * Two rules the layer is built on:
92
+ *
93
+ * - **A line that is still waiting is always live.** Retention describes a line that is over and is
94
+ * being held on screen for a moment; a line whose click callback has not been called yet is not
95
+ * that, whatever came and went over its box in the meantime. So a snapshot is only ever taken
96
+ * when the scene has nothing left to say, and any snapshot in hand is dropped the moment it has
97
+ * something again.
98
+ * - **A layer only takes the pointer when it has something to take it for.** See
99
+ * {@link DialogPresentation.interactive}.
100
+ */
101
+ export declare function resolveDialogPresentation({ sources, menuCount, presence, retained, lastActive, sceneId, }: DialogPresentationInput): DialogPresentation;
@@ -237,6 +237,27 @@ export declare class GameState {
237
237
  addScene(scene: Scene): this;
238
238
  flush(): this;
239
239
  popScene(): this;
240
+ /**
241
+ * Advance every line still waiting for a click on `scene`, because the scene is leaving.
242
+ *
243
+ * A pending line lives as a `Clickable` in the scene's own stage entry, and the click callback
244
+ * in it is the only thing that settles the action waiting on it. Taking the entry away used to
245
+ * drop the line with it: nothing rendered the line any more, so no click could reach it, and
246
+ * whatever was waiting on it waited for ever.
247
+ *
248
+ * On the main stack that never showed, because every path that unloads a scene also clears the
249
+ * stack it would have blocked. A concurrent branch has a stack of its own and `Control.all`
250
+ * waits for every branch, so a single line left behind by a returning scene call stopped the
251
+ * story with no error and every click inert.
252
+ *
253
+ * Advanced rather than abandoned, because advancing is the only handle a pending line offers -
254
+ * its click callback is what settles it. The line is over either way, since it is no longer on
255
+ * screen; this way the branch that queued it carries on instead of stopping where it stood.
256
+ *
257
+ * Menus are deliberately not touched: choosing on the player's behalf would decide the story,
258
+ * which is worse than any of this.
259
+ */
260
+ settlePendingLines(scene: Scene): this;
240
261
  removeScene(scene: Scene): this;
241
262
  getSceneElements(): PlayerStateElement[];
242
263
  /**