narraleaf-react 0.46.0 → 0.47.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.
@@ -1,6 +1,7 @@
1
1
  import type { Scene } from "../elements/scene";
2
2
  import type { Sound } from "../elements/sound";
3
3
  import type { Story } from "../elements/story";
4
+ import type { Video } from "../elements/video";
4
5
  /**
5
6
  * The preload seam: what the player warms, when it warms it, and where the bytes come from.
6
7
  *
@@ -28,7 +29,16 @@ import type { Story } from "../elements/story";
28
29
  * A game that supplies no strategy gets the built-in one, which reproduces the walk and the tiers
29
30
  * exactly as they were, driven by the same `preload*` fields of `GameConfig`.
30
31
  */
31
- /** What kind of thing a plan entry names. Audio is not one of these - see {@link PreloadPlan.audio}. */
32
+ /**
33
+ * What kind of thing a preload resource names.
34
+ *
35
+ * Only images are named this way in a plan's {@link PreloadPlan.entries}: they are the resources
36
+ * the player fetches, caches and decodes, so a url is the whole of what it needs to know. Audio and
37
+ * video are named by element instead ({@link PreloadPlan.audio}, {@link PreloadPlan.video}),
38
+ * because what warming them means is a property of the element and not of the url. `video` survives
39
+ * here for {@link PreloadStrategy.onMissing} and {@link PreloadStrategy.acquire}, which speak about
40
+ * a resource rather than about a plan.
41
+ */
32
42
  export type PreloadResourceType = "image" | "video";
33
43
  /** One thing a plan can ask for, named by the url the stage will show. */
