@vgai/engine 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +35 -0
  3. package/package.json +55 -0
  4. package/src/adapter/authoring.ts +402 -0
  5. package/src/adapter/colyseus-networking-adapter.ts +72 -0
  6. package/src/adapter/first-party-systems.ts +103 -0
  7. package/src/adapter/game-adapter.ts +151 -0
  8. package/src/adapter/host-context.ts +77 -0
  9. package/src/adapter/index.ts +85 -0
  10. package/src/adapter/ingest/game-contract.ts +59 -0
  11. package/src/adapter/ingest/overlay-applier.ts +207 -0
  12. package/src/adapter/ingest/overlay-apply.ts +124 -0
  13. package/src/adapter/ingest/overlay-file.ts +126 -0
  14. package/src/adapter/ingest/overlay-report.ts +176 -0
  15. package/src/adapter/ingest/scene-capture.ts +307 -0
  16. package/src/adapter/ingest/upstream-pin.ts +52 -0
  17. package/src/adapter/loop-gate-report.ts +54 -0
  18. package/src/adapter/rapier-physics-adapter.ts +56 -0
  19. package/src/adapter/system-adapter.ts +154 -0
  20. package/src/adapter/transform.ts +18 -0
  21. package/src/adapter/vgai-scene-game-adapter.ts +886 -0
  22. package/src/adapter/world-kind.ts +34 -0
  23. package/src/ai/navigation.ts +164 -0
  24. package/src/animation/anim-graph-types.ts +56 -0
  25. package/src/animation/anim-graph.ts +406 -0
  26. package/src/animation/anim-system.ts +28 -0
  27. package/src/animation/blend-node.ts +119 -0
  28. package/src/animation/property-track.ts +178 -0
  29. package/src/animation/schema.ts +204 -0
  30. package/src/assets.ts +80 -0
  31. package/src/audio/ambient.ts +300 -0
  32. package/src/audio/impacts.ts +212 -0
  33. package/src/audio/index.ts +7 -0
  34. package/src/audio/movement.ts +140 -0
  35. package/src/audio/musical.ts +200 -0
  36. package/src/audio/ui-sounds.ts +171 -0
  37. package/src/audio/vehicle.ts +235 -0
  38. package/src/audio/weapons.ts +152 -0
  39. package/src/core/game-loop.ts +127 -0
  40. package/src/core/system-runner.ts +298 -0
  41. package/src/core/types.ts +58 -0
  42. package/src/dev/console-bridge.ts +83 -0
  43. package/src/dev/debug-draw.ts +80 -0
  44. package/src/dev/logger.ts +119 -0
  45. package/src/ecs/component-manager.ts +748 -0
  46. package/src/ecs/game-component.ts +147 -0
  47. package/src/ecs/hmr-swap-report.ts +65 -0
  48. package/src/input/input-manager.ts +439 -0
  49. package/src/input/input-types.ts +19 -0
  50. package/src/input/schema.ts +129 -0
  51. package/src/loader.ts +70 -0
  52. package/src/manifest/index.ts +24 -0
  53. package/src/manifest/load-file.ts +16 -0
  54. package/src/manifest/load.ts +378 -0
  55. package/src/manifest/schema.ts +375 -0
  56. package/src/physics/collision-system.ts +76 -0
  57. package/src/physics/physics-registry.ts +83 -0
  58. package/src/physics/transform-writer.ts +41 -0
  59. package/src/physics/trigger-dispatch.ts +97 -0
  60. package/src/react/game-state.tsx +172 -0
  61. package/src/render/auto-batcher.ts +169 -0
  62. package/src/render/render-batch-system.ts +268 -0
  63. package/src/render/render-features.ts +146 -0
  64. package/src/render/render-settings.ts +72 -0
  65. package/src/runtime/create-runtime.ts +1152 -0
  66. package/src/runtime/frame-selector-cache.ts +81 -0
  67. package/src/runtime/game.ts +1003 -0
  68. package/src/runtime/input-router.ts +213 -0
  69. package/src/runtime/mount-game.ts +269 -0
  70. package/src/runtime/mount-manifest.ts +361 -0
  71. package/src/runtime/scene-ui-bridge.ts +86 -0
  72. package/src/runtime/scene-ui-data.ts +119 -0
  73. package/src/runtime/state-bridge.ts +79 -0
  74. package/src/runtime/types.ts +196 -0
  75. package/src/scene/asset-loaders.ts +195 -0
  76. package/src/scene/asset-paths.ts +123 -0
  77. package/src/scene/asset-registry.ts +67 -0
  78. package/src/scene/collider-dimensions.ts +125 -0
  79. package/src/scene/component-registry.ts +40 -0
  80. package/src/scene/defaults.ts +164 -0
  81. package/src/scene/geometries/index.ts +7 -0
  82. package/src/scene/geometries/terrain.ts +42 -0
  83. package/src/scene/geometry-registry.ts +42 -0
  84. package/src/scene/instance-registry.ts +84 -0
  85. package/src/scene/instancers/grid.ts +38 -0
  86. package/src/scene/instancers/index.ts +7 -0
  87. package/src/scene/light-camera-factory.ts +97 -0
  88. package/src/scene/material-factory.ts +211 -0
  89. package/src/scene/material-registry.ts +73 -0
  90. package/src/scene/materials/index.ts +7 -0
  91. package/src/scene/materials/water.ts +56 -0
  92. package/src/scene/parse.ts +71 -0
  93. package/src/scene/particles-factory.ts +383 -0
  94. package/src/scene/scene-apply.ts +356 -0
  95. package/src/scene/scene-diff-schema.ts +115 -0
  96. package/src/scene/scene-diff-types.ts +29 -0
  97. package/src/scene/scene-loader.ts +1533 -0
  98. package/src/scene/scene-query.ts +63 -0
  99. package/src/scene/scene-types.ts +34 -0
  100. package/src/scene/scene-version.ts +40 -0
  101. package/src/scene/schema/animation.ts +95 -0
  102. package/src/scene/schema/audio.ts +25 -0
  103. package/src/scene/schema/camera.ts +21 -0
  104. package/src/scene/schema/collider.ts +69 -0
  105. package/src/scene/schema/entity-ref.ts +78 -0
  106. package/src/scene/schema/entity.ts +169 -0
  107. package/src/scene/schema/environment.ts +384 -0
  108. package/src/scene/schema/index.ts +95 -0
  109. package/src/scene/schema/instances.ts +35 -0
  110. package/src/scene/schema/joint.ts +26 -0
  111. package/src/scene/schema/light.ts +38 -0
  112. package/src/scene/schema/material.ts +113 -0
  113. package/src/scene/schema/mesh.ts +108 -0
  114. package/src/scene/schema/particles.ts +398 -0
  115. package/src/scene/schema/physics.ts +49 -0
  116. package/src/scene/schema/scene-file.ts +299 -0
  117. package/src/scene/schema/shadow.ts +24 -0
  118. package/src/scene/schema/spline.ts +21 -0
  119. package/src/scene/schema/tuples.ts +21 -0
  120. package/src/scene/schema/ui.ts +602 -0
  121. package/src/scene/user-data.ts +203 -0
  122. package/src/setup/setup-audio.ts +60 -0
  123. package/src/setup/setup-particles.ts +23 -0
  124. package/src/setup/setup-physics.ts +67 -0
  125. package/src/setup/setup-renderer.ts +529 -0
  126. package/src/types-n8ao.d.ts +37 -0
  127. package/src/types-realism-effects.d.ts +61 -0
  128. package/src/world2d/authoring-2d.ts +208 -0
  129. package/src/world2d/capture-to-scene2d.ts +52 -0
  130. package/src/world2d/collision-2d.ts +106 -0
  131. package/src/world2d/components-2d.ts +86 -0
  132. package/src/world2d/index.ts +66 -0
  133. package/src/world2d/ingest-iframe-2d.ts +255 -0
  134. package/src/world2d/ingest2d.ts +131 -0
  135. package/src/world2d/physics2d-registry.ts +49 -0
  136. package/src/world2d/pixi-game-adapter.ts +325 -0
  137. package/src/world2d/pixi-surface.ts +78 -0
  138. package/src/world2d/scene-capture-2d.ts +117 -0
  139. package/src/world2d/scene2d-loader.ts +308 -0
  140. package/src/world2d/schema/entity2d.ts +145 -0
  141. package/src/world2d/schema/physics2d.ts +53 -0
  142. package/src/world2d/schema/sprite.ts +71 -0
  143. package/src/world2d/schema/tilemap.ts +22 -0
  144. package/src/world2d/schema/tuples2d.ts +25 -0
  145. package/src/world2d/system-adapters-2d.ts +49 -0
  146. package/src/world2d/transform-writer-2d.ts +24 -0
  147. package/src/world2d/types.ts +55 -0
