narraleaf-react 0.19.2 → 0.20.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.
Files changed (39) hide show
  1. package/dist/built-in.d.ts +1 -1
  2. package/dist/game/nlcore/action/actionTypes.d.ts +13 -0
  3. package/dist/game/nlcore/action/actions/puppetAction.d.ts +59 -0
  4. package/dist/game/nlcore/action/logicAction.d.ts +15 -13
  5. package/dist/game/nlcore/common/elements.d.ts +4 -1
  6. package/dist/game/nlcore/elements/built-in/DevTools.d.ts +53 -0
  7. package/dist/game/nlcore/elements/built-in/screenEffects.d.ts +2 -2
  8. package/dist/game/nlcore/elements/camera.d.ts +3 -0
  9. package/dist/game/nlcore/elements/character/pause.d.ts +1 -1
  10. package/dist/game/nlcore/elements/character.d.ts +3 -0
  11. package/dist/game/nlcore/elements/condition.d.ts +13 -0
  12. package/dist/game/nlcore/elements/control.d.ts +29 -24
  13. package/dist/game/nlcore/elements/displayable/image.d.ts +4 -0
  14. package/dist/game/nlcore/elements/displayable/puppet.d.ts +230 -0
  15. package/dist/game/nlcore/elements/displayable/text.d.ts +4 -0
  16. package/dist/game/nlcore/elements/layer.d.ts +4 -0
  17. package/dist/game/nlcore/elements/persistent/serialize.d.ts +1 -0
  18. package/dist/game/nlcore/elements/persistent/storable.d.ts +11 -0
  19. package/dist/game/nlcore/elements/persistent/type.d.ts +51 -2
  20. package/dist/game/nlcore/elements/persistent.d.ts +12 -3
  21. package/dist/game/nlcore/elements/scene.d.ts +3 -0
  22. package/dist/game/nlcore/elements/script.d.ts +1 -0
  23. package/dist/game/nlcore/elements/sound.d.ts +3 -0
  24. package/dist/game/nlcore/elements/story.d.ts +1 -0
  25. package/dist/game/nlcore/elements/transform/position.d.ts +46 -0
  26. package/dist/game/nlcore/elements/vfx.d.ts +14 -5
  27. package/dist/game/nlcore/elements/video.d.ts +15 -7
  28. package/dist/game/nlcore/game/liveGame.d.ts +47 -0
  29. package/dist/game/nlcore/game/puppet/puppetBackend.d.ts +247 -0
  30. package/dist/game/nlcore/game.d.ts +43 -0
  31. package/dist/game/player/elements/displayable/Puppet.d.ts +1 -0
  32. package/dist/game/player/elements/displayable/type.d.ts +2 -1
  33. package/dist/game/player/elements/image/AspectScaleImage.d.ts +8 -0
  34. package/dist/game/player/elements/image/Image.d.ts +6 -0
  35. package/dist/game/player/gameState.d.ts +5 -0
  36. package/dist/game/player/gameState.type.d.ts +21 -1
  37. package/dist/game/player/type.d.ts +11 -1
  38. package/dist/main.js +52 -43
  39. package/package.json +3 -2
