@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.
- package/README.md +48 -15
- package/package.json +11 -25
- package/schemas/engine-capabilities.json +10 -10
- package/schemas/entity2d.schema.json +468 -0
- package/schemas/mat.schema.json +2 -33
- package/schemas/prefab.schema.json +16 -172
- package/schemas/scn2d.schema.json +42 -23
- package/schemas/vgai-game.schema.json +34 -0
- package/schemas/vscn.schema.json +16 -172
- package/src/adapter/authoring.ts +152 -2
- package/src/adapter/colyseus-networking-adapter.ts +35 -1
- package/src/adapter/first-party-systems.ts +7 -1
- package/src/adapter/game-adapter.ts +13 -0
- package/src/adapter/index.ts +25 -0
- package/src/adapter/rapier-physics-adapter.ts +55 -2
- package/src/adapter/system-adapter.ts +249 -2
- package/src/adapter/vgai-scene-game-adapter.ts +149 -25
- package/src/ai/navigation.ts +28 -0
- package/src/animation/clip-map.ts +1 -8
- package/src/animation/theatre-director.ts +50 -0
- package/src/animation/xstate-animation-binding.ts +6 -0
- package/src/audio/audio-introspection.ts +290 -0
- package/src/audio/tone-context.ts +46 -0
- package/src/dev/chrome-trace.ts +153 -0
- package/src/dev/performance-profiler.ts +93 -6
- package/src/dev/render-debug-adapter.ts +199 -0
- package/src/dev/render-memory.ts +243 -0
- package/src/dev/webgl-frame-capture.ts +424 -0
- package/src/ecs/component-manager.ts +43 -10
- package/src/ecs/game-component.ts +39 -10
- package/src/input/input-manager.ts +24 -19
- package/src/input/input-types.ts +1 -1
- package/src/loader.ts +7 -0
- package/src/manifest/load.ts +14 -0
- package/src/manifest/schema.ts +65 -0
- package/src/react/game-state.tsx +1 -1
- package/src/render/render-batch-system.ts +26 -12
- package/src/render/spark-renderer-lifecycle.ts +64 -0
- package/src/runtime/create-runtime.ts +33 -5
- package/src/runtime/debug-bridge.ts +5 -5
- package/src/runtime/game.ts +102 -20
- package/src/runtime/mount-manifest.ts +1 -1
- package/src/runtime/render-control.ts +121 -0
- package/src/runtime/types.ts +1 -1
- package/src/scene/asset-loaders.ts +77 -3
- package/src/scene/instance-mesh.ts +25 -0
- package/src/scene/material-factory.ts +4 -15
- package/src/scene/mesh-shadow.ts +18 -0
- package/src/scene/particles-factory.ts +59 -0
- package/src/scene/scene-loader.ts +41 -16
- package/src/scene/schema/instances.ts +1 -2
- package/src/scene/schema/material.ts +83 -94
- package/src/scene/schema/mesh.ts +76 -90
- package/src/scene/schema/scene-file.ts +1 -2
- package/src/scene/user-data.ts +30 -14
- package/src/setup/setup-renderer.ts +6 -1
- package/src/world2d/asset-paths2d.ts +44 -0
- package/src/world2d/collision-2d.ts +7 -14
- package/src/world2d/entity2d-asset.ts +22 -0
- package/src/world2d/index.ts +27 -2
- package/src/world2d/physics2d-transform.ts +173 -0
- package/src/world2d/physics2d-units.ts +10 -0
- package/src/world2d/pixi-game-adapter.ts +148 -36
- package/src/world2d/scene2d-identity.ts +49 -0
- package/src/world2d/scene2d-loader.ts +243 -119
- package/src/world2d/schema/entity2d.ts +51 -33
- package/src/world2d/schema/physics2d.ts +14 -3
- package/src/world2d/schema/sprite.ts +32 -4
- package/src/world2d/schema/tilemap.ts +26 -9
- package/src/world2d/transform-writer-2d.ts +29 -11
- package/src/world2d/types.ts +21 -8
- package/src/world3d-react/behavior.tsx +138 -0
- package/src/world3d-react/engine-bridge.ts +48 -0
- package/src/world3d-react/index.ts +44 -0
- package/src/world3d-react/r3f-adapter.tsx +303 -0
- package/src/world3d-react/world-context.ts +294 -0
- package/vendor/realism-effects/LICENSE.md +21 -0
- package/vendor/realism-effects/UPSTREAM.md +19 -0
- package/vendor/realism-effects/dist/index.cjs +3447 -0
- package/vendor/realism-effects/dist/index.d.ts +59 -0
- package/vendor/realism-effects/dist/index.js +3434 -0
- package/vendor/realism-effects/package.json +23 -0
- package/src/character/cloth-sim.ts +0 -533
- package/src/character/spring-chain.ts +0 -307
- package/src/humanoid/body.ts +0 -663
- package/src/humanoid/clips.ts +0 -149
- package/src/humanoid/compose.ts +0 -209
- package/src/humanoid/generate.ts +0 -189
- package/src/humanoid/index.ts +0 -36
- package/src/humanoid/schema.ts +0 -108
- package/src/humanoid/skeleton.ts +0 -345
- package/src/react/humanoid-bake.document.tsx +0 -337
- package/src/scene/geometries/index.ts +0 -7
- package/src/scene/geometries/terrain.ts +0 -42
- package/src/scene/geometry-registry.ts +0 -42
- package/src/scene/instance-registry.ts +0 -84
- package/src/scene/instancers/grid.ts +0 -38
- package/src/scene/instancers/index.ts +0 -7
- package/src/scene/material-registry.ts +0 -73
- package/src/scene/materials/index.ts +0 -7
- package/src/scene/materials/water.ts +0 -56
- package/src/world2d/components-2d.ts +0 -86
- 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
|
-
|
|
58
|
+
fps: z
|
|
51
59
|
.number()
|
|
52
|
-
.
|
|
60
|
+
.positive()
|
|
53
61
|
.optional()
|
|
54
|
-
.describe('
|
|
55
|
-
|
|
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.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
.describe('
|
|
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(
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
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
|
}
|
package/src/world2d/types.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
8
|
-
import type {
|
|
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 =
|
|
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';
|