narraleaf-react 0.41.1 → 0.42.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.
@@ -11,7 +11,7 @@ import type { Script } from "../elements/script";
11
11
  import type { Sentence } from "../elements/character/sentence";
12
12
  import type { TransformDefinitions } from "../elements/transform/type";
13
13
  import type { Image, TagGroupDefinition } from "../elements/displayable/image";
14
- import type { FadeOptions } from "../elements/type";
14
+ import type { FadeOptions, SoundPlayOptions } from "../elements/type";
15
15
  import type { Transition } from "../elements/transition/transition";
16
16
  import type { ImageTransition } from "../elements/transition/transitions/image/imageTransition";
17
17
  import type { Layer } from "../elements/layer";
@@ -107,7 +107,7 @@ export declare const SoundActionTypes: {
107
107
  readonly seek: "sound:seek";
108
108
  };
109
109
  export type SoundActionContentType = {
110
- [K in typeof SoundActionTypes[keyof typeof SoundActionTypes]]: K extends "sound:play" ? [FadeOptions] : K extends "sound:stop" ? [FadeOptions] : K extends "sound:setVolume" ? [volume: number, duration: number] : K extends "sound:setRate" ? [number] : K extends "sound:pause" ? [FadeOptions] : K extends "sound:resume" ? [FadeOptions] : K extends "sound:mute" ? [boolean] : K extends "sound:seek" ? [time: number] : any;
110
+ [K in typeof SoundActionTypes[keyof typeof SoundActionTypes]]: K extends "sound:play" ? [SoundPlayOptions] : K extends "sound:stop" ? [FadeOptions] : K extends "sound:setVolume" ? [volume: number, duration: number] : K extends "sound:setRate" ? [number] : K extends "sound:pause" ? [FadeOptions] : K extends "sound:resume" ? [FadeOptions] : K extends "sound:mute" ? [boolean] : K extends "sound:seek" ? [time: number] : any;
111
111
  };
