@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,78 +0,0 @@
1
- import { Application, Container } from 'pixi.js';
2
-
3
- export interface PixiSurface {
4
- app: Application;
5
- /** The world container (the 2D "camera": pan/zoom by transforming this). */
6
- world: Container;
7
- /** Render one frame (called from the engine's `render` phase). */
8
- render: () => void;
9
- resize: (width: number, height: number) => void;
10
- dispose: () => void;
11
- }
12
-
13
- export interface PixiSurfaceOptions {
14
- canvas: HTMLCanvasElement;
15
- width: number;
16
- height: number;
17
- background?: string;
18
- resolution?: number;
19
- antialias?: boolean;
20
- /** Stacked-composition transparency (COMPOSITION-DESIGN.md D5 §1, T6.1
21
- * slice 1): when true, the surface clears with alpha 0
22
- * (`backgroundAlpha:0`) instead of an opaque `background`, so a world
23
- * layered ABOVE another shows the layer below through it. */
24
- transparent?: boolean;
25
- /** Capture tier cost (D5 §4): flat on `Application.init`, never nested
26
- * under `context:` (the documented pixi trap) — required for any later
27
- * `drawImage()`-based composite capture of this surface. */
28
- preserveDrawingBuffer?: boolean;
29
- }
30
-
31
- /**
32
- * Create the world2d render surface — a PixiJS v8 `Application` whose auto-ticker
33
- * is DISABLED (`autoStart:false`), so the engine's fixed-timestep loop drives
34
- * rendering by calling `render()` from the `render` phase (mirroring how the
35
- * Three.js adapter calls `composer.render(dt)`).
36
- *
37
- * `app.stage` holds a single `world` Container — the 2D camera lives here as a
38
- * transform on `world` (pan = world.position, zoom = world.scale), per the spec's
39
- * backend-2d-camera resolution. Screen-space HUD stays in the React/DOM overlay.
40
- */
41
- export async function createPixiSurface(opts: PixiSurfaceOptions): Promise<PixiSurface> {
42
- const app = new Application();
43
- await app.init({
44
- canvas: opts.canvas,
45
- width: opts.width,
46
- height: opts.height,
47
- background: opts.background ?? '#10101a',
48
- backgroundAlpha: opts.transparent ? 0 : 1,
49
- resolution: opts.resolution ?? (typeof window !== 'undefined' ? window.devicePixelRatio : 1),
50
- autoDensity: true,
51
- antialias: opts.antialias ?? true,
52
- autoStart: false, // engine loop drives rendering — NOT Pixi's ticker
53
- preference: 'webgl',
54
- // Capture tier cost (D5 §4) — FLAT here, never nested under `context:`
55
- // (the documented pixi trap: nesting silently breaks GL bootstrap).
56
- ...(opts.preserveDrawingBuffer ? { preserveDrawingBuffer: true } : {}),
57
- });
58
- // Stop the shared/internal ticker defensively (autoStart:false already prevents start).
59
- app.ticker.stop();
60
-
61
- const world = new Container();
62
- world.label = 'world2d-root';
63
- app.stage.addChild(world);
64
-
65
- return {
66
- app,
67
- world,
68
- render: () => app.render(),
69
- resize: (width: number, height: number) => {
70
- app.renderer.resize(width, height);
71
- },
72
- dispose: () => {
73
- // Tear down the Pixi app + GPU context. Children are destroyed by the loader's
74
- // own dispose; here we release the renderer/canvas binding.
75
- app.destroy({ removeView: false }, { children: true });
76
- },
77
- };
78
- }
@@ -1,49 +0,0 @@
1
- import type { Entity2D, Scene2DFile } from './schema/entity2d';
2
-
3
- /** Resolve every entity to one collision-free identity without mutating source. */
4
- export function resolveScene2DEntityIds(scene: Scene2DFile): Map<Entity2D, string> {
5
- const authored = new Set<string>();
6
- const collect = (entities: Entity2D[]): void => {
7
- for (const entity of entities) {
8
- if (entity.id) {
9
- if (authored.has(entity.id)) throw new Error(`Duplicate world2d entity id: "${entity.id}"`);
10
- authored.add(entity.id);
11
- }
12
- if (entity.children) collect(entity.children);
13
- }
14
- };
15
- collect(scene.entities);
16
-
17
- const used = new Set(authored);
18
- const result = new Map<Entity2D, string>();
19
- const visit = (entities: Entity2D[], parentPath: readonly number[]): void => {
20
- entities.forEach((entity, index) => {
21
- const path = [...parentPath, index];
22
- let id = entity.id;
23
- if (!id) {
24
- const base = `scn2d:${path.join('.')}`;
25
- id = base;
26
- let suffix = 1;
27
- while (used.has(id)) id = `${base}~${suffix++}`;
28
- used.add(id);
29
- }
30
- result.set(entity, id);
31
- if (entity.children) visit(entity.children, path);
32
- });
33
- };
34
- visit(scene.entities, []);
35
- return result;
36
- }
37
-
38
- /** Materialize the same identities the preview/hierarchy already project. */
39
- export function materializeScene2DEntityIds(scene: Scene2DFile): boolean {
40
- const ids = resolveScene2DEntityIds(scene);
41
- let changed = false;
42
- for (const [entity, id] of ids) {
43
- if (!entity.id) {
44
- entity.id = id;
45
- changed = true;
46
- }
47
- }
48
- return changed;
49
- }
@@ -1,433 +0,0 @@
1
- import type RAPIER from '@dimforge/rapier2d-compat';
2
- import { CompositeTilemap } from '@pixi/tilemap';
3
- import type { Ticker } from 'pixi.js';
4
- import {
5
- AnimatedSprite,
6
- Assets,
7
- BlurFilter,
8
- ColorMatrixFilter,
9
- Container,
10
- Sprite,
11
- Text,
12
- Texture,
13
- } from 'pixi.js';
14
- import type { ComponentManager } from '../ecs/component-manager';
15
- import { SceneParseError } from '../scene/parse';
16
- import {
17
- assertMovingPhysicsParent2D,
18
- computePhysicsPose2D,
19
- scaleCollider2D,
20
- } from './physics2d-transform';
21
- import { resolveScene2DEntityIds } from './scene2d-identity';
22
- import { type Entity2D, type Scene2DFile, Scene2DSchema } from './schema/entity2d';
23
- import type { SceneSprite, SceneText2D } from './schema/sprite';
24
- import type { SceneTilemap } from './schema/tilemap';
25
- import type { Component2DRegistry, World2DContext } from './types';
26
-
27
- const BLEND: Record<string, 'normal' | 'add' | 'multiply' | 'screen'> = {
28
- normal: 'normal',
29
- add: 'add',
30
- multiply: 'multiply',
31
- screen: 'screen',
32
- };
33
-
34
- // Pixi implements Sprite width/height by mutating native scale. Keep that
35
- // implementation scale separate from authored transform2d.scale so the visual
36
- // and its pixel-authored collider compose the same way.
37
- const spriteBaseScales = new WeakMap<Container, readonly [number, number]>();
38
- const tilemapTextures = new WeakMap<CompositeTilemap, readonly Texture[]>();
39
-
40
- /** Native atlas sub-textures retained for source-backed tile painting. */
41
- export function getSceneTilemapTextures2D(display: Container): readonly Texture[] | null {
42
- return display instanceof CompositeTilemap ? (tilemapTextures.get(display) ?? null) : null;
43
- }
44
-
45
- /** Refresh the native Sprite sizing scale after an authoring operation writes
46
- * `display.width`/`height` from new source dimensions. The next authored
47
- * transform application multiplies this base by transform2d.scale exactly once. */
48
- export function captureSceneSpriteBaseScale2D(
49
- display: Container,
50
- axes: { x: boolean; y: boolean } = { x: true, y: true },
51
- authoredScale: readonly [number, number] = [1, 1],
52
- ): void {
53
- const previous = spriteBaseScales.get(display) ?? [display.scale.x, display.scale.y];
54
- const authoredSignX = authoredScale[0] < 0 ? -1 : 1;
55
- const authoredSignY = authoredScale[1] < 0 ? -1 : 1;
56
- spriteBaseScales.set(display, [
57
- axes.x ? display.scale.x * authoredSignX : previous[0],
58
- axes.y ? display.scale.y * authoredSignY : previous[1],
59
- ]);
60
- }
61
-
62
- function hexToNum(hex: string | undefined, fallback: number): number {
63
- if (!hex) return fallback;
64
- return Number.parseInt(hex.replace('#', ''), 16);
65
- }
66
-
67
- function applyFilter(display: Sprite | AnimatedSprite, filter: string | undefined): void {
68
- if (!filter || filter === 'none') return;
69
- if (filter === 'blur') display.filters = [new BlurFilter({ strength: 6 })];
70
- else if (filter === 'desaturate') {
71
- const cm = new ColorMatrixFilter();
72
- cm.desaturate();
73
- display.filters = [cm];
74
- }
75
- }
76
-
77
- type LoadedSpriteAsset = Texture & {
78
- animations?: Record<string, Texture[]>;
79
- textures?: Record<string, Texture>;
80
- };
81
-
82
- function createAnimatedSprite(data: SceneSprite, loaded: LoadedSpriteAsset): AnimatedSprite {
83
- const frames = loaded.animations?.[data.animation!];
84
- if (!frames?.length) {
85
- throw new Error(`Spritesheet "${data.texture}" does not contain animation "${data.animation}"`);
86
- }
87
- const display = new AnimatedSprite({ autoUpdate: false, textures: frames });
88
- display.animationSpeed = (data.fps ?? 8) / 60;
89
- display.loop = data.loop ?? true;
90
- display.play();
91
- return display;
92
- }
93
-
94
- function applySpriteAppearance(display: Sprite | AnimatedSprite, data: SceneSprite): void {
95
- const [ax, ay] = data.anchor ?? [0.5, 0.5];
96
- display.anchor.set(ax, ay);
97
- if (data.width !== undefined) display.width = data.width;
98
- if (data.height !== undefined) display.height = data.height;
99
- captureSceneSpriteBaseScale2D(display);
100
- display.tint = hexToNum(data.tint, 0xffffff);
101
- if (data.alpha !== undefined) display.alpha = data.alpha;
102
- if (data.blendMode) display.blendMode = BLEND[data.blendMode] ?? 'normal';
103
- if (data.interactive) {
104
- display.eventMode = 'static';
105
- display.cursor = 'pointer';
106
- }
107
- applyFilter(display, data.filter);
108
- }
109
-
110
- function resolveStaticSpriteTexture(
111
- data: SceneSprite,
112
- loaded: LoadedSpriteAsset | undefined,
113
- ): Texture {
114
- if (!loaded) return Texture.WHITE;
115
- if (!data.frame) return loaded as Texture;
116
- const frame = loaded.textures?.[data.frame];
117
- if (!frame) {
118
- throw new Error(`Spritesheet "${data.texture}" does not contain frame "${data.frame}"`);
119
- }
120
- return frame;
121
- }
122
-
123
- /** Instantiate a PIXI.Sprite / AnimatedSprite from authored `sprite` data. */
124
- async function createSprite(data: SceneSprite): Promise<Sprite | AnimatedSprite> {
125
- const loaded = data.texture
126
- ? ((await Assets.load(data.texture)) as LoadedSpriteAsset)
127
- : undefined;
128
- const display = data.animation
129
- ? createAnimatedSprite(data, loaded!)
130
- : new Sprite(resolveStaticSpriteTexture(data, loaded));
131
- applySpriteAppearance(display, data);
132
- return display;
133
- }
134
-
135
- /** Instantiate a @pixi/tilemap CompositeTilemap from a native Pixi atlas. */
136
- async function createTilemap(data: SceneTilemap): Promise<CompositeTilemap> {
137
- const loaded = (await Assets.load(data.texture)) as LoadedSpriteAsset;
138
- const textures = data.frames.map((frame) => {
139
- const texture = loaded.textures?.[frame];
140
- if (!texture) {
141
- throw new Error(`Spritesheet "${data.texture}" does not contain tile frame "${frame}"`);
142
- }
143
- return texture;
144
- });
145
- const tilemap = new CompositeTilemap();
146
- tilemapTextures.set(tilemap, textures);
147
- for (let y = 0; y < data.tiles.length; y++) {
148
- const row = data.tiles[y]!;
149
- for (let x = 0; x < row.length; x++) {
150
- const index = row[x]!;
151
- if (index === -1) continue;
152
- tilemap.tile(textures[index]!, x * data.tileSize, y * data.tileSize);
153
- }
154
- }
155
- return tilemap;
156
- }
157
-
158
- /** Instantiate a PIXI.Text from authored `text` data (world-space label). */
159
- function createText(data: SceneText2D): Text {
160
- const text = new Text({
161
- text: data.text,
162
- style: { fontSize: data.fontSize ?? 24, fill: data.fill ?? '#ffffff' },
163
- });
164
- const [ax, ay] = data.anchor ?? [0.5, 0.5];
165
- text.anchor.set(ax, ay);
166
- return text;
167
- }
168
-
169
- /** Map the bodyType enum to a Rapier 2D RigidBodyDesc. */
170
- function bodyDesc(rapier2d: typeof RAPIER, type: string): RAPIER.RigidBodyDesc {
171
- switch (type) {
172
- case 'fixed':
173
- return rapier2d.RigidBodyDesc.fixed();
174
- case 'kinematicVelocity':
175
- return rapier2d.RigidBodyDesc.kinematicVelocityBased();
176
- case 'kinematicPosition':
177
- return rapier2d.RigidBodyDesc.kinematicPositionBased();
178
- default:
179
- return rapier2d.RigidBodyDesc.dynamic();
180
- }
181
- }
182
-
183
- interface MovingPhysicsAncestor2D {
184
- bodyType: string;
185
- name: string;
186
- }
187
-
188
- function entityLabel(entity: Entity2D): string {
189
- return entity.id ? `${entity.name} (id: "${entity.id}")` : entity.name;
190
- }
191
-
192
- function isMovingBodyType(bodyType: string): boolean {
193
- return bodyType !== 'fixed';
194
- }
195
-
196
- /** Create the Rapier 2D body + collider for an entity and register it. */
197
- function createPhysics2D(
198
- ctx: World2DContext,
199
- entity: Entity2D,
200
- display: Container,
201
- movingAncestor: MovingPhysicsAncestor2D | undefined,
202
- ): void {
203
- const p = entity.physics2d;
204
- if (!p) return;
205
- const { rapier2d, rapier2dWorld, physics2d } = ctx;
206
- const label = entityLabel(entity);
207
- if (movingAncestor) {
208
- throw new Error(
209
- `world2d physics entity "${label}" has a "${p.bodyType}" body nested under ` +
210
- `"${movingAncestor.name}", which has a moving "${movingAncestor.bodyType}" body. ` +
211
- 'Rapier has no parent/child body relationship, so a moving ancestor cannot carry ' +
212
- 'another physics body. Move the nested body out of that hierarchy or make the ' +
213
- 'ancestor fixed.',
214
- );
215
- }
216
- if (isMovingBodyType(p.bodyType)) assertMovingPhysicsParent2D(display, ctx.stage, label);
217
- const pose = computePhysicsPose2D(entity, display, ctx.stage, label);
218
- const colliderShape = scaleCollider2D(p.collider, pose.scale, label);
219
-
220
- const rbDesc = bodyDesc(rapier2d, p.bodyType)
221
- .setTranslation(pose.position[0], pose.position[1])
222
- .setRotation(pose.rotation);
223
- if (p.linearDamping !== undefined) rbDesc.setLinearDamping(p.linearDamping);
224
- if (p.gravityScale !== undefined) rbDesc.setGravityScale(p.gravityScale);
225
- if (p.lockRotations) rbDesc.lockRotations();
226
- const body = rapier2dWorld.createRigidBody(rbDesc);
227
-
228
- let colDesc: RAPIER.ColliderDesc;
229
- if (colliderShape.shape === 'cuboid') {
230
- colDesc = rapier2d.ColliderDesc.cuboid(
231
- colliderShape.halfExtents[0],
232
- colliderShape.halfExtents[1],
233
- );
234
- } else if (colliderShape.shape === 'ball') {
235
- colDesc = rapier2d.ColliderDesc.ball(colliderShape.radius);
236
- } else {
237
- colDesc = rapier2d.ColliderDesc.capsule(colliderShape.halfHeight, colliderShape.radius);
238
- }
239
-
240
- if (p.density !== undefined) colDesc.setDensity(p.density);
241
- if (p.friction !== undefined) colDesc.setFriction(p.friction);
242
- if (p.restitution !== undefined) colDesc.setRestitution(p.restitution);
243
- if (p.sensor) {
244
- colDesc.setSensor(true);
245
- colDesc.setActiveCollisionTypes(rapier2d.ActiveCollisionTypes.ALL);
246
- colDesc.setActiveEvents(rapier2d.ActiveEvents.COLLISION_EVENTS);
247
- } else {
248
- colDesc.setActiveEvents(rapier2d.ActiveEvents.COLLISION_EVENTS);
249
- }
250
- const collider = rapier2dWorld.createCollider(colDesc, body);
251
- physics2d.add(display, body, collider);
252
- }
253
-
254
- /** Hydrate the `components` field — schema.parse + Object.assign + attach (2D analog of applyComponents).
255
- * `manager` is the unified `ComponentManager` (`ecs/component-manager.ts`), constructed
256
- * for kind `'pixijs'` — the world2d silo manager this replaced (T7.3) is deleted. */
257
- function applyComponents2D(
258
- registry: Component2DRegistry,
259
- display: Container,
260
- components: Record<string, Record<string, unknown>> | undefined,
261
- manager: ComponentManager,
262
- ): void {
263
- if (!components) return;
264
- for (const [name, data] of Object.entries(components)) {
265
- const Cls = registry[name];
266
- if (!Cls) throw new Error(`Unknown world2d component: "${name}"`);
267
- const parsed = Cls.schema ? Cls.schema.parse(data ?? {}) : (data ?? {});
268
- const instance = new Cls();
269
- Object.assign(instance, parsed);
270
- // T5.4: record the registry name + raw authored data — same reasoning as
271
- // the 3D `applyComponents` (`scene/component-registry.ts`).
272
- manager.attach(display, instance, { key: name, props: data ?? {} });
273
- }
274
- }
275
-
276
- /** Apply authored entity state to its live Pixi object. Exported so the source
277
- * authoring adapter uses exactly the loader's Sprite-size/scale composition. */
278
- export function applySceneEntityTransform2D(display: Container, entity: Entity2D): void {
279
- const t = entity.transform2d;
280
- if (t?.position) display.position.set(t.position[0], t.position[1]);
281
- if (t?.rotation !== undefined) display.rotation = t.rotation;
282
- const authoredScale = t?.scale ?? [1, 1];
283
- let baseScale = spriteBaseScales.get(display);
284
- if (!baseScale) {
285
- baseScale = entity.sprite ? [display.scale.x, display.scale.y] : [1, 1];
286
- if (entity.sprite) spriteBaseScales.set(display, baseScale);
287
- }
288
- display.scale.set(baseScale[0] * authoredScale[0], baseScale[1] * authoredScale[1]);
289
- const pivot = t?.pivot ?? entity.pivot;
290
- if (pivot) display.pivot.set(pivot[0], pivot[1]);
291
- if (entity.visible === false) display.visible = false;
292
- }
293
-
294
- /** A loaded world2d scene instance: the root display objects + an update/dispose. */
295
- export interface Scene2DInstance {
296
- roots: Container[];
297
- /** id → display object map for editor/lookup (the 2D analog of the loader's entity map). */
298
- byId: Map<string, Container>;
299
- /** Advance authored AnimatedSprites through Pixi's native update method from the engine loop. */
300
- updateAnimations(dt: number): void;
301
- dispose(): void;
302
- }
303
-
304
- type SpawnHook = (
305
- entity: Entity2D,
306
- display: Container,
307
- movingAncestor: MovingPhysicsAncestor2D | undefined,
308
- ) => void;
309
-
310
- async function spawnScene2DVisuals(
311
- scene: Scene2DFile,
312
- stage: Container,
313
- onSpawn?: SpawnHook,
314
- ): Promise<Scene2DInstance> {
315
- const byId = new Map<string, Container>();
316
- const animatedSprites: AnimatedSprite[] = [];
317
- const roots: Container[] = [];
318
- const ids = resolveScene2DEntityIds(scene);
319
-
320
- async function spawn(
321
- entity: Entity2D,
322
- parent: Container,
323
- path: readonly number[],
324
- movingAncestor?: MovingPhysicsAncestor2D,
325
- ): Promise<Container> {
326
- let display: Container;
327
- if (entity.sprite) display = await createSprite(entity.sprite);
328
- else if (entity.text) display = createText(entity.text);
329
- else if (entity.tilemap) display = await createTilemap(entity.tilemap);
330
- else display = new Container();
331
- if (display instanceof AnimatedSprite) animatedSprites.push(display);
332
-
333
- display.label = entity.name;
334
- // Authored ids are canonical. The path fallback is deterministic across
335
- // edit-mode reloads, unlike the old process-global counter, while keeping
336
- // id optional for existing source documents.
337
- const id = ids.get(entity)!;
338
- (display as Container & { __entityId?: string }).__entityId = id;
339
- byId.set(id, display);
340
-
341
- applySceneEntityTransform2D(display, entity);
342
- parent.addChild(display);
343
- onSpawn?.(entity, display, movingAncestor);
344
-
345
- if (entity.children) {
346
- const childMovingAncestor =
347
- entity.physics2d && isMovingBodyType(entity.physics2d.bodyType)
348
- ? { bodyType: entity.physics2d.bodyType, name: entityLabel(entity) }
349
- : movingAncestor;
350
- for (let index = 0; index < entity.children.length; index++) {
351
- await spawn(entity.children[index]!, display, [...path, index], childMovingAncestor);
352
- }
353
- }
354
- return display;
355
- }
356
-
357
- for (let index = 0; index < scene.entities.length; index++) {
358
- roots.push(await spawn(scene.entities[index]!, stage, [index]));
359
- }
360
-
361
- return {
362
- roots,
363
- byId,
364
- updateAnimations(dt) {
365
- const ticker = { deltaTime: dt * 60 } as Ticker;
366
- for (const display of animatedSprites) display.update(ticker);
367
- },
368
- dispose() {
369
- for (const root of roots) {
370
- root.removeFromParent();
371
- root.destroy({ children: true });
372
- }
373
- roots.length = 0;
374
- byId.clear();
375
- animatedSprites.length = 0;
376
- },
377
- };
378
- }
379
-
380
- /**
381
- * Instantiate only authored Pixi display objects. This is the editor's
382
- * stopped-mode preview seam: no component construction/init, Rapier bodies,
383
- * input manager, systems, or gameplay loop are created.
384
- */
385
- export async function loadScene2DVisualsFromData(
386
- data: Scene2DFile,
387
- stage: Container,
388
- sourceName?: string,
389
- ): Promise<Scene2DInstance> {
390
- const result = Scene2DSchema.safeParse(data);
391
- if (!result.success) throw new SceneParseError(result.error.issues, sourceName);
392
- return spawnScene2DVisuals(result.data, stage);
393
- }
394
-
395
- /**
396
- * Load a world2d scene from authored data — the 2D analog of `loadSceneFromData`.
397
- * Three passes: (1) spawn all entities (recursing into children), building an
398
- * id→display map and creating Rapier 2D bodies; (2) [no joints in 2D yet];
399
- * (3) `componentManager.initAll()` once everything exists.
400
- *
401
- * Re-validates `data` even though the fetch path (`fetchScene2DFile` in
402
- * `packages/editor/src/adapter-resolver.ts`) already validates before calling
403
- * in — not every caller goes through that path (test-constructed
404
- * `PixiSceneConfig.sceneData` literals reach here unvalidated), so a malformed
405
- * payload still throws a `SceneParseError` naming the file, not a raw
406
- * `ZodError` (T4.6 convention, closing §7.1-14). `sourceName` is optional
407
- * because not every caller knows the originating file path; callers that do
408
- * (or gain the ability to) should thread it through for a better message.
409
- */
410
- export async function loadScene2DFromData(
411
- data: Scene2DFile,
412
- ctx: World2DContext,
413
- registry: Component2DRegistry,
414
- sourceName?: string,
415
- ): Promise<Scene2DInstance> {
416
- const result = Scene2DSchema.safeParse(data);
417
- if (!result.success) throw new SceneParseError(result.error.issues, sourceName);
418
- const scene = result.data;
419
- const instance = await spawnScene2DVisuals(
420
- scene,
421
- ctx.stage,
422
- (entity, display, movingAncestor) => {
423
- // Rapier bodies live in world space. The loader composes the Pixi parent
424
- // chain at spawn; the post-physics writer converts the simulated pose back
425
- // to parent-local display coordinates.
426
- createPhysics2D(ctx, entity, display, movingAncestor);
427
- applyComponents2D(registry, display, entity.components, ctx.components);
428
- },
429
- );
430
-
431
- await ctx.components.initAll();
432
- return instance;
433
- }