@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,173 +0,0 @@
1
- import { type Container, Matrix } from 'pixi.js';
2
- import type { Entity2D } from './schema/entity2d';
3
- import type { SceneCollider2D } from './schema/physics2d';
4
-
5
- const EPSILON = 1e-6;
6
-
7
- export interface PhysicsPose2D {
8
- matrix: Matrix;
9
- position: [number, number];
10
- rotation: number;
11
- scale: [number, number];
12
- }
13
-
14
- export type ScaledCollider2D =
15
- | { shape: 'cuboid'; halfExtents: [number, number] }
16
- | { shape: 'ball'; radius: number }
17
- | { shape: 'capsule'; halfHeight: number; radius: number };
18
-
19
- function isFiniteMatrix(matrix: Matrix): boolean {
20
- return [matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty].every(Number.isFinite);
21
- }
22
-
23
- function linearMetrics(matrix: Matrix): {
24
- determinant: number;
25
- orthogonality: number;
26
- scaleX: number;
27
- scaleY: number;
28
- } {
29
- return {
30
- determinant: matrix.a * matrix.d - matrix.b * matrix.c,
31
- orthogonality: Math.abs(matrix.a * matrix.c + matrix.b * matrix.d),
32
- scaleX: Math.hypot(matrix.a, matrix.b),
33
- scaleY: Math.hypot(matrix.c, matrix.d),
34
- };
35
- }
36
-
37
- function assertRigidRepresentable(matrix: Matrix, label: string): [number, number] {
38
- if (!isFiniteMatrix(matrix)) {
39
- throw new Error(`world2d physics entity "${label}" has a non-finite composed transform`);
40
- }
41
- const { determinant, orthogonality, scaleX, scaleY } = linearMetrics(matrix);
42
- if (scaleX <= EPSILON || scaleY <= EPSILON || Math.abs(determinant) <= EPSILON) {
43
- throw new Error(`world2d physics entity "${label}" has a singular composed transform`);
44
- }
45
- if (orthogonality > EPSILON * scaleX * scaleY) {
46
- throw new Error(
47
- `world2d physics entity "${label}" has a sheared composed transform; ` +
48
- 'Rapier rigid bodies cannot represent shear',
49
- );
50
- }
51
- return [scaleX, scaleY];
52
- }
53
-
54
- /** Matrix from `container` local coordinates into `root` coordinates. The
55
- * root's own camera/pan transform is deliberately cancelled: Rapier lives in
56
- * the world2d root's logical pixel space, not application/screen space. */
57
- export function relativeTransform2D(container: Container, root: Container): Matrix {
58
- let ancestor: Container | null = container;
59
- while (ancestor && ancestor !== root) ancestor = ancestor.parent;
60
- if (ancestor !== root) {
61
- throw new Error('world2d physics object is not a descendant of the scene root');
62
- }
63
-
64
- const rootGlobal = root.getGlobalTransform();
65
- const global = container.getGlobalTransform();
66
- const origin = rootGlobal.applyInverse(global.apply({ x: 0, y: 0 }));
67
- const unitX = rootGlobal.applyInverse(global.apply({ x: 1, y: 0 }));
68
- const unitY = rootGlobal.applyInverse(global.apply({ x: 0, y: 1 }));
69
- return new Matrix(
70
- unitX.x - origin.x,
71
- unitX.y - origin.y,
72
- unitY.x - origin.x,
73
- unitY.y - origin.y,
74
- origin.x,
75
- origin.y,
76
- );
77
- }
78
-
79
- /** Moving Rapier bodies may rotate after spawn. Their Pixi parent chain must
80
- * therefore be conformal (uniform scale plus rotation/reflection); otherwise a
81
- * later rigid-body rotation would become shear when converted back to the
82
- * parent-local display transform. Fixed bodies can retain a non-uniform parent
83
- * because their authored relative rotation does not change. */
84
- export function assertMovingPhysicsParent2D(
85
- display: Container,
86
- root: Container,
87
- label: string,
88
- ): void {
89
- const parent = display.parent;
90
- if (!parent) throw new Error(`world2d physics entity "${label}" is not mounted`);
91
- if (parent === root) return;
92
- const parentMatrix = relativeTransform2D(parent, root);
93
- const [scaleX, scaleY] = assertRigidRepresentable(parentMatrix, label);
94
- if (Math.abs(scaleX - scaleY) > EPSILON * Math.max(scaleX, scaleY)) {
95
- throw new Error(
96
- `world2d moving physics entity "${label}" has a non-conformal parent transform ` +
97
- `(composed scale [${scaleX}, ${scaleY}]); rotating a Rapier body beneath ` +
98
- 'non-uniform parent scale would shear the Pixi visual',
99
- );
100
- }
101
- }
102
-
103
- /** Compose the transform Rapier actually owns for one authored entity.
104
- *
105
- * The parent uses its real Pixi transform so nested visuals and physics agree.
106
- * The entity's own linear transform comes from `transform2d`, not from the
107
- * display object's `scale`: Sprite width/height are implemented internally by
108
- * Pixi as scale, but collider dimensions are already authored in pixels and
109
- * must not be multiplied by that implementation detail. The body origin is
110
- * the entity position (the world position of Pixi's pivot), matching the
111
- * existing world2d physics contract. */
112
- export function computePhysicsPose2D(
113
- entity: Entity2D,
114
- display: Container,
115
- root: Container,
116
- label = entity.name,
117
- ): PhysicsPose2D {
118
- const parent = display.parent;
119
- if (!parent) throw new Error(`world2d physics entity "${label}" is not mounted`);
120
- const parentMatrix = parent === root ? new Matrix() : relativeTransform2D(parent, root);
121
- const transform = entity.transform2d;
122
- const position = transform?.position ?? [0, 0];
123
- const scale = transform?.scale ?? [1, 1];
124
- const local = new Matrix().setTransform(
125
- position[0],
126
- position[1],
127
- 0,
128
- 0,
129
- scale[0],
130
- scale[1],
131
- transform?.rotation ?? 0,
132
- 0,
133
- 0,
134
- );
135
- const matrix = new Matrix().appendFrom(local, parentMatrix);
136
- const [scaleX, scaleY] = assertRigidRepresentable(matrix, label);
137
-
138
- return {
139
- matrix,
140
- position: [matrix.tx, matrix.ty],
141
- rotation: Math.atan2(matrix.b, matrix.a),
142
- scale: [scaleX, scaleY],
143
- };
144
- }
145
-
146
- /** Bake the composed visual scale into the native Rapier collider dimensions.
147
- * Round shapes require uniform scale because Rapier has no ellipse collider. */
148
- export function scaleCollider2D(
149
- collider: SceneCollider2D,
150
- scale: readonly [number, number],
151
- label: string,
152
- ): ScaledCollider2D {
153
- const [scaleX, scaleY] = scale;
154
- if (collider.shape === 'cuboid') {
155
- return {
156
- shape: 'cuboid',
157
- halfExtents: [collider.halfExtents[0] * scaleX, collider.halfExtents[1] * scaleY],
158
- };
159
- }
160
- if (Math.abs(scaleX - scaleY) > EPSILON * Math.max(scaleX, scaleY)) {
161
- throw new Error(
162
- `world2d physics entity "${label}" uses non-uniform composed scale ` +
163
- `[${scaleX}, ${scaleY}] with a ${collider.shape} collider; Rapier cannot represent ` +
164
- 'an elliptical round collider',
165
- );
166
- }
167
- if (collider.shape === 'ball') return { shape: 'ball', radius: collider.radius * scaleX };
168
- return {
169
- shape: 'capsule',
170
- halfHeight: collider.halfHeight * scaleY,
171
- radius: collider.radius * scaleX,
172
- };
173
- }
@@ -1,10 +0,0 @@
1
- /** Canonical Pixi-world scale: 100 authored pixels represent one physics meter. */
2
- export const DEFAULT_WORLD2D_PIXELS_PER_METER = 100;
3
-
4
- /** Earth-like downward acceleration used when a 2D scene omits gravity. */
5
- export const DEFAULT_WORLD2D_GRAVITY_METERS_PER_SECOND_SQUARED = 9.8;
6
-
7
- /** Derive the pixel-space default while preserving friendly decimal values. */
8
- export function defaultWorld2DGravityY(pixelsPerMeter: number): number {
9
- return (DEFAULT_WORLD2D_GRAVITY_METERS_PER_SECOND_SQUARED * 10 * pixelsPerMeter) / 10;
10
- }
@@ -1,439 +0,0 @@
1
- import RAPIER from '@dimforge/rapier2d-compat';
2
- import type { Container } from 'pixi.js';
3
- import { createInputManagerAdapter } from '../adapter/first-party-systems';
4
- import type { MountedPixiWorld } from '../adapter/game-adapter';
5
- import type { SystemAdapters } from '../adapter/system-adapter';
6
- import { createAssetCache } from '../assets';
7
- import { createGameLoop } from '../core/game-loop';
8
- import {
9
- createSeededRandom,
10
- DEFAULT_SEEDED_RANDOM_SEED,
11
- getSeededRandom,
12
- } from '../core/seeded-random';
13
- import { createSystemRunner } from '../core/system-runner';
14
- import { createComponentManager } from '../ecs/component-manager';
15
- import { InputManager } from '../input/input-manager';
16
- import {
17
- createDebugRegistry,
18
- type DebugRegistry,
19
- getDebugRegistry,
20
- } from '../runtime/debug-registry';
21
- import {
22
- createGame,
23
- createWorldInstance,
24
- type GameInternal,
25
- type WorldFrameHooks,
26
- } from '../runtime/game';
27
- import { createCollision2DSystem, createTriggerDispatch2D } from './collision-2d';
28
- import { createPhysics2DRegistry } from './physics2d-registry';
29
- import { DEFAULT_WORLD2D_PIXELS_PER_METER, defaultWorld2DGravityY } from './physics2d-units';
30
- import { createPixiSurface, type PixiSurface } from './pixi-surface';
31
- import { loadScene2DFromData, type Scene2DInstance } from './scene2d-loader';
32
- import type { Scene2DFile } from './schema/entity2d';
33
- import { createTransformWriter2D } from './transform-writer-2d';
34
- import type { Component2DRegistry, World2DContext } from './types';
35
-
36
- /** Minimal host surface for world2d (the stacked-own-root model from the spec). */
37
- export interface World2DHost {
38
- canvas: HTMLCanvasElement;
39
- width: number;
40
- height: number;
41
- /** Headless validation mount: skip browser-only asset/input initialization. */
42
- headless?: boolean;
43
- /** Shared Game root when mounted by the universal multi-world host. */
44
- game?: GameInternal;
45
- /** DPR override (T6.1 slice 1, COMPOSITION-DESIGN.md D5 §3 — "one DPR
46
- * everywhere"): the roots-path host computes ONE dpr for every stacked
47
- * surface and passes it here; omitted -> `createPixiSurface`'s own
48
- * default (`window.devicePixelRatio`, uncapped) — today's behavior. */
49
- dpr?: number;
50
- /** This world is stacked ABOVE another world (D5 §1): clear with alpha 0
51
- * instead of an opaque background so the layer below shows through. */
52
- transparent?: boolean;
53
- /** Capture tier cost (D5 §4) — see `PixiSurfaceOptions.preserveDrawingBuffer`. */
54
- preserveDrawingBuffer?: boolean;
55
- }
56
-
57
- /** A world2d setup function — build the scene from the context, return cleanup. */
58
- export type World2DSetupFn = (
59
- ctx: World2DContext,
60
- ) => Promise<{ dispose: () => void }> | { dispose: () => void };
61
-
62
- /**
63
- * MountedGame for world2d — the pixi peer of the 3D `MountedThreeWorld`,
64
- * satisfying `MountedPixiWorld` (`adapter/game-adapter.ts`, T7.5) via `kind`/
65
- * `stage` so a `WorldInstance` built from this mount needs no cast through
66
- * the generic `MountedWorld` union. `scene`/`camera` (both the SAME `Container`
67
- * as `stage`) are kept as extra fields for pre-T7.5 call sites/tests that
68
- * already read them — `stage` is the canonical name going forward.
69
- */
70
- export interface MountedGame2D extends MountedPixiWorld {
71
- /** First-party brand (T7.3 slice 2 — closes the gap `adapter/vgai-scene-
72
- * game-adapter.ts`'s `VgaiMountedGame.firstParty` doc flagged as a "T7.3
73
- * lookahead"): `runtime/game.ts`'s `isFirstPartyMounted` brand-checks
74
- * this literal, NOT a structural `'ctx' in mounted` probe (this mount's
75
- * `ctx` is a `World2DContext`, unrelated to `GameContext`). Without this,
76
- * a pixijs `WorldInstance` is silently skipped by every first-party-only
77
- * Game feature — `Game.queryByComponent` in particular, the T7.3 AC
78
- * (`test/game-three-plus-pixi.test.ts`). */
79
- readonly firstParty: true;
80
- /** Pre-T7.5 field names, kept for compatibility — both alias `stage`
81
- * (pixi has no separate camera concept; `MountedGame2D.mount` always sets
82
- * `scene === camera === stage`). */
83
- readonly scene: Container;
84
- readonly camera: Container;
85
- readonly ctx: World2DContext;
86
- update?(dt: number): void;
87
- resize?(width: number, height: number): void;
88
- dispose(): void;
89
- /** Phase-partitioned frame entry point (T7.3 slice 1) — the pixi analog of
90
- * `VgaiMountedGame.frame` (`adapter/vgai-scene-game-adapter.ts`). Delegates
91
- * to this mount's own `SystemRunner`, so a `WorldInstance` built from this
92
- * mount ticks it through `Game.runFrame` instead of a standalone loop. */
93
- readonly frame?: WorldFrameHooks;
94
- }
95
-
96
- export interface PixiSceneConfig {
97
- /** A world2d scene file to load (declarative path). */
98
- sceneData?: Scene2DFile;
99
- /** A setup function (imperative path) — mutually exclusive with sceneData. */
100
- setup?: World2DSetupFn;
101
- /** Component registry the scene's `components` reference. */
102
- registry?: Component2DRegistry;
103
- /** Input map JSON path. Defaults to the same conventional map as Three;
104
- * explicit `null` disables loading for deliberately input-free/test mounts. */
105
- inputMapPath?: string | null;
106
- id?: string;
107
- }
108
-
109
- /**
110
- * The first-party implementer that mounts an authored world2d scene — the 2D
111
- * analog of `VgaiSceneGameAdapter`. It builds the Pixi surface + Rapier 2D world,
112
- * registers the engine systems into the shared phase runner, loads the scene (or
113
- * runs a setup), and returns a `MountedGame2D`.
114
- */
115
- export class PixiSceneGameAdapter {
116
- readonly id: string;
117
- constructor(private readonly config: PixiSceneConfig = {}) {
118
- this.id = config.id ?? 'pixi-scene';
119
- }
120
-
121
- async mount(host: World2DHost): Promise<MountedGame2D> {
122
- const {
123
- inputMapPath = '/inputmaps/default.inputmap.json',
124
- sceneData,
125
- setup,
126
- registry = {},
127
- } = this.config;
128
-
129
- // --- Pixi surface (autoStart:false; engine loop drives render) ---
130
- const surface: PixiSurface = await createPixiSurface({
131
- canvas: host.canvas,
132
- width: host.width,
133
- height: host.height,
134
- ...(sceneData?.background ? { background: sceneData.background } : {}),
135
- // T6.1 slice 1 (roots-path host hints — see `World2DHost`'s fields):
136
- // all optional, all no-ops when the host is the legacy
137
- // `createWorld2DRuntime` entry (which never sets them), so behavior
138
- // there is unchanged.
139
- ...(host.dpr !== undefined ? { resolution: host.dpr } : {}),
140
- ...(host.transparent ? { transparent: true } : {}),
141
- ...(host.preserveDrawingBuffer ? { preserveDrawingBuffer: true } : {}),
142
- });
143
-
144
- // --- Rapier 2D world (separate from the 3D world) ---
145
- await RAPIER.init();
146
- const pixelsPerMeter = sceneData?.pixelsPerMeter ?? DEFAULT_WORLD2D_PIXELS_PER_METER;
147
- const gravity = sceneData?.gravity ?? [0, defaultWorld2DGravityY(pixelsPerMeter)];
148
- const rapier2dWorld = new RAPIER.World({ x: gravity[0], y: gravity[1] });
149
- // Keep Rapier's public API in authored pixels. Its native length-unit
150
- // setting scales solver/contact tolerances for those pixel-sized bodies.
151
- rapier2dWorld.lengthUnit = pixelsPerMeter;
152
- const eventQueue = new RAPIER.EventQueue(true);
153
-
154
- const physics2d = createPhysics2DRegistry();
155
- const writeTransforms2D = createTransformWriter2D(physics2d, surface.world);
156
- const collisions2d = createCollision2DSystem(rapier2dWorld, eventQueue);
157
- const ownsInput = !host.game;
158
- const input = host.game?.input ?? new InputManager();
159
- if (!host.headless && inputMapPath !== null) {
160
- if (host.game) await host.game.loadInputMap(inputMapPath);
161
- else await input.loadMap(inputMapPath);
162
- }
163
-
164
- const systemAdapters: SystemAdapters = {};
165
- const debugRegistry: DebugRegistry =
166
- (host.game ? getDebugRegistry(host.game) : null) ??
167
- createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
168
- if (ownsInput) systemAdapters.input = createInputManagerAdapter(input);
169
- systemAdapters.debug = debugRegistry.adapter;
170
- // W4b (F11 frame debugger): the pixijs adapter deliberately does NOT
171
- // register a `renderDebug` SystemAdapter. The first-party WebGL2 frame
172
- // capture (`dev/webgl-frame-capture.ts`) and its `onBeforeRender`-based
173
- // attribution are three.js-shaped (three's own per-object pre-draw hook,
174
- // a THREE.Scene traversal, `renderer.info` memory). PixiJS draws through
175
- // its own batch renderer with no equivalent per-object seam, so a faithful
176
- // pixi capture is a separate instrument — deferred, not faked. Absence of
177
- // `renderDebug` here is the honest "capability not supported" signal; the
178
- // editor's frame-debugger panel marks a pixi world's section absent rather
179
- // than showing fabricated draws.
180
-
181
- const systems = createSystemRunner(host.game?.profiler.systemObserver, 'pixijs');
182
- if (ownsInput) systems.add('input', () => input.poll(), { name: 'input.poll' });
183
- systems.add('physics', () => rapier2dWorld.step(eventQueue), { name: 'physics2d.step' });
184
- systems.add(
185
- 'postPhysics',
186
- () => {
187
- collisions2d.drain();
188
- writeTransforms2D();
189
- },
190
- { name: 'physics2d.sync' },
191
- );
192
- systems.add('render', () => surface.render(), { name: 'pixi.render' });
193
-
194
- const ctx: World2DContext = {
195
- app: surface.app,
196
- stage: surface.world,
197
- rapier2dWorld,
198
- rapier2d: RAPIER,
199
- physics2d,
200
- collisions2d,
201
- components: null!,
202
- systems,
203
- input,
204
- debug: debugRegistry.forWorld(this.id),
205
- random:
206
- (host.game ? getSeededRandom(host.game) : null) ??
207
- createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED),
208
- registerSystemAdapter: (kind, adapter) => {
209
- systemAdapters[kind] = adapter;
210
- },
211
- ...(host.game ? { game: host.game, roots: host.game.roots } : {}),
212
- };
213
-
214
- debugRegistry.setInputActionsSource(this.id, () =>
215
- input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) })),
216
- );
217
- debugRegistry.setInputTraceSource(this.id, () => {
218
- const raw = input.getInputTrace();
219
- return {
220
- version: raw.version,
221
- seed: (host.game ? getSeededRandom(host.game) : null)?.seed ?? null,
222
- fixedDt: host.game?.loop.fixedDt ?? null,
223
- ticks: raw.ticks,
224
- };
225
- });
226
- debugRegistry.setVirtualInputTarget(this.id, {
227
- setVirtualAction: (action, value) => input.setVirtualAction(action, value),
228
- tapVirtualAction: (action) => input.tapVirtualAction(action),
229
- clearVirtualActions: () => input.clearVirtualActions(),
230
- scheduleActionAtTick: (tick, action, value) =>
231
- input.scheduleActionAtTick(tick, action, value),
232
- startInputRecording: () => input.startInputRecording(),
233
- stopInputRecording: () => input.stopInputRecording(),
234
- isInputRecording: () => input.isInputRecording(),
235
- injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
236
- injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
237
- injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
238
- injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
239
- });
240
- if (!host.game) input.setDebugEmit((event, detail) => ctx.debug?.emit(event, detail));
241
-
242
- // T7.3 slice 1/2: the pixi world's components attach onto the SAME
243
- // unified `ComponentManager` (`ecs/component-manager.ts`) the threejs
244
- // path uses, constructed for kind `'pixijs'` — the world2d-silo manager
245
- // is deleted (T7.3 slice 2). The manager accepts this native
246
- // `World2DContext`, so Pixi component lifecycles need no union/cast.
247
- // `createComponentManager`'s positional `physics`
248
- // parameter (the 3D `PhysicsRegistry`) is optional (T7.3 slice 2) and
249
- // omitted here — a `'pixijs'`-kind manager never reads it (rigidBody/
250
- // collider resolve from `physics2d` instead — see `resolvePhysicsRefs`
251
- // in `ecs/component-manager.ts`).
252
- ctx.components = createComponentManager(ctx, undefined, {
253
- kind: 'pixijs',
254
- physics2d,
255
- });
256
- collisions2d.onCollision(
257
- createTriggerDispatch2D(physics2d, ctx.components, rapier2dWorld, ctx),
258
- );
259
-
260
- // --- Load scene or run setup ---
261
- let sceneInstance: Scene2DInstance | null = null;
262
- let cleanup: { dispose: () => void } | null = null;
263
- let disposed = false;
264
- systems.add('animation', (dt) => sceneInstance?.updateAnimations(dt), {
265
- name: 'pixi.animated-sprites',
266
- });
267
- const disposeResources = (): void => {
268
- if (disposed) return;
269
- disposed = true;
270
- const errors: unknown[] = [];
271
- const attempt = (dispose: () => void): void => {
272
- try {
273
- dispose();
274
- } catch (error) {
275
- errors.push(error);
276
- }
277
- };
278
- attempt(() => cleanup?.dispose());
279
- attempt(() => sceneInstance?.dispose());
280
- attempt(() => ctx.components.clear());
281
- attempt(() => debugRegistry.strip(this.id));
282
- if (ownsInput) attempt(() => input.dispose());
283
- attempt(() => physics2d.clear());
284
- attempt(() => rapier2dWorld.free());
285
- attempt(() => eventQueue.free());
286
- attempt(() => surface.dispose());
287
- if (errors.length > 0) {
288
- throw new AggregateError(errors, `Failed to dispose Pixi world "${this.id}" cleanly`);
289
- }
290
- };
291
- try {
292
- if (sceneData) {
293
- sceneInstance = await loadScene2DFromData(sceneData, ctx, registry);
294
- } else if (setup) {
295
- cleanup = await setup(ctx);
296
- }
297
- } catch (error) {
298
- try {
299
- disposeResources();
300
- } catch (cleanupError) {
301
- throw new AggregateError(
302
- [error, cleanupError],
303
- `Pixi world "${this.id}" failed to mount and roll back cleanly`,
304
- );
305
- }
306
- throw error;
307
- }
308
-
309
- return {
310
- kind: 'pixijs',
311
- firstParty: true,
312
- stage: surface.world,
313
- scene: surface.world,
314
- camera: surface.world,
315
- drivesOwnLoop: false,
316
- ctx,
317
- update: (dt: number) => systems.run(dt),
318
- resize: (w: number, h: number) => surface.resize(w, h),
319
- // Bare adapters own and close their private input frame here. A
320
- // game-hosted adapter shares the one game-owned manager, whose frame is
321
- // closed once after every root has advanced.
322
- frame: {
323
- runPhase: (phase, dt) => systems.runPhase(phase, dt),
324
- ...(ownsInput ? { endFrame: () => input.endFrame() } : {}),
325
- },
326
- systems: systemAdapters,
327
- dispose: disposeResources,
328
- };
329
- }
330
- }
331
-
332
- /** Handle returned by {@link createWorld2DRuntime}. */
333
- export interface World2DSession {
334
- stop(): void;
335
- pause(): void;
336
- resume(): void;
337
- /** Advance exactly one fixed substep — the pixi analog of
338
- * `GameSession.step()` (`runtime/create-runtime.ts`), delegating to the
339
- * same `Game.play.step()` (D10, T7.6). */
340
- step(): void;
341
- resize(width: number, height: number): void;
342
- readonly mounted: MountedGame2D;
343
- }
344
-
345
- /**
346
- * Host entry for world2d — the sibling of `createGameRuntime`, kept separate so the
347
- * proven THREE host/interfaces are untouched (the stacked-own-root option the spec's
348
- * surface-host-backend-neutral allows). Owns the canvas and constructs a real `Game`
349
- * (T7.3 slice 1) with exactly one `'pixijs'`-kind `WorldInstance`, driven each fixed
350
- * substep through `Game.runFrame` — the world2d analog of `createGameRuntime`'s
351
- * `registerThreeWorld` (`runtime/create-runtime.ts`). Before T7.3 this ran its
352
- * own standalone `createGameLoop` calling `mounted.update` directly; that loop is
353
- * retired here in favor of one shared frame executor per game root.
354
- */
355
- export async function createWorld2DRuntime(
356
- config: PixiSceneConfig & World2DHost,
357
- ): Promise<World2DSession> {
358
- const { canvas, width, height, ...sceneConfig } = config;
359
- const adapter = new PixiSceneGameAdapter(sceneConfig);
360
-
361
- const assets = createAssetCache();
362
- // `game` is referenced here before its `const` below only textually — this
363
- // closure isn't invoked until `loop.start()`, well after `game` exists
364
- // (same forward-reference pattern `create-runtime.ts` uses).
365
- const loop = createGameLoop({
366
- fixedTimestep: 1 / 60,
367
- maxSubSteps: 8,
368
- update: (dt) => {
369
- game.runFrame(dt);
370
- },
371
- });
372
- const game: GameInternal = createGame({ loop, assets });
373
- const mounted = await adapter.mount({ canvas, width, height, game });
374
-
375
- // T7.5: `mounted` (a `MountedGame2D`, extending `MountedPixiWorld`) and
376
- // `adapter` (this `PixiSceneGameAdapter`, satisfying `WorldInstance.adapter`'s
377
- // minimal `AdapterHandle` shape — see that field's doc comment in
378
- // `runtime/game.ts`) now pass through with ZERO cast — previously this cast
379
- // through `GameAdapter`/`MountedGame`, which hardwired THREE `scene`/`camera`.
380
- const pixiWorld = createWorldInstance({
381
- id: 'main',
382
- kind: 'pixijs',
383
- adapter,
384
- mounted,
385
- stage: mounted.stage,
386
- physics2d: mounted.ctx.physics2d,
387
- collisions2d: mounted.ctx.collisions2d,
388
- frame: mounted.frame,
389
- });
390
- // T7.2/T7.3 world-wiring backfill (mirrors `registerThreeWorld` in
391
- // `create-runtime.ts`): scene-authored components attach DURING `mount()`,
392
- // before this `WorldInstance` exists, so `instance.world` is left
393
- // `undefined` for each of them — `adoptWorld` backfills it now, once, for
394
- // every already-attached instance still missing a world.
395
- mounted.ctx.components.adoptWorld(pixiWorld);
396
- game.registerWorld(pixiWorld);
397
-
398
- loop.start();
399
-
400
- if (typeof window !== 'undefined' && import.meta.env?.DEV) {
401
- (window as unknown as Record<string, unknown>)['__vgaiWorld2d'] = {
402
- app: mounted.ctx.app,
403
- stage: mounted.stage,
404
- physics2d: mounted.ctx.physics2d,
405
- };
406
- }
407
-
408
- return {
409
- stop() {
410
- loop.stop();
411
- mounted.dispose();
412
- game.dispose();
413
- },
414
- // D10/T7.6: route through the real `Game.play` control surface instead of
415
- // `loop.timeScale = 0/1` — that starved the accumulator entirely (per D1,
416
- // zero substeps ⇒ zero renders too), the exact bug this control surface
417
- // replaces. `Game.play.pause()` freezes this world's simulation (it is
418
- // `pausable` by default) while its `render` phase keeps running every
419
- // substep — the screen does not go black while paused.
420
- pause() {
421
- game.play.pause();
422
- },
423
- resume() {
424
- game.play.resume();
425
- },
426
- step() {
427
- game.play.step();
428
- },
429
- resize(w: number, h: number) {
430
- // T7.5/T7.6 own real per-world resize routing through the Game/host
431
- // surface split (the host is not yet surface-neutral for world2d) —
432
- // kept as today's direct host→surface call for this slice.
433
- mounted.resize?.(w, h);
434
- },
435
- get mounted() {
436
- return mounted;
437
- },
438
- };
439
- }