34
44
  export type PreloadResource = {
@@ -53,7 +63,17 @@ export type PreloadResource = {
53
63
  * abandoned without ceremony when the moment is superseded.
54
64
  */
55
65
  export type PreloadBand = "gate" | "soon" | "idle";
56
- export type PreloadEntry = PreloadResource & {
66
+ export type PreloadEntry = {
67
+ /**
68
+ * Always an image.
69
+ *
70
+ * Video used to be nameable here, and warming it meant reading the bytes once and dropping
71
+ * them, in the hope that whatever served them kept a copy. That hope is unfounded on the hosts
72
+ * that matter - a game serving assets through its own protocol has no cache to warm - so it
73
+ * reported warming that had not happened. {@link PreloadPlan.video} is the answer that works.
74
+ */
75
+ type: "image";
76
+ src: string;
57
77
  band: PreloadBand;
58
78
  /**
59
79
  * Whether to decode the image off-screen before anything shows it, and hold the bitmap.
@@ -88,6 +108,25 @@ export type PreloadPlan = {
88
108
  * so a loading screen that waited for a clip could wait for ever.
89
109
  */
90
110
  readonly audio?: readonly Sound[];
111
+ /**
112
+ * Clips this moment wants buffering, nearest first, and only these.
113
+ *
114
+ * Named by element for the same reason audio is, only more so: warming a video means putting
115
+ * its element in the document and letting the browser buffer into it, and the element that
116
+ * buffered has to be the one that plays or nothing was gained. There is no video cache in the
117
+ * player, and there is no url-shaped way to fill one.
118
+ *
119
+ * The order is a statement of what is coming first. The player admits the clips one at a time
120
+ * and starts the next when the current one reports it can play, so how many are really being
121
+ * fetched at once follows the connection rather than the length of this list - which is why
122
+ * there is no setting for it and no number for a host to choose. A clip the story has already
123
+ * declared is left alone: it is on the stage on the author's own instruction.
124
+ *
125
+ * Never gated on. A clip can take arbitrarily long to buffer, and a loading screen that waited
126
+ * for one would be a download bar wearing a story's clothes. Omit the field to leave the warm
127
+ * set as it is; an empty array releases it.
128
+ */
129
+ readonly video?: readonly Video[];
91
130
  /**
92
131
  * Every url the image cache may keep, which is normally the plan's own entries.
93
132
  *
@@ -26,6 +26,7 @@ import { Vfx, VfxStateRaw } from "../nlcore/elements/vfx";
26
26
  import { Timelines } from "./Tasks";
27
27
  import { Notification, NotificationManager } from "./lib/notification";
28
28
  import type { ImageCacheManager } from "./lib/ImageCacheManager";
29
+ import type { PreloadResource } from "../nlcore/preload/types";
29
30
  import { ActionHistoryManager } from "../../game/nlcore/action/actionHistory";
30
31
  import { GameHistoryManager } from "../../game/nlcore/action/gameHistory";
31
32
  import type { Character } from "../nlcore/elements/character";
@@ -204,6 +205,14 @@ export declare class GameState {
204
205
  preloadingScene: Scene | null;
205
206
  flushDep: number;
206
207
  rollLock: Lock;
208
+ /**
209
+ * Clips held on the stage, hidden, because a plan said they are coming - never part of a save.
210
+ * See {@link VideoWarmQueue}.
211
+ */
212
+ private readonly videoWarmQueue;
213
+ private videoMissingReporter;
214
+ /** Sources already reported as unwarmed, so one clip in a loop does not report every pass. */
215
+ private readonly reportedUnwarmedVideos;
207
216
  readonly notificationMgr: NotificationManager;
208
217
  readonly events: EventDispatcher<GameStateEvents>;
209
218
  readonly logger: Logger;
@@ -222,8 +231,50 @@ export declare class GameState {
222
231
  get deps(): number;
223
232
  addVideo(video: Video): this;
224
233
  removeVideo(video: Video): this;
234
+ /**
235
+ * Whether the STORY has put this clip on the stage - a declaration row, a show, a play.
236
+ *
237
+ * Deliberately not "is it in the document": a clip the warm queue is holding is mounted and
238
+ * buffering but nothing has shown it, and the two answers are wanted in different places. What
239
+ * a save records, what an undo takes back and what a second declaration skips are all this one;
240
+ * whether an action may talk to the element is {@link GameState.isVideoOnStage}.
241
+ */
225
242
  isVideoAdded(video: Video): boolean;
243
+ /** Whether the clip is in the document at all, warmed or shown - so whether it has a state to talk to. */
244
+ isVideoOnStage(video: Video): boolean;
245
+ /**
246
+ * Every clip the stage renders: the story's own, then whatever the warm queue is holding.
247
+ *
248
+ * The two lists are kept apart rather than merged on insert because only the first is state.
249
+ * A save is a list of what the story put on the stage; a warmed clip is a performance detail of
250
+ * this run of it, and writing it into a save would make a story edit able to invalidate a save
251
+ * that has nothing to do with the change (`fromData` throws on a video id it cannot find).
252
+ */
226
253
  getVideos(): Video[];
254
+ /**
255
+ * Hold exactly these clips buffering ahead of the story, in this order of preference.
256
+ *
257
+ * What a {@link PreloadPlan.video} asks for. The queue starts them one at a time and waits for
258
+ * each to become playable before starting the next, so how many are really being fetched at
259
+ * once follows the connection rather than the plan; see {@link VideoWarmQueue}.
260
+ */
261
+ retainWarmVideos(videos: readonly Video[]): this;
262
+ /**
263
+ * Where to report a clip the story played that no plan named, or null for nowhere.
264
+ *
265
+ * The image half of this lives on the cache manager, which is the thing that would have fetched
266
+ * the image; a video is never fetched by the player, so the report has to come from the action
267
+ * that starts it.
268
+ */
269
+ useVideoMissingReporter(reporter: ((resource: PreloadResource) => void) | null): this;
270
+ /**
271
+ * Note that a clip is about to be shown or played with nothing having warmed it.
272
+ *
273
+ * Once per source, and only when no plan named it and nothing has it on the stage: a clip the
274
+ * author declared a few rows earlier is warm by authorship, and reporting that would be
275
+ * reporting the feature working.
276
+ */
277
+ reportUnwarmedVideo(video: Video): this;
227
278
  addVfx(vfx: Vfx): this;
228
279
  removeVfx(vfx: Vfx): this;
229
280
  isVfxAdded(vfx: Vfx): boolean;
@@ -0,0 +1,78 @@
1
+ import type { Video } from "../../nlcore/elements/video";
2
+ export type VideoWarmQueueOptions = {
3
+ /**
4
+ * Whether the story itself has put this clip on the stage.
5
+ *
6
+ * Declared clips are the author's own instruction (`/video`, or a `Video.preload()` call) and
7
+ * are never queued: they are already buffering, they are not subject to the ceiling, and
8
+ * releasing one would take away something the story asked for.
9
+ */
10
+ isDeclared(video: Video): boolean;
11
+ /** Ask the player to re-render the stage, because what is mounted has changed. */
12
+ onChange(): void;
13
+ };
14
+ /**
15
+ * Which clips are held on the stage buffering, and in what order they are started.
16
+ *
17
+ * ## Why a queue and not a number
18
+ *
19
+ * Warming a video is not warming an image. An image is bytes plus a decode, and the player can hold
20
+ * both in a cache under a budget it can measure. A video is a `<video>` element that is in the
21
+ * document: the browser buffers into it, the buffer belongs to that element, and the element that
22
+ * buffered has to be the element that plays or the buffering bought nothing. So "warm this clip"
23
+ * means "mount it hidden, early", and the only questions left are how many and in what order.
24
+ *
25
+ * Both are answered here rather than by whoever wrote the story or configured the game. The host's
26
+ * plan says which clips are coming and in what order (nearest first); this admits them one at a
27
+ * time and starts the next when the current one reports it can play. On a fast local disk that
28
+ * empties the queue almost at once; on a slow connection it keeps a single read running instead of
29
+ * five that finish together and too late. Nobody has to know what number to write, which is the
30
+ * point - it is not a number an author could be expected to get right, and the browser is telling
31
+ * us the answer anyway.
32
+ */
33
+ export declare class VideoWarmQueue {
34
+ private readonly options;
35
+ /** Everything the current plan named, declared clips included - what {@link isPlanned} answers. */
36
+ private planned;
37
+ /** What this queue may mount, in plan order, minus anything the story declared itself. */
38
+ private desired;
39
+ /** Mounted by this queue, hidden and buffering. A subset of {@link desired}, in admission order. */
40
+ private admitted;
41
+ /** The clip whose "can play" the queue is waiting for before it starts another. */
42
+ private pending;
43
+ private pendingTimer;
44
+ constructor(options: VideoWarmQueueOptions);
45
+ /**
46
+ * Take a plan's clips as the complete set this moment wants warm.
47
+ *
48
+ * Total replacement, like every other part of a plan: a clip the new plan does not name is
49
+ * released as soon as this returns, because the scene that wanted it is behind the reader. What
50
+ * the story declared is untouched - releasing that is the story's business, not the plan's.
51
+ */
52
+ retain(next: readonly Video[]): void;
53
+ /**
54
+ * A clip is playable, or has failed trying - either way the queue stops waiting for it.
55
+ *
56
+ * Fed from the player's exposed-state mount, which the video component performs on `canplay`
57
+ * and, for a source that will not load, on `error`. Anything that is not the clip being waited
58
+ * for is ignored, so this can be wired to every state mount there is.
59
+ */
60
+ noteReady(video: unknown): void;
61
+ /**
62
+ * Stop holding a clip the story has taken over.
63
+ *
64
+ * Called when a row declares, shows or plays something this queue had mounted. The element does
65
+ * not move and does not reload - it is the same element, now on the stage for a better reason -
66
+ * so this only stops counting it against the ceiling and frees the queue to start another.
67
+ */
68
+ forget(video: Video): void;
69
+ /** Clips this queue is holding on the stage, hidden. */
70
+ getAdmitted(): readonly Video[];
71
+ /** Whether the current plan named this clip at all, declared or not. */
72
+ isPlanned(video: Video): boolean;
73
+ /** Drop everything, for a player being torn down or reset. */
74
+ clear(): void;
75
+ /** Start the next clip if there is room and nothing is being waited on. Reports whether it did. */
76
+ private admit;
77
+ private clearPending;
78
+ }