@vgai/engine 0.4.1 → 0.5.0-canary.20260719.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 (103) hide show
  1. package/README.md +48 -15
  2. package/package.json +11 -25
  3. package/schemas/engine-capabilities.json +10 -10
  4. package/schemas/entity2d.schema.json +468 -0
  5. package/schemas/mat.schema.json +2 -33
  6. package/schemas/prefab.schema.json +16 -172
  7. package/schemas/scn2d.schema.json +42 -23
  8. package/schemas/vgai-game.schema.json +34 -0
  9. package/schemas/vscn.schema.json +16 -172
  10. package/src/adapter/authoring.ts +152 -2
  11. package/src/adapter/colyseus-networking-adapter.ts +35 -1
  12. package/src/adapter/first-party-systems.ts +7 -1
  13. package/src/adapter/game-adapter.ts +13 -0
  14. package/src/adapter/index.ts +25 -0
  15. package/src/adapter/rapier-physics-adapter.ts +55 -2
  16. package/src/adapter/system-adapter.ts +249 -2
  17. package/src/adapter/vgai-scene-game-adapter.ts +149 -25
  18. package/src/ai/navigation.ts +28 -0
  19. package/src/animation/clip-map.ts +1 -8
  20. package/src/animation/theatre-director.ts +50 -0
  21. package/src/animation/xstate-animation-binding.ts +6 -0
  22. package/src/audio/audio-introspection.ts +290 -0
  23. package/src/audio/tone-context.ts +46 -0
  24. package/src/dev/chrome-trace.ts +153 -0
  25. package/src/dev/performance-profiler.ts +93 -6
  26. package/src/dev/render-debug-adapter.ts +199 -0
  27. package/src/dev/render-memory.ts +243 -0
  28. package/src/dev/webgl-frame-capture.ts +424 -0
  29. package/src/ecs/component-manager.ts +43 -10
  30. package/src/ecs/game-component.ts +39 -10
  31. package/src/input/input-manager.ts +24 -19
  32. package/src/input/input-types.ts +1 -1
  33. package/src/loader.ts +7 -0
  34. package/src/manifest/load.ts +14 -0
  35. package/src/manifest/schema.ts +65 -0
  36. package/src/react/game-state.tsx +1 -1
  37. package/src/render/render-batch-system.ts +26 -12
  38. package/src/render/spark-renderer-lifecycle.ts +64 -0
  39. package/src/runtime/create-runtime.ts +33 -5
  40. package/src/runtime/debug-bridge.ts +5 -5
  41. package/src/runtime/game.ts +102 -20
  42. package/src/runtime/mount-manifest.ts +1 -1
  43. package/src/runtime/render-control.ts +121 -0
  44. package/src/runtime/types.ts +1 -1
  45. package/src/scene/asset-loaders.ts +77 -3
  46. package/src/scene/instance-mesh.ts +25 -0
  47. package/src/scene/material-factory.ts +4 -15
  48. package/src/scene/mesh-shadow.ts +18 -0
  49. package/src/scene/particles-factory.ts +59 -0
  50. package/src/scene/scene-loader.ts +41 -16
  51. package/src/scene/schema/instances.ts +1 -2
  52. package/src/scene/schema/material.ts +83 -94
  53. package/src/scene/schema/mesh.ts +76 -90
  54. package/src/scene/schema/scene-file.ts +1 -2
  55. package/src/scene/user-data.ts +30 -14
  56. package/src/setup/setup-renderer.ts +6 -1
  57. package/src/world2d/asset-paths2d.ts +44 -0
  58. package/src/world2d/collision-2d.ts +7 -14
  59. package/src/world2d/entity2d-asset.ts +22 -0
  60. package/src/world2d/index.ts +27 -2
  61. package/src/world2d/physics2d-transform.ts +173 -0
  62. package/src/world2d/physics2d-units.ts +10 -0
  63. package/src/world2d/pixi-game-adapter.ts +148 -36
  64. package/src/world2d/scene2d-identity.ts +49 -0
  65. package/src/world2d/scene2d-loader.ts +243 -119
  66. package/src/world2d/schema/entity2d.ts +51 -33
  67. package/src/world2d/schema/physics2d.ts +14 -3
  68. package/src/world2d/schema/sprite.ts +32 -4
  69. package/src/world2d/schema/tilemap.ts +26 -9
  70. package/src/world2d/transform-writer-2d.ts +29 -11
  71. package/src/world2d/types.ts +21 -8
  72. package/src/world3d-react/behavior.tsx +138 -0
  73. package/src/world3d-react/engine-bridge.ts +48 -0
  74. package/src/world3d-react/index.ts +44 -0
  75. package/src/world3d-react/r3f-adapter.tsx +303 -0
  76. package/src/world3d-react/world-context.ts +294 -0
  77. package/vendor/realism-effects/LICENSE.md +21 -0
  78. package/vendor/realism-effects/UPSTREAM.md +19 -0
  79. package/vendor/realism-effects/dist/index.cjs +3447 -0
  80. package/vendor/realism-effects/dist/index.d.ts +59 -0
  81. package/vendor/realism-effects/dist/index.js +3434 -0
  82. package/vendor/realism-effects/package.json +23 -0
  83. package/src/character/cloth-sim.ts +0 -533
  84. package/src/character/spring-chain.ts +0 -307
  85. package/src/humanoid/body.ts +0 -663
  86. package/src/humanoid/clips.ts +0 -149
  87. package/src/humanoid/compose.ts +0 -209
  88. package/src/humanoid/generate.ts +0 -189
  89. package/src/humanoid/index.ts +0 -36
  90. package/src/humanoid/schema.ts +0 -108
  91. package/src/humanoid/skeleton.ts +0 -345
  92. package/src/react/humanoid-bake.document.tsx +0 -337
  93. package/src/scene/geometries/index.ts +0 -7
  94. package/src/scene/geometries/terrain.ts +0 -42
  95. package/src/scene/geometry-registry.ts +0 -42
  96. package/src/scene/instance-registry.ts +0 -84
  97. package/src/scene/instancers/grid.ts +0 -38
  98. package/src/scene/instancers/index.ts +0 -7
  99. package/src/scene/material-registry.ts +0 -73
  100. package/src/scene/materials/index.ts +0 -7
  101. package/src/scene/materials/water.ts +0 -56
  102. package/src/world2d/components-2d.ts +0 -86
  103. package/tools/humanoid-bake.tool.ts +0 -274
