@vgai/engine 0.5.2 → 0.5.3
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 +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /package/src/{scene → render}/mesh-shadow.ts +0 -0
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { Physics2DSchema } from './physics2d';
|
|
3
|
-
import { SpriteSchema, Text2DSchema } from './sprite';
|
|
4
|
-
import { TilemapSchema } from './tilemap';
|
|
5
|
-
import { Transform2DSchema, Vec2Schema } from './tuples2d';
|
|
6
|
-
|
|
7
|
-
/** Component data: same shape as the 3D ComponentsSchema (scalars + scalar arrays). */
|
|
8
|
-
const ComponentScalarSchema = z.union([z.number(), z.string(), z.boolean()]);
|
|
9
|
-
const ComponentValueSchema = z.union([
|
|
10
|
-
ComponentScalarSchema,
|
|
11
|
-
z.array(z.number()),
|
|
12
|
-
z.array(z.string()),
|
|
13
|
-
z.array(z.boolean()),
|
|
14
|
-
]);
|
|
15
|
-
const ComponentDataSchema = z.record(z.string(), ComponentValueSchema);
|
|
16
|
-
const ComponentsSchema = z.record(z.string(), ComponentDataSchema);
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A world2d entity — the 2D analog of the 3D `SceneEntity`. The SAME engine model
|
|
20
|
-
* (named components validated against a registry, a tree of children) reused; the
|
|
21
|
-
* sections (`sprite`/`text`/`physics2d`/`transform2d`) name engine-owned data, NOT
|
|
22
|
-
* `PIXI.Sprite`. `surface` defaults to 'world2d' for entities in a Scene2D file.
|
|
23
|
-
*/
|
|
24
|
-
const Entity2DBase = z.object({
|
|
25
|
-
id: z.string().optional().describe('Unique entity ID. Auto-generated if omitted'),
|
|
26
|
-
name: z.string().describe('Human-readable entity name (shown in hierarchy panel)'),
|
|
27
|
-
surface: z
|
|
28
|
-
.enum(['world2d', 'world3d'])
|
|
29
|
-
.optional()
|
|
30
|
-
.describe(
|
|
31
|
-
"Render surface that instantiates this entity. Default 'world2d' in a Scene2D file. The " +
|
|
32
|
-
"'world3d' value is REJECTED at parse (T4.1): declared but has no runtime reader — " +
|
|
33
|
-
"authoring surface: 'world3d' throws. Do not author it ('world2d', the default, is fine).",
|
|
34
|
-
),
|
|
35
|
-
visible: z.boolean().optional().describe('Whether the entity and its children are rendered'),
|
|
36
|
-
locked: z
|
|
37
|
-
.boolean()
|
|
38
|
-
.optional()
|
|
39
|
-
.describe('Prevent selection and editing in the native 2D scene editor'),
|
|
40
|
-
tags: z
|
|
41
|
-
.array(z.string())
|
|
42
|
-
.optional()
|
|
43
|
-
.describe(
|
|
44
|
-
'Arbitrary string tags for runtime queries. REJECTED at parse (T4.1): declared but has no ' +
|
|
45
|
-
'runtime reader — authoring this field throws. Do not author.',
|
|
46
|
-
),
|
|
47
|
-
transform2d: Transform2DSchema.optional().describe('Local 2D transform relative to parent'),
|
|
48
|
-
pivot: Vec2Schema.optional().describe('Local-space pivot point [x, y]'),
|
|
49
|
-
sprite: SpriteSchema.optional().describe('2D sprite appearance'),
|
|
50
|
-
text: Text2DSchema.optional().describe('World-space text label'),
|
|
51
|
-
tilemap: TilemapSchema.optional().describe('2D tilemap (CompositeTilemap)'),
|
|
52
|
-
physics2d: Physics2DSchema.optional().describe('2D physics rigid body + collider'),
|
|
53
|
-
components: ComponentsSchema.optional().describe(
|
|
54
|
-
'User-defined components (validated against the world2d component registry)',
|
|
55
|
-
),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export type Entity2D = z.infer<typeof Entity2DBase> & {
|
|
59
|
-
children?: Entity2D[] | undefined;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const Entity2DSchema: z.ZodType<Entity2D> = z.lazy(() =>
|
|
63
|
-
Entity2DBase.extend({
|
|
64
|
-
children: z.array(Entity2DSchema).optional().describe('Child entities'),
|
|
65
|
-
})
|
|
66
|
-
// T4.1: two fields are authored/typed but have no runtime reader in
|
|
67
|
-
// `scene2d-loader.ts`'s `spawn()` (unlike their 3D `SceneEntity` siblings,
|
|
68
|
-
// which ARE read — `scene-query.ts` for `tags`):
|
|
69
|
-
// - `tags` — never copied onto userData; no 2D equivalent of scene-query.ts.
|
|
70
|
-
// - `surface: 'world3d'` — spawn() always instantiates as world2d
|
|
71
|
-
// regardless of this field's value, so a 'world3d' entity is silently
|
|
72
|
-
// spawned into the 2D world anyway. ('world2d', the default, is a
|
|
73
|
-
// harmless no-op and is not flagged.)
|
|
74
|
-
// Loud load errors until readers exist — see T4.1.
|
|
75
|
-
.superRefine((entity, ctx) => {
|
|
76
|
-
if (entity.tags !== undefined) {
|
|
77
|
-
ctx.addIssue({
|
|
78
|
-
code: z.ZodIssueCode.custom,
|
|
79
|
-
message:
|
|
80
|
-
`world2d entity "${entity.name}": \`tags\` is authored but not implemented — ` +
|
|
81
|
-
'no 2D equivalent of scene-query.ts reads it yet. See T4.1.',
|
|
82
|
-
path: ['tags'],
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
if (entity.surface === 'world3d') {
|
|
86
|
-
ctx.addIssue({
|
|
87
|
-
code: z.ZodIssueCode.custom,
|
|
88
|
-
message:
|
|
89
|
-
`world2d entity "${entity.name}": \`surface: 'world3d'\` is authored but not ` +
|
|
90
|
-
'implemented — spawn() always instantiates entities as world2d regardless of this ' +
|
|
91
|
-
'field, so a world3d entity would be silently spawned into the 2D world. See T4.1.',
|
|
92
|
-
path: ['surface'],
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}),
|
|
96
|
-
) as z.ZodType<Entity2D>;
|
|
97
|
-
|
|
98
|
-
/** A world2d scene file — the 2D analog of `.vscn.json` (`.scn2d.json`). */
|
|
99
|
-
export const Scene2DSchema = z
|
|
100
|
-
.object({
|
|
101
|
-
version: z.number().optional().describe('Scene format version'),
|
|
102
|
-
name: z
|
|
103
|
-
.string()
|
|
104
|
-
.optional()
|
|
105
|
-
.describe('Scene name shown in the native 2D document hierarchy and inspector'),
|
|
106
|
-
surface: z.literal('world2d').optional().describe("Render surface. Always 'world2d'"),
|
|
107
|
-
gravity: Vec2Schema.optional().describe(
|
|
108
|
-
'World gravity [x, y] in px/s^2. Default [0, 9.8 * pixelsPerMeter] ([0, 980] at the default scale)',
|
|
109
|
-
),
|
|
110
|
-
pixelsPerMeter: z
|
|
111
|
-
.number()
|
|
112
|
-
.positive()
|
|
113
|
-
.optional()
|
|
114
|
-
.describe(
|
|
115
|
-
'Authored pixels per physics meter. Configures native Rapier World.lengthUnit and scales default gravity. Default 100',
|
|
116
|
-
),
|
|
117
|
-
background: z.string().optional().describe("Background color hex string. Default '#10101a'"),
|
|
118
|
-
entities: z.array(Entity2DSchema).describe('Root entities of the world2d scene'),
|
|
119
|
-
})
|
|
120
|
-
// Duplicate authored ids are rejected at the document boundary so source
|
|
121
|
-
// history and editor selection always address one stable native Container.
|
|
122
|
-
.superRefine((scene, ctx) => {
|
|
123
|
-
const authoredIds = new Map<string, Array<string | number>>();
|
|
124
|
-
const visit = (entities: Entity2D[], parentPath: Array<string | number>): void => {
|
|
125
|
-
entities.forEach((entity, index) => {
|
|
126
|
-
const entityPath = [...parentPath, index];
|
|
127
|
-
if (entity.id !== undefined) {
|
|
128
|
-
const idPath = [...entityPath, 'id'];
|
|
129
|
-
const firstPath = authoredIds.get(entity.id);
|
|
130
|
-
if (firstPath) {
|
|
131
|
-
ctx.addIssue({
|
|
132
|
-
code: z.ZodIssueCode.custom,
|
|
133
|
-
message:
|
|
134
|
-
`Duplicate world2d entity id "${entity.id}"; first authored at ` +
|
|
135
|
-
firstPath.join('.'),
|
|
136
|
-
path: idPath,
|
|
137
|
-
});
|
|
138
|
-
} else {
|
|
139
|
-
authoredIds.set(entity.id, idPath);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (entity.children) visit(entity.children, [...entityPath, 'children']);
|
|
143
|
-
});
|
|
144
|
-
};
|
|
145
|
-
visit(scene.entities, ['entities']);
|
|
146
|
-
})
|
|
147
|
-
.describe('A world2d scene (PixiJS render surface)');
|
|
148
|
-
|
|
149
|
-
export type Scene2DFile = z.infer<typeof Scene2DSchema>;
|
|
150
|
-
|
|
151
|
-
/** A copy-on-instantiate reusable native 2D entity subtree. This is a
|
|
152
|
-
* separate format from Three's `.prefab.json`: its root is an `Entity2D`, and
|
|
153
|
-
* insertion assigns fresh scene identities instead of creating a linked
|
|
154
|
-
* override layer. */
|
|
155
|
-
export const Entity2DAssetSchema = z
|
|
156
|
-
.object({
|
|
157
|
-
version: z.literal(1).describe('2D entity asset format version'),
|
|
158
|
-
name: z.string().describe('Reusable 2D entity asset display name'),
|
|
159
|
-
root: Entity2DSchema.describe('Native Entity2D subtree copied into a Scene2D document'),
|
|
160
|
-
})
|
|
161
|
-
.describe('A reusable native 2D entity asset (.entity2d.json)');
|
|
162
|
-
|
|
163
|
-
export type Entity2DAsset = z.infer<typeof Entity2DAssetSchema>;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { Vec2Schema } from './tuples2d';
|
|
3
|
-
|
|
4
|
-
/** A single 2D collider shape (Rapier 2D). Cuboid takes 2 half-extents; ball a radius. */
|
|
5
|
-
export const Collider2DSchema = z
|
|
6
|
-
.discriminatedUnion('shape', [
|
|
7
|
-
z.object({
|
|
8
|
-
shape: z.literal('cuboid').describe('Box collider aligned to the rigid-body rotation'),
|
|
9
|
-
halfExtents: Vec2Schema.describe('Half-width and half-height [hx, hy] in pixels'),
|
|
10
|
-
}),
|
|
11
|
-
z.object({
|
|
12
|
-
shape: z.literal('ball').describe('Circle collider'),
|
|
13
|
-
radius: z.number().describe('Radius in pixels'),
|
|
14
|
-
}),
|
|
15
|
-
z.object({
|
|
16
|
-
shape: z.literal('capsule').describe('Capsule collider (vertical)'),
|
|
17
|
-
halfHeight: z.number().describe('Half the straight segment length'),
|
|
18
|
-
radius: z.number().describe('Cap radius'),
|
|
19
|
-
}),
|
|
20
|
-
])
|
|
21
|
-
.describe('2D collider shape (Rapier 2D)');
|
|
22
|
-
|
|
23
|
-
export type SceneCollider2D = z.infer<typeof Collider2DSchema>;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* `physics2d` authored section — engine-owned 2D physics data, instantiated as a
|
|
27
|
-
* Rapier 2D RigidBody + Collider. The world2d analog of the 3D `physics` section.
|
|
28
|
-
*/
|
|
29
|
-
export const Physics2DSchema = z
|
|
30
|
-
.object({
|
|
31
|
-
bodyType: z
|
|
32
|
-
.enum(['dynamic', 'fixed', 'kinematicVelocity', 'kinematicPosition'])
|
|
33
|
-
.describe(
|
|
34
|
-
'Rapier 2D rigid-body type. A body of any type nested below a dynamic or kinematic ' +
|
|
35
|
-
'body is rejected: Rapier has no parent/child body relationship. Nesting below a ' +
|
|
36
|
-
'fixed or physics-less transformed parent is supported for fixed bodies. Dynamic ' +
|
|
37
|
-
'and kinematic bodies additionally require a conformal parent chain (uniform scale ' +
|
|
38
|
-
'plus rotation/reflection), because later body rotation cannot round-trip through ' +
|
|
39
|
-
'non-uniform parent scale without shearing the Pixi visual.',
|
|
40
|
-
),
|
|
41
|
-
collider: Collider2DSchema.describe(
|
|
42
|
-
'Collider shape attached to the body. Its pose and dimensions use the composed parent ' +
|
|
43
|
-
'transform at spawn. Cuboids support non-uniform scale; ball/capsule colliders reject ' +
|
|
44
|
-
'non-uniform scale because Rapier cannot represent an ellipse.',
|
|
45
|
-
),
|
|
46
|
-
sensor: z
|
|
47
|
-
.boolean()
|
|
48
|
-
.optional()
|
|
49
|
-
.describe(
|
|
50
|
-
'If true, a sensor (trigger) collider — overlaps fire onTriggerEnter/Exit, no solid response',
|
|
51
|
-
),
|
|
52
|
-
density: z.number().optional().describe('Collider density (drives mass). Default 1'),
|
|
53
|
-
friction: z.number().optional().describe('Coefficient of friction. Default 0.5'),
|
|
54
|
-
restitution: z.number().optional().describe('Bounciness 0..1. Default 0'),
|
|
55
|
-
linearDamping: z.number().optional().describe('Linear velocity damping. Default 0'),
|
|
56
|
-
gravityScale: z.number().optional().describe('Per-body gravity multiplier. Default 1'),
|
|
57
|
-
lockRotations: z
|
|
58
|
-
.boolean()
|
|
59
|
-
.optional()
|
|
60
|
-
.describe('Lock the body so it cannot rotate. Default false'),
|
|
61
|
-
})
|
|
62
|
-
.describe('2D physics rigid body + collider (world2d, Rapier 2D)');
|
|
63
|
-
|
|
64
|
-
export type ScenePhysics2D = z.infer<typeof Physics2DSchema>;
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { Vec2Schema } from './tuples2d';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `sprite` authored section — engine-owned 2D sprite data, NOT a `PIXI.Sprite`.
|
|
6
|
-
*
|
|
7
|
-
* Per ARCHITECTURE.md §Render Surfaces: authored data says `sprite`, and a runtime
|
|
8
|
-
* adapter instantiates it onto the appropriate backend (here, a `PIXI.Sprite` on
|
|
9
|
-
* the world2d surface). A 1x1 white texture is used when `texture` is omitted, so a
|
|
10
|
-
* tinted rectangle sprite needs no asset.
|
|
11
|
-
*/
|
|
12
|
-
export const SpriteSchema = z
|
|
13
|
-
.object({
|
|
14
|
-
texture: z
|
|
15
|
-
.string()
|
|
16
|
-
.optional()
|
|
17
|
-
.describe(
|
|
18
|
-
'Texture asset URL (loaded via PIXI.Assets). Omit for a tintable 1x1 white texture',
|
|
19
|
-
),
|
|
20
|
-
frame: z
|
|
21
|
-
.string()
|
|
22
|
-
.min(1)
|
|
23
|
-
.optional()
|
|
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
|
-
),
|
|
32
|
-
anchor: Vec2Schema.optional().describe('Normalized anchor [x, y] (0..1). Default [0.5, 0.5]'),
|
|
33
|
-
tint: z
|
|
34
|
-
.string()
|
|
35
|
-
.optional()
|
|
36
|
-
.describe("Tint color as hex string (e.g. '#ff8800'). Default white"),
|
|
37
|
-
alpha: z.number().min(0).max(1).optional().describe('Opacity 0..1. Default 1'),
|
|
38
|
-
width: z
|
|
39
|
-
.number()
|
|
40
|
-
.optional()
|
|
41
|
-
.describe('Explicit display width in pixels (overrides texture size)'),
|
|
42
|
-
height: z
|
|
43
|
-
.number()
|
|
44
|
-
.optional()
|
|
45
|
-
.describe('Explicit display height in pixels (overrides texture size)'),
|
|
46
|
-
blendMode: z
|
|
47
|
-
.enum(['normal', 'add', 'multiply', 'screen'])
|
|
48
|
-
.optional()
|
|
49
|
-
.describe('PixiJS blend mode. Default normal'),
|
|
50
|
-
interactive: z
|
|
51
|
-
.boolean()
|
|
52
|
-
.optional()
|
|
53
|
-
.describe('Enable federated pointer events (eventMode=static) so this sprite can be picked'),
|
|
54
|
-
filter: z
|
|
55
|
-
.enum(['none', 'blur', 'desaturate'])
|
|
56
|
-
.optional()
|
|
57
|
-
.describe('Apply a PixiJS filter (BlurFilter / ColorMatrix desaturate). Default none'),
|
|
58
|
-
fps: z
|
|
59
|
-
.number()
|
|
60
|
-
.positive()
|
|
61
|
-
.optional()
|
|
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
|
-
}
|
|
84
|
-
})
|
|
85
|
-
.describe('2D sprite appearance (world2d). Instantiated as a PIXI.Sprite or AnimatedSprite');
|
|
86
|
-
|
|
87
|
-
export type SceneSprite = z.infer<typeof SpriteSchema>;
|
|
88
|
-
|
|
89
|
-
/** `text` authored section — a world-space PIXI.Text label (world-space UI). */
|
|
90
|
-
export const Text2DSchema = z
|
|
91
|
-
.object({
|
|
92
|
-
text: z.string().describe('The string to display'),
|
|
93
|
-
fontSize: z.number().optional().describe('Font size in pixels. Default 24'),
|
|
94
|
-
fill: z.string().optional().describe("Text color as hex string. Default '#ffffff'"),
|
|
95
|
-
anchor: Vec2Schema.optional().describe('Normalized anchor [x, y] (0..1). Default [0.5, 0.5]'),
|
|
96
|
-
})
|
|
97
|
-
.describe('2D world-space text label (world2d). Instantiated as a PIXI.Text');
|
|
98
|
-
|
|
99
|
-
export type SceneText2D = z.infer<typeof Text2DSchema>;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* `tilemap` authored section — engine-owned 2D tilemap data (NOT a CompositeTilemap),
|
|
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.
|
|
8
|
-
*/
|
|
9
|
-
export const TilemapSchema = z
|
|
10
|
-
.object({
|
|
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'),
|
|
17
|
-
tiles: z
|
|
18
|
-
.array(z.array(z.number().int()))
|
|
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
|
-
}
|
|
36
|
-
})
|
|
37
|
-
.describe('2D tilemap (world2d). Instantiated as a @pixi/tilemap CompositeTilemap');
|
|
38
|
-
|
|
39
|
-
export type SceneTilemap = z.infer<typeof TilemapSchema>;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
/** 2D vector as [x, y]. The world2d analog of the 3D Vec3. */
|
|
4
|
-
export const Vec2Schema = z.tuple([z.number(), z.number()]).describe('2D vector as [x, y]');
|
|
5
|
-
|
|
6
|
-
export type Vec2 = z.infer<typeof Vec2Schema>;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 2D local transform — the world2d analog of the 3D TransformSchema.
|
|
10
|
-
*
|
|
11
|
-
* Orientation is a single scalar `rotation` (radians, about the screen Z axis),
|
|
12
|
-
* NOT a quaternion: PixiJS display objects rotate in the plane. The runtime/editor
|
|
13
|
-
* neutral `Transform` tuple (position[3]/quaternion[4]/scale[3]) maps onto this as
|
|
14
|
-
* z = 0 and rotation about Z (see PIXI-WORLD2D-MAXIMAL-SPEC model-transform2d-section).
|
|
15
|
-
*/
|
|
16
|
-
export const Transform2DSchema = z
|
|
17
|
-
.object({
|
|
18
|
-
position: Vec2Schema.optional().describe('Local position [x, y] in world2d pixels'),
|
|
19
|
-
rotation: z.number().optional().describe('Rotation in radians about the Z axis. Default 0'),
|
|
20
|
-
scale: Vec2Schema.optional().describe('Scale per axis [x, y]. Default [1, 1]'),
|
|
21
|
-
pivot: Vec2Schema.optional().describe('Local pivot point [x, y] (center of rotation/scale)'),
|
|
22
|
-
})
|
|
23
|
-
.describe('2D local transform relative to parent (world2d surface)');
|
|
24
|
-
|
|
25
|
-
export type SceneTransform2D = z.infer<typeof Transform2DSchema>;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { Container } from 'pixi.js';
|
|
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
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Build the single postPhysics transform writer for world2d — the ~5-line 2D
|
|
11
|
-
* analog of `createTransformWriter`.
|
|
12
|
-
*
|
|
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).
|
|
17
|
-
*/
|
|
18
|
-
export function createTransformWriter2D(physics: Physics2DRegistry, sceneRoot: Container) {
|
|
19
|
-
return function writeTransforms2D(): void {
|
|
20
|
-
for (const [display, { body }] of physics.entries()) {
|
|
21
|
-
const t = body.translation();
|
|
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
|
-
}
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
package/src/world2d/types.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type RAPIER from '@dimforge/rapier2d-compat';
|
|
2
|
-
import type { Application, Container } from 'pixi.js';
|
|
3
|
-
import type { SystemAdapters } from '../adapter/system-adapter';
|
|
4
|
-
import type { SeededRandom } from '../core/seeded-random';
|
|
5
|
-
import type { createSystemRunner } from '../core/system-runner';
|
|
6
|
-
import type { ComponentManager } from '../ecs/component-manager';
|
|
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';
|
|
11
|
-
import type { Collision2DSystem } from './collision-2d';
|
|
12
|
-
import type { Physics2DRegistry } from './physics2d-registry';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* All world2d subsystems, passed to every world2d `setup(ctx)` and to each
|
|
16
|
-
* `GameComponent<'pixijs'>` lifecycle method — the 2D analog of `GameContext`.
|
|
17
|
-
*
|
|
18
|
-
* Raw libraries (call directly, no wrappers): `app` / `stage` (PixiJS),
|
|
19
|
-
* `rapier2dWorld` / `rapier2d` (Rapier 2D). Engine handles bridge them into the
|
|
20
|
-
* SAME phase-ordered loop the 3D path uses (`systems` is the very same
|
|
21
|
-
* `createSystemRunner`, and `PHASE_ORDER` is shared).
|
|
22
|
-
*/
|
|
23
|
-
export interface World2DContext {
|
|
24
|
-
/** The PixiJS application. Its renderer draws `stage`; auto-ticker is disabled. */
|
|
25
|
-
app: Application;
|
|
26
|
-
/** The world2d scene-graph root (the camera/world container under app.stage). */
|
|
27
|
-
stage: Container;
|
|
28
|
-
/** The Rapier 2D physics `World`. One shared world per world2d scene. */
|
|
29
|
-
rapier2dWorld: RAPIER.World;
|
|
30
|
-
/** The Rapier 2D module namespace (RigidBodyDesc, ColliderDesc, Vector2, …). */
|
|
31
|
-
rapier2d: typeof RAPIER;
|
|
32
|
-
/** `Container ↔ Rapier2D body/collider` registry (+ collider→entity reverse index). */
|
|
33
|
-
physics2d: Physics2DRegistry;
|
|
34
|
-
/** Rapier 2D collision/trigger event dispatcher; drained each postPhysics. */
|
|
35
|
-
collisions2d: Collision2DSystem;
|
|
36
|
-
/** Manager for all live GameComponent instances in this pixi world — the
|
|
37
|
-
* unified `ComponentManager` (`ecs/component-manager.ts`, constructed with
|
|
38
|
-
* `{ kind: 'pixijs' }`). The world2d-silo-only manager this replaced
|
|
39
|
-
* (T7.3) is deleted; every 2D component attaches here, exactly like a
|
|
40
|
-
* threejs component attaches onto its world's manager. */
|
|
41
|
-
components: ComponentManager;
|
|
42
|
-
/** The phase-ordered system runner (shared with the 3D path). */
|
|
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;
|
|
56
|
-
/**
|
|
57
|
-
* The shared Game root when this world is mounted by a first-party host.
|
|
58
|
-
* This is the cross-world data path: a Pixi component can query gameplay
|
|
59
|
-
* components owned by a sibling Three.js world without a mirror store or a
|
|
60
|
-
* bespoke synchronization bridge.
|
|
61
|
-
*/
|
|
62
|
-
game?: Game | undefined;
|
|
63
|
-
/** Live alias of `game.roots`, when mounted by the universal host. */
|
|
64
|
-
roots?: ReadonlyArray<WorldInstance> | undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Constructor type for `GameComponent<'pixijs'>` subclasses used in a world2d
|
|
68
|
-
* registry (carries static phase/schema) — the 2D-registry analog of
|
|
69
|
-
* `GameComponentClass` (`ecs/game-component.ts`), narrowed to the pixijs kind
|
|
70
|
-
* so `new Cls()` returns a properly-typed `GameComponent<'pixijs'>` without a cast. */
|
|
71
|
-
export type Pixi2DComponentClass = GameComponentClass<'pixijs'>;
|
|
72
|
-
|
|
73
|
-
/** Component registry for world2d — a plain name→class map (the 2D analog). */
|
|
74
|
-
export type Component2DRegistry = Record<string, Pixi2DComponentClass>;
|
|
@@ -1,146 +0,0 @@
|
|
|
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 carries the `<Behavior>` call site's `userData-oid` stamp so
|
|
26
|
-
* its config stays source-addressable. It is an INTERNAL attachment node,
|
|
27
|
-
* not an independent spatial authoring object: the R3F authoring adapter
|
|
28
|
-
* projects this config onto the wrapped object's Inspector and promotes
|
|
29
|
-
* the child through the hierarchy.
|
|
30
|
-
* - Transform semantics: the group stays at identity, so the child's
|
|
31
|
-
* authored `position`/`rotation`/`scale` are unchanged by wrapping.
|
|
32
|
-
*
|
|
33
|
-
* The component attaches to the FIRST CHILD's Object3D (`group.children[0]`)
|
|
34
|
-
* — the wrapper contract from the spec — not to the group, so `this.object3D`
|
|
35
|
-
* inside the GameComponent is the mesh the author wrapped.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
// NOTE: the fiber types import below is type-only; at runtime this module
|
|
39
|
-
// depends only on `react`. It is still R3F-COUPLED by contract: only a
|
|
40
|
-
// project that already mounts through fiber (createR3FAdapter) can render it
|
|
41
|
-
// meaningfully, and the editor's OID stamping marks R3F-dialect files by
|
|
42
|
-
// their fiber import — project entry files importing this module keep their
|
|
43
|
-
// own `@react-three/fiber` import for the scene itself.
|
|
44
|
-
import type { ThreeElements } from '@react-three/fiber';
|
|
45
|
-
import { createElement, useContext, useEffect, useLayoutEffect, useRef } from 'react';
|
|
46
|
-
import type { Group } from 'three';
|
|
47
|
-
import type { GameComponent } from '../ecs/game-component';
|
|
48
|
-
import { type BehaviorRegistry, EngineBridge, resolveBehaviorClass } from './engine-bridge';
|
|
49
|
-
|
|
50
|
-
export interface BehaviorProps<Reg extends BehaviorRegistry = BehaviorRegistry>
|
|
51
|
-
extends Pick<ThreeElements['group'], 'children'> {
|
|
52
|
-
/** Project-registry key of the GameComponent class to attach. */
|
|
53
|
-
type: Extract<keyof Reg, string>;
|
|
54
|
-
/** Authored config fields, validated through the class's static Zod schema
|
|
55
|
-
* — plus the editor's `userData-oid` stamp, forwarded to the group. */
|
|
56
|
-
[field: string]: unknown;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function Behavior<Reg extends BehaviorRegistry = BehaviorRegistry>(
|
|
60
|
-
props: BehaviorProps<Reg>,
|
|
61
|
-
) {
|
|
62
|
-
const { type, children, 'userData-oid': oid, ...config } = props;
|
|
63
|
-
const bridge = useContext(EngineBridge);
|
|
64
|
-
const groupRef = useRef<Group>(null);
|
|
65
|
-
|
|
66
|
-
// Latest config, readable from the attach effect without re-running it.
|
|
67
|
-
const configRef = useRef(config);
|
|
68
|
-
configRef.current = config;
|
|
69
|
-
// The live instance the config-reapply effect targets (never re-attach).
|
|
70
|
-
const liveRef = useRef<GameComponent | null>(null);
|
|
71
|
-
|
|
72
|
-
// Attachment is part of the committed world, not deferred follow-up work.
|
|
73
|
-
// A layout effect runs before `createR3FAdapter.mount()` resolves its first
|
|
74
|
-
// committed tree, so deterministic hosts can begin stepping immediately
|
|
75
|
-
// without losing every warm-up tick to an unflushed passive effect.
|
|
76
|
-
useLayoutEffect(() => {
|
|
77
|
-
if (!bridge?.registry) {
|
|
78
|
-
// Loud degrade, never a crash: a mount without the adapter's bridge
|
|
79
|
-
// (e.g. a foreign harness rendering this tree directly) — or a bridge
|
|
80
|
-
// whose adapter was built without a `components` registry — renders
|
|
81
|
-
// the children plain; behavior simply doesn't attach.
|
|
82
|
-
console.warn(
|
|
83
|
-
`<Behavior type="${type}">: no EngineBridge ${
|
|
84
|
-
bridge ? 'registry' : 'provider'
|
|
85
|
-
} — component not attached.`,
|
|
86
|
-
);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const target = groupRef.current?.children[0];
|
|
90
|
-
if (!target) {
|
|
91
|
-
console.warn(`<Behavior type="${type}">: no child Object3D to attach to.`);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const registry = bridge.registry;
|
|
95
|
-
const Cls = resolveBehaviorClass(registry, type);
|
|
96
|
-
const instance = new Cls();
|
|
97
|
-
const parsed = Cls.schema ? Cls.schema.parse(configRef.current) : configRef.current;
|
|
98
|
-
Object.assign(instance, parsed);
|
|
99
|
-
// `attach`'s registry info carries the raw authored props so engine-side
|
|
100
|
-
// HMR (`hotSwap`) can re-validate them against a future class version.
|
|
101
|
-
bridge.components.attach(target, instance, {
|
|
102
|
-
key: type,
|
|
103
|
-
props: { ...configRef.current },
|
|
104
|
-
});
|
|
105
|
-
liveRef.current = instance;
|
|
106
|
-
return () => {
|
|
107
|
-
// `detach(node)` removes every component on the node (the manager's
|
|
108
|
-
// public teardown op). This wrapper attaches exactly one component per
|
|
109
|
-
// child, so this is exact here.
|
|
110
|
-
bridge.components.detach(target);
|
|
111
|
-
liveRef.current = null;
|
|
112
|
-
};
|
|
113
|
-
}, [bridge, type]);
|
|
114
|
-
|
|
115
|
-
// Re-apply config on prop change, onto the LIVE instance (no re-attach).
|
|
116
|
-
const configJson = JSON.stringify(config);
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
const instance = liveRef.current;
|
|
119
|
-
if (!instance || !bridge?.registry) return;
|
|
120
|
-
const Cls = resolveBehaviorClass(bridge.registry, type);
|
|
121
|
-
const parsed: Record<string, unknown> = JSON.parse(configJson);
|
|
122
|
-
Object.assign(instance, Cls.schema ? Cls.schema.parse(parsed) : parsed);
|
|
123
|
-
}, [configJson, type, bridge]);
|
|
124
|
-
|
|
125
|
-
// Built with createElement, NOT a JSX literal, deliberately: the editor's
|
|
126
|
-
// OID stamper rewrites JSX syntax only, so a JSX `<group>` here would get
|
|
127
|
-
// behavior.tsx's OWN stamp AND the forwarded call-site oid (a duplicate
|
|
128
|
-
// `userData-oid` attribute). The wrapper group must carry exactly ONE
|
|
129
|
-
// identity — the `<Behavior>` CALL SITE's oid — so the editor can edit the
|
|
130
|
-
// config while projecting the wrapper as a component of its child.
|
|
131
|
-
return createElement(
|
|
132
|
-
'group',
|
|
133
|
-
{
|
|
134
|
-
ref: groupRef,
|
|
135
|
-
// `Behavior: ` WITH the space: the editor's R3F adapter labels this node
|
|
136
|
-
// `Behavior: ${behaviorType}` when it projects it as an entity, but falls
|
|
137
|
-
// back to this `name` when the same node is projected as a component
|
|
138
|
-
// boundary. Without the space the hierarchy showed one of two different
|
|
139
|
-
// labels for the identical concept depending on which branch ran.
|
|
140
|
-
name: `Behavior: ${type}`,
|
|
141
|
-
'userData-oid': oid,
|
|
142
|
-
'userData-behaviorType': type,
|
|
143
|
-
},
|
|
144
|
-
children,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
File without changes
|