@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
@@ -15,24 +15,24 @@
15
15
  *
16
16
  * ## Key reference
17
17
  *
18
- * Serialization / identity (load-bearing — editor identity + scene I/O):
19
- * - `entity` — the full {@link SceneEntity} descriptor backing this
20
- * Object3D. Source of truth the editor reads/writes and
21
- * the serializer walks to rebuild `.vscn.json`.
18
+ * Serialization / identity (load-bearing — editor identity):
22
19
  * - `entityId` — the entity's stable string id. Presence also marks an
23
20
  * Object3D as a "real" entity (vs editor helper/env object).
24
21
  *
25
- * Runtime scene-query overrides (read by `scene-query.ts`):
26
- * - `tags` — string[] tag override; falls back to `entity.tags`.
27
- * - `components` — component record or name[] override; falls back to
28
- * `entity.components`.
22
+ * Runtime scene-query metadata (read by `scene-query.ts` and `scene-index.ts`):
23
+ * - `tags` — string[] tags for `queryByTag` and the live index.
24
+ * - `attributes` — per-object `Record<string, string|number|boolean>`
25
+ * of game-defined attributes (P2 `observe`). Written
26
+ * through `SceneIndex.setAttribute`, which emits an
27
+ * `attributechanged` signal; JSON-simple values only,
28
+ * so an attribute survives serialization unchanged.
29
29
  *
30
- * Runtime gameplay metadata (set by the scene loader):
30
+ * Runtime gameplay metadata:
31
31
  * - `forward` — `[x,y,z]` model-space visual forward exported as
32
32
  * ordinary glTF extras for asset-facing validation.
33
33
  * - `navRole` — `'walkable' | 'obstacle'` navmesh role; collected at runtime.
34
34
  * - `pivot` — `[x,y,z]` local-space pivot for rotate/scale-around-pivot.
35
- * - `splineCurve` — resolved THREE curve built from the entity's spline.
35
+ * - `splineCurve` — resolved THREE curve for a spline-following object.
36
36
  * - `_camera` — THREE.Camera owned by a camera entity (collected on load).
37
37
  * - `_particleSystem` — three.quarks ParticleSystem (scene-sync registration + census).
38
38
  * - `gaussianSplat` — native Spark splat metadata used for renderer discovery,
@@ -41,7 +41,7 @@
41
41
  * Animation (load-bearing — ED5 disposal contract):
42
42
  * - `_animMixer` — THREE.AnimationMixer driving this subtree's clips.
43
43
  * - `_animClips` — Map<string, AnimationClip> discovered on the GLTF (E5 —
44
- * lets a GameComponent build its own XState-driven
44
+ * lets game code build its own XState-driven
45
45
  * binding via `bindXStateAnimation` over the SAME
46
46
  * mixer; see xstate-animation-binding.ts).
47
47
  * - `_availableClips` — string[] of clip names discovered on the GLTF
@@ -58,9 +58,8 @@
58
58
  * - `__shadeUnlit` — generated unlit material(s) for the unlit view mode.
59
59
  *
60
60
  * Unmodified-game ingestion (editor-only, see `editor/src/ingest/`):
61
- * - `__ingest` — `true` on every Object3D minted by ingestion (vs the
62
- * first-party scene loader); routes edits to live
63
- * mutation instead of descriptor rebuild.
61
+ * - `__ingest` — `true` on every Object3D minted by ingestion; routes
62
+ * edits to live mutation.
64
63
  * - `__ingestNextId` — monotonic id counter stashed on the captured Scene
65
64
  * root so re-reflects keep minting unique ids.
66
65
  *
@@ -69,10 +68,6 @@
69
68
  * (particle BatchedRenderer, debug-draw + its subtree).
70
69
  * The editor's play-mode hierarchy skips these so they
71
70
  * don't show up as selectable "entities".
72
- * - `liveObject` — `true` on a runtime/procedural Object3D that the editor
73
- * surfaced in the play-mode hierarchy with a *synthetic*
74
- * descriptor (it has no `.vscn` record). Marks it as
75
- * ephemeral: selectable/movable for testing, never saved.
76
71
  * - `authoringRoot` — explicit opt-in for a runtime descendant to become
77
72
  * its own authoring object instead of a part of the
78
73
  * nearest ancestor owner.
@@ -81,8 +76,6 @@
81
76
  * implementation parts without adding wrappers.
82
77
  * - `authoringLabel` — authored literal name (or component tag fallback)
83
78
  * for that source-backed component instance.
84
- * - `behaviorType` — registry key on the compatibility R3F Behavior
85
- * attachment group; authoring projects it as a component.
86
79
  * - `editorHelper` — `true` for editor-only helper objects (gizmos, wireframes).