@@ -19,8 +19,16 @@ export const SpriteSchema = z
19
19
  ),
20
20
  frame: z
21
21
  .string()
22
+ .min(1)
22
23
  .optional()
23
24
  .describe('Spritesheet frame/sub-texture name when `texture` is a spritesheet atlas'),
25
+ animation: z
26
+ .string()
27
+ .min(1)
28
+ .optional()
29
+ .describe(
30
+ 'Named spritesheet animation from `texture` (PIXI.Spritesheet.animations); mutually exclusive with `frame`',
31
+ ),
24
32
  anchor: Vec2Schema.optional().describe('Normalized anchor [x, y] (0..1). Default [0.5, 0.5]'),
25
33
  tint: z
26
34
  .string()
@@ -47,12 +55,32 @@ export const SpriteSchema = z
47
55
  .enum(['none', 'blur', 'desaturate'])
48
56
  .optional()
49
57
  .describe('Apply a PixiJS filter (BlurFilter / ColorMatrix desaturate). Default none'),
50
- animationFrames: z
58
+ fps: z
51
59
  .number()
52
- .int()
60
+ .positive()
53
61
  .optional()
54
- .describe('If set, build an AnimatedSprite cycling N generated tinted frames'),
55
- fps: z.number().optional().describe('AnimatedSprite playback speed in frames/sec. Default 8'),
62
+ .describe('AnimatedSprite playback speed in frames/sec. Default 8'),
63
+ loop: z
64
+ .boolean()
65
+ .optional()
66
+ .describe('Whether the named spritesheet animation loops. Default true'),
67
+ })
68
+ .strict()
69
+ .superRefine((sprite, ctx) => {
70
+ if (sprite.animation && !sprite.texture) {
71
+ ctx.addIssue({
72
+ code: z.ZodIssueCode.custom,
73
+ message: '`animation` requires a spritesheet `texture`',
74
+ path: ['animation'],
75
+ });
76
+ }
77
+ if (sprite.animation && sprite.frame) {
78
+ ctx.addIssue({
79
+ code: z.ZodIssueCode.custom,
80
+ message: '`animation` and `frame` are mutually exclusive',
81
+ path: ['animation'],
82
+ });
83
+ }
56
84
  })
