@vgai/engine 0.5.21 → 0.5.22

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 (87) hide show
  1. package/dist/adapter/manifest-interpreter.d.ts +59 -0
  2. package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
  3. package/dist/adapter/manifest-interpreter.js +50 -0
  4. package/dist/adapter/native-debug-module.d.ts +45 -3
  5. package/dist/adapter/native-debug-module.d.ts.map +1 -1
  6. package/dist/adapter/native-debug-module.js +128 -10
  7. package/dist/adapter/renderer-config.d.ts +1 -1
  8. package/dist/adapter/renderer-config.js +1 -1
  9. package/dist/canvas-react/index.d.ts +5 -12
  10. package/dist/canvas-react/index.d.ts.map +1 -1
  11. package/dist/canvas-react/index.js +5 -12
  12. package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
  13. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  14. package/dist/canvas-react/pixi-react-root-factory.js +274 -36
  15. package/dist/core/countdown-timer.d.ts +118 -0
  16. package/dist/core/countdown-timer.d.ts.map +1 -0
  17. package/dist/core/countdown-timer.js +137 -0
  18. package/dist/core/deferred-commands.d.ts +127 -0
  19. package/dist/core/deferred-commands.d.ts.map +1 -0
  20. package/dist/core/deferred-commands.js +132 -0
  21. package/dist/core/sim-clock.d.ts +14 -2
  22. package/dist/core/sim-clock.d.ts.map +1 -1
  23. package/dist/core/sim-clock.js +14 -2
  24. package/dist/dev/instruments.d.ts +38 -11
  25. package/dist/dev/instruments.d.ts.map +1 -1
  26. package/dist/dev/instruments.js +81 -11
  27. package/dist/input/input-manager.d.ts +15 -0
  28. package/dist/input/input-manager.d.ts.map +1 -1
  29. package/dist/input/input-manager.js +15 -0
  30. package/dist/render/directional-shadow-fit.d.ts +88 -0
  31. package/dist/render/directional-shadow-fit.d.ts.map +1 -0
  32. package/dist/render/directional-shadow-fit.js +133 -0
  33. package/dist/render/environment-capture.d.ts +68 -0
  34. package/dist/render/environment-capture.d.ts.map +1 -0
  35. package/dist/render/environment-capture.js +90 -0
  36. package/dist/render/ibl-override-material.d.ts +97 -0
  37. package/dist/render/ibl-override-material.d.ts.map +1 -0
  38. package/dist/render/ibl-override-material.js +114 -0
  39. package/dist/runtime/game-input-seams.d.ts +51 -0
  40. package/dist/runtime/game-input-seams.d.ts.map +1 -0
  41. package/dist/runtime/game-input-seams.js +86 -0
  42. package/dist/runtime/game.d.ts +15 -0
  43. package/dist/runtime/game.d.ts.map +1 -1
  44. package/dist/runtime/game.js +40 -8
  45. package/dist/runtime/mount-game.d.ts.map +1 -1
  46. package/dist/runtime/mount-game.js +18 -2
  47. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  48. package/dist/runtime/mount-manifest.js +4 -2
  49. package/dist/runtime/types.d.ts +1 -1
  50. package/dist/world3d-react/index.d.ts +15 -25
  51. package/dist/world3d-react/index.d.ts.map +1 -1
  52. package/dist/world3d-react/index.js +15 -25
  53. package/dist/world3d-react/r3f-root-factory.d.ts +19 -25
  54. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  55. package/dist/world3d-react/r3f-root-factory.js +396 -50
  56. package/dist/world3d-react/rapier-physics-bridge.d.ts +24 -12
  57. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  58. package/dist/world3d-react/rapier-physics-bridge.js +82 -49
  59. package/package.json +1 -1
  60. package/schemas/engine-capabilities.json +3 -3
  61. package/src/adapter/manifest-interpreter.ts +64 -0
  62. package/src/adapter/native-debug-module.ts +212 -15
  63. package/src/adapter/renderer-config.ts +1 -1
  64. package/src/canvas-react/index.ts +5 -21
  65. package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
  66. package/src/core/countdown-timer.ts +188 -0
  67. package/src/core/deferred-commands.ts +174 -0
  68. package/src/core/sim-clock.ts +14 -2
  69. package/src/dev/instruments.ts +108 -11
  70. package/src/input/input-manager.ts +15 -0
  71. package/src/render/directional-shadow-fit.ts +156 -0
  72. package/src/render/environment-capture.ts +102 -0
  73. package/src/render/ibl-override-material.ts +170 -0
  74. package/src/runtime/game-input-seams.ts +108 -0
  75. package/src/runtime/game.ts +65 -7
  76. package/src/runtime/mount-game.ts +23 -1
  77. package/src/runtime/mount-manifest.ts +4 -2
  78. package/src/runtime/types.ts +1 -1
  79. package/src/world3d-react/index.ts +15 -31
  80. package/src/world3d-react/r3f-root-factory.tsx +444 -53
  81. package/src/world3d-react/rapier-physics-bridge.tsx +88 -49
  82. package/src/canvas-react/engine-bridge.ts +0 -59
  83. package/src/canvas-react/pixi-react-adapter.tsx +0 -356
  84. package/src/canvas-react/world-context.ts +0 -253
  85. package/src/world3d-react/engine-bridge.ts +0 -86
  86. package/src/world3d-react/r3f-adapter.tsx +0 -717
  87. package/src/world3d-react/world-context.ts +0 -358