87
80
  * - `editorHelperType` — which kind of helper (lights/particles/pivot/navmesh/...).
88
81
  * - `editorIcon` — `true` for editor billboard icon sprites.
@@ -95,9 +88,7 @@
95
88
 
96
89
  import type * as THREE from 'three';
97
90
  import type { ParticleSystem } from 'three.quarks';
98
- import type { AnyActor } from 'xstate';
99
91
  import type { XStateAnimationBinding } from '../animation/xstate-animation-binding';
100
- import type { SceneEntity } from './scene-types';
101
92
 
102
93
  /** The kinds of editor helper objects tagged via `editorHelperType`. */
103
94
  export type EditorHelperType =
@@ -121,22 +112,17 @@ export type EditorHelperType =
121
112
  | 'particle-shape'
122
113
  | 'pivot'
123
114
  | 'navmesh'
124
- | 'skeletons'
125
- // Per-component viewport gizmo contributions (W3a extension contract,
126
- // critique O11): objects a project GameComponent's `static editorGizmo`
127
- // hook draws for entities carrying that component.
128
- | 'components';
115
+ | 'skeletons';
129
116
 
130
117
  /**
131
118
  * Maps each canonical accessor name to its value type. This is the single
132
119
  * source of truth for what every known `userData` key holds.
133
120
  */