@@ -0,0 +1,247 @@
1
+ /**
2
+ * The seam a host uses to draw something the engine does not understand.
3
+ *
4
+ * A puppet is a box on the stage. The engine owns the outside of that box — where it sits, which
5
+ * layer it belongs to, its transform, its opacity, and its place in a saved game — and hands the
6
+ * inside to a backend the host registered. The engine never looks in: `src`, `options`, command
7
+ * names and payloads are opaque values it stores, serialises and forwards untouched.
8
+ *
9
+ * There is exactly one thing the engine will assume about `src`, and only when it is asked to.
10
+ * {@link PuppetMountContext.resolveSibling} reads `src` as a *location* — it takes the part before
11
+ * the last `/` and resolves a path against it. It still does not know what `src` **means**: not the
12
+ * format, not the contents, not which files it will pull in. A backend whose `src` is not a
13
+ * location simply never calls it, and nothing else in the engine does.
14
+ *
15
+ * That is why this module imports nothing but its own string arithmetic. The contract below is the
16
+ * whole of what a renderer has to satisfy, so a host can take these types on their own, and the
17
+ * engine stays free of any renderer's code.
18
+ */
19
+ /** Logical pixel size of the box a puppet is drawn into. */
20
+ export type PuppetSize = {
21
+ width: number;
22
+ height: number;
23
+ };
24
+ /**
25
+ * What an editor host sees of a puppet's backend instance.
26
+ *
27
+ * - `unmounted` — the element is not on the stage, or its component has not mounted yet.
28
+ * - `missing-backend` — the element is on the stage, but nothing answers to its `backend` name.
29
+ * The box still takes part in transforms, layers and saves; it simply draws nothing.
30
+ * - `loading` — the backend was mounted and its {@link PuppetInstance.ready} has not resolved.
31
+ * - `ready` — the first frame has been drawn.
32
+ * - `error` — mounting, applying state, or loading threw. The stage stays alive regardless.
33
+ */
34
+ export type PuppetStatus = "unmounted" | "missing-backend" | "loading" | "ready" | "error";
35
+ /**
36
+ * The persistent state of a puppet — and the whole of what a saved game carries about one.
37
+ *
38
+ * One-shot actions are deliberately absent: they go through {@link PuppetInstance.command}, so that
39
+ * restoring a puppet is a single {@link PuppetInstance.apply} of a complete state rather than a
40
+ * replay of everything that ever happened to it.
41
+ *
42
+ * None of these names belong to any particular renderer. `motion` / `expression` / `skin` are the
43
+ * three ideas every 2D character renderer has; anything genuinely proprietary belongs in `params`,
44
+ * `slots`, or a command.
45
+ *
46
+ * **What `null` means, field by field.** Every field is a request; `null` is the *absence* of one,
47
+ * and never "leave whatever is there". A state is applied whole, so a field that has been cleared
48
+ * has to visibly clear — otherwise loading a saved game would not reproduce what it recorded, and
49
+ * neither would an undo.
50
+ *
51
+ * A name the model does not have is a {@link PuppetMountContext.warn}, not a throw: throwing out of
52
+ * {@link PuppetInstance.apply} puts the whole element in `"error"` over what is usually one typo.
53
+ */
54
+ export type PuppetState = {
55
+ /**
56
+ * The named action currently requested — usually the loop the model settles into.
57
+ *
58
+ * `null` means nothing is playing: the model rests at whatever it looks like with no motion
59
+ * applied — its setup / rest / bind pose, or the backend's own default where the format has no
60
+ * such thing.
61
+ */
62
+ motion: string | null;
63
+ /**
64
+ * The named expression currently requested.
65
+ *
66
+ * `null` means no expression is applied, and the face is whatever the motion and the skin make
67
+ * it. Clear the expression rather than substituting a model's own named "neutral": if the
68
+ * author wants that one, they can name it, and then `null` and `"neutral"` still differ.
69
+ */
70
+ expression: string | null;
71
+ /**
72
+ * The named skin / costume currently requested.
73
+ *
74
+ * `null` means the model's default skin — the one it shows before anybody picks one.
75
+ */
76
+ skin: string | null;
77
+ /**
78
+ * Free numeric parameters.
79
+ *
80
+ * There is no `null` here: a parameter the map does not mention keeps the model's own default
81
+ * for it, so clearing one means dropping the key rather than nulling it.
82
+ */
83
+ params: Record<string, number>;
84
+ /**
85
+ * Free string slots, for whatever the three names above do not cover.
86
+ *
87
+ * A slot set to `null` is cleared — exactly the same state as a key that is not there at all.
88
+ * The key survives only because `setSlot(id, null)` merges over what is already in the map.
89
+ */
90
+ slots: Record<string, string | null>;
91
+ };
92
+ /**
93
+ * A model describing itself to an editor host.
94
+ *
95
+ * This is what spares a host from parsing model files: it can fill its inspector's dropdowns from
96
+ * the live instance instead. Backends that cannot answer simply do not implement
97
+ * {@link PuppetInstance.describe}, and the host falls back to letting the author type names.
98
+ */
99
+ export type PuppetDescription = {
100
+ motions: string[];
101
+ expressions: string[];
102
+ skins: string[];
103
+ params: {
104
+ id: string;
105
+ min: number;
106
+ max: number;
107
+ default: number;
108
+ }[];
109
+ /** The model's own canvas size, or null when it does not report one. */
110
+ size: PuppetSize | null;
111
+ };
112
+ /** Everything the engine tells a backend when it mounts one. */
113
+ export interface PuppetMountContext {
114
+ /**
115
+ * The resource descriptor the puppet declared, passed through verbatim.
116
+ *
117
+ * Whatever this string means is the backend's business. The engine stores it, saves it and
118
+ * hands it over; the only structure it will ever read out of it is the directory `src` sits in,
119
+ * and only through {@link PuppetMountContext.resolveSibling}.
120
+ */
121
+ readonly src: string;
122
+ /** The author's options for this backend, passed through verbatim. */
123
+ readonly options: Readonly<Record<string, unknown>>;
124
+ /** The logical size of the box, in pixels. Later changes arrive via {@link PuppetInstance.resize}. */
125
+ readonly size: PuppetSize;
126
+ /**
127
+ * Resolve a source to a URL by the same rules images use: a data URI is returned unchanged, and
128
+ * anything else is looked up in the preload cache before being handed back untouched.
129
+ *
130
+ * So this serves whatever the author warmed with `scene.preloadImage()`, and nothing else.
131
+ * **A puppet's own `src` is not registered for preloading** — it is a model manifest, not an
132
+ * image, and the engine has no idea what textures it will pull in. A backend that wants its
133
+ * textures warmed has to have the author warm them by hand; everything not in the cache is a
134
+ * plain URL the backend fetches itself.
135
+ */
136
+ resolveSrc(src: string): string;
137
+ /**
138
+ * Resolve a path that is relative to this puppet's own `src` — a sibling in the same bundle.
139
+ *
140
+ * No real 2D character model is one file. It is a manifest plus an atlas plus texture pages, or
141
+ * a model file plus motions plus physics plus textures, and **which siblings exist is only
142
+ * knowable after parsing the first one**: the manifest names them. So a backend cannot be given
143
+ * a list up front, and asking the author to enumerate one would move parsing to the party least
144
+ * able to do it. It is given the arithmetic instead.
145
+ *
146
+ * The path is resolved against everything in `src` before its last `/`, `.` and `..` are
147
+ * folded, and the result goes through the same rules as {@link PuppetMountContext.resolveSrc} —
148
+ * so a texture the author warmed with `scene.preloadImage()` is served from the preload cache
149
+ * here, and everything else comes back as a plain URL to fetch.
150
+ *
151
+ * ```ts
152
+ * // src: "models/alice/alice.model.json"
153
+ * ctx.resolveSibling("alice.atlas"); // -> "models/alice/alice.atlas"
154
+ * ctx.resolveSibling("textures/page-0.png"); // -> "models/alice/textures/page-0.png"
155
+ * ctx.resolveSibling("../shared/eyes.png"); // -> "models/shared/eyes.png"
156
+ * ctx.resolveSibling("https://cdn/x.png"); // -> unchanged; absolute wins
157
+ * ```
158
+ *
159
+ * A path that is already absolute — a scheme, a leading `/`, a protocol-relative `//host/…`, a
160
+ * data URI — is returned as it stands, because that is what a manifest naming a remote texture
161
+ * means. An empty path resolves to `src` itself. `\` is read as `/`, so a host handing over a
162
+ * native Windows path still gets a usable answer, and the answer always comes back with `/`.
163
+ *
164
+ * The one assumption: that `src` is a location. A backend whose `src` is an opaque key, or a
165
+ * data URI, has no directory to resolve against and gets the path back untouched — such a
166
+ * backend should be reading its own `options` instead, which the engine forwards just as
167
+ * verbatim and where a host can put a map, a base URL, or anything else it likes.
168
+ */
169
+ resolveSibling(relativePath: string): string;
170
+ /** Report a non-fatal problem. The engine logs it and keeps the stage alive; it never throws. */
171
+ warn(message: string, detail?: unknown): void;
172
+ }
173
+ /**
174
+ * One mounted model. The engine holds this handle and nothing else.
175
+ *
176
+ * Every member below that is not marked optional is **required**: implement all of them. The engine
177
+ * nevertheless checks `ready` and `resize` for existence before calling them, because this object
178
+ * crosses a boundary no compiler watches — it is built by the host, and a plain JavaScript host, or
179
+ * a `PuppetBackend` cast into place, can hand over an object that does not satisfy this contract.
180
+ * Those checks are damage control for a broken backend, not permission to leave the two out.
181
+ *
182
+ * **The order the engine calls these in**, because the first step of it surprises everybody:
183
+ *
184
+ * 1. `mount()` returns this object.
185
+ * 2. `apply()` is called at once with the complete initial state — **before `ready()` is called at
186
+ * all**, never mind resolved. The first pose therefore arrives while the model is still loading.
187
+ * 3. `ready()` is called once whatever `apply()` returned has settled, and the element reaches
188
+ * `"ready"` when it resolves.
189
+ * 4. `apply()`, `command()` and `resize()` follow for as long as the element is on stage. Any of
190
+ * them can arrive before `ready()` has resolved.
191
+ * 5. `dispose()` ends it, at any point, loading included. The engine calls nothing on this object
192
+ * afterwards.
193
+ *
194
+ * Step 2 is deliberate and is not going to change. A backend wants the pose it is meant to load
195
+ * into *at* load time; gating it on `ready` would buy a tidier contract at the price of every model
196
+ * visibly snapping from its setup pose to the author's pose a frame after it appears. There are two
197
+ * ways to take it and both are fine: hold the state and re-apply it once the model is up, or return
198
+ * a promise from `apply()` that waits for the load — which also holds `ready()` back until the pose
199
+ * has landed, so the element is not called ready before it looks right.
200
+ */
201
+ export interface PuppetInstance {
202
+ /** Resolves once the model is loaded and its first frame has been drawn. */
203
+ ready(): Promise<void>;
204
+ /**
205
+ * Apply a **complete** state. Called once on mount, then on every change.
206
+ *
207
+ * The first call comes before `ready()` — see the lifecycle above.
208
+ */
209
+ apply(state: Readonly<PuppetState>): void | Promise<void>;
210
+ /**
211
+ * Run a named command. The engine never interprets `name` or `payload`.
212
+ *
213
+ * Returning a promise lets a caller that opted in wait for the command — playing a motion to
214
+ * its end, for instance. Nothing waits by default.
215
+ *
216
+ * `payload` is optional on the authoring side (`puppet.command("wave")`), so it arrives as
217
+ * `undefined` whenever the story omitted it. Treat a missing payload as valid input.
218
+ */
219
+ command(name: string, payload?: unknown): void | Promise<void>;
220
+ /** The box changed size. */
221
+ resize(size: PuppetSize): void;
222
+ /**
223
+ * Optional: describe the model to an editor host. See {@link PuppetDescription}.
224
+ *
225
+ * **Nothing gates this on status.** `DevTools.describePuppet` forwards it the moment it is
226
+ * asked, which is any time between `mount` and `dispose` — before `ready()` has resolved, and
227
+ * more than once. That is the design, not an oversight: an editor opens an inspector when the
228
+ * author clicks, not when a model happens to have finished loading, and a backend knows better
229
+ * than the engine what it can answer and when. So a backend that can only describe a loaded
230
+ * model awaits its own load in here. Rejecting is safe too — the host logs it and falls back to
231
+ * letting the author type names.
232
+ */
233
+ describe?(): Promise<PuppetDescription>;
234
+ dispose(): void;
235
+ }
236
+ /** A drawing backend registered by the host. The engine knows nothing of its internals. */
237
+ export interface PuppetBackend {
238
+ /** The key a puppet's `backend` config refers to. */
239
+ readonly name: string;
240
+ /**
241
+ * Create an instance bound to a host element.
242
+ *
243
+ * The engine owns the box (position / scale / opacity / rotation / layer); the backend owns
244
+ * what is inside it. The container is emptied when the instance is disposed.
245
+ */
246
+ mount(container: HTMLDivElement, ctx: PuppetMountContext): PuppetInstance;
247
+ }
@@ -5,6 +5,7 @@ import { LiveGame } from "./game/liveGame";
5
5
  import { Preference } from "./game/preference";