@@ -1,253 +0,0 @@
1
- /**
2
- * `createCanvasRootContext` — the ENGINE runtime a canvas world owns, split out
3
- * of `./pixi-react-adapter.tsx` so it stays react-free and headlessly
4
- * unit-testable (`test/canvas-react-context-parity.test.tsx`).
5
- *
6
- * This is the canvas surface's answer to the same question
7
- * `world3d-react/world-context.ts` answers for three: what does a component
8
- * inside the world tree reach when it asks the engine for something?
9
- *
10
- * 1. **`ctx.debug` / `ctx.random` / `ctx.game` / `ctx.roots` are wired** from
11
- * the ONE game-scoped registry (`getDebugRegistry(host.game)`), so a
12
- * component's effect can `ctx.debug.registerStateProvider(...)` and be
13
- * visible to `vgai eval`/the editor's debug panels.
14
- * 2. **`ctx.input` is the game-owned `InputManager` when a Game hosts the
15
- * root** (`host.game.input` — the
16
- * same instance every sibling root shares), and this module registers the
17
- * per-world debug-registry seams (`setVirtualInputTarget`/
18
- * `setInputActionsSource`/`setInputTraceSource`) that make `game.input.*`
19
- * (the bot-input doctrine) resolve instead of throwing
20
- * `DEBUG_INPUT_UNAVAILABLE`. The project's input map loads through
21
- * `Game.loadInputMap` (the game-owned, load-once path every root shares).
22
- * A missing/unparseable map degrades LOUDLY without failing the mount — a
23
- * canvas world with no declared actions is legal. A bare/foreign mount
24
- * receives `null`, visibly in the type, rather than a throwing getter.
25
- * 3. **Absent subsystems are absent from the type.** A canvas world builds no
26
- * THREE scene/camera, Rapier world, composer, first-party audio/particles/
27
- * debug-draw, or Three-flavoured `AssetCache`. Its explicit
28
- * {@link CanvasGameContext} cannot name those fields, so misuse fails at
29
- * the call site during typechecking instead of from a throwing getter at
30
- * runtime.
31
- *
32
- * The Pixi handles themselves are deliberately NOT re-exposed here: a component
33
- * reaches the live `Application` (and therefore `app.stage`, `app.renderer`,
34
- * `app.ticker`) through `@pixi/react`'s own `useApplication()`, and its assets
35
- * through Pixi's own `Assets`. Re-publishing them on `ctx` would be a wrapper
36
- * standing between the author and the library.
37
- */
38
-
39
- import type { Application } from 'pixi.js';
40
- import type { CanvasHostContext } from '../adapter';
41
- import type { SystemAdapters } from '../adapter/system-adapter';
42
- import {
43
- createSeededRandom,
44
- DEFAULT_SEEDED_RANDOM_SEED,
45
- getSeededRandom,
46
- } from '../core/seeded-random';
47
- import type { SimClock } from '../core/sim-clock';
48
- import { createSimClock, getSimClock, type SimClockInternal } from '../core/sim-clock';
49
- import { createSystemRunner } from '../core/system-runner';
50
- import {
51
- createDebugRegistry,
52
- type DebugRegistry,
53
- getDebugRegistry,
54
- } from '../runtime/debug-registry';
55
- import type { ComponentContextBase } from '../runtime/types';
56
-
57
- /** The conventional input-map location every scaffolded project ships. Shared
58
- * verbatim with the three lane — a project has ONE input map, whatever surface
59
- * reads it. */
60
- export const DEFAULT_INPUT_MAP_PATH = '/inputmaps/default.inputmap.json';
61
-
62
- export interface CanvasRootContextOptions {
63
- /** World id — provenance for every debug-registry registration (matches the
64
- * manifest root id). */
65
- readonly id: string;
66
- /**
67
- * Input map to load through `Game.loadInputMap` (game-owned, load-once —
68
- * several roots asking for the SAME path share one load; competing paths
69
- * throw there). Defaults to {@link DEFAULT_INPUT_MAP_PATH}. Pass `null` to
70
- * skip loading (a world with no actions). Ignored when no `host.game` is
71
- * present.
72
- */
73
- readonly inputMapPath?: string | null | undefined;
74
- }
75
-
76
- /** The engine services a Canvas component genuinely receives. Pixi's own
77
- * `useApplication()` and `Assets` remain the doors to rendering and assets.
78
- * `disposeAfter` is excluded because it accepts a `THREE.Object3D`; Canvas
79
- * code schedules `displayObject.destroy()` with `clock.after()` instead. */
80
- export type CanvasGameContext = ComponentContextBase & {
81
- readonly clock: Omit<SimClock, 'disposeAfter'>;
82
- };
83
-
84
- /** What {@link createCanvasRootContext} returns — the engine runtime one canvas
85
- * world owns, plus the wiring hooks the adapter's `mount()` drives. */
86
- export interface CanvasRootRuntime {
87
- /** The explicit Canvas context this world's components read through
88
- * `useGameContext`. See {@link CanvasGameContext}. */
89
- readonly ctx: CanvasGameContext;
90
- /** The world-local phase runner `mount().update(dt)` ticks. */
91
- readonly systems: ReturnType<typeof createSystemRunner>;
92
- /** The mounted world's `SystemAdapters` bag (`mounted.systems`) —
93
- * `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded. */
94
- readonly systemAdapters: SystemAdapters;
95
- /** Resolves once the input map load settles (immediately when skipped).
96
- * Never rejects — a failed load reports loudly and resolves. */
97
- readonly inputMapReady: Promise<void>;
98
- /** Backfill the live `Application` once `@pixi/react` has initialized it —
99
- * before any tick can run. Read back via {@link CanvasRootRuntime.app}. */
100
- setApplication(app: Application): void;
101
- /** The live `Application`, or `null` before the first commit resolves it. */
102
- readonly app: Application | null;
103
- /** Strip this world's debug registrations (scoped, so sibling roots and
104
- * react-door registrations are untouched when a Game hosts this world) and
105
- * dispose a clock this mount created. */
106
- dispose(): void;
107
- }
108
-
109
- /**
110
- * Build the engine runtime for one canvas world from a `CanvasHostContext`.
111
- * React-free on purpose: everything here is plain engine wiring the adapter
112
- * threads into the `@pixi/react` tree through `EngineBridge`.
113
- */
114
- export function createCanvasRootContext(
115
- host: CanvasHostContext,
116
- options: CanvasRootContextOptions,
117
- ): CanvasRootRuntime {
118
- const { id, inputMapPath = DEFAULT_INPUT_MAP_PATH } = options;
119
-
120
- // Same observer threading as the three lane — this world's phases show up
121
- // under the shared game profiler when hosted.
122
- const systems = createSystemRunner(host.game?.profiler.systemObserver, 'canvas');
123
-
124
- // The mounted world's adapter surface (`mounted.systems`) — declared before
125
- // ctx so `ctx.registerSystemAdapter` can close over it (a game-owned
126
- // capability like networking registers here from component code).
127
- const systemAdapters: SystemAdapters = {};
128
-
129
- let app: Application | null = null;
130
-
131
- const ctxRaw: Record<string, unknown> = {
132
- systems,
133
- input: host.game?.input ?? null,
134
- registerSystemAdapter: (kind: keyof SystemAdapters, adapter: SystemAdapters[typeof kind]) => {
135
- // biome-ignore lint/suspicious/noExplicitAny: same per-key record write the three lane performs; correct by construction
136
- (systemAdapters as any)[kind] = adapter;
137
- host.game?.notifySystemAdaptersChanged();
138
- },
139
- };
140
- const ctx = ctxRaw as unknown as CanvasGameContext;
141
- if (host.game) {
142
- ctx.game = host.game;
143
- ctx.roots = host.game.roots;
144
- ctx.playtest = host.game.playtest ?? null;
145
- }
146
-
147
- // --- ctx.debug — the ONE registry per Game root, shared across every world
148
- // mounted onto it; a bare mount with no Game shell gets a private,
149
- // mount-local registry instead. ---
150
- const debugRegistry: DebugRegistry =
151
- (host.game ? getDebugRegistry(host.game) : null) ??
152
- createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
153
- systemAdapters.debug = debugRegistry.adapter;
154
- ctx.debug = debugRegistry.forRoot(id);
155
- ctx.random =
156
- (host.game ? getSeededRandom(host.game) : null) ??
157
- createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
158
-
159
- // --- ctx.clock — a REAL value, never a throwing getter: every world has a
160
- // sim clock. A bare mount with no Game shell gets a private, mount-local one.
161
- // `SimClock.disposeAfter` takes a THREE.Object3D, so the mount-local
162
- // fallback's disposer is a loud refusal rather than a fabricated no-op: a
163
- // canvas world's debris is a `PIXI.Container`, destroyed with its own
164
- // `container.destroy()`.
165
- const gameClock: SimClockInternal | null = host.game ? getSimClock(host.game) : null;
166
- const clock: SimClockInternal =
167
- gameClock ??
168
- createSimClock({
169
- dispose: () => {
170
- throw new Error(
171
- `canvas world "${id}": clock.disposeAfter() disposes a THREE.Object3D and has no ` +
172
- 'meaning in a canvas world — destroy a display object with its own ' +
173
- '`container.destroy({ children: true })`, scheduled with `clock.after(seconds, ...)`.',
174
- );
175
- },
176
- });
177
- ctxRaw['clock'] = clock;
178
-
179
- // --- ctx.input + the debug-registry input seams — the game-owned
180
- // InputManager, polled by the game-scoped runner each frame; this world
181
- // registers the per-world seams so `game.input.*` (virtual input) and the
182
- // built-in `input.actions`/`input.trace` providers resolve to it. ---
183
- let inputMapReady: Promise<void> = Promise.resolve();
184
- if (host.game) {
185
- const game = host.game;
186
- const input = game.input;
187
- debugRegistry.setInputActionsSource(id, () =>
188
- input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) })),
189
- );
190
- debugRegistry.setInputTraceSource(id, () => {
191
- const raw = input.getInputTrace();
192
- return {
193
- version: raw.version,
194
- seed: getSeededRandom(game)?.seed ?? null,
195
- fixedDt: game.loop.fixedDt,
196
- ticks: raw.ticks,
197
- };
198
- });
199
- debugRegistry.setVirtualInputTarget(id, {
200
- setVirtualAction: (action, value) => input.setVirtualAction(action, value),
201
- tapVirtualAction: (action) => input.tapVirtualAction(action),
202
- clearVirtualActions: () => input.clearVirtualActions(),
203
- scheduleActionAtTick: (tick, action, value) =>
204
- input.scheduleActionAtTick(tick, action, value),
205
- startInputRecording: () => input.startInputRecording(),
206
- stopInputRecording: () => input.stopInputRecording(),
207
- isInputRecording: () => input.isInputRecording(),
208
- injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
209
- injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
210
- injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
211
- injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
212
- });
213
- if (!host.headless && inputMapPath !== null) {
214
- // Load-once through the game-owned path (competing paths across roots
215
- // throw THERE, loudly). A FAILED load must not fail this mount: a canvas
216
- // world with no declared actions is legal. It degrades loudly instead.
217
- inputMapReady = game.loadInputMap(inputMapPath).catch((err: unknown) => {
218
- // biome-ignore lint/suspicious/noConsole: deliberate loud degrade — the documented alternative to failing the mount
219
- console.error(
220
- `canvas world "${id}": failed to load input map "${inputMapPath}" — declared input ` +
221
- 'actions and `game.input.*` (bot/virtual input) will not work until a valid map ' +
222
- 'loads. Ship one at the conventional path or pass `inputMapPath` to ' +
223
- `createPixiReactAdapter (null to opt out). Cause: ${err instanceof Error ? err.message : String(err)}`,
224
- );
225
- });
226
- }
227
- }
228
-
229
- return {
230
- ctx,
231
- systems,
232
- systemAdapters,
233
- inputMapReady,
234
- get app(): Application | null {
235
- return app;
236
- },
237
- setApplication(next): void {
238
- app = next;
239
- },
240
- dispose(): void {
241
- // Same ownership rule the three lane follows: dispose ONLY a clock this
242
- // mount created. The game-scoped one belongs to `GameInternal.dispose()`
243
- // — killing it here would freeze sim time and cancel every SIBLING
244
- // world's timers.
245
- if (!gameClock) clock.dispose();
246
- app = null;
247
- // Scoped strip when a Game hosts this world (sibling roots stay live);
248
- // a bare standalone mount owns its private registry and clears it all.
249
- if (host.game) debugRegistry.strip(id);
250
- else debugRegistry.strip();
251
- },
252
- };
253
- }
@@ -1,86 +0,0 @@
1
- /**
2
- * `@engine/world3d-react` — the React context that carries this R3F world's
3
- * engine runtime into its fiber tree: the world's `R3FGameContext`, read
4
- * with {@link useGameContext}.
5
- *
6
- * Upstreamed from `examples/r3f-first-party/src/engine-bridge.ts` (R4).
7
- * The bridge value
8
- * is built by `createR3FAdapter`'s `mount()` (./r3f-adapter.tsx): a real
9
- * `createSystemRunner` whose phases the adapter's `update(dt)` runs under the
10
- * HOST loop — so registered systems tick in engine phase order, pause included
11
- * (no update while the world is frozen).
12
- *
13
- * Behavior in an R3F world is components and hooks: `useGameContext` is how a
14
- * component reaches engine services.
15
- */
16
-
17
- import { createContext, useContext } from 'react';
18
- import { useOptionalGame } from '../react/world-state';
19
- import type { R3FGameContext } from './world-context';
20
-
21
- export interface EngineBridgeValue {
22
- /** This root's context — `input`, `debug`, `random`, `game`, `roots`, … .
23
- * Present so ordinary components can reach engine services with a HOOK
24
- * (see {@link useGameContext}). */
25
- readonly ctx?: R3FGameContext | undefined;
26
- }
27
-
28
- export const EngineBridge = createContext<EngineBridgeValue | null>(null);
29
-
30
- /**
31
- * This R3F root's context, for use inside any component in the tree — an
32
- * {@link R3FGameContext}, which is exactly the subsystems an R3F world builds.
33
- * The ones it does not build are absent from the type, so reaching for one is
34
- * a compile error at the call site; that type's doc comment says what to reach
35
- * for instead.
36
- *
37
- * An ordinary component does what a model would write:
38
- *
39
- * ```tsx
40
- * function Player({ speed = 4.5 }) {
41
- * const { input } = useGameContext();
42
- * const ref = useRef<THREE.Group>(null);
43
- * useFrame((_, dt) => { if (input?.isPressed('move_forward')) … });
44
- * return <group ref={ref}><PlayerCharacter /></group>;
45
- * }
46
- * ```
47
- *
48
- * Throws when called outside an R3F root mounted by `createR3FAdapter` —
49
- * absent context is a wiring bug, and a silent `null` would surface as a
50
- * mystery `undefined` deep in a frame callback.
51
- */
52
- export function useGameContext(): R3FGameContext {
53
- const bridge = useContext(EngineBridge);
54
- // Read unconditionally (rules of hooks) — used only to shape the throw below.
55
- const hostedByGame = useOptionalGame() !== null;
56
- if (!bridge?.ctx) {
57
- // NAME THE REAL CAUSE. This throw used to say "check that the component is
58
- // rendered inside your world entry" — advice a scaffolded game has already
59
- // followed, because the shape `vgai create` produces (`export default
60
- // function World()`) mounts as a COMPONENT ROOT and receives no vgai
61
- // runtime context at all: `r3f-root-factory.tsx` passes
62
- // `engineRuntime: false` deliberately, so this hook can never resolve
63
- // there however the component is nested. A world whose first render calls
64
- // it dies with "fiber crashed before its first commit" and a fix
65
- // instruction that cannot work. The `Game` in scope tells the two cases
66
- // apart: present means a vgai-mounted world, absent means no root at all.
67
- throw new Error(
68
- hostedByGame
69
- ? 'useGameContext(): this world mounts as a COMPONENT ROOT (the default-exported ' +
70
- 'shape `vgai create` scaffolds) and receives no vgai runtime context — by design, ' +
71
- 'so nesting cannot fix it. Game-scoped services are on the Game handle instead: ' +
72
- '`useGame().input` (from @vgai/engine/react/world-state) is the game-owned ' +
73
- "InputManager the host already loaded this project's input map into and the same " +
74
- 'one `game.input.hold/tap` drives; `useDebugProvider`/`useDebugCommand`/' +
75
- '`useWorldState` are the state/command doors. If this world genuinely needs the ' +
76
- 'imperative runtime (`ctx.systems`, adapter options like `inputMapPath`), export ' +
77
- '`adapter = createR3FAdapter({ id, content: <World/> })` from the entry instead of ' +
78
- 'default-exporting the component.'
79
- : 'useGameContext(): no engine root context and no Game in scope — this component is ' +
80
- 'rendering outside any mounted vgai root (a standalone <Canvas>, a story, or a ' +
81
- "bare test render). Mount it through your project's three root, or read services " +
82
- 'the component root way (native hooks and project-owned state).',
83
- );
84
- }
85
- return bridge.ctx;
86
- }