57
85
  .describe('2D sprite appearance (world2d). Instantiated as a PIXI.Sprite or AnimatedSprite');
58
86
 
@@ -2,20 +2,37 @@ import { z } from 'zod';
2
2
 
3
3
  /**
4
4
  * `tilemap` authored section — engine-owned 2D tilemap data (NOT a CompositeTilemap),
5
- * instantiated to a `@pixi/tilemap` CompositeTilemap by the 2D spawn path. Asset-light:
6
- * a `palette` of hex colors generates one tile texture each, and `tiles` is a row-major
7
- * grid of palette indices. A real tileset texture/Tiled `.tmj` import is the natural
8
- * extension (per the spec's optional Tiled note).
5
+ * instantiated to a `@pixi/tilemap` CompositeTilemap by the 2D spawn path. `texture`
6
+ * points at a native Pixi spritesheet JSON asset; `frames` gives its ordered tile
7
+ * palette and `tiles` stores indices into that palette.
9
8
  */
10
9
  export const TilemapSchema = z
11
10
  .object({
12
- tileSize: z.number().describe('Tile width/height in pixels'),
13
- palette: z
14
- .array(z.string())
15
- .describe('Hex colors; one generated tile texture per entry (index → color)'),
11
+ frames: z
12
+ .array(z.string().min(1))
13
+ .min(1)
14
+ .describe('Ordered Pixi spritesheet frame names used as tile indices'),
15
+ texture: z.string().min(1).describe('URL or project path to a Pixi spritesheet JSON asset'),
16
+ tileSize: z.number().positive().describe('Positive square tile width/height in pixels'),
16
17
  tiles: z
17
18
  .array(z.array(z.number().int()))
18
- .describe('Row-major grid of palette indices (each row is an array of tile indices)'),
19
+ .describe(
20
+ 'Row-major grid of frame indices; -1 is an empty cell (each row is an array of tile indices)',
21
+ ),
22
+ })
23
+ .strict()
24
+ .superRefine((tilemap, ctx) => {
25
+ for (let row = 0; row < tilemap.tiles.length; row++) {
26
+ for (let column = 0; column < tilemap.tiles[row]!.length; column++) {
27
+ const index = tilemap.tiles[row]![column]!;
28
+ if (index >= -1 && index < tilemap.frames.length) continue;
29
+ ctx.addIssue({
30
+ code: z.ZodIssueCode.custom,
31
+ message: `Tile index must be -1 (empty) or a frame index from 0 to ${tilemap.frames.length - 1}`,
32
+ path: ['tiles', row, column],
33
+ });
34
+ }
35
+ }
19
36
  })
20
37
  .describe('2D tilemap (world2d). Instantiated as a @pixi/tilemap CompositeTilemap');
21
38
 
@@ -1,24 +1,42 @@
1
+ import type { Container } from 'pixi.js';
1
2
  import type { Physics2DRegistry } from './physics2d-registry';
3
+ import { relativeTransform2D } from './physics2d-transform';
4
+
5
+ function normalizeAngle(angle: number): number {
6
+ return Math.atan2(Math.sin(angle), Math.cos(angle));
7
+ }
2
8
 
3
9
  /**
4
10
  * Build the single postPhysics transform writer for world2d — the ~5-line 2D
5
11
  * analog of `createTransformWriter`.
6
12
  *
7
- * Rapier 2D reports `translation()` as `{x, y}` and `rotation()` as a SCALAR angle
8
- * (radians), so the writeback is simply `display.position.set(x, y)` +
9
- * `display.rotation = angle`. Scale is left untouched (Rapier carries no scale).
10
- *
11
- * Bodies are simulated in world2d pixel space directly (the surface uses a
12
- * y-down, pixel-unit world), so no unit conversion is needed; parented entities
13
- * are kept flat under the world container by the loader, mirroring the 3D writer's
14
- * scene-root fast path.
13
+ * Rapier reports world-space poses. A display nested below the scene root needs
14
+ * that pose converted through its parent's inverse transform before assignment;
15
+ * writing the world pose as local coordinates makes parented visuals drift away
16
+ * from their colliders. Scale is left untouched (Rapier carries no scale).
15
17
  */