134
121
  export interface UserDataSchema {
135
- entity: SceneEntity;
136
122
  entityId: string;
137
123
  forward: [number, number, number];
138
124
  tags: string[];
139
- components: Record<string, unknown> | string[];
125
+ attributes: Record<string, string | number | boolean>;
140
126
  navRole: 'walkable' | 'obstacle';
141
127
  pivot: [number, number, number];
142
128
  splineCurve: THREE.Curve<THREE.Vector3>;
@@ -147,18 +133,16 @@ export interface UserDataSchema {
147
133
  _animMixer: THREE.AnimationMixer;
148
134
  _animClips: Map<string, THREE.AnimationClip>;
149
135
  _availableClips: string[];
150
- _xstateAnimation: XStateAnimationBinding & { readonly actor: AnyActor };
136
+ _xstateAnimation: XStateAnimationBinding;
151
137
  __sharedGeometry: boolean;
152
138
  __shadeOrig: THREE.Material | THREE.Material[];
153
139
  __shadeUnlit: THREE.Material[];
154
140
  __ingest: boolean;
155
141
  __ingestNextId: number;
156
142
  engineInternal: boolean;
157
- liveObject: boolean;
158
143
  authoringRoot: boolean;
159
144
  authoringInstance: string;
160
145
  authoringLabel: string;
161
- behaviorType: string;
162
146
  editorHelper: boolean;
163
147
  editorHelperType: EditorHelperType;
164
148
  editorIcon: boolean;
@@ -181,11 +165,10 @@ export type UserDataKey = keyof UserDataSchema;
181
165
  * literal strings appear in exactly this one place.
182
166
  */
183
167
  export const UserDataKeys = {
184
- entity: 'entity',
185
168
  entityId: 'entityId',
186
169
  forward: 'forward',
187
170
  tags: 'tags',
188
- components: 'components',
171
+ attributes: 'attributes',
189
172
  navRole: 'navRole',
190
173
  pivot: 'pivot',
191
174
  splineCurve: 'splineCurve',
@@ -203,11 +186,9 @@ export const UserDataKeys = {
203
186
  __ingest: '__ingest',
204
187
  __ingestNextId: '__ingestNextId',
205
188
  engineInternal: 'engineInternal',
206
- liveObject: 'liveObject',
207
189
  authoringRoot: 'authoringRoot',
208
190
  authoringInstance: 'authoringInstance',
209
191
  authoringLabel: 'authoringLabel',
210
- behaviorType: 'behaviorType',
211
192
  editorHelper: 'editorHelper',
212
193
  editorHelperType: 'editorHelperType',
213
194
  editorIcon: 'editorIcon',
package/src/index.ts CHANGED
@@ -18,15 +18,15 @@
18
18
  * Groups (import order below is alphabetical-by-specifier, per the repo's
19
19
  * biome `organizeImports` rule, not by group — see this list instead):
20
20
  * - game setup contract: `GameContext`/`GameCleanup`/`GameSetupFn`, `fromSetup`
21
- * - component model: `GameComponent`, `AdapterSurface`
22
- * - manifest-driven mount: `mountManifestWorlds` + its option/result types
23
- * - scene file entry point: `parseSceneFile`/`parsePrefabFile`
24
- * - game manifest (vgai.game.json) entry point: `loadGameManifest`
21
+ * - surface vocabulary: `AdapterSurface`
22
+ * - manifest-driven mount: `mountManifestRoots` + its option/result types
23
+ * - asset-format entry points: `parseMaterialFile` (.mat.json), `parseInstancesFile`
24
+ * - game manifest (vgai.project.json) entry point: `loadGameManifest`
25
25
  */
26
26
 
27
27
  export type { AdapterSurface } from './adapter/adapter-surface';
28
- export { fromSetup } from './adapter/vgai-scene-game-adapter';
29
- export { GameComponent } from './ecs/game-component';
28
+ export { fromSetup } from './adapter/setup-three-root-adapter';
29
+ export { AssetParseError, parseInstancesFile, parseMaterialFile } from './asset-formats/parse';
30
30
  export type {
31
31
  AdapterRoot,
32
32
  GameManifest,
@@ -42,7 +42,5 @@ export type {
42
42
  MountedManifestSession,
43
43
  MountManifestOptions,
44
44
  } from './runtime/mount-manifest';
45
- export { mountManifestWorlds } from './runtime/mount-manifest';
45
+ export { mountManifestRoots } from './runtime/mount-manifest';
46
46
  export type { GameCleanup, GameContext, GameSetupFn } from './runtime/types';
47
- export { parsePrefabFile, parseSceneFile, SceneParseError } from './scene/parse';
48
- export type { PrefabFile, SceneFile } from './scene/schema';
@@ -1,5 +1,5 @@
1
+ import { AssetParseError } from '../asset-parse-error';
1
2
  import { resolveUrl } from '../loader';
2
- import { SceneParseError } from '../scene/parse';
3
3
  import type {
4
4
  ActionValueOf,
5
5
  ActionValueSource,
@@ -39,9 +39,8 @@ function isTextEntryFocused(): boolean {
39
39
  }
40
40
 
41
41
  /**
42
- * Thrown by `InputManager.setVirtualAction`/`tapVirtualAction` (Task 1.4,
43
- * E2E-TESTING-BUILD-PLAN.md / SYNTHETIC-PLAYER-SPEC.md §3.2) for an
44
- * action name that was never `registerAction`/`loadMap`-ed. Carries a
42
+ * Thrown by `InputManager.setVirtualAction`/`tapVirtualAction` (Task 1.4)
43
+ * for an action name that was never `registerAction`/`loadMap`-ed. Carries a
45
44
  * machine-readable `code` and `data.registered` (every declared action name)
46
45
  * rather than requiring a caller to parse the message.
47
46
  */
@@ -60,12 +59,12 @@ export class InputActionError extends Error {
60
59
  }
61
60
 
62
61
  /**
63
- * Thrown by `InputManager.scheduleActionAtTick` (D15/T-D15.5,
64
- * docs/D15-DETERMINISM-DESIGN.md §2.c) when the named `tick` has already
65
- * elapsed scheduling only ever applies to the CURRENT or a future tick,
66
- * never one already serviced by `poll()`. Carries `data.currentTick` — the
67
- * NEXT tick `poll()` will service (not the tick that just elapsed) — so a
68
- * caller can retry by scheduling for `data.currentTick` or later.
62
+ * Thrown by `InputManager.scheduleActionAtTick` (D15/T-D15.5) when the
63
+ * named `tick` has already elapsed — scheduling only ever applies to the
64
+ * CURRENT or a future tick, never one already serviced by `poll()`.
65
+ * Carries `data.currentTick` the NEXT tick `poll()` will service (not
66
+ * the tick that just elapsed) — so a caller can retry by scheduling for
67
+ * `data.currentTick` or later.
69
68
  */
70
69
  export class InputTickError extends Error {
71
70
  readonly code = 'TICK_ALREADY_PASSED' as const;
@@ -360,7 +359,7 @@ export class InputManager {
360
359
  private static isStickActive(v: Vector2): boolean {
361
360
  return Math.hypot(v.x, v.y) > 0.3;
362
361
  }
363
- // Task 1.4 (E2E-TESTING-BUILD-PLAN.md) — action-level virtual input
362
+ // Task 1.4 — action-level virtual input
364
363
  // for a synthetic player, promoted from hollowstone's `VirtualInput`. Held
365
364
  // digitals OR into isPressed alongside binding contributions (like
366
365
  // touchButtonsDown above); tap queues, then is promoted to "active" for
@@ -382,14 +381,13 @@ export class InputManager {
382
381
  private virtualDigitalJustReleased = new Set<string>();
383
382
  private virtualScalarValues = new Map<string, number>();
384
383
  private virtualVector2Values = new Map<string, Vector2>();
385
- // D15/T-D15.5 (docs/D15-DETERMINISM-DESIGN.md §2.c) — tick-indexed input
386
- // scheduling + post-gate recording.
384
+ // D15/T-D15.5 — tick-indexed input scheduling + post-gate recording.
387
385
  //
388
386
  // `currentTick` is a FALLBACK, self-incrementing counter, used only when a
389
387
  // caller never tells `poll()` which tick it's servicing (every existing
390
388
  // bare/headless caller — tests, a mount with no `Game` shell behind it —
391
389
  // keeps this exact pre-existing behavior, zero regression). The REAL
392
- // per-world wiring (`vgai-scene-game-adapter.ts`) instead passes the
390
+ // per-world wiring (`setup-three-root-adapter.ts`) instead passes the
393
391
  // shared `Game`-level tick counter into every `poll(tick)` call — this is
394
392
  // deliberate: an InputManager-local counter drifts from the actual game
395
393
  // tick for a paused/frozen world (its `poll()` isn't called every game
@@ -422,7 +420,7 @@ export class InputManager {
422
420
  * action}`) emitted whenever a scheduled entry is discarded because its
423
421
  * target tick's input phase never ran. Wired by whoever constructs this
424
422
  * `InputManager` with access to a `DebugRegistry`
425
- * (`vgai-scene-game-adapter.ts`, the same seed spot as
423
+ * (`setup-three-root-adapter.ts`, the same seed spot as
426
424
  * `setVirtualInputTarget`/`setInputActionsSource`) — `null` (the
427
425
  * default) for a bare/headless `InputManager`, in which case a drop
428
426
  * stays silent (matching pre-D15/T-D15.5 behavior). */
@@ -634,7 +632,7 @@ export class InputManager {
634
632
  /**
635
633
  * Load action map from a .inputmap.json file. Validated via
636
634
  * `InputMapFileSchema` (T4.6) — a malformed input map throws a
637
- * `SceneParseError` naming the file, not a deep TypeError once the bad
635
+ * `AssetParseError` naming the file, not a deep TypeError once the bad
638
636
  * data reaches `isPressed`/`isJustPressed`/`isJustReleased`.
639
637
  */
640
638
  async loadMap(url: string) {
@@ -646,14 +644,14 @@ export class InputManager {
646
644
  }
647
645
  const json = await res.json();
648
646
  const result = InputMapFileSchema.safeParse(json);
649
- if (!result.success) throw new SceneParseError(result.error.issues, url);
647
+ if (!result.success) throw new AssetParseError(result.error.issues, url);
650
648
  data = result.data;
651
649
  } catch (err) {
652
650
  const message = `InputManager.loadMap: failed to load input map "${url}": ${
653
651
  err instanceof Error ? err.message : String(err)
654
652
  }`;
655
653
  console.error(message);
656
- throw err instanceof SceneParseError ? err : new Error(message);
654
+ throw err instanceof AssetParseError ? err : new Error(message);
657
655
  }
658
656
  this.applyParsedMap(data);
659
657
  }
@@ -661,7 +659,7 @@ export class InputManager {
661
659
  /**
662
660
  * F3 — `loadMap`'s synchronous, no-`fetch` sibling: apply an already-in-
663
661
  * memory (or freshly-deserialized) input-map DOCUMENT, validated through
664
- * the identical `InputMapFileSchema` (throws `SceneParseError` naming no
662
+ * the identical `InputMapFileSchema` (throws `AssetParseError` naming no
665
663
  * file, matching `loadMap`'s error shape minus the URL). This is the other
666
664
  * half of the F3 persist round-trip: `toInputMapFile()` serializes,
667
665
  * `loadMapObject` re-applies — no network/file I/O required, so it's also
@@ -669,7 +667,7 @@ export class InputManager {
669
667
  */
670
668
  loadMapObject(data: unknown): InputMapFile {
671
669
  const result = InputMapFileSchema.safeParse(data);
672
- if (!result.success) throw new SceneParseError(result.error.issues);
670
+ if (!result.success) throw new AssetParseError(result.error.issues);
673
671
  this.applyParsedMap(result.data);
674
672
  return result.data;
675
673
  }
@@ -723,7 +721,8 @@ export class InputManager {
723
721
  }
724
722
  }
725
723
 
726
- /** The names of all registered actions (for the InputAdapter to enumerate). */
724
+ /** The names of all registered actions (r3f `world-context` enumerates these
725
+ * for its input-binding projection). */
727
726
  actionNames(): string[] {
728
727
  return [...this.actions.keys()];
729
728
  }
@@ -732,7 +731,7 @@ export class InputManager {
732
731
  * Poll gamepads and virtual look stick (call at start of frame).
733
732
  *
734
733
  * `tick` (D15/T-D15.3/.5) — the GAME tick this call services. Pass the
735
- * live, shared `Game` tick counter here (`vgai-scene-game-adapter.ts` does,
734
+ * live, shared `Game` tick counter here (`setup-three-root-adapter.ts` does,
736
735
  * via `DebugRegistry.getGameTick()`) so `scheduleActionAtTick`'s numbering
737
736
  * never drifts from the actual game tick — the bug an earlier revision of
738
737
  * this feature had: an InputManager-LOCAL counter, incremented once per
@@ -2007,8 +2006,7 @@ export class InputManager {
2007
2006
  }
2008
2007
 
2009
2008
  // ==========================================================================
2010
- // Task 1.4 (E2E-TESTING-BUILD-PLAN.md / SYNTHETIC-PLAYER-SPEC.md §3.2)
2011
- // — action-level virtual input for a synthetic player. Promoted from
2009
+ // Task 1.4 — action-level virtual input for a synthetic player. Promoted from
2012
2010
  // hollowstone's proven `VirtualInput` (`bot/virtual-input.ts`): a bot drives
2013
2011
  // NAMED ACTIONS (not raw devices), and its contributions join action
2014
2012
  // resolution at exactly the same points binding contributions do (see
@@ -2183,13 +2181,12 @@ export class InputManager {
2183
2181
  }
2184
2182
 
2185
2183
  /**
2186
- * D15/T-D15.5 (`docs/D15-DETERMINISM-DESIGN.md` §2.c) — schedule a virtual
2187
- * actuation for exactly `tick`: applied at the START of that tick's
2188
- * `poll()` (composing with `runTicks` a schedule for tick 500 fires
2189
- * exactly once the sim has been driven through tick 500, regardless of
2190
- * burst size). A digital `true` produces a genuine `isJustPressed` edge
2191
- * exactly at the target tick (not merely a held value — see
2192
- * `applyScheduledActionsForTick`).
2184
+ * D15/T-D15.5 — schedule a virtual actuation for exactly `tick`: applied
2185
+ * at the START of that tick's `poll()` (composing with `runTicks` — a
2186
+ * schedule for tick 500 fires exactly once the sim has been driven through
2187
+ * tick 500, regardless of burst size). A digital `true` produces a genuine
2188
+ * `isJustPressed` edge exactly at the target tick (not merely a held value
2189
+ * — see `applyScheduledActionsForTick`).
2193
2190
  *
2194
2191
  * Throws `InputActionError` for an unregistered `action` and the same
2195
2192
  * valueType-mismatch error every typed getter throws (`assertValueType`) —
@@ -2222,7 +2219,7 @@ export class InputManager {
2222
2219
  * `'input.schedule.dropped'` (`{tick, action}`) whenever it discards a
2223
2220
  * scheduled entry for a tick whose input phase never ran. Called once by
2224
2221
  * whoever constructs this `InputManager` with a `DebugRegistry` behind it
2225
- * (`vgai-scene-game-adapter.ts`), the same seed spot as
2222
+ * (`setup-three-root-adapter.ts`), the same seed spot as
2226
2223
  * `setVirtualInputTarget`/`setInputActionsSource` — never called directly
2227
2224
  * by gameplay code. */
2228
2225
  setDebugEmit(fn: ((event: string, detail?: unknown) => void) | null): void {
@@ -90,8 +90,8 @@ export type ActionValueOf<T extends ActionValueType> = T extends 'digital'
90
90
  * value. Returned by the `*Source` sibling of each typed getter (e.g. `getScalarSource`).
91
91
  */
92
92
  export interface ActionValueSource {
93
- /** The binding kind that produced the current value. `'virtual'` (Task 1.4,
94
- * E2E-TESTING-BUILD-PLAN.md) is not a real `InputBinding` — it marks a
93
+ /** The binding kind that produced the current value. `'virtual'`
94
+ * (Task 1.4) is not a real `InputBinding` — it marks a
95
95
  * value written by `InputManager.setVirtualAction` (the synthetic-player
96
96
  * seat), not a device binding. */
97
97
  bindingType: InputBinding['type'] | 'virtual';
@@ -1,10 +1,10 @@
1
1
  // T4.6 — the `.inputmap.json` schema.
2
2
  //
3
- // Was TS-types-only (`input-types.ts`) until T4.6 authored this Zod schema
4
- // see docs/BACKBONE-TASKS.md T4.6. `InputManager.loadMap` (input-manager.ts)
5
- // is this schema's runtime reader: it parses via this schema, then copies
6
- // each action's `bindings` array (and its declared `valueType`, F1) straight
7
- // into `this.actions`/`this.actionValueTypes`.
3
+ // Was TS-types-only (`input-types.ts`) until T4.6 authored this Zod schema.
4
+ // `InputManager.loadMap` (input-manager.ts) is this schema's runtime reader: it
5
+ // parses via this schema, then copies each action's `bindings` array (and its
6
+ // declared `valueType`, F1) straight into
7
+ // `this.actions`/`this.actionValueTypes`.
8
8
  //
9
9
  // Every field has a `.describe()` (repo policy — powers
10
10
  // `scripts/generate-schema.ts` and the T4.1/T4.6 schema-walk coverage test).
package/src/loader.ts CHANGED
@@ -14,6 +14,7 @@ import * as THREE from 'three';
14
14
  import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
15
15
  import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
16
16
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
17
+ import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
17
18
 
18
19
  let _prefix = '/';
19
20
 
@@ -49,9 +50,65 @@ export const textureLoader = new THREE.TextureLoader(loadingManager);
49
50
  export const dracoLoader = new DRACOLoader(loadingManager);
50
51
  dracoLoader.setDecoderPath('/jsm/libs/draco/gltf/');
51
52
 
53
+ /**
54
+ * Shared KTX2Loader for decoding KHR_texture_basisu (KTX2/Basis) textures.
55
+ *
56
+ * Vendored and served EXACTLY like the DRACO decoder above — the Basis
57
+ * transcoder js/wasm live in repo-root `public/jsm/libs/basis/` (the editor)
58
+ * and `packages/editor/template/public/jsm/libs/basis/` (a scaffolded game's
59
+ * own build), pinned in `vendor/upstream-assets.lock.json`, and KTX2Loader
60
+ * appends `basis_transcoder.js` / `.wasm` to this absolute path.
61
+ *
62
+ * Without this wiring, the Asset Budget's "Compress textures · KTX2" output
63
+ * is a GLB nothing in the engine can load — which is why the op and this
64
+ * loader landed together.
65
+ */
66
+ export const ktx2Loader = new KTX2Loader(loadingManager);
67
+ ktx2Loader.setTranscoderPath('/jsm/libs/basis/');
68
+
69
+ let ktx2SupportDetected = false;
70
+
71
+ /**
72
+ * Teach the shared KTX2Loader which compressed formats THIS GPU accepts.
73
+ *
74
+ * KTX2Loader refuses to transcode until it has seen a renderer, and the
75
+ * answer is a property of the page's GPU rather than of any one renderer —
76
+ * so the FIRST renderer to exist supplies it and every later one is a no-op.
77
+ * Called by the engine's own renderer setup (`setup/setup-renderer.ts`) and
78
+ * by the editor's viewport, which between them cover every surface that
79
+ * parses a GLB.
80
+ *
81
+ * This sits on the MOUNT path, so it must never throw for a renderer it
82
+ * cannot question. `detectSupport` reads `renderer.extensions.has/get` (or
83
+ * `renderer.hasFeature` on a WebGPU renderer) — surface a headless stand-in
84
+ * (the test harnesses' fake renderers, any non-WebGL host) does not have.
85
+ * Such a renderer is skipped WITHOUT latching, so the first renderer that
86
+ * can actually answer still configures the loader for the whole page.
87
+ */
88
+ export function detectKtx2Support(renderer: THREE.WebGLRenderer): void {
89
+ if (ktx2SupportDetected) return;
90
+ const probe = renderer as unknown as
91
+ | {
92
+ isWebGPURenderer?: boolean;
93
+ hasFeature?: unknown;
94
+ extensions?: { has?: unknown; get?: unknown };
95
+ }
96
+ | null
97
+ | undefined;
98
+ const canAnswer =
99
+ probe != null &&
100
+ (probe.isWebGPURenderer === true
101
+ ? typeof probe.hasFeature === 'function'
102
+ : typeof probe.extensions?.has === 'function' && typeof probe.extensions?.get === 'function');
103
+ if (!canAnswer) return;
104
+ ktx2Loader.detectSupport(renderer);
105
+ ktx2SupportDetected = true;
106
+ }
107
+
52
108
  /** Pre-configured GLTFLoader using the shared manager, with DRACO decoding wired in. */
53
109
  export const gltfLoader = new GLTFLoader(loadingManager);
54
110
  gltfLoader.setDRACOLoader(dracoLoader);
111
+ gltfLoader.setKTX2Loader(ktx2Loader);
55
112
  // EXT_meshopt_compression decoding (three's own bundled decoder — inline
56
113
  // wasm, no fetch). Without this, a meshopt-compressed GLB — including the
57
114
  // output of the editor's Asset Budget "Compress · meshopt" action (W4a M3) —
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The editor's built-in port fallback, and the paired HMR-socket derivation.
3
+ *
4
+ * ONE owner for the number, because it is spelled in a launcher (`vgai edit`),
5
+ * three servers (dev / prod / packaged), and a client default — a duplicated
6
+ * literal across those drifts silently.
7
+ *
8
+ * **Why not Vite's 5173.** It used to be exactly that, and 5173 is the default
9
+ * of every Vite app on the machine. On a box running any other Vite dev server
10
+ * the engine-repo editor and that app fight over one port, and — worse —
11
+ * `http://…:5173` can resolve to a FOREIGN application. Measured 2026-08-07:
12
+ * an unrelated process on 5173 was what the editor's tab self-heal opened.
13
+ *
14
+ * **The band.** 20000-20199 is vgai's own reserved band: fixed, hand-picked
15
+ * ports that must never be minted for a worktree session. Machine-local
16
+ * allocations start at 20200, so one can never land on this default by chance.
17
+ * 20173 keeps Vite's memorable `173` tail while sitting in a range nothing
18
+ * else claims.
19
+ */
20
+ export const DEFAULT_EDITOR_PORT = 20_173;
21
+
22
+ /**
23
+ * The Vite HMR websocket port paired with an editor port.
24
+ *
25
+ * Editor ports in 20000-29999 map into the parallel 30000-39999 band, so two
26
+ * worktree sessions cannot collide merely because their editor ports differ
27
+ * by 100. Anything outside that band keeps the simple +100 offset.
28
+ *
29
+ * `DEFAULT_EDITOR_PORT` derives 30173 through the first arm — no special case,
30
+ * which is the point: the old expression carried a hardcoded `5173 -> 24678`
31
+ * exception precisely because the default sat outside the band it designed.
32
+ */
33
+ export function editorHmrPort(editorPort: number): number {
34
+ return editorPort >= 20_000 && editorPort < 30_100 ? editorPort + 10_000 : editorPort + 100;
35
+ }
36
+
37
+ /**
38
+ * Wrap a bare IPv6 literal in brackets so it is legal inside a URL authority.
39
+ * `127.0.0.1`, a hostname, and an already-bracketed literal pass through.
40
+ */
41
+ function urlAuthorityHost(host: string): string {
42
+ if (host.startsWith('[')) return host;
43
+ return host.includes(':') ? `[${host}]` : host;
44
+ }
45
+
46
+ /**
47
+ * The ONE composer for an editor origin — every URL vgai PRINTS, OPENS,
48
+ * SELF-HEALS with, records in `.vgai/session.json`, or dials.
49
+ *
50
+ * **It never says `localhost`.** A bare-`localhost` URL is a different address
51
+ * from the one the server is listening on: on a dual-stack box `localhost`
52
+ * resolves `::1` first while the editor binds `127.0.0.1`, so the URL reaches
53
+ * whatever else happens to hold that port on IPv6. Measured 2026-08-07 — the
54
+ * tab bijection's self-heal opened an UNRELATED application in the owner's
55
+ * browser because it composed its reopen URL from `localhost` instead of the
56
+ * host the server had actually bound.
57
+ *
58
+ * `bindHost` is the host the server bound (`resolveBindHost`); callers that
59
+ * only know a port — the CLI and SDK clients dialing a local session — omit it
60
+ * and get the IPv4 loopback literal. A wildcard bind (`0.0.0.0` / `::`, which
61
+ * is what WSL gets) is not an address a browser can visit, so it degrades to
62
+ * that same literal: reachable from the same box, and reachable from Windows
63
+ * through WSL2's loopback forwarding.
64
+ */
65
+ export function editorOrigin(port: number, bindHost?: string): string {
66
+ const host = (bindHost ?? '').trim();
67
+ const unusable = host === '' || host === '0.0.0.0' || host === '::' || host === '[::]';
68
+ return `http://${unusable ? '127.0.0.1' : urlAuthorityHost(host)}:${port}`;
69
+ }
@@ -0,0 +1,49 @@
1
+ // The project manifest's FILENAME. There is exactly ONE.
2
+ //
3
+ // The manifest is `vgai.project.json`. It used to be `vgai.game.json`, and for
4
+ // a while both names were read — WO-9 #62 recorded that dual-read as
5
+ // permanent, on the presumption of an EXTERNAL constituency of projects
6
+ // already on disk. The owner reversed that on 2026-08-02 (the legacy-removal
7
+ // doctrine, `docs/ARCHITECTURE-CORE.md` §Vocabulary): this is pre-release
8
+ // software, no such constituency exists, and a second way to spell the
9
+ // manifest is the defect — two accepted names meant both kept being written,
10
+ // and copies of the "try both" probe grew in five modules that agreed with the
11
+ // real resolver only by coincidence.
12
+ //
13
+ // So the old name is REMOVED, and removed means it REJECTS LOUDLY naming the
14
+ // exact one-line fix — never silently ignored, never redirected. A directory
15
+ // carrying only `vgai.game.json` is an error with a `git mv` in the message,
16
+ // not a project that quietly fails to open.
17
+ //
18
+ // This module is the ONE place either literal is spelled out, and it is
19
+ // dependency-free (no `node:fs`) so browser bundles can import it. The
20
+ // filesystem side lives in `./locate` (`resolveManifestPath`, `hasManifest`,
21
+ // `assertNoRemovedManifestFilename`).
22
+
23
+ /** The manifest filename. Every reader reads it; every writer writes it. */
24
+ export const MANIFEST_FILENAME = 'vgai.project.json';
25
+
26
+ /** The removed filename. Read by nothing — its only job is the error message. */
27
+ export const REMOVED_MANIFEST_FILENAME = 'vgai.game.json';
28
+
29
+ /** True for the manifest filename (basename comparison). */
30
+ export function isManifestFilename(name: string): boolean {
31
+ return name === MANIFEST_FILENAME;
32
+ }
33
+
34
+ /**
35
+ * The verbatim rejection an on-disk `vgai.game.json` earns — the one-line fix
36
+ * spelled out, per the removed-format contract. Shared by the Node
37
+ * (`./locate`) and browser (storage-backend) halves so a project author sees
38
+ * the same sentence wherever the stale name is found.
39
+ */
40
+ export function removedManifestFilenameMessage(where: string): string {
41
+ return (
42
+ `${where}: found \`${REMOVED_MANIFEST_FILENAME}\` and no \`${MANIFEST_FILENAME}\`. ` +
43
+ `\`${REMOVED_MANIFEST_FILENAME}\` was REMOVED (WO-LR1; the legacy-removal doctrine, ` +
44
+ 'docs/ARCHITECTURE-CORE.md §Vocabulary) — nothing reads it any more, and it is ' +
45
+ 'deliberately NOT read as a fallback, because a second accepted name is the defect.\n' +
46
+ `Fix: rename it — \`git mv ${REMOVED_MANIFEST_FILENAME} ${MANIFEST_FILENAME}\`. ` +
47
+ 'The contents are unchanged; only the filename moved.'
48
+ );
49
+ }
@@ -1,6 +1,12 @@
1
- // Game manifest (vgai.game.json) — schema + loader (T3.1 slice 1).
2
- // See docs/GAME-MANIFEST-DESIGN.md.
1
+ // Game manifest (vgai.project.json) — schema + loader (T3.1 slice
2
+ // 1)..
3
3
 
4
+ export {
5
+ isManifestFilename,
6
+ MANIFEST_FILENAME,
7
+ REMOVED_MANIFEST_FILENAME,
8
+ removedManifestFilenameMessage,
9
+ } from './filename';
4
10
  export type { ResolvedAdapter, ResolvedAdapterRoot, ResolvedGameManifest } from './load';
5
11
  export { CAPABILITY_CEILINGS, DEFAULT_SERVER_MODULE, loadGameManifest } from './load';
6
12
  export type {
@@ -8,9 +8,20 @@
8
8
 
9
9
  import { readFileSync } from 'node:fs';
10
10
  import { loadGameManifest, type ResolvedGameManifest } from './load';
11
+ import { resolveManifestPath } from './locate';
12
+
13
+ // The dual-name filesystem lookup lives in `./locate` (Zod-free on purpose);
14
+ // re-exported here so the common "load a project's manifest" import is one
15
+ // module.
16
+ export { hasManifest, manifestWritePath, resolveManifestPath } from './locate';
11
17
 
12
18
  /** Read + JSON.parse `path`, then resolve it via `loadGameManifest`. Node-only. */
13
19
  export function loadGameManifestFile(path: string): ResolvedGameManifest {
14
20
  const raw: unknown = JSON.parse(readFileSync(path, 'utf-8'));
15
21
  return loadGameManifest(raw);
16
22
  }
23
+
24
+ /** {@link loadGameManifestFile} against `dir`'s `vgai.project.json`. */
25
+ export function loadGameManifestDir(dir: string): ResolvedGameManifest {
26
+ return loadGameManifestFile(resolveManifestPath(dir));
27
+ }