@vgai/engine 0.5.2 → 0.5.4

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 (185) hide show
  1. package/README.md +18 -11
  2. package/package.json +10 -6
  3. package/schemas/engine-api.json +1 -68
  4. package/schemas/engine-api.md +1 -32
  5. package/schemas/engine-capabilities.json +30 -42
  6. package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
  7. package/src/adapter/adapter-surface.ts +5 -5
  8. package/src/adapter/authoring.ts +168 -226
  9. package/src/adapter/colyseus-networking-adapter.ts +44 -5
  10. package/src/adapter/first-party-systems.ts +156 -42
  11. package/src/adapter/host-context.ts +177 -46
  12. package/src/adapter/index.ts +47 -51
  13. package/src/adapter/ingest/game-contract.ts +2 -2
  14. package/src/adapter/ingest/scene-capture.ts +18 -19
  15. package/src/adapter/ingest/structural-ids.ts +127 -0
  16. package/src/adapter/ingest/upstream-pin.ts +9 -12
  17. package/src/adapter/loop-gate-report.ts +11 -11
  18. package/src/adapter/rapier-physics-adapter.ts +27 -9
  19. package/src/adapter/root-adapter.ts +217 -0
  20. package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
  21. package/src/adapter/system-adapter.ts +80 -63
  22. package/src/ai/navigation.ts +1 -1
  23. package/src/animation/animation-clock.ts +1 -1
  24. package/src/animation/camera-ownership.ts +1 -2
  25. package/src/animation/cubic-spline-interpolant.ts +132 -0
  26. package/src/animation/theatre-clock-binding.ts +2 -2
  27. package/src/animation/theatre-object-binding.ts +4 -4
  28. package/src/animation/xstate-animation-binding.ts +75 -5
  29. package/src/{scene/schema → asset-formats}/camera.ts +2 -5
  30. package/src/{scene/schema → asset-formats}/collider.ts +2 -5
  31. package/src/asset-formats/index.ts +54 -0
  32. package/src/{scene/schema → asset-formats}/instances.ts +10 -6
  33. package/src/{scene/schema → asset-formats}/light.ts +3 -6
  34. package/src/{scene/schema → asset-formats}/material.ts +4 -7
  35. package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
  36. package/src/asset-formats/parse.ts +39 -0
  37. package/src/{scene/schema → asset-formats}/particles.ts +3 -6
  38. package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
  39. package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
  40. package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
  41. package/src/asset-parse-error.ts +33 -0
  42. package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
  43. package/src/assets.ts +1 -1
  44. package/src/audio/wav-encode.ts +9 -9
  45. package/src/canvas-react/engine-bridge.ts +59 -0
  46. package/src/canvas-react/index.ts +50 -0
  47. package/src/canvas-react/pixi-primitive.tsx +202 -0
  48. package/src/canvas-react/pixi-react-adapter.tsx +290 -0
  49. package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
  50. package/src/canvas-react/world-context.ts +328 -0
  51. package/src/core/frame-pacing.ts +100 -0
  52. package/src/core/game-loop.ts +50 -28
  53. package/src/core/seeded-random.ts +7 -7
  54. package/src/core/sim-clock.ts +388 -0
  55. package/src/core/system-runner.ts +17 -63
  56. package/src/core/types.ts +34 -15
  57. package/src/data/data-asset.ts +3 -3
  58. package/src/data/data-check-core.ts +6 -7
  59. package/src/data/data-ref.ts +11 -11
  60. package/src/data/vite-plugin-data.ts +10 -10
  61. package/src/{scene/defaults.ts → defaults.ts} +18 -40
  62. package/src/dev/render-debug-adapter.ts +1 -1
  63. package/src/dev/webgl-frame-capture.ts +1 -1
  64. package/src/ecs/scene-index.ts +439 -0
  65. package/src/ecs/scene-query.ts +43 -0
  66. package/src/{scene → ecs}/user-data.ts +17 -36
  67. package/src/index.ts +7 -9
  68. package/src/input/input-manager.ts +29 -32
  69. package/src/input/input-types.ts +2 -2
  70. package/src/input/schema.ts +5 -5
  71. package/src/loader.ts +57 -0
  72. package/src/manifest/editor-port.ts +69 -0
  73. package/src/manifest/filename.ts +49 -0
  74. package/src/manifest/index.ts +8 -2
  75. package/src/manifest/load-file.ts +11 -0
  76. package/src/manifest/load.ts +65 -77
  77. package/src/manifest/locate.ts +55 -0
  78. package/src/manifest/schema.ts +400 -233
  79. package/src/{scene → physics}/collider-dimensions.ts +3 -3
  80. package/src/physics/physics-registry.ts +1 -1
  81. package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
  82. package/src/pixi/index.ts +43 -0
  83. package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
  84. package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
  85. package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
  86. package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
  87. package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
  88. package/src/react/unmanaged-root-detector.ts +26 -1
  89. package/src/react/use-data.ts +5 -5
  90. package/src/react/use-selection.tsx +15 -42
  91. package/src/react/{game-state.tsx → world-state.tsx} +44 -46
  92. package/src/render/auto-batcher.ts +1 -2
  93. package/src/{scene → render}/instance-mesh.ts +1 -1
  94. package/src/{scene → render}/light-camera-factory.ts +14 -13
  95. package/src/render/lod.ts +17 -0
  96. package/src/{scene → render}/material-factory.ts +8 -7
  97. package/src/{scene → render}/particles-factory.ts +62 -12
  98. package/src/render/render-batch-system.ts +14 -41
  99. package/src/render/render-features.ts +1 -1
  100. package/src/render/render-settings.ts +1 -2
  101. package/src/render/spark-renderer-lifecycle.ts +1 -1
  102. package/src/runtime/create-runtime.ts +352 -463
  103. package/src/runtime/debug-bridge.ts +148 -93
  104. package/src/runtime/debug-registry.ts +79 -54
  105. package/src/runtime/dev-layers.ts +40 -0
  106. package/src/runtime/frame-selector-cache.ts +4 -4
  107. package/src/runtime/game.ts +618 -369
  108. package/src/runtime/gameplay-rng-trap.ts +6 -7
  109. package/src/runtime/input-router.ts +11 -11
  110. package/src/runtime/mount-game.ts +54 -55
  111. package/src/runtime/mount-manifest.ts +154 -150
  112. package/src/runtime/presentation.ts +141 -0
  113. package/src/runtime/render-audio-control.ts +64 -53
  114. package/src/runtime/render-control.ts +45 -51
  115. package/src/runtime/render-seed.ts +3 -4
  116. package/src/runtime/state-bridge.ts +17 -18
  117. package/src/runtime/types.ts +94 -61
  118. package/src/setup/setup-renderer.ts +11 -6
  119. package/src/world3d-react/engine-bridge.ts +46 -33
  120. package/src/world3d-react/index.ts +31 -26
  121. package/src/world3d-react/r3f-adapter.tsx +211 -96
  122. package/src/world3d-react/r3f-root-factory.tsx +91 -0
  123. package/src/world3d-react/renderer-config.ts +137 -0
  124. package/src/world3d-react/world-context.ts +141 -111
  125. package/schemas/entity2d.schema.json +0 -468
  126. package/schemas/prefab.schema.json +0 -9992
  127. package/schemas/scn2d.schema.json +0 -494
  128. package/schemas/vscn.schema.json +0 -10851
  129. package/src/adapter/game-adapter.ts +0 -164
  130. package/src/adapter/ingest/overlay-applier.ts +0 -207
  131. package/src/adapter/ingest/overlay-apply.ts +0 -168
  132. package/src/adapter/ingest/overlay-file.ts +0 -126
  133. package/src/adapter/ingest/overlay-report.ts +0 -176
  134. package/src/animation/gsap-registration.ts +0 -184
  135. package/src/audio/audio-introspection.ts +0 -290
  136. package/src/audio/index.ts +0 -39
  137. package/src/audio/tone-clock-binding.ts +0 -98
  138. package/src/audio/tone-context.ts +0 -175
  139. package/src/audio/tone-offline-render.ts +0 -167
  140. package/src/ecs/component-manager.ts +0 -814
  141. package/src/ecs/game-component.ts +0 -260
  142. package/src/ecs/hmr-swap-report.ts +0 -65
  143. package/src/physics/trigger-dispatch.ts +0 -97
  144. package/src/react/root-adapter.tsx +0 -49
  145. package/src/scene/asset-paths.ts +0 -121
  146. package/src/scene/asset-ref-check.ts +0 -248
  147. package/src/scene/component-registry.ts +0 -51
  148. package/src/scene/parse.ts +0 -204
  149. package/src/scene/scene-apply.ts +0 -407
  150. package/src/scene/scene-diff-schema.ts +0 -115
  151. package/src/scene/scene-diff-types.ts +0 -29
  152. package/src/scene/scene-loader.ts +0 -1526
  153. package/src/scene/scene-query.ts +0 -63
  154. package/src/scene/scene-types.ts +0 -33
  155. package/src/scene/scene-version.ts +0 -40
  156. package/src/scene/schema/animation.ts +0 -46
  157. package/src/scene/schema/audio.ts +0 -25
  158. package/src/scene/schema/entity-ref.ts +0 -78
  159. package/src/scene/schema/entity.ts +0 -189
  160. package/src/scene/schema/index.ts +0 -51
  161. package/src/scene/schema/joint.ts +0 -26
  162. package/src/scene/schema/physics.ts +0 -49
  163. package/src/scene/schema/scene-file.ts +0 -292
  164. package/src/scene/schema/shadow.ts +0 -24
  165. package/src/scene/schema/spline.ts +0 -21
  166. package/src/world2d/asset-paths2d.ts +0 -44
  167. package/src/world2d/capture-to-scene2d.ts +0 -52
  168. package/src/world2d/collision-2d.ts +0 -99
  169. package/src/world2d/entity2d-asset.ts +0 -22
  170. package/src/world2d/index.ts +0 -91
  171. package/src/world2d/physics2d-transform.ts +0 -173
  172. package/src/world2d/physics2d-units.ts +0 -10
  173. package/src/world2d/pixi-game-adapter.ts +0 -439
  174. package/src/world2d/pixi-surface.ts +0 -78
  175. package/src/world2d/scene2d-identity.ts +0 -49
  176. package/src/world2d/scene2d-loader.ts +0 -433
  177. package/src/world2d/schema/entity2d.ts +0 -163
  178. package/src/world2d/schema/physics2d.ts +0 -64
  179. package/src/world2d/schema/sprite.ts +0 -99
  180. package/src/world2d/schema/tilemap.ts +0 -39
  181. package/src/world2d/schema/tuples2d.ts +0 -25
  182. package/src/world2d/transform-writer-2d.ts +0 -42
  183. package/src/world2d/types.ts +0 -74
  184. package/src/world3d-react/behavior.tsx +0 -146
  185. /package/src/{scene → render}/mesh-shadow.ts +0 -0