6
6
  import { GameState } from "../player/gameState";
7
7
  import { Plugins, IGamePluginRegistry } from "./game/plugin/plugin";
8
+ import { PuppetBackend } from "./game/puppet/puppetBackend";
8
9
  import { LayoutRouter } from "../player/lib/PageRouter/router";
9
10
  import { KeyMap } from "./game/keyMap";
10
11
  import type { Storable } from "./elements/persistent/storable";
@@ -69,6 +70,7 @@ export declare class Game {
69
70
  plugins: Plugins;
70
71
  router: LayoutRouter;
71
72
  private readonly lifecycleEvents;
73
+ private readonly puppetBackends;
72
74
  private preloadCompleteContext;
73
75
  private firstSceneReadyContext;
74
76
  /**
@@ -99,6 +101,47 @@ export declare class Game {
99
101
  * @param plugin - The plugin to use
100
102
  */
101
103
  use(plugin: IGamePluginRegistry): this;
104
+ /**
105
+ * Register a backend that draws {@link import("./elements/displayable/puppet").Puppet}
106
+ * elements.
107
+ *
108
+ * The engine ships no renderer and understands none: a puppet is a box it positions, layers,
109
+ * transforms and saves, and the backend registered here draws whatever belongs inside that box.
110
+ * Register before the game mounts — a puppet whose backend is missing keeps its place on the
111
+ * stage and draws nothing, warning once.
112
+ *
113
+ * Registering under a name already taken replaces the previous backend.
114
+ *
115
+ * This lives on `Game` rather than in the config on purpose: the config is deep-merged and can
116
+ * be frozen, and a backend is a live object with methods, not serialisable data. A plugin can
117
+ * call this from its own `register(game)`.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * game.registerPuppetBackend({
122
+ * name: "my-renderer",
123
+ * mount(container, ctx) {
124
+ * const model = MyRenderer.create(container, ctx.resolveSrc(ctx.src), ctx.size);
125
+ * return {
126
+ * ready: () => model.loaded,
127
+ * apply: (state) => model.setPose(state),
128
+ * command: (name, payload) => model.run(name, payload),
129
+ * resize: (size) => model.resize(size.width, size.height),
130
+ * dispose: () => model.destroy(),
131
+ * };
132
+ * },
133
+ * });
134
+ * ```
135
+ */
136
+ registerPuppetBackend(backend: PuppetBackend): this;
137
+ /**
138
+ * The backend registered under the given name, or null.
139
+ */
140
+ getPuppetBackend(name: string): PuppetBackend | null;
141
+ /**
142
+ * The names of every registered puppet backend, in registration order.
143
+ */
144
+ listPuppetBackends(): string[];
102
145
  /**
103
146
  * Listen for the initial preload pass completing.
104
147
  *
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,2 @@
1
- export {};
1
+ export interface EventfulDisplayable {
2
+ }
@@ -1 +1,9 @@
1
+ import React from "react";
2
+ declare const AspectScaleImage: React.ForwardRefExoticComponent<{
3
+ onSizeChanged?: (width: number, height: number) => void;
4
+ onLoad?: () => void;
5
+ autoFit?: boolean;
6
+ src?: string;
7
+ style?: React.CSSProperties;
8
+ } & React.RefAttributes<HTMLImageElement>>;
1
9
  export default AspectScaleImage;
@@ -1,7 +1,13 @@
1
+ import { Image as GameImage } from "../../../nlcore/elements/displayable/image";
1
2
  import React from "react";
3
+ import { GameState } from "../../gameState";
2
4
  export type ImageEvents = {
3
5
  "event:image.onLoad": [];
4
6
  };
5
7
  export declare function stackStyle(darkness: number): React.CSSProperties;
8
+ declare function ImageComponent({ image, state, }: Readonly<{
9
+ image: GameImage;
10
+ state: GameState;
11
+ }>): React.JSX.Element;
6
12
  declare const Image: React.MemoExoticComponent<typeof ImageComponent>;
7
13
  export default Image;
@@ -131,6 +131,11 @@ export type PresentationSnapshot = {
131
131
  scenes: SceneSnapshot[];
132
132
  nvlState: NvlState;
133
133
  };
134
+ export type PlayerStateElementSnapshot = {
135
+ scene: Scene;
136
+ layers: Map<Layer, [LogicAction.DisplayableElements, Record<string, any>][]>;
137
+ };
138
+ export type PlayerAction = CalledActionResult;
134
139
  interface StageUtils {
135
140
  update: () => void;
136
141
  forceUpdate: () => void;
@@ -1 +1,21 @@
1
- export {};
1
+ import { Character } from "../nlcore/elements/character";
2
+ import { Choice } from "../nlcore/elements/menu";
3
+ import { Sentence } from "../nlcore/elements/character/sentence";
4
+ import { Word } from "../nlcore/elements/character/word";
5
+ import { Pausing } from "../nlcore/elements/character/pause";
6
+ import { TextEvent } from "../nlcore/elements/character/textEvent";
7
+ export type Clickable<T, U = undefined> = {
8
+ action: T;
9
+ onClick: U extends undefined ? () => void : (arg0: U) => void;
10
+ };
11
+ export type TextElement = {
12
+ character: Character | null;
13
+ sentence: Sentence;
14
+ id: string;
15
+ words: Word<Pausing | string | TextEvent>[];
16
+ };
17
+ export type MenuElement = {
18
+ prompt: Sentence | null;
19
+ choices: Choice[];
20
+ words: Word<Pausing | string | TextEvent>[] | null;
21
+ };
@@ -12,6 +12,7 @@ import { Scene } from "../nlcore/elements/scene";
12
12
  import { Sound } from "../nlcore/elements/sound";
13
13
  import { Video } from "../nlcore/elements/video";
14
14
  import { Vfx, VfxFadeOptions } from "../nlcore/elements/vfx";
15
+ import { Puppet } from "../nlcore/elements/displayable/puppet";
15
16
  import { Timeline } from "./Tasks";
16
17
  export * from "./elements/type";
17
18
  export type Chosen = Choice & {
@@ -24,7 +25,8 @@ export declare enum ExposedStateType {
24
25
  scene = "narraleaf:scene",
25
26
  video = "narraleaf:video",
26
27
  vfx = "narraleaf:vfx",
27
- camera = "narraleaf:camera"
28
+ camera = "narraleaf:camera",
29
+ puppet = "narraleaf:puppet"
28
30
  }
29
31
  export type ExposedState = {
30
32
  [ExposedStateType.image]: {
@@ -56,6 +58,13 @@ export type ExposedState = {
56
58
  applyTransition: (transition: Transition<any>, onResolve: () => void) => Timeline;
57
59
  updateStyleSync: () => void;
58
60
  };
61
+ [ExposedStateType.puppet]: {
62
+ initDisplayable: (onResolve: () => void) => Timeline;
63
+ applyTransform: (transform: Transform, onResolve: () => void) => Timeline;
64
+ applyTransition: (transition: Transition<any>, onResolve: () => void) => Timeline;
65
+ updateStyleSync: () => void;
66
+ flush: () => void;
67
+ };
59
68
  [ExposedStateType.scene]: {
60
69
  setBackgroundMusic: (music: Sound | null, fade: number) => Promise<void>;
61
70
  };
@@ -81,6 +90,7 @@ export type ExposedKeys = {
81
90
  [ExposedStateType.text]: Text | Displayable<any, any>;
82
91
  [ExposedStateType.layer]: Layer | Displayable<any, any>;
83
92
  [ExposedStateType.camera]: Camera | Displayable<any, any>;
93
+ [ExposedStateType.puppet]: Puppet | Displayable<any, any>;
84
94
  [ExposedStateType.scene]: Scene;
85
95
  [ExposedStateType.video]: Video;
86
96
  [ExposedStateType.vfx]: Vfx;