112
112
  export declare const ControlActionTypes: {
113
113
  readonly action: "control:action";
@@ -152,14 +152,20 @@ export declare class Sound extends Actionable<SoundDataRaw, Sound> {
152
152
  * It is *not* the same as {@link Scene.setBackgroundMusic}: a clip played here is not in the
153
153
  * scene's background-music slot, so leaving the scene will not stop it and no cross-fade is
154
154
  * arranged for it. That is true of every clip played this way, on any bus.
155
+ * The script carries on as soon as the clip is playing. A clip that is meant to hold the script
156
+ * until it finishes says so: `sound.play(0, {waitForEnd: true})`.
157
+ *
155
158
  * @param duration - Optional fade duration in milliseconds.
159
+ * @param options.waitForEnd - Hold the script until the clip finishes. Ignored for a looping clip.
156
160
  * @chainable
157
161
  * @example
158
162
  * ```ts
159
163
  * sound.play(1000);
160
164
  * ```
161
165
  */
162
- play(duration?: number): ChainedSound;
166
+ play(duration?: number, options?: {
167
+ waitForEnd?: boolean;
168
+ }): ChainedSound;
163
169
  /**
164
170
  * Stop the sound and optionally fade out.
165
171
  * @param duration - Fade duration in milliseconds.
@@ -11,6 +11,18 @@ export type FadeOptions = {
11
11
  end: number;
12
12
  duration: number;
13
13
  };
14
+ /**
15
+ * What {@link AudioManager.play} is asked for, on top of the fade.
16
+ *
17
+ * `waitForEnd` is what decides whether the action that started the clip holds the script until the
18
+ * clip finishes. A sound effect written between two lines is not a wait the author asked for - a
19
+ * seven-second chime stopped the script for seven seconds with nothing on screen saying why - so
20
+ * {@link Sound.play} leaves it off and a row that means "hold here" turns it on.
21
+ */
22
+ export type SoundPlayOptions = FadeOptions & {
23
+ /** Resolve only once the clip has finished playing. Ignored for a looping clip. */
24
+ waitForEnd?: boolean;
25
+ };
14
26
  export type ChainedActions = (Proxied<LogicAction.GameElement, Chained<LogicAction.Actions>> | LogicAction.Actions)[];
15
27
  export type ActionStatements = ChainedActions | string[];
16
28
  export type { TransitionAnimationType, TransitionTask, };
@@ -185,6 +185,21 @@ export type GameConfig = {
185
185
  * @default true
186
186
  */
187
187
  waitForPreload: boolean;
188
+ /**
189
+ * How much of a scene has to be warm before the game is shown.
190
+ *
191
+ * `"firstFrame"` waits for the scene's opening background and nothing else, then keeps fetching
192
+ * the rest behind the game. `"scene"` waits for every image the scene registers anywhere in it,
193
+ * which is what this always did.
194
+ *
195
+ * The two differ by an order of magnitude on a real project: a chapter's registered set is
196
+ * every pose of every character it shows and every background it cuts to, while its first frame
197
+ * is one picture. Choose `"scene"` when a game would rather open late than risk an image
198
+ * arriving after the frame that wanted it.
199
+ *
200
+ * @default "firstFrame"
201
+ */
202
+ preloadGate: "firstFrame" | "scene";
188
203
  /**
189
204
  * Preload all possible images in the scene
190
205
  *
@@ -0,0 +1,40 @@
1
+ /**
2
+ * One advance of an NVL line, decided and carried out.
3
+ *
4
+ * NVL differs from ADV in that the page owns the line: `requestNvlAdvance` is not a query but the
5
+ * move itself, and it settles a line that has finished revealing without the dialog hearing about
6
+ * it. Only when it answers `typing` is there anything left for this dialog to do - and what to do
7
+ * then is the same rule ADV follows, {@link resolveDialogAdvanceIntent}: an advance asks the line
8
+ * to complete, and only the skip mode forces.
9
+ *
10
+ * The skip key used to force here on every emission, tap included, which walked it past the pauses
11
+ * an author wrote. It is kept out of the hook so that a test can drive it against a real dialog
12
+ * state without a React tree.
13
+ *
14
+ * Comments in English per project convention.
15
+ */
16
+ /** The page state, as this module needs it. */
17
+ export type NvlAdvancePage = {
18
+ requestNvlAdvance(dialogId: string): "ignore" | "typing" | "advance";
19
+ };
20
+ /** The part of a dialog state an advance touches. Structural, as in `dialogAdvanceIntent`. */
21
+ export type NvlAdvanceTarget = {
22
+ forceSkip(): void;
23
+ requestComplete(): void;
24
+ };
25
+ export type NvlAdvanceInput = {
26
+ dialogId: string;
27
+ /** Whether this dialog is the one holding the line. */
28
+ active: boolean;
29
+ /** Whether this is the skip mode rather than one advance. */
30
+ forced: boolean;
31
+ };
32
+ /**
33
+ * What one advance of an NVL line ended up doing.
34
+ *
35
+ * - `ignore` - not this dialog's to answer.
36
+ * - `pageHandled` - the page moved on its own; there was nothing left to reveal.
37
+ * - `requestComplete` / `forceSkip` - what was asked of the line still revealing.
38
+ */
39
+ export type NvlAdvanceResult = "ignore" | "pageHandled" | "requestComplete" | "forceSkip";
40
+ export declare function applyNvlAdvance(page: NvlAdvancePage, dialog: NvlAdvanceTarget, { dialogId, active, forced }: NvlAdvanceInput): NvlAdvanceResult;
@@ -1,10 +1,22 @@
1
1
  import type { Scene } from "../../../nlcore/elements/scene";
2
2
  import type { Sound } from "../../../nlcore/elements/sound";
3
3
  export type ScenePreloadPlan = {
4
+ /**
5
+ * The image the scene's own first painted frame cannot do without: its opening background.
6
+ *
7
+ * Split out of {@link ScenePreloadPlan.critical} because those two answer different questions.
8
+ * "What does this scene use anywhere in it" is a whole chapter's artwork - every pose of every
9
+ * character it shows, every background it cuts to - and on a real project that is most of the
10
+ * library. "What is on screen when it opens" is one picture. A host that holds its loading
11
+ * screen up until the scene is warm was holding it for the first of those; this tier is the
12
+ * second, and it is what {@link GameConfig.preloadGate} lets a game wait on instead.
13
+ */
14
+ firstFrame: string[];
4
15
  /**
5
16
  * Image urls the scene that is about to paint registers directly: its own backgrounds and
6
- * images, plus the immediate background of any scene it jumps to. This tier is on the path to
7
- * the first painted frame, so it is fetched unpaced and nothing is revealed until it is warm.
17
+ * images, plus the immediate background of any scene it jumps to, minus anything already in
18
+ * {@link ScenePreloadPlan.firstFrame}. Fetched unpaced, because the player is a click away
19
+ * from needing it.
8
20
  */
9
21
  critical: string[];
10
22
  /**
@@ -1,6 +1,6 @@
1
1
  import { Sound as SoundElement, SoundBusId } from "../../nlcore/elements/sound";
2
2
  import { SoundToken } from "@narraleaf/sound";
3
- import { FadeOptions } from "../../nlcore/elements/type";
3
+ import { FadeOptions, SoundPlayOptions } from "../../nlcore/elements/type";
4
4
  import { Awaitable } from "../../../util/data";
5
5
  import { GameState } from "../gameState";
6
6
  import { LogicAction } from "../../nlcore/action/logicAction";
@@ -139,7 +139,16 @@ export declare class AudioManager {
139
139
  * back to whatever the author first wrote down.
140
140
  */
141
141
  private static defaultFade;
142
- play(sound: SoundElement, options?: FadeOptions): Awaitable<void>;
142
+ /**
143
+ * Start `sound`, and resolve when it is playing.
144
+ *
145
+ * `options.waitForEnd` moves the resolution to the end of the clip. It defaults to ON here and
146
+ * OFF at {@link Sound.play}, and the asymmetry is deliberate: the callers inside the engine - a
147
+ * voice line, a text-event effect - are tracking a clip's whole life and were written against
148
+ * that, while an authored sound-effect row is not asking the script to stop for the length of
149
+ * the file. A looping clip never ends, so it is always resolved once it is playing.
150
+ */
151
+ play(sound: SoundElement, options?: SoundPlayOptions): Awaitable<void>;
143
152
  /**
144
153
  * Start a clip and hand the token back once playback is under way.
145
154
  *
@@ -31,6 +31,15 @@ export declare class ImageCacheManager {
31
31
  */
32
32
  private decoded;
33
33
  constructor(game: Game);
34
+ /**
35
+ * Release the object URL an entry holds.
36
+ *
37
+ * Every path that drops an entry goes through this. An object URL pins its blob for the
38
+ * lifetime of the document, so a cache that forgets an entry without revoking leaks the whole
39
+ * image - which on a scene change is most of a scene's artwork.
40
+ */
41
+ private release;
42
+ private releaseAll;
34
43
  has(name: string): boolean;
35
44
  add(name: string, src: string): this;
36
45
  remove(name: string): this;
@@ -44,7 +53,7 @@ export declare class ImageCacheManager {
44
53
  size(): number;
45
54
  isPreloading(src: string): boolean;
46
55
  /**
47
- * Fetch `url`, cache it as a data URL and decode it, resolving the returned token's
56
+ * Fetch `url`, cache it as an object URL and decode it, resolving the returned token's
48
57
  * `onFinished` only once the decode has settled.
49
58
  *
50
59
  * @param options.retainDecoded keep the decoded bitmap alive until this source leaves the