@@ -1,260 +0,0 @@
1
- import type RAPIER2D from '@dimforge/rapier2d-compat';
2
- import type RAPIER3D from '@dimforge/rapier3d-compat';
3
- import type * as PIXI from 'pixi.js';
4
- import type * as THREE from 'three';
5
- import type { z } from 'zod';
6
- import type { SystemPhaseName } from '../core/types';
7
- import type { AdapterSurface, WorldInstance } from '../runtime/game';
8
- import type { GameContext } from '../runtime/types';
9
- import type { World2DContext } from '../world2d/types';
10
-
11
- /**
12
- * Map a {@link AdapterSurface} to its native world node type (T7.2, D8 —
13
- * `docs/GAME-COMPONENT-GENERALIZATION.md` §2). `react` maps to `never`: react
14
- * entities host no GameComponents (they render from game state via the T7.4
15
- * state bridge instead).
16
- */
17
- export type NodeOf<K extends AdapterSurface> = K extends 'threejs'
18
- ? THREE.Object3D
19
- : K extends 'pixijs'
20
- ? PIXI.Container
21
- : never;
22
-
23
- /** Map a {@link AdapterSurface} to its native Rapier rigid-body type. */
24
- export type BodyOf<K extends AdapterSurface> = K extends 'threejs'
25
- ? RAPIER3D.RigidBody
26
- : K extends 'pixijs'
27
- ? RAPIER2D.RigidBody
28
- : never;
29
-
30
- /** Map a {@link AdapterSurface} to its native Rapier collider type. */
31
- export type ColliderOf<K extends AdapterSurface> = K extends 'threejs'
32
- ? RAPIER3D.Collider
33
- : K extends 'pixijs'
34
- ? RAPIER2D.Collider
35
- : never;
36
-
37
- /** Native lifecycle context for a component's declared surface. */
38
- export type ComponentContextOf<K extends AdapterSurface> = K extends 'pixijs'
39
- ? World2DContext
40
- : GameContext;
41
-
42
- /**
43
- * Base class for game components attached to entities.
44
- *
45
- * Generalized (T7.2, D8 — `docs/GAME-COMPONENT-GENERALIZATION.md`) over the
46
- * world kind `K` (default `'threejs'`, so every existing component — all of
47
- * which extend the bare `GameComponent` — compiles and runs unchanged). The
48
- * world's native node IS the entity — there is no separate entity id and no
49
- * mirror/sync layer (Track A). Each component instance holds a direct
50
- * reference to its node and (if any) its Rapier rigid body/collider. The
51
- * ComponentManager wires these up in `attach()`.
52
- *
53
- * Similar to Unity's MonoBehaviour or Godot's Node scripts:
54
- * - `init()` — called once after the entity is fully constructed
55
- * - `update(dt, ctx)` — called every fixed timestep, in the component's phase
56
- * - `dispose(ctx)` — called when the entity is destroyed
57
- * - `onTriggerEnter/onTriggerExit` — called when a sensor collider overlap
58
- * starts/stops, with the other party's native node
59
- *
60
- * State lives on `this` (instance properties). On HMR, the engine swaps the
61
- * prototype via Object.setPrototypeOf — instance state survives, method bodies
62
- * update to the new code.
63
- *
64
- * `update` is deliberately mandatory, not optional like `init`/`dispose`
65
- * (owner decision, issue #101): components are for BEHAVIOR. If your
66
- * subclass's `update(dt, ctx)` body is empty, that is a signal the class may
67
- * not need to be a component at all — a plain state-holder with only
68
- * `init`/event methods (score/hits counters, etc.) usually belongs as a
69
- * plain object or a static-scenery/UI object instead (see the "GameComponents
70
- * are for things with behavior only" rule).
71
- */
72
- /**
73
- * Cross-copy brand (`Symbol.for` is process-global, so every physical copy of
74
- * the engine shares ONE symbol): `x.prototype instanceof GameComponent` is
75
- * FALSE when a project resolves TWO copies of the engine at once — the
76
- * published `@vgai/engine/*` subpaths next to the `@engine/*` source alias,
77
- * exactly what a scaffolded project's node-side scripts do (meteor-dodge
78
- * dry-run friction: `validate-scenes --components` reported every field of a
79
- * pristine scaffold's `SceneCamera` as unknown). Structural checks
80
- * (`isGameComponentClass`) test this brand as well as `instanceof`.
81
- */
82
- export const GAME_COMPONENT_BRAND = Symbol.for('vgai.GameComponent');
83
-
84
- /**
85
- * HMR class lineage. A true component hot-swap deliberately replaces a live
86
- * instance's prototype with the newly imported class, but other already-live
87
- * project modules can still hold the previous class object. Native
88
- * `instanceof PreviousClass` would then become false even though the object is
89
- * still the same component under the same hot-reload lineage. That broke both
90
- * ordinary trigger guards (`component instanceof PlayerController`) and the
91
- * canonical React-state selector (`game.queryByComponent(PlayerController)`).
92
- *
93
- * Keep this as class-to-class lineage rather than a string/name registry:
94
- * minified classes, aliases, and two unrelated components with the same name
95
- * remain distinct. The WeakMap owns no game/entity state and introduces no
96
- * second identity model; it records only the exact constructor pairs the
97
- * existing HMR boundary has actually swapped.
98
- */
99
- type HmrComponentConstructor = abstract new (...args: never[]) => object;
100
-
101
- const hmrLineages = new WeakMap<HmrComponentConstructor, Set<HmrComponentConstructor>>();
102
-
103
- /** @internal Called only by ComponentManager's successful HMR swap path. */
104
- export function linkGameComponentHmrClasses(
105
- previous: HmrComponentConstructor,
106
- next: HmrComponentConstructor,
107
- ): void {
108
- const previousLineage = hmrLineages.get(previous);
109
- const nextLineage = hmrLineages.get(next);
110
- const merged = new Set<HmrComponentConstructor>([previous, next]);
111
- if (previousLineage) {
112
- for (const member of previousLineage) merged.add(member);
113
- }
114
- if (nextLineage) {
115
- for (const member of nextLineage) merged.add(member);
116
- }
117
- for (const member of merged) hmrLineages.set(member, merged);
118
- }
119
-
120
- function classesShareHmrLineage(a: HmrComponentConstructor, b: HmrComponentConstructor): boolean {
121
- return a === b || hmrLineages.get(a)?.has(b) === true;
122
- }
123
-
124
- export abstract class GameComponent<K extends AdapterSurface = 'threejs'> {
125
- /** Which phase this component's update runs in. Override in subclasses. */
126
- static phase: SystemPhaseName = 'gameLogic';
127
-
128
- /** See {@link GAME_COMPONENT_BRAND} — inherited by every subclass. */
129
- static readonly [GAME_COMPONENT_BRAND] = true;
130
-
131
- /**
132
- * Preserve ordinary JavaScript `instanceof` across a true HMR prototype
133
- * swap. Subclasses inherit this static hook, so `value instanceof
134
- * PlayerController` first performs the native prototype-chain check and,
135
- * only when that fails, accepts an exact constructor lineage previously
136
- * linked by ComponentManager.hotSwap(). Non-HMR values retain byte-for-byte
137
- * native semantics.
138
- */
139
- static [Symbol.hasInstance](value: unknown): boolean {
140
- // biome-ignore lint/complexity/noThisInStatic: Symbol.hasInstance is inherited; `this` must be the queried subclass, not the GameComponent base.
141
- const expected = this as HmrComponentConstructor;
142
- const nativeMatch = Function.prototype[Symbol.hasInstance].call(expected, value) as boolean;
143
- if (nativeMatch) return true;
144
- if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false;
145
- const actual = (value as { constructor?: unknown }).constructor;
146
- return (
147
- typeof actual === 'function' &&
148
- classesShareHmrLineage(expected, actual as HmrComponentConstructor)
149
- );
150
- }
151
-
152
- /**
153
- * Runtime-checkable declaration of which {@link AdapterSurface} this component
154
- * is written for (T7.2, D8 §3 — `docs/GAME-COMPONENT-GENERALIZATION.md`).
155
- * TS's `K` type parameter is erased at runtime, so this static is what the
156
- * ComponentManager checks at attach time: a node whose world's kind
157
- * doesn't match `declaredKind` THROWS at attach (a `GameComponent<'pixijs'>`
158
- * attached to a threejs entity, or vice versa). Kind-agnostic components —
159
- * written only against `node`/`this.world`, never the typed `object3D`
160
- * accessor or a kind-specific field — may declare `'any'` and attach in
161
- * any world components are legal in (every world except `'react'`, which
162
- * always throws regardless of `declaredKind` — see the ComponentManager).
163
- */
164
- static declaredKind: AdapterSurface | 'any' = 'threejs';
165
-
166
- /**
167
- * Optional Zod object schema for this component's authored fields. When set,
168
- * scene data is validated + defaulted through `schema.parse(data)` before being
169
- * assigned onto the instance. Also powers (later) the editor's schema-driven
170
- * inspector and HMR defaults.
171
- */
172
- static schema?: z.ZodObject<z.ZodRawShape>;
173
-
174
- /** The native world node. The node IS the entity. */
175
- node!: NodeOf<K>;
176
-
177
- /** The world instance this component's entity lives in (wired at attach). */
178
- world!: WorldInstance;
179
-
180
- /**
181
- * Typed accessor, threejs roots only — kept for compatibility. On a
182
- * `GameComponent` (default `K`) this types as `THREE.Object3D`, so every
183
- * existing component compiles unchanged. On a non-threejs instance it
184
- * THROWS a descriptive error rather than ever returning a wrong-kind node —
185
- * use `this.node` (or `this.world.kind` to check first) instead.
186
- */
187
- get object3D(): THREE.Object3D {
188
- const kind = this.world?.kind;
189
- if (kind !== undefined && kind !== 'threejs') {
190
- throw new Error(
191
- `${this.constructor.name}.object3D: this component's world (kind: "${kind}") is not a ` +
192
- 'threejs world — `object3D` only ever returns a THREE.Object3D; use `this.node` instead.',
193
- );
194
- }
195
- // When the world isn't wired yet (silo-attached 2D components until
196
- // T7.3; bare instances), fall back to the static declaration so a
197
- // GameComponent<'pixijs'> can never silently hand out its Container
198
- // typed as an Object3D.
199
- if (kind === undefined) {
200
- const declared = (this.constructor as typeof GameComponent).declaredKind;
201
- if (declared !== 'threejs' && declared !== 'any') {
202
- throw new Error(
203
- `${this.constructor.name}.object3D: this component declares kind "${declared}" — ` +
204
- '`object3D` only ever returns a THREE.Object3D; use `this.node` instead.',
205
- );
206
- }
207
- }
208
- return this.node as unknown as THREE.Object3D;
209
- }
210
-
211
- /** The entity's native rigid body, if it has physics. */
212
- rigidBody: BodyOf<K> | null = null;
213
-
214
- /** The entity's native collider, if it has physics. */
215
- collider: ColliderOf<K> | null = null;
216
-
217
- /** Called once after the entity is fully constructed (node, physics, etc.). */
218
- init?(ctx: ComponentContextOf<K>): void | Promise<void>;
219
-
220
- /** Called every fixed timestep. */
221
- abstract update(dt: number, ctx: ComponentContextOf<K>): void;
222
-
223
- /** Called when the entity is destroyed or the component is removed. */
224
- dispose?(ctx: ComponentContextOf<K>): void;
225
-
226
- /** Called when a sensor overlap with `other` begins. */
227
- onTriggerEnter?(other: NodeOf<K>, ctx: ComponentContextOf<K>): void;
228
-
229
- /** Called when a sensor overlap with `other` ends. */
230
- onTriggerExit?(other: NodeOf<K>, ctx: ComponentContextOf<K>): void;
231
- }
232
-
233
- /** Constructor type for one surface's GameComponent subclasses. The default
234
- * remains Three so existing scene registries stay surface-narrowed. */
235
- export type GameComponentClass<K extends AdapterSurface = 'threejs'> =
236
- (new () => GameComponent<K>) & {
237
- declaredKind: AdapterSurface | 'any';
238
- phase?: SystemPhaseName;
239
- schema?: z.ZodObject<z.ZodRawShape>;
240
- };
241
-
242
- /** Cross-surface constructor contract for tooling such as HMR and script
243
- * discovery. React contributes no class because React nodes cannot host
244
- * GameComponents. Runtime attach still enforces each class's declaredKind. */
245
- export type AnyGameComponentClass = GameComponentClass<'threejs'> | GameComponentClass<'pixijs'>;
246
-
247
- export function gameComponentClassMatchesSurface(
248
- component: AnyGameComponentClass,
249
- surface: 'threejs',
250
- ): component is GameComponentClass<'threejs'>;
251
- export function gameComponentClassMatchesSurface(
252
- component: AnyGameComponentClass,
253
- surface: 'pixijs',
254
- ): component is GameComponentClass<'pixijs'>;
255
- export function gameComponentClassMatchesSurface(
256
- component: AnyGameComponentClass,
257
- surface: 'threejs' | 'pixijs',
258
- ): boolean {
259
- return component.declaredKind === surface || component.declaredKind === 'any';
260
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * HMR swap-miss report — T5.4 (docs/BACKBONE-TASKS.md's Track 5 table:
3
- * "Deterministic HMR: registry-keyed hotSwap (not `constructor.name`), schema
4
- * re-parse on swap, loud swap-miss warnings"). `ComponentManager.hotSwap` keys
5
- * live instances by the `componentRegistry` name recorded on them at attach
6
- * (see `component-manager.ts`'s `attach`/`hotSwap`), NOT the live
7
- * `constructor.name` — fragile under minification, class renames, or two
8
- * classes sharing one name. This module is the structured, greppable
9
- * `console.warn` a swap-miss warrants, mirrored on the established
10
- * prefix + JSON-payload pattern (`packages/editor/src/achieved-tier.ts`'s
11
- * `TIER_SHORTFALL_PREFIX`, `packages/editor/src/authoring/overlay-report.ts`'s
12
- * `OVERLAY_REPORT_PREFIX`) — kept dependency-free (no `window`/editor-console
13
- * import) so it stays importable from a headless engine/runtime context,
14
- * same discipline as those two files.
15
- *
16
- * Two independent miss reasons, per the BACKBONE-TASKS detail note:
17
- * - `'no-live-instance'` — the incoming hot update's key (the
18
- * `componentRegistry` name the recompiled module was re-exported under)
19
- * matches no currently-attached instance's recorded registry key (nor,
20
- * for an instance attached WITHOUT a registry key — an ad-hoc,
21
- * code-attached component — its `constructor.name`). This is the
22
- * "renamed class → loud miss" case: a class rename changes the export
23
- * key a hot update arrives under, so no live instance is found under the
24
- * NEW key — the OLD prototype is simply left in place (never silently
25
- * dropped, never crashed).
26
- * - `'schema-reparse-failed'` — at least one matched instance's swap
27
- * succeeded (prototype replaced), but re-validating its authored props
28
- * against the NEW class's `static schema` threw; that instance's fields
29
- * are left exactly as they were (last-good props), never partially or
30
- * invalidly overwritten.
31
- */
32
-
33
- /** Greppable prefix shared by every swap-miss log line this module produces. */
34
- export const HMR_SWAP_MISS_PREFIX = 'hmr-swap-miss';
35
-
36
- export type HmrSwapMissReport =
37
- | { key: string; reason: 'no-live-instance' }
38
- | { key: string; reason: 'schema-reparse-failed'; error: string };
39
-
40
- /**
41
- * The one shared message shape — greppable on the prefix, JSON-parseable on
42
- * the trailing payload (the LAST `{...}` in the string — the human-readable
43
- * `detail` clause never itself contains a brace, even for the
44
- * `'schema-reparse-failed'` reason, whose full Zod error text — itself a
45
- * JSON-issues blob, braces included — is carried only in `report.error`
46
- * inside that trailing payload, never inlined into `detail`).
47
- */
48
- export function formatHmrSwapMissMessage(report: HmrSwapMissReport): string {
49
- const detail =
50
- report.reason === 'no-live-instance'
51
- ? `no live instance's registry key (or, for an ad-hoc/unregistered instance, its class name) ` +
52
- `matches "${report.key}" — the hot update had nothing to swap (old class, if any, left in place)`
53
- : `schema re-parse failed for "${report.key}" after swap — keeping the instance's last-good ` +
54
- `props (Zod error in the trailing payload's "error" field)`;
55
- return (
56
- `${HMR_SWAP_MISS_PREFIX} ${detail} (T5.4, docs/BACKBONE-TASKS.md). ` +
57
- `${HMR_SWAP_MISS_PREFIX} ${JSON.stringify(report)}`
58
- );
59
- }
60
-
61
- /** Emit the ONE structured `console.warn` a swap-miss warrants — never a silent no-op. */
62
- export function logHmrSwapMiss(report: HmrSwapMissReport): void {
63
- // biome-ignore lint/suspicious/noConsole: structured, greppable swap-miss warning (T5.4) — mirrors achieved-tier.ts/overlay-report.ts's deliberate direct console.warn
64
- console.warn(formatHmrSwapMissMessage(report));
65
- }
@@ -1,97 +0,0 @@
1
- import type RAPIER from '@dimforge/rapier3d-compat';
2
- import type * as THREE from 'three';
3
- import type { ComponentManager } from '../ecs/component-manager';
4
- import type { GameContext } from '../runtime/types';
5
- import type { CollisionHandler } from './collision-system';
6
- import type { PhysicsRegistry } from './physics-registry';
7
-
8
- /**
9
- * Build a {@link CollisionHandler} that turns Rapier sensor overlaps into
10
- * `onTriggerEnter` / `onTriggerExit` callbacks on the GameComponents of the two
11
- * involved Object3Ds.
12
- *
13
- * Rapier collision events report collider *handles*; we resolve each to its
14
- * Object3D via the physics registry's reverse index, then notify each side's
15
- * components with the *other* Object3D. Only sensor-involved collisions dispatch
16
- * triggers (a solid-vs-solid contact is a regular collision, not a trigger).
17
- *
18
- * Tracks currently-active trigger partners so that if one side is unregistered
19
- * (despawned) from the physics registry before Rapier's own exit event is
20
- * drained, the still-live side still gets its `onTriggerExit` (T1.12d) — see
21
- * `physics.onRemove` below.
22
- *
23
- * Register with `collisions.onCollision(createTriggerDispatch(...))`.
24
- */
25
- export function createTriggerDispatch(
26
- physics: PhysicsRegistry,
27
- components: ComponentManager,
28
- rapierWorld: RAPIER.World,
29
- ctx: GameContext,
30
- ): CollisionHandler {
31
- const activePartners = new Map<THREE.Object3D, Set<THREE.Object3D>>();
32
-
33
- const link = (a: THREE.Object3D, b: THREE.Object3D): void => {
34
- let set = activePartners.get(a);
35
- if (!set) {
36
- set = new Set();
37
- activePartners.set(a, set);
38
- }
39
- set.add(b);
40
- };
41
- const unlink = (a: THREE.Object3D, b: THREE.Object3D): void => {
42
- const set = activePartners.get(a);
43
- if (!set) return;
44
- set.delete(b);
45
- if (set.size === 0) activePartners.delete(a);
46
- };
47
-
48
- // Flush pending exits at unregister: an entity removed from the physics
49
- // registry (e.g. despawned) may still have queued Rapier "stopped" events
50
- // that never reach drain(), or may never generate one at all (the collider
51
- // is simply gone). Either way, any object still tracked as its trigger
52
- // partner must get its onTriggerExit now, not silently never.
53
- physics.onRemove((object3D) => {
54
- const partners = activePartners.get(object3D);
55
- if (!partners || partners.size === 0) return;
56
- for (const other of partners) {
57
- notify(components, other, object3D, false, ctx);
58
- notify(components, object3D, other, false, ctx);
59
- unlink(other, object3D);
60
- }
61
- activePartners.delete(object3D);
62
- });
63
-
64
- return (handleA, handleB, started) => {
65
- const colA = rapierWorld.getCollider(handleA);
66
- const colB = rapierWorld.getCollider(handleB);
67
- if (!colA?.isSensor() && !colB?.isSensor()) return;
68
-
69
- const objA = physics.getByColliderHandle(handleA);
70
- const objB = physics.getByColliderHandle(handleB);
71
- if (!objA || !objB) return;
72
-
73
- if (started) {
74
- link(objA, objB);
75
- link(objB, objA);
76
- } else {
77
- unlink(objA, objB);
78
- unlink(objB, objA);
79
- }
80
-
81
- notify(components, objA, objB, started, ctx);
82
- notify(components, objB, objA, started, ctx);
83
- };
84
- }
85
-
86
- function notify(
87
- components: ComponentManager,
88
- self: THREE.Object3D,
89
- other: THREE.Object3D,
90
- started: boolean,
91
- ctx: GameContext,
92
- ): void {
93
- for (const inst of components.getComponents(self)) {
94
- if (started) inst.onTriggerEnter?.(other, ctx);
95
- else inst.onTriggerExit?.(other, ctx);
96
- }
97
- }
@@ -1,49 +0,0 @@
1
- import { type ComponentType, createElement } from 'react';
2
- import { flushSync } from 'react-dom';
3
- import { createRoot } from 'react-dom/client';
4
- import type { MountedReactGame, ReactRootAdapter, ReactWorldHost } from '../runtime/create-runtime';
5
- import { isAdapterRegistered, registerAdapter } from '../runtime/mount-game';
6
- import { GameProvider } from './game-state';
7
-
8
- interface ReactEntryModule {
9
- readonly default?: ComponentType;
10
- }
11
-
12
- /** Construct one canonical React root adapter for direct host composition. */
13
- export function createReactRootAdapter(id: string, Entry: ComponentType): ReactRootAdapter {
14
- return {
15
- id,
16
- async mount(host: ReactWorldHost): Promise<MountedReactGame> {
17
- if (!host.game) throw new Error(`React root "${id}" requires a Game host.`);
18
- const reactRoot = createRoot(host.container);
19
- flushSync(() => {
20
- reactRoot.render(createElement(GameProvider, { game: host.game! }, createElement(Entry)));
21
- });
22
- return {
23
- kind: 'react',
24
- container: host.container,
25
- drivesOwnLoop: false,
26
- dispose: () => reactRoot.unmount(),
27
- };
28
- },
29
- };
30
- }
31
-
32
- /**
33
- * Register the engine's React adapter-root factory. This module is an
34
- * explicit optional React entry point: engine core never imports it, while a
35
- * React game host imports it once and receives the canonical root lifecycle.
36
- */
37
- export function registerReactAdapter(): void {
38
- if (isAdapterRegistered('react')) return;
39
- registerAdapter('react', (root, { entryModule }) => {
40
- const Entry = (entryModule as ReactEntryModule | undefined)?.default;
41
- if (!Entry) {
42
- throw new Error(
43
- `React root "${root.id}" entry "${root.entry ?? '(missing)'}" must default-export a component.`,
44
- );
45
- }
46
-
47
- return { kind: 'react', adapter: createReactRootAdapter(root.id, Entry) };
48
- });
49
- }
@@ -1,121 +0,0 @@
1
- import type { SceneEntity, SceneEnvironment, SceneFile } from './scene-types';
2
- import type { SceneMaterial } from './schema/material';
3
-
4
- /**
5
- * Every material field that holds a texture-file path, as [group, key] pairs —
6
- * group `null` for top-level fields, otherwise the physical feature group
7
- * (clearcoat/transmission/sheen/iridescence) the key nests under. One table
8
- * drives both collect and rename so the two can never disagree;
9
- * `asset-paths.test.ts` derives the expected set from SceneMaterialSchema's
10
- * `.describe()` strings so this list can't silently fall behind the schema.
11
- */
12
- const MATERIAL_TEXTURE_FIELDS: ReadonlyArray<readonly [string | null, string]> = [
13
- [null, 'map'],
14
- [null, 'normalMap'],
15
- [null, 'emissiveMap'],
16
- [null, 'aoMap'],
17
- [null, 'lightMap'],
18
- [null, 'roughnessMap'],
19
- [null, 'metalnessMap'],
20
- [null, 'displacementMap'],
21
- ['clearcoat', 'clearcoatMap'],
22
- ['clearcoat', 'clearcoatRoughnessMap'],
23
- ['transmission', 'transmissionMap'],
24
- ['sheen', 'sheenColorMap'],
25
- ['sheen', 'sheenRoughnessMap'],
26
- ['iridescence', 'iridescenceMap'],
27
- ['iridescence', 'iridescenceThicknessMap'],
28
- ];
29
-
30
- /** Visit every authored texture path on a material (inline or override). */
31
- function forEachMaterialTexturePath(
32
- material: Partial<SceneMaterial>,
33
- fn: (holder: Record<string, unknown>, key: string, value: string) => void,
34
- ): void {
35
- for (const [group, key] of MATERIAL_TEXTURE_FIELDS) {
36
- const holder = group
37
- ? (material as Record<string, unknown>)[group]
38
- : (material as Record<string, unknown>);
39
- if (!holder || typeof holder !== 'object') continue;
40
- const value = (holder as Record<string, unknown>)[key];
41
- if (typeof value === 'string') fn(holder as Record<string, unknown>, key, value);
42
- }
43
- }
44
-
45
- function isFilePrefab(value: string): boolean {
46
- return value.includes('/');
47
- }
48
-
49
- function walkEntities(entities: SceneEntity[], fn: (e: SceneEntity) => void): void {
50
- for (const e of entities) {
51
- fn(e);
52
- if (e.children) walkEntities(e.children, fn);
53
- }
54
- }
55
-
56
- /**
57
- * Collect all external asset paths referenced by a scene's entities and environment.
58
- */
59
- export function collectAssetPaths(
60
- entities: SceneEntity[],
61
- environment?: SceneEnvironment,
62
- ): Set<string> {
63
- const paths = new Set<string>();
64
-
65
- walkEntities(entities, (e) => {
66
- if (e.mesh?.type === 'gltf' && e.mesh.src) paths.add(e.mesh.src);
67
- if (e.mesh?.instances) paths.add(e.mesh.instances);
68
-
69
- if (e.material) {
70
- forEachMaterialTexturePath(e.material, (_holder, _key, value) => paths.add(value));
71
- }
72
-
73
- if (e.materialRef) paths.add(e.materialRef);
74
-
75
- if (e.particles?.material?.map) paths.add(e.particles.material.map);
76
-
77
- if (e.audio?.src) paths.add(e.audio.src);
78
- if (e.prefab && isFilePrefab(e.prefab)) paths.add(e.prefab);
79
- });
80
-
81
- if (environment?.skybox) paths.add(environment.skybox);
82
-
83
- return paths;
84
- }
85
-
86
- /** Replace oldPath with newPath in a single entity's asset fields. Mutates in place. */
87
- function renameInEntity(e: SceneEntity, oldPath: string, newPath: string): void {
88
- if (e.mesh?.type === 'gltf' && e.mesh.src === oldPath) {
89
- e.mesh.src = newPath;
90
- }
91
- if (e.mesh?.instances === oldPath) e.mesh.instances = newPath;
92
-
93
- if (e.material) {
94
- forEachMaterialTexturePath(e.material, (holder, key, value) => {
95
- if (value === oldPath) holder[key] = newPath;
96
- });
97
- }
98
-
99
- if (e.materialRef === oldPath) e.materialRef = newPath;
100
-
101
- if (e.particles?.material?.map === oldPath) e.particles.material.map = newPath;
102
-
103
- if (e.audio?.src === oldPath) e.audio.src = newPath;
104
- if (e.prefab === oldPath) e.prefab = newPath;
105
- }
106
-
107
- /**
108
- * Deep-clone a scene document and replace all occurrences of oldPath with newPath
109
- * in asset reference fields. Returns the new document.
110
- */
111
- export function applyAssetRename(doc: SceneFile, oldPath: string, newPath: string): SceneFile {
112
- const cloned: SceneFile = JSON.parse(JSON.stringify(doc));
113
-
114
- walkEntities(cloned.entities, (e) => renameInEntity(e, oldPath, newPath));
115
-
116
- if (cloned.environment?.skybox === oldPath) {
117
- cloned.environment.skybox = newPath;
118
- }
119
-
120
- return cloned;
121
- }