@@ -0,0 +1,325 @@
1
+ import RAPIER from '@dimforge/rapier2d-compat';
2
+ import type { Container } from 'pixi.js';
3
+ import type { MountedPixiWorld } from '../adapter/game-adapter';
4
+ import { createAssetCache } from '../assets';
5
+ import { createGameLoop } from '../core/game-loop';
6
+ import { createSystemRunner } from '../core/system-runner';
7
+ import { createComponentManager } from '../ecs/component-manager';
8
+ import {
9
+ createGame,
10
+ createWorldInstance,
11
+ type GameInternal,
12
+ type WorldFrameHooks,
13
+ } from '../runtime/game';
14
+ import type { GameContext } from '../runtime/types';
15
+ import { createCollision2DSystem, createTriggerDispatch2D } from './collision-2d';
16
+ import { createPhysics2DRegistry } from './physics2d-registry';
17
+ import { createPixiSurface, type PixiSurface } from './pixi-surface';
18
+ import { loadScene2DFromData, type Scene2DInstance } from './scene2d-loader';
19
+ import type { Scene2DFile } from './schema/entity2d';
20
+ import { createTransformWriter2D } from './transform-writer-2d';
21
+ import type { Component2DRegistry, World2DContext } from './types';
22
+
23
+ /** Minimal host surface for world2d (the stacked-own-root model from the spec). */
24
+ export interface World2DHost {
25
+ canvas: HTMLCanvasElement;
26
+ width: number;
27
+ height: number;
28
+ /** Screen-space React/HUD overlay container. */
29
+ ui?: HTMLDivElement;
30
+ /** DPR override (T6.1 slice 1, COMPOSITION-DESIGN.md D5 §3 — "one DPR
31
+ * everywhere"): the worlds-path host computes ONE dpr for every stacked
32
+ * surface and passes it here; omitted -> `createPixiSurface`'s own
33
+ * default (`window.devicePixelRatio`, uncapped) — today's behavior. */
34
+ dpr?: number;
35
+ /** This world is stacked ABOVE another world (D5 §1): clear with alpha 0
36
+ * instead of an opaque background so the layer below shows through. */
37
+ transparent?: boolean;
38
+ /** Capture tier cost (D5 §4) — see `PixiSurfaceOptions.preserveDrawingBuffer`. */
39
+ preserveDrawingBuffer?: boolean;
40
+ }
41
+
42
+ /** A world2d setup function — build the scene from the context, return cleanup. */
43
+ export type World2DSetupFn = (
44
+ ctx: World2DContext,
45
+ ) => Promise<{ dispose: () => void }> | { dispose: () => void };
46
+
47
+ /**
48
+ * MountedGame for world2d — the pixi peer of the 3D `MountedThreeWorld`,
49
+ * satisfying `MountedPixiWorld` (`adapter/game-adapter.ts`, T7.5) via `kind`/
50
+ * `stage` so a `WorldInstance` built from this mount needs no cast through
51
+ * the generic `MountedWorld` union. `scene`/`camera` (both the SAME `Container`
52
+ * as `stage`) are kept as extra fields for pre-T7.5 call sites/tests that
53
+ * already read them — `stage` is the canonical name going forward.
54
+ */
55
+ export interface MountedGame2D extends MountedPixiWorld {
56
+ /** First-party brand (T7.3 slice 2 — closes the gap `adapter/vgai-scene-
57
+ * game-adapter.ts`'s `VgaiMountedGame.firstParty` doc flagged as a "T7.3
58
+ * lookahead"): `runtime/game.ts`'s `isFirstPartyMounted` brand-checks
59
+ * this literal, NOT a structural `'ctx' in mounted` probe (this mount's
60
+ * `ctx` is a `World2DContext`, unrelated to `GameContext`). Without this,
61
+ * a pixijs `WorldInstance` is silently skipped by every first-party-only
62
+ * Game feature — `Game.queryByComponent` in particular, the T7.3 AC
63
+ * (`test/game-three-plus-pixi.test.ts`). */
64
+ readonly firstParty: true;
65
+ /** Pre-T7.5 field names, kept for compatibility — both alias `stage`
66
+ * (pixi has no separate camera concept; `MountedGame2D.mount` always sets
67
+ * `scene === camera === stage`). */
68
+ readonly scene: Container;
69
+ readonly camera: Container;
70
+ readonly ctx: World2DContext;
71
+ update?(dt: number): void;
72
+ resize?(width: number, height: number): void;
73
+ dispose(): void;
74
+ /** Phase-partitioned frame entry point (T7.3 slice 1) — the pixi analog of
75
+ * `VgaiMountedGame.frame` (`adapter/vgai-scene-game-adapter.ts`). Delegates
76
+ * to this mount's own `SystemRunner`, so a `WorldInstance` built from this
77
+ * mount ticks it through `Game.runFrame` instead of a standalone loop. */
78
+ readonly frame?: WorldFrameHooks;
79
+ }
80
+
81
+ export interface PixiSceneConfig {
82
+ /** A world2d scene file to load (declarative path). */
83
+ sceneData?: Scene2DFile;
84
+ /** A setup function (imperative path) — mutually exclusive with sceneData. */
85
+ setup?: World2DSetupFn;
86
+ /** Component registry the scene's `components` reference. */
87
+ registry?: Component2DRegistry;
88
+ id?: string;
89
+ }
90
+
91
+ /**
92
+ * The first-party implementer that mounts an authored world2d scene — the 2D
93
+ * analog of `VgaiSceneGameAdapter`. It builds the Pixi surface + Rapier 2D world,
94
+ * registers the engine systems into the shared phase runner, loads the scene (or
95
+ * runs a setup), and returns a `MountedGame2D`.
96
+ */
97
+ export class PixiSceneGameAdapter {
98
+ readonly id: string;
99
+ constructor(private readonly config: PixiSceneConfig = {}) {
100
+ this.id = config.id ?? 'pixi-scene';
101
+ }
102
+
103
+ async mount(host: World2DHost): Promise<MountedGame2D> {
104
+ const { sceneData, setup, registry = {} } = this.config;
105
+
106
+ // --- Pixi surface (autoStart:false; engine loop drives render) ---
107
+ const surface: PixiSurface = await createPixiSurface({
108
+ canvas: host.canvas,
109
+ width: host.width,
110
+ height: host.height,
111
+ ...(sceneData?.background ? { background: sceneData.background } : {}),
112
+ // T6.1 slice 1 (worlds-path host hints — see `World2DHost`'s fields):
113
+ // all optional, all no-ops when the host is the legacy
114
+ // `createWorld2DRuntime` entry (which never sets them), so behavior
115
+ // there is unchanged.
116
+ ...(host.dpr !== undefined ? { resolution: host.dpr } : {}),
117
+ ...(host.transparent ? { transparent: true } : {}),
118
+ ...(host.preserveDrawingBuffer ? { preserveDrawingBuffer: true } : {}),
119
+ });
120
+
121
+ // --- Rapier 2D world (separate from the 3D world) ---
122
+ await RAPIER.init();
123
+ const gravity = sceneData?.gravity ?? [0, 980];
124
+ const rapier2dWorld = new RAPIER.World({ x: gravity[0], y: gravity[1] });
125
+ const eventQueue = new RAPIER.EventQueue(true);
126
+
127
+ const physics2d = createPhysics2DRegistry();
128
+ const writeTransforms2D = createTransformWriter2D(physics2d);
129
+ const collisions2d = createCollision2DSystem(rapier2dWorld, eventQueue);
130
+
131
+ const systems = createSystemRunner();
132
+ systems.add('physics', () => rapier2dWorld.step(eventQueue));
133
+ systems.add('postPhysics', () => {
134
+ collisions2d.drain();
135
+ writeTransforms2D();
136
+ });
137
+ systems.add('render', () => surface.render());
138
+
139
+ const uiContainer = host.ui ?? document.createElement('div');
140
+
141
+ const ctx: World2DContext = {
142
+ app: surface.app,
143
+ stage: surface.world,
144
+ rapier2dWorld,
145
+ rapier2d: RAPIER,
146
+ physics2d,
147
+ collisions2d,
148
+ components: null!,
149
+ systems,
150
+ uiContainer,
151
+ };
152
+
153
+ // T7.3 slice 1/2: the pixi world's components attach onto the SAME
154
+ // unified `ComponentManager` (`ecs/component-manager.ts`) the threejs
155
+ // path uses, constructed for kind `'pixijs'` — the world2d-silo manager
156
+ // is deleted (T7.3 slice 2). `ctx` is cast to `GameContext` for the
157
+ // manager's internal typing only: at runtime this object IS the real
158
+ // `World2DContext` above, so `GameComponent<'pixijs'>` subclasses that
159
+ // need real 2D ctx fields (e.g. `Camera2DFollow`, which widens its own
160
+ // `update`'s ctx param to `GameContext | World2DContext` and narrows
161
+ // back down) still receive their real ctx, unchanged behavior — the
162
+ // same cast `test/mirror-tree-guard.test.ts` uses for its unit-level
163
+ // pixi manager. `createComponentManager`'s positional `physics`
164
+ // parameter (the 3D `PhysicsRegistry`) is optional (T7.3 slice 2) and
165
+ // omitted here — a `'pixijs'`-kind manager never reads it (rigidBody/
166
+ // collider resolve from `physics2d` instead — see `resolvePhysicsRefs`
167
+ // in `ecs/component-manager.ts`).
168
+ ctx.components = createComponentManager(ctx as unknown as GameContext, undefined, {
169
+ kind: 'pixijs',
170
+ physics2d,
171
+ });
172
+ collisions2d.onCollision(
173
+ createTriggerDispatch2D(
174
+ physics2d,
175
+ ctx.components,
176
+ rapier2dWorld,
177
+ ctx as unknown as GameContext,
178
+ ),
179
+ );
180
+
181
+ // --- Load scene or run setup ---
182
+ let sceneInstance: Scene2DInstance | null = null;
183
+ let cleanup: { dispose: () => void } | null = null;
184
+ if (sceneData) {
185
+ sceneInstance = await loadScene2DFromData(sceneData, ctx, registry);
186
+ } else if (setup) {
187
+ cleanup = await setup(ctx);
188
+ }
189
+
190
+ return {
191
+ kind: 'pixijs',
192
+ firstParty: true,
193
+ stage: surface.world,
194
+ scene: surface.world,
195
+ camera: surface.world,
196
+ drivesOwnLoop: false,
197
+ ctx,
198
+ update: (dt: number) => systems.run(dt),
199
+ resize: (w: number, h: number) => surface.resize(w, h),
200
+ // T7.3 slice 1: no `endFrame` hook — world2d has no per-substep
201
+ // post-phase work today (no `input.endFrame()` analog; HUD input is
202
+ // plain DOM events, not routed through an engine InputManager).
203
+ frame: {
204
+ runPhase: (phase, dt) => systems.runPhase(phase, dt),
205
+ },
206
+ dispose: () => {
207
+ cleanup?.dispose();
208
+ sceneInstance?.dispose();
209
+ ctx.components.clear();
210
+ physics2d.clear();
211
+ rapier2dWorld.free();
212
+ eventQueue.free();
213
+ surface.dispose();
214
+ },
215
+ };
216
+ }
217
+ }
218
+
219
+ /** Handle returned by {@link createWorld2DRuntime}. */
220
+ export interface World2DSession {
221
+ stop(): void;
222
+ pause(): void;
223
+ resume(): void;
224
+ /** Advance exactly one fixed substep — the pixi analog of
225
+ * `GameSession.step()` (`runtime/create-runtime.ts`), delegating to the
226
+ * same `Game.play.step()` (D10, T7.6). */
227
+ step(): void;
228
+ resize(width: number, height: number): void;
229
+ readonly mounted: MountedGame2D;
230
+ }
231
+
232
+ /**
233
+ * Host entry for world2d — the sibling of `createGameRuntime`, kept separate so the
234
+ * proven THREE host/interfaces are untouched (the stacked-own-root option the spec's
235
+ * surface-host-backend-neutral allows). Owns the canvas and constructs a real `Game`
236
+ * (T7.3 slice 1) with exactly one `'pixijs'`-kind `WorldInstance`, driven each fixed
237
+ * substep through `Game.runFrame` — the world2d analog of `createGameRuntime`'s
238
+ * `registerDefaultThreeWorld` (`runtime/create-runtime.ts`). Before T7.3 this ran its
239
+ * own standalone `createGameLoop` calling `mounted.update` directly; that loop is
240
+ * retired here in favor of one shared frame executor per game root.
241
+ */
242
+ export async function createWorld2DRuntime(
243
+ config: PixiSceneConfig & World2DHost,
244
+ ): Promise<World2DSession> {
245
+ const { canvas, width, height, ui, ...sceneConfig } = config;
246
+ const adapter = new PixiSceneGameAdapter(sceneConfig);
247
+ const mounted = await adapter.mount({ canvas, width, height, ...(ui ? { ui } : {}) });
248
+
249
+ const assets = createAssetCache();
250
+ // `game` is referenced here before its `const` below only textually — this
251
+ // closure isn't invoked until `loop.start()`, well after `game` exists
252
+ // (same forward-reference pattern `create-runtime.ts` uses).
253
+ const loop = createGameLoop({
254
+ fixedTimestep: 1 / 60,
255
+ maxSubSteps: 8,
256
+ update: (dt) => {
257
+ game.runFrame(dt);
258
+ },
259
+ });
260
+ const game: GameInternal = createGame({ loop, assets });
261
+
262
+ // T7.5: `mounted` (a `MountedGame2D`, extending `MountedPixiWorld`) and
263
+ // `adapter` (this `PixiSceneGameAdapter`, satisfying `WorldInstance.adapter`'s
264
+ // minimal `AdapterHandle` shape — see that field's doc comment in
265
+ // `runtime/game.ts`) now pass through with ZERO cast — previously this cast
266
+ // through `GameAdapter`/`MountedGame`, which hardwired THREE `scene`/`camera`.
267
+ const pixiWorld = createWorldInstance({
268
+ id: 'main',
269
+ kind: 'pixijs',
270
+ adapter,
271
+ mounted,
272
+ stage: mounted.stage,
273
+ physics2d: mounted.ctx.physics2d,
274
+ collisions2d: mounted.ctx.collisions2d,
275
+ frame: mounted.frame,
276
+ });
277
+ // T7.2/T7.3 world-wiring backfill (mirrors `registerThreeWorld` in
278
+ // `create-runtime.ts`): scene-authored components attach DURING `mount()`,
279
+ // before this `WorldInstance` exists, so `instance.world` is left
280
+ // `undefined` for each of them — `adoptWorld` backfills it now, once, for
281
+ // every already-attached instance still missing a world.
282
+ mounted.ctx.components.adoptWorld(pixiWorld);
283
+ game.registerWorld(pixiWorld);
284
+
285
+ loop.start();
286
+
287
+ if (typeof window !== 'undefined' && import.meta.env?.DEV) {
288
+ (window as unknown as Record<string, unknown>)['__vgaiWorld2d'] = {
289
+ app: mounted.ctx.app,
290
+ stage: mounted.stage,
291
+ physics2d: mounted.ctx.physics2d,
292
+ };
293
+ }
294
+
295
+ return {
296
+ stop() {
297
+ loop.stop();
298
+ mounted.dispose();
299
+ },
300
+ // D10/T7.6: route through the real `Game.play` control surface instead of
301
+ // `loop.timeScale = 0/1` — that starved the accumulator entirely (per D1,
302
+ // zero substeps ⇒ zero renders too), the exact bug this control surface
303
+ // replaces. `Game.play.pause()` freezes this world's simulation (it is
304
+ // `pausable` by default) while its `render` phase keeps running every
305
+ // substep — the screen does not go black while paused.
306
+ pause() {
307
+ game.play.pause();
308
+ },
309
+ resume() {
310
+ game.play.resume();
311
+ },
312
+ step() {
313
+ game.play.step();
314
+ },
315
+ resize(w: number, h: number) {
316
+ // T7.5/T7.6 own real per-world resize routing through the Game/host
317
+ // surface split (the host is not yet surface-neutral for world2d) —
318
+ // kept as today's direct host→surface call for this slice.
319
+ mounted.resize?.(w, h);
320
+ },
321
+ get mounted() {
322
+ return mounted;
323
+ },
324
+ };
325
+ }
@@ -0,0 +1,78 @@
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
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Scene capture for world2d — the PixiJS analog of the three.js render
3
+ * accessor-trap (`adapter/ingest/scene-capture.ts`).
4
+ *
5
+ * An unmodified PixiJS game owns its own `Application`, `stage`, renderer, and
6
+ * ticker. To inspect/host that live scene with ZERO edits to the game, we trap the
7
+ * render call and capture the game's `stage` + renderer + app on its first frame.
8
+ *
9
+ * Unlike three.js's `WebGLRenderer` (whose `render` is an own-instance property,
10
+ * forcing a prototype getter/setter), PixiJS's `Application.prototype.render` is a
11
+ * normal prototype method — the standard ticker render path (`ticker → app.render()`)
12
+ * goes through it — so a direct prototype wrap suffices.
13
+ *
14
+ * CRITICAL (same module-identity gatekeeper as the 3D path): the trap must be
15
+ * installed on the SAME `pixi.js` module instance the game uses. A bundler/dev-server
16
+ * that dedupes `pixi.js` makes an ESM game's `import 'pixi.js'` resolve to the host
17
+ * instance, so it is trapped; a game bundling its own pixi cannot be captured this
18
+ * way (degrade to iframe-reachable or opaque-embed tiers — see the spec).
19
+ */
20
+
21
+ /** A live world2d runtime captured from an external PixiJS game. */
22
+ export interface CapturedRuntime2D {
23
+ /** The game's root stage Container. */
24
+ stage: unknown;
25
+ /** The game's PixiJS renderer. */
26
+ renderer: unknown;
27
+ /** The game's Application (so the host can pause/resume its ticker). */
28
+ app: unknown;
29
+ }
30
+
31
+ export interface SceneCapture2DHandle {
32
+ readonly captured: CapturedRuntime2D | null;
33
+ waitForCapture(timeoutMs?: number): Promise<CapturedRuntime2D>;
34
+ getDrawCount(): number;
35
+ /** Pause/resume the captured game's own ticker (loop-host gating). */
36
+ setPaused(paused: boolean): void;
37
+ uninstall(): void;
38
+ }
39
+
40
+ interface PixiLike {
41
+ Application: { prototype: Record<string, unknown> };
42
+ }
43
+
44
+ /**
45
+ * Install the render trap on `pixiNamespace.Application.prototype.render`. Pass the
46
+ * host's `pixi.js` namespace so a shared-instance game is trapped. Install once per
47
+ * ingest session; `uninstall()` on teardown.
48
+ */
49
+ export function installSceneCapture2D(pixiNamespace: unknown): SceneCapture2DHandle {
50
+ const PIXI = pixiNamespace as PixiLike;
51
+ const proto = PIXI.Application.prototype;
52
+
53
+ let captured: CapturedRuntime2D | null = null;
54
+ let drawCount = 0;
55
+ const waiters: Array<(rt: CapturedRuntime2D) => void> = [];
56
+
57
+ const priorRender = Object.getOwnPropertyDescriptor(proto, 'render');
58
+ const realRender = proto['render'] as ((...a: unknown[]) => unknown) | undefined;
59
+
60
+ Object.defineProperty(proto, 'render', {
61
+ configurable: true,
62
+ writable: true,
63
+ value: function (this: Record<string, unknown>, ...args: unknown[]) {
64
+ drawCount++;
65
+ const stage = this['stage'];
66
+ if (!captured && stage) {
67
+ captured = { stage, renderer: this['renderer'], app: this };
68
+ for (const resolve of waiters.splice(0)) resolve(captured);
69
+ }
70
+ return realRender?.apply(this, args);
71
+ },
72
+ });
73
+
74
+ return {
75
+ get captured() {
76
+ return captured;
77
+ },
78
+ getDrawCount() {
79
+ return drawCount;
80
+ },
81
+ setPaused(paused: boolean) {
82
+ const app = captured?.app as { ticker?: { start(): void; stop(): void } } | undefined;
83
+ if (!app?.ticker) return;
84
+ if (paused) app.ticker.stop();
85
+ else app.ticker.start();
86
+ },
87
+ waitForCapture(timeoutMs = 10_000) {
88
+ if (captured) return Promise.resolve(captured);
89
+ return new Promise<CapturedRuntime2D>((resolve, reject) => {
90
+ const timer = setTimeout(() => {
91
+ const i = waiters.indexOf(wrapped);
92
+ if (i >= 0) waiters.splice(i, 1);
93
+ reject(
94
+ new Error(
95
+ `world2d scene capture timed out after ${timeoutMs}ms — the game never ` +
96
+ `rendered, or it bundles its own (un-shared) copy of pixi.js.`,
97
+ ),
98
+ );
99
+ }, timeoutMs);
100
+ const wrapped = (rt: CapturedRuntime2D) => {
101
+ clearTimeout(timer);
102
+ resolve(rt);
103
+ };
104
+ waiters.push(wrapped);
105
+ });
106
+ },
107
+ uninstall() {
108
+ if (priorRender) Object.defineProperty(proto, 'render', priorRender);
109
+ else if (realRender)
110
+ Object.defineProperty(proto, 'render', {
111
+ configurable: true,
112
+ writable: true,
113
+ value: realRender,
114
+ });
115
+ },
116
+ };
117
+ }