16
- export function createTransformWriter2D(physics: Physics2DRegistry) {
18
+ export function createTransformWriter2D(physics: Physics2DRegistry, sceneRoot: Container) {
17
19
  return function writeTransforms2D(): void {
18
20
  for (const [display, { body }] of physics.entries()) {
19
21
  const t = body.translation();
20
- display.position.set(t.x, t.y);
21
- display.rotation = body.rotation();
22
+ const rotation = body.rotation();
23
+ const parent = display.parent;
24
+ const ownReflection = display.scale.x < 0 ? Math.PI : 0;
25
+ if (parent && parent !== sceneRoot) {
26
+ const parentMatrix = relativeTransform2D(parent, sceneRoot);
27
+ const localOrigin = parentMatrix.applyInverse({ x: t.x, y: t.y });
28
+ const localXAxis = parentMatrix.applyInverse({
29
+ x: t.x + Math.cos(rotation),
30
+ y: t.y + Math.sin(rotation),
31
+ });
32
+ display.position.set(localOrigin.x, localOrigin.y);
33
+ display.rotation = normalizeAngle(
34
+ Math.atan2(localXAxis.y - localOrigin.y, localXAxis.x - localOrigin.x) - ownReflection,
35
+ );
36
+ } else {
37
+ display.position.set(t.x, t.y);
38
+ display.rotation = normalizeAngle(rotation - ownReflection);
39
+ }
22
40
  }
23
41
  };
24
42
  }
@@ -1,11 +1,13 @@
1
1
  import type RAPIER from '@dimforge/rapier2d-compat';
2
2
  import type { Application, Container } from 'pixi.js';
3
- import type { z } from 'zod';
3
+ import type { SystemAdapters } from '../adapter/system-adapter';
4
+ import type { SeededRandom } from '../core/seeded-random';
4
5
  import type { createSystemRunner } from '../core/system-runner';
5
- import type { SystemPhaseName } from '../core/types';
6
6
  import type { ComponentManager } from '../ecs/component-manager';
7
- import type { GameComponent } from '../ecs/game-component';
8
- import type { Game } from '../runtime/game';
7
+ import type { GameComponentClass } from '../ecs/game-component';
8
+ import type { InputManager } from '../input/input-manager';
9
+ import type { Game, WorldInstance } from '../runtime/game';
10
+ import type { DebugCtxSurface } from '../runtime/types';
9
11
  import type { Collision2DSystem } from './collision-2d';
10
12
  import type { Physics2DRegistry } from './physics2d-registry';
11
13
 
@@ -39,6 +41,18 @@ export interface World2DContext {
39
41
  components: ComponentManager;
40
42
  /** The phase-ordered system runner (shared with the 3D path). */
41
43
  systems: ReturnType<typeof createSystemRunner>;
44
+ /** The game-owned input manager. Pixi components use the same action maps,
45
+ * rebinding, gamepad, tracing, and virtual-input path as Three components. */
46
+ input: InputManager;
47
+ /** The shared debug/synthetic-player surface, scoped to this Pixi root. */
48
+ debug?: DebugCtxSurface;
49
+ /** The game-scoped seeded random surface shared by every first-party root. */
50
+ random: SeededRandom;
51
+ /** Register a game-owned editor-facing system adapter on this root. */
52
+ registerSystemAdapter?<K extends keyof SystemAdapters>(
53
+ kind: K,
54
+ adapter: NonNullable<SystemAdapters[K]>,
55
+ ): void;
42
56
  /**
43
57
  * The shared Game root when this world is mounted by a first-party host.
44
58
  * This is the cross-world data path: a Pixi component can query gameplay
@@ -46,16 +60,15 @@ export interface World2DContext {
46
60
  * bespoke synchronization bridge.
47
61
  */
48
62
  game?: Game | undefined;
63
+ /** Live alias of `game.roots`, when mounted by the universal host. */
64
+ roots?: ReadonlyArray<WorldInstance> | undefined;
49
65
  }
50
66
 
51
67
  /** Constructor type for `GameComponent<'pixijs'>` subclasses used in a world2d
52
68
  * registry (carries static phase/schema) — the 2D-registry analog of
53
69
  * `GameComponentClass` (`ecs/game-component.ts`), narrowed to the pixijs kind
54
70
  * so `new Cls()` returns a properly-typed `GameComponent<'pixijs'>` without a cast. */
55
- export type Pixi2DComponentClass = (new () => GameComponent<'pixijs'>) & {
56
- phase?: SystemPhaseName;
57
- schema?: z.ZodObject<z.ZodRawShape>;
58
- };
71
+ export type Pixi2DComponentClass = GameComponentClass<'pixijs'>;
59
72
 
60
73
  /** Component registry for world2d — a plain name→class map (the 2D analog). */
61
74
  export type Component2DRegistry = Record<string, Pixi2DComponentClass>;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * `<Behavior type="…" {...config}>` — the rapier-`<RigidBody>`-shaped wrapper
3
+ * that attaches a registry GameComponent to its child's Object3D under ENGINE
4
+ * phases (the manager provided by `EngineBridge`, ticked by the host loop via
5
+ * ./r3f-adapter.tsx — pause freezes it), detaches on unmount, and re-applies
6
+ * schema-parsed config on prop change.
7
+ *
8
+ * Upstreamed VERBATIM in semantics from `examples/r3f-first-party/src/
9
+ * behavior.tsx` (R4, docs/R3F-FOLLOW-THROUGH-SPEC.md): the Zod-parse +
10
+ * live-instance re-config behavior and the group-wrapper single-stamp shape
11
+ * are live-proven and deliberately unchanged. The one structural difference:
12
+ * the class lookup is REGISTRY-DRIVEN through the bridge (`EngineBridgeValue.
13
+ * registry`, handed to `createR3FAdapter({ components })` by the project)
14
+ * instead of a project-module import — an engine module cannot import
15
+ * project code.
16
+ *
17
+ * WRAPPER SHAPE (the documented W3 decision): a `<group>` wrapper, NOT
18
+ * `cloneElement` ref-injection. Reasons:
19
+ * - Write-back: the child element keeps its OWN JSX stamp untouched (its
20
+ * `userData-oid` prop pierces onto the child's Object3D exactly as it
21
+ * does un-wrapped), so the child's prop/gizmo write-back path is
22
+ * byte-identical to an un-wrapped mesh. `cloneElement` would have to
23
+ * merge a ref without clobbering an authored one — fragile across
24
+ * forwardRef/ref-as-prop shapes — and adds nothing the group doesn't.
25
+ * - The wrapper is also the Behavior's OWN presence in the scene graph: it
26
+ * carries the `<Behavior>` call site's `userData-oid` stamp (fiber
27
+ * pierces the forwarded dashed prop onto the group's userData), which is
28
+ * what makes the Behavior node itself selectable/source-addressable in
29
+ * the editor hierarchy, with its schema-declared config editable like any
30
+ * other JSX prop.
31
+ * - Transform semantics: the group stays at identity, so the child's
32
+ * authored `position`/`rotation`/`scale` are unchanged by wrapping.
33
+ *
34
+ * The component attaches to the FIRST CHILD's Object3D (`group.children[0]`)
35
+ * — the wrapper contract from the spec — not to the group, so `this.object3D`
36
+ * inside the GameComponent is the mesh the author wrapped.
37
+ */
38
+
39
+ // NOTE: the fiber types import below is type-only; at runtime this module
40
+ // depends only on `react`. It is still R3F-COUPLED by contract: only a
41
+ // project that already mounts through fiber (createR3FAdapter) can render it
42
+ // meaningfully, and the editor's OID stamping marks R3F-dialect files by
43
+ // their fiber import — project entry files importing this module keep their
44
+ // own `@react-three/fiber` import for the scene itself.
45
+ import type { ThreeElements } from '@react-three/fiber';
46
+ import { createElement, useContext, useEffect, useRef } from 'react';
47
+ import type { Group } from 'three';
48
+ import type { GameComponent } from '../ecs/game-component';
49
+ import { type BehaviorRegistry, EngineBridge, resolveBehaviorClass } from './engine-bridge';
50
+
51
+ export interface BehaviorProps<Reg extends BehaviorRegistry = BehaviorRegistry>
52
+ extends Pick<ThreeElements['group'], 'children'> {
53
+ /** Project-registry key of the GameComponent class to attach. */
54
+ type: Extract<keyof Reg, string>;
55
+ /** Authored config fields, validated through the class's static Zod schema
56
+ * — plus the editor's `userData-oid` stamp, forwarded to the group. */
57
+ [field: string]: unknown;
58
+ }
59
+
60
+ export function Behavior<Reg extends BehaviorRegistry = BehaviorRegistry>(
61
+ props: BehaviorProps<Reg>,
62
+ ) {
63
+ const { type, children, 'userData-oid': oid, ...config } = props;
64
+ const bridge = useContext(EngineBridge);
65
+ const groupRef = useRef<Group>(null);
66
+
67
+ // Latest config, readable from the attach effect without re-running it.
68
+ const configRef = useRef(config);
69
+ configRef.current = config;
70
+ // The live instance the config-reapply effect targets (never re-attach).
71
+ const liveRef = useRef<GameComponent | null>(null);
72
+
73
+ useEffect(() => {
74
+ if (!bridge?.registry) {
75
+ // Loud degrade, never a crash: a mount without the adapter's bridge
76
+ // (e.g. a foreign harness rendering this tree directly) — or a bridge
77
+ // whose adapter was built without a `components` registry — renders
78
+ // the children plain; behavior simply doesn't attach.
79
+ console.warn(
80
+ `<Behavior type="${type}">: no EngineBridge ${
81
+ bridge ? 'registry' : 'provider'
82
+ } — component not attached.`,
83
+ );
84
+ return;
85
+ }
86
+ const target = groupRef.current?.children[0];
87
+ if (!target) {
88
+ console.warn(`<Behavior type="${type}">: no child Object3D to attach to.`);
89
+ return;
90
+ }
91
+ const registry = bridge.registry;
92
+ const Cls = resolveBehaviorClass(registry, type);
93
+ const instance = new Cls();
94
+ const parsed = Cls.schema ? Cls.schema.parse(configRef.current) : configRef.current;
95
+ Object.assign(instance, parsed);
96
+ // `attach`'s registry info carries the raw authored props so engine-side
97
+ // HMR (`hotSwap`) can re-validate them against a future class version.
98
+ bridge.components.attach(target, instance, {
99
+ key: type,
100
+ props: { ...configRef.current },
101
+ });
102
+ liveRef.current = instance;
103
+ return () => {
104
+ // `detach(node)` removes every component on the node (the manager's
105
+ // public teardown op). This wrapper attaches exactly one component per
106
+ // child, so this is exact here.
107
+ bridge.components.detach(target);
108
+ liveRef.current = null;
109
+ };
110
+ }, [bridge, type]);
111
+
112
+ // Re-apply config on prop change, onto the LIVE instance (no re-attach).
113
+ const configJson = JSON.stringify(config);
114
+ useEffect(() => {
115
+ const instance = liveRef.current;
116
+ if (!instance || !bridge?.registry) return;
117
+ const Cls = resolveBehaviorClass(bridge.registry, type);
118
+ const parsed: Record<string, unknown> = JSON.parse(configJson);
119
+ Object.assign(instance, Cls.schema ? Cls.schema.parse(parsed) : parsed);
120
+ }, [configJson, type, bridge]);
121
+
122
+ // Built with createElement, NOT a JSX literal, deliberately: the editor's
123
+ // OID stamper rewrites JSX syntax only, so a JSX `<group>` here would get
124
+ // behavior.tsx's OWN stamp AND the forwarded call-site oid (a duplicate
125
+ // `userData-oid` attribute). The wrapper group must carry exactly ONE
126
+ // identity — the `<Behavior>` CALL SITE's oid — so the editor's hierarchy
127
+ // selects/edits the Behavior element the author actually wrote.
128
+ return createElement(
129
+ 'group',
130
+ {
131
+ ref: groupRef,
132
+ name: `Behavior:${type}`,
133
+ 'userData-oid': oid,
134
+ 'userData-behaviorType': type,
135
+ },
136
+ children,
137
+ );
138
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `@engine/world3d-react` — the React context that carries the engine
3
+ * component runtime into an R3F tree, so `<Behavior>` (./behavior.tsx) can
4
+ * reach the REAL `ComponentManager.attach` path from inside fiber JSX.
5
+ *
6
+ * Upstreamed from `examples/r3f-first-party/src/engine-bridge.ts` (R4,
7
+ * docs/R3F-FOLLOW-THROUGH-SPEC.md — the opt-in module
8
+ * R3F-FIRST-PARTY-DESIGN §1.B Phase 2 names). The bridge value is built by
9
+ * `createR3FAdapter`'s `mount()` (./r3f-adapter.tsx): a real
10
+ * `createSystemRunner` + `createComponentManager` pair whose phases the
11
+ * adapter's `update(dt)` runs under the HOST loop — so attached
12
+ * GameComponents tick in engine phase order, pause included (no update while
13
+ * the world is frozen), exactly like scene-authored components.
14
+ *
15
+ * `registry` is the project's own behavior registry (`<Behavior type="…">`
16
+ * resolves against it) — REGISTRY-DRIVEN by design: the engine module never
17
+ * imports project code; the project hands its registry to
18
+ * `createR3FAdapter({ components })`, which places it here.
19
+ */
20
+
21
+ import { createContext } from 'react';
22
+ import type { ComponentManager } from '../ecs/component-manager';
23
+ import type { GameComponentClass } from '../ecs/game-component';
24
+
25
+ /** A project's behavior registry: `<Behavior type>` keys → component classes. */
26
+ export type BehaviorRegistry = Readonly<Record<string, GameComponentClass>>;
27
+
28
+ export interface EngineBridgeValue {
29
+ /** The engine's real component manager for this R3F world. */
30
+ readonly components: ComponentManager;
31
+ /** The project's behavior registry (absent ⇒ `<Behavior>` degrades loudly). */
32
+ readonly registry?: BehaviorRegistry | undefined;
33
+ }
34
+
35
+ export const EngineBridge = createContext<EngineBridgeValue | null>(null);
36
+
37
+ /** Resolve a `<Behavior type>` key against the bridge's registry, throwing a
38
+ * teaching error naming the registered keys (the same loud failure shape the
39
+ * example's project-owned `getComponent` had). */
40
+ export function resolveBehaviorClass(registry: BehaviorRegistry, type: string): GameComponentClass {
41
+ const cls = registry[type];
42
+ if (!cls) {
43
+ throw new Error(
44
+ `Unknown behavior type "${type}" — registered: ${Object.keys(registry).join(', ')}`,
45
+ );
46
+ }
47
+ return cls;
48
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * world3d-react — the react-three-fiber bridge for vgai's threejs surface
3
+ * (`@engine/world3d-react`).
4
+ *
5
+ * The OPT-IN module R3F-FIRST-PARTY-DESIGN §1.B Phase 2 names, upstreamed
6
+ * from the live-proven `examples/r3f-first-party` project copies (R4,
7
+ * docs/R3F-FOLLOW-THROUGH-SPEC.md; decision D24 — R3F TSX is the blessed
8
+ * three.js authoring path). Mirrors how `world2d/` is pixi's opt-in home: a
9
+ * peer surface module the engine CORE never imports (enforced by
10
+ * `packages/engine/test/react-core-import-ban.test.ts` — this directory is
11
+ * an allowed react-importing entry alongside `react/`, and core files may
12
+ * not import it).
13
+ *
14
+ * Peer contract: an importing PROJECT already depends on `react`,
15
+ * `@react-three/fiber`, and `three` (fiber's three must be deduped to the
16
+ * host's single instance — `resolve.dedupe: ['three', 'react', 'react-dom']`
17
+ * in the project's Vite config; see `createR3FAdapter`'s header). The engine
18
+ * package deliberately declares no hard dependency on fiber: only projects
19
+ * that already mount through fiber ever import this module.
20
+ *
21
+ * Surface:
22
+ * - `createR3FAdapter({ id, content, components })` — mount an R3F tree as
23
+ * a first-party `kind: "threejs"` world under the host's gated loop.
24
+ * - `<Behavior type="…" {...config}>` — attach a registry GameComponent to
25
+ * the wrapped child's Object3D under engine phases (registry-driven via
26
+ * `EngineBridge`; generic over the project's registry for typed keys).
27
+ * - `EngineBridge` / `EngineBridgeValue` / `BehaviorRegistry` — the context
28
+ * contract, exported for advanced adopters building their own bridge.
29
+ */
30
+
31
+ export { Behavior, type BehaviorProps } from './behavior';
32
+ export {
33
+ type BehaviorRegistry,
34
+ EngineBridge,
35
+ type EngineBridgeValue,
36
+ resolveBehaviorClass,
37
+ } from './engine-bridge';
38
+ export { type CreateR3FAdapterOptions, createR3FAdapter } from './r3f-adapter';
39
+ export {
40
+ createR3FWorldContext,
41
+ DEFAULT_INPUT_MAP_PATH,
42
+ type R3FWorldContextOptions,
43
+ type R3FWorldRuntime,
44
+ } from './world-context';