@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.
- package/LICENSE +202 -0
- package/README.md +35 -0
- package/package.json +55 -0
- package/src/adapter/authoring.ts +402 -0
- package/src/adapter/colyseus-networking-adapter.ts +72 -0
- package/src/adapter/first-party-systems.ts +103 -0
- package/src/adapter/game-adapter.ts +151 -0
- package/src/adapter/host-context.ts +77 -0
- package/src/adapter/index.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +59 -0
- package/src/adapter/ingest/overlay-applier.ts +207 -0
- package/src/adapter/ingest/overlay-apply.ts +124 -0
- package/src/adapter/ingest/overlay-file.ts +126 -0
- package/src/adapter/ingest/overlay-report.ts +176 -0
- package/src/adapter/ingest/scene-capture.ts +307 -0
- package/src/adapter/ingest/upstream-pin.ts +52 -0
- package/src/adapter/loop-gate-report.ts +54 -0
- package/src/adapter/rapier-physics-adapter.ts +56 -0
- package/src/adapter/system-adapter.ts +154 -0
- package/src/adapter/transform.ts +18 -0
- package/src/adapter/vgai-scene-game-adapter.ts +886 -0
- package/src/adapter/world-kind.ts +34 -0
- package/src/ai/navigation.ts +164 -0
- package/src/animation/anim-graph-types.ts +56 -0
- package/src/animation/anim-graph.ts +406 -0
- package/src/animation/anim-system.ts +28 -0
- package/src/animation/blend-node.ts +119 -0
- package/src/animation/property-track.ts +178 -0
- package/src/animation/schema.ts +204 -0
- package/src/assets.ts +80 -0
- package/src/audio/ambient.ts +300 -0
- package/src/audio/impacts.ts +212 -0
- package/src/audio/index.ts +7 -0
- package/src/audio/movement.ts +140 -0
- package/src/audio/musical.ts +200 -0
- package/src/audio/ui-sounds.ts +171 -0
- package/src/audio/vehicle.ts +235 -0
- package/src/audio/weapons.ts +152 -0
- package/src/core/game-loop.ts +127 -0
- package/src/core/system-runner.ts +298 -0
- package/src/core/types.ts +58 -0
- package/src/dev/console-bridge.ts +83 -0
- package/src/dev/debug-draw.ts +80 -0
- package/src/dev/logger.ts +119 -0
- package/src/ecs/component-manager.ts +748 -0
- package/src/ecs/game-component.ts +147 -0
- package/src/ecs/hmr-swap-report.ts +65 -0
- package/src/input/input-manager.ts +439 -0
- package/src/input/input-types.ts +19 -0
- package/src/input/schema.ts +129 -0
- package/src/loader.ts +70 -0
- package/src/manifest/index.ts +24 -0
- package/src/manifest/load-file.ts +16 -0
- package/src/manifest/load.ts +378 -0
- package/src/manifest/schema.ts +375 -0
- package/src/physics/collision-system.ts +76 -0
- package/src/physics/physics-registry.ts +83 -0
- package/src/physics/transform-writer.ts +41 -0
- package/src/physics/trigger-dispatch.ts +97 -0
- package/src/react/game-state.tsx +172 -0
- package/src/render/auto-batcher.ts +169 -0
- package/src/render/render-batch-system.ts +268 -0
- package/src/render/render-features.ts +146 -0
- package/src/render/render-settings.ts +72 -0
- package/src/runtime/create-runtime.ts +1152 -0
- package/src/runtime/frame-selector-cache.ts +81 -0
- package/src/runtime/game.ts +1003 -0
- package/src/runtime/input-router.ts +213 -0
- package/src/runtime/mount-game.ts +269 -0
- package/src/runtime/mount-manifest.ts +361 -0
- package/src/runtime/scene-ui-bridge.ts +86 -0
- package/src/runtime/scene-ui-data.ts +119 -0
- package/src/runtime/state-bridge.ts +79 -0
- package/src/runtime/types.ts +196 -0
- package/src/scene/asset-loaders.ts +195 -0
- package/src/scene/asset-paths.ts +123 -0
- package/src/scene/asset-registry.ts +67 -0
- package/src/scene/collider-dimensions.ts +125 -0
- package/src/scene/component-registry.ts +40 -0
- package/src/scene/defaults.ts +164 -0
- package/src/scene/geometries/index.ts +7 -0
- package/src/scene/geometries/terrain.ts +42 -0
- package/src/scene/geometry-registry.ts +42 -0
- package/src/scene/instance-registry.ts +84 -0
- package/src/scene/instancers/grid.ts +38 -0
- package/src/scene/instancers/index.ts +7 -0
- package/src/scene/light-camera-factory.ts +97 -0
- package/src/scene/material-factory.ts +211 -0
- package/src/scene/material-registry.ts +73 -0
- package/src/scene/materials/index.ts +7 -0
- package/src/scene/materials/water.ts +56 -0
- package/src/scene/parse.ts +71 -0
- package/src/scene/particles-factory.ts +383 -0
- package/src/scene/scene-apply.ts +356 -0
- package/src/scene/scene-diff-schema.ts +115 -0
- package/src/scene/scene-diff-types.ts +29 -0
- package/src/scene/scene-loader.ts +1533 -0
- package/src/scene/scene-query.ts +63 -0
- package/src/scene/scene-types.ts +34 -0
- package/src/scene/scene-version.ts +40 -0
- package/src/scene/schema/animation.ts +95 -0
- package/src/scene/schema/audio.ts +25 -0
- package/src/scene/schema/camera.ts +21 -0
- package/src/scene/schema/collider.ts +69 -0
- package/src/scene/schema/entity-ref.ts +78 -0
- package/src/scene/schema/entity.ts +169 -0
- package/src/scene/schema/environment.ts +384 -0
- package/src/scene/schema/index.ts +95 -0
- package/src/scene/schema/instances.ts +35 -0
- package/src/scene/schema/joint.ts +26 -0
- package/src/scene/schema/light.ts +38 -0
- package/src/scene/schema/material.ts +113 -0
- package/src/scene/schema/mesh.ts +108 -0
- package/src/scene/schema/particles.ts +398 -0
- package/src/scene/schema/physics.ts +49 -0
- package/src/scene/schema/scene-file.ts +299 -0
- package/src/scene/schema/shadow.ts +24 -0
- package/src/scene/schema/spline.ts +21 -0
- package/src/scene/schema/tuples.ts +21 -0
- package/src/scene/schema/ui.ts +602 -0
- package/src/scene/user-data.ts +203 -0
- package/src/setup/setup-audio.ts +60 -0
- package/src/setup/setup-particles.ts +23 -0
- package/src/setup/setup-physics.ts +67 -0
- package/src/setup/setup-renderer.ts +529 -0
- package/src/types-n8ao.d.ts +37 -0
- package/src/types-realism-effects.d.ts +61 -0
- package/src/world2d/authoring-2d.ts +208 -0
- package/src/world2d/capture-to-scene2d.ts +52 -0
- package/src/world2d/collision-2d.ts +106 -0
- package/src/world2d/components-2d.ts +86 -0
- package/src/world2d/index.ts +66 -0
- package/src/world2d/ingest-iframe-2d.ts +255 -0
- package/src/world2d/ingest2d.ts +131 -0
- package/src/world2d/physics2d-registry.ts +49 -0
- package/src/world2d/pixi-game-adapter.ts +325 -0
- package/src/world2d/pixi-surface.ts +78 -0
- package/src/world2d/scene-capture-2d.ts +117 -0
- package/src/world2d/scene2d-loader.ts +308 -0
- package/src/world2d/schema/entity2d.ts +145 -0
- package/src/world2d/schema/physics2d.ts +53 -0
- package/src/world2d/schema/sprite.ts +71 -0
- package/src/world2d/schema/tilemap.ts +22 -0
- package/src/world2d/schema/tuples2d.ts +25 -0
- package/src/world2d/system-adapters-2d.ts +49 -0
- package/src/world2d/transform-writer-2d.ts +24 -0
- package/src/world2d/types.ts +55 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Container } from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AuthoringAdapter-2D — the world2d analog of the editor AuthoringAdapter /
|
|
5
|
+
* IngestAuthoringAdapter, implemented DIRECTLY over a live PixiJS display tree
|
|
6
|
+
* (no fabricated Scene2D / SceneEntity — the anti-shim rule). Used by the editor to
|
|
7
|
+
* inspect + edit world2d entities (first-party OR an ingested unmodified game),
|
|
8
|
+
* with stable structural-path ids so edits re-bind after the tree rebuilds.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface EditorNode2D {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
kind: 'sprite' | 'animatedsprite' | 'text' | 'container' | string;
|
|
15
|
+
parentId: string | null;
|
|
16
|
+
childIds: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Transform2DValue {
|
|
20
|
+
position: [number, number];
|
|
21
|
+
rotation: number;
|
|
22
|
+
scale: [number, number];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Property2D {
|
|
26
|
+
path: string;
|
|
27
|
+
label: string;
|
|
28
|
+
type: 'string' | 'number' | 'boolean' | 'vec2' | 'color';
|
|
29
|
+
value: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A persisted edit overlay keyed by structural-path id (the 2D analog of IngestOverlay). */
|
|
33
|
+
export interface Override2D {
|
|
34
|
+
position?: [number, number];
|
|
35
|
+
rotation?: number;
|
|
36
|
+
scale?: [number, number];
|
|
37
|
+
tint?: number;
|
|
38
|
+
visible?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export type Overlay2D = Record<string, Override2D>;
|
|
41
|
+
|
|
42
|
+
function kindOf(o: Container): string {
|
|
43
|
+
const ctor = o.constructor?.name?.toLowerCase() ?? 'container';
|
|
44
|
+
if (ctor.includes('animatedsprite')) return 'animatedsprite';
|
|
45
|
+
if (ctor.includes('sprite')) return 'sprite';
|
|
46
|
+
if (ctor.includes('text')) return 'text';
|
|
47
|
+
return 'container';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class AuthoringAdapter2D {
|
|
51
|
+
private readonly byId = new Map<string, Container>();
|
|
52
|
+
private overlay: Overlay2D = {};
|
|
53
|
+
private dirty = false;
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly root: Container,
|
|
57
|
+
opts: { overlay?: Overlay2D } = {},
|
|
58
|
+
) {
|
|
59
|
+
if (opts.overlay) this.overlay = opts.overlay;
|
|
60
|
+
this.refresh();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Walk the live tree assigning deterministic structural-path ids, reapply overlay. */
|
|
64
|
+
refresh(): { count: number; sprites: number } {
|
|
65
|
+
this.byId.clear();
|
|
66
|
+
let count = 0;
|
|
67
|
+
let sprites = 0;
|
|
68
|
+
const visit = (o: Container, path: string): void => {
|
|
69
|
+
const id = `w2d:${path}:${kindOf(o)}:${o.label ?? ''}`;
|
|
70
|
+
(o as Container & { __authId?: string }).__authId = id;
|
|
71
|
+
this.byId.set(id, o);
|
|
72
|
+
count++;
|
|
73
|
+
if (kindOf(o).includes('sprite')) sprites++;
|
|
74
|
+
o.children.forEach((c, i) => {
|
|
75
|
+
visit(c as Container, `${path}/${i}`);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
this.root.children.forEach((c, i) => {
|
|
79
|
+
visit(c as Container, `${i}`);
|
|
80
|
+
});
|
|
81
|
+
// Reapply any saved overlay edits to the live tree.
|
|
82
|
+
for (const [id, ov] of Object.entries(this.overlay)) this.applyOverride(id, ov);
|
|
83
|
+
return { count, sprites };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
roots(): EditorNode2D[] {
|
|
87
|
+
return this.root.children.map((c) => this.toNode(c as Container));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
node(id: string): EditorNode2D | null {
|
|
91
|
+
const o = this.byId.get(id);
|
|
92
|
+
return o ? this.toNode(o) : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
displayObject(id: string): Container | null {
|
|
96
|
+
return this.byId.get(id) ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private toNode(o: Container): EditorNode2D {
|
|
100
|
+
const self = o as Container & { __authId?: string };
|
|
101
|
+
return {
|
|
102
|
+
id: self.__authId ?? '',
|
|
103
|
+
label: o.label ?? kindOf(o),
|
|
104
|
+
kind: kindOf(o),
|
|
105
|
+
parentId: (o.parent as (Container & { __authId?: string }) | null)?.__authId ?? null,
|
|
106
|
+
childIds: o.children.map((c) => (c as Container & { __authId?: string }).__authId ?? ''),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Transforms ---
|
|
111
|
+
getTransform(id: string): Transform2DValue | null {
|
|
112
|
+
const o = this.byId.get(id);
|
|
113
|
+
if (!o) return null;
|
|
114
|
+
return {
|
|
115
|
+
position: [o.position.x, o.position.y],
|
|
116
|
+
rotation: o.rotation,
|
|
117
|
+
scale: [o.scale.x, o.scale.y],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setTransform(id: string, t: Partial<Transform2DValue>): void {
|
|
122
|
+
const o = this.byId.get(id);
|
|
123
|
+
if (!o) return;
|
|
124
|
+
if (t.position) o.position.set(t.position[0], t.position[1]);
|
|
125
|
+
if (t.rotation !== undefined) o.rotation = t.rotation;
|
|
126
|
+
if (t.scale) o.scale.set(t.scale[0], t.scale[1]);
|
|
127
|
+
this.overlay[id] ??= {};
|
|
128
|
+
const ov = this.overlay[id];
|
|
129
|
+
if (t.position) ov.position = t.position;
|
|
130
|
+
if (t.rotation !== undefined) ov.rotation = t.rotation;
|
|
131
|
+
if (t.scale) ov.scale = t.scale;
|
|
132
|
+
this.dirty = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Inspector (reflected fields) ---
|
|
136
|
+
properties(id: string): Property2D[] {
|
|
137
|
+
const o = this.byId.get(id);
|
|
138
|
+
if (!o) return [];
|
|
139
|
+
const props: Property2D[] = [
|
|
140
|
+
{ path: 'label', label: 'Name', type: 'string', value: o.label ?? '' },
|
|
141
|
+
{ path: 'visible', label: 'Visible', type: 'boolean', value: o.visible },
|
|
142
|
+
{ path: 'alpha', label: 'Alpha', type: 'number', value: o.alpha },
|
|
143
|
+
];
|
|
144
|
+
if ('tint' in o)
|
|
145
|
+
props.push({
|
|
146
|
+
path: 'tint',
|
|
147
|
+
label: 'Tint',
|
|
148
|
+
type: 'color',
|
|
149
|
+
value: (o as { tint: number }).tint,
|
|
150
|
+
});
|
|
151
|
+
return props;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
set(id: string, path: string, value: unknown): void {
|
|
155
|
+
const o = this.byId.get(id) as (Container & Record<string, unknown>) | undefined;
|
|
156
|
+
if (!o) return;
|
|
157
|
+
o[path] = value;
|
|
158
|
+
this.overlay[id] ??= {};
|
|
159
|
+
if (path === 'tint') this.overlay[id].tint = value as number;
|
|
160
|
+
if (path === 'visible') this.overlay[id].visible = value as boolean;
|
|
161
|
+
this.dirty = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private applyOverride(id: string, ov: Override2D): void {
|
|
165
|
+
const o = this.byId.get(id);
|
|
166
|
+
if (!o) return;
|
|
167
|
+
if (ov.position) o.position.set(ov.position[0], ov.position[1]);
|
|
168
|
+
if (ov.rotation !== undefined) o.rotation = ov.rotation;
|
|
169
|
+
if (ov.scale) o.scale.set(ov.scale[0], ov.scale[1]);
|
|
170
|
+
if (ov.tint !== undefined && 'tint' in o) (o as { tint: number }).tint = ov.tint;
|
|
171
|
+
if (ov.visible !== undefined) o.visible = ov.visible;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Re-apply a saved/external overlay onto the (already-walked) live tree, merging
|
|
176
|
+
* it into the in-memory overlay so a later `serializeOverlay()`/save preserves it
|
|
177
|
+
* and a later `refresh()` re-applies it again. Ids absent from the current tree
|
|
178
|
+
* are skipped (orphaned, not an error) — the 2D analog of
|
|
179
|
+
* `IngestAuthoringAdapter.applyOverlay` (T3.2 slice 3). Used both for the initial
|
|
180
|
+
* reload (constructor `opts.overlay`) and for `PersistenceProvider.applyExternal`
|
|
181
|
+
* (re-apply a changed overlay file without a full remount).
|
|
182
|
+
*/
|
|
183
|
+
applyOverlay(overlay: Overlay2D): { applied: number; orphaned: number } {
|
|
184
|
+
let applied = 0;
|
|
185
|
+
let orphaned = 0;
|
|
186
|
+
for (const [id, ov] of Object.entries(overlay)) {
|
|
187
|
+
if (!this.byId.has(id)) {
|
|
188
|
+
orphaned++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
this.applyOverride(id, ov);
|
|
192
|
+
this.overlay[id] = { ...this.overlay[id], ...ov };
|
|
193
|
+
applied++;
|
|
194
|
+
}
|
|
195
|
+
return { applied, orphaned };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
isDirty(): boolean {
|
|
199
|
+
return this.dirty;
|
|
200
|
+
}
|
|
201
|
+
serializeOverlay(): Overlay2D {
|
|
202
|
+
return this.overlay;
|
|
203
|
+
}
|
|
204
|
+
/** Clear the dirty flag after a successful save (mirrors IngestAuthoringAdapter). */
|
|
205
|
+
markClean(): void {
|
|
206
|
+
this.dirty = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Container } from 'pixi.js';
|
|
2
|
+
import type { Entity2D, Scene2DFile } from './schema/entity2d';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Capture an ingested/live PixiJS subtree into authored Scene2D entities — the
|
|
6
|
+
* world2d analog of capture-to-vscn (`ingest-capture.ts`). Recognizable sprites/
|
|
7
|
+
* text/containers map to first-party `sprite`/`text`/`transform2d` sections;
|
|
8
|
+
* turns inspection into portable authored content.
|
|
9
|
+
*
|
|
10
|
+
* Honest ceiling (mirrors the 3D one): a Scene2D `sprite` expresses tint + size +
|
|
11
|
+
* anchor, not an arbitrary texture/GraphicsContext, so a captured display keeps its
|
|
12
|
+
* transform + tint + size; bespoke Graphics geometry is captured as a plain tinted
|
|
13
|
+
* sprite placeholder.
|
|
14
|
+
*/
|
|
15
|
+
function numToHex(n: number | undefined): string {
|
|
16
|
+
return `#${((n ?? 0xffffff) & 0xffffff).toString(16).padStart(6, '0')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function captureNode(o: Container): Entity2D {
|
|
20
|
+
const ctor = o.constructor?.name?.toLowerCase() ?? '';
|
|
21
|
+
const entity: Entity2D = {
|
|
22
|
+
name: o.label ?? ctor ?? 'entity',
|
|
23
|
+
surface: 'world2d',
|
|
24
|
+
transform2d: {
|
|
25
|
+
position: [o.position.x, o.position.y],
|
|
26
|
+
rotation: o.rotation,
|
|
27
|
+
scale: [o.scale.x, o.scale.y],
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
if (ctor.includes('text')) {
|
|
31
|
+
entity.text = { text: (o as { text?: string }).text ?? '' };
|
|
32
|
+
} else if (ctor.includes('sprite')) {
|
|
33
|
+
entity.sprite = {
|
|
34
|
+
tint: numToHex((o as { tint?: number }).tint),
|
|
35
|
+
width: Math.round(o.width),
|
|
36
|
+
height: Math.round(o.height),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const kids = o.children?.filter((c) => (c as Container).label !== undefined) ?? [];
|
|
40
|
+
if (kids.length) entity.children = kids.map((c) => captureNode(c as Container));
|
|
41
|
+
return entity;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Capture selected subtree roots (by display object) into a Scene2D file. */
|
|
45
|
+
export function captureToScene2D(roots: Container[], name = 'captured-world2d'): Scene2DFile {
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
name,
|
|
49
|
+
surface: 'world2d',
|
|
50
|
+
entities: roots.map(captureNode),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type RAPIER from '@dimforge/rapier2d-compat';
|
|
2
|
+
import type { NodeOf } from '../ecs/game-component';
|
|
3
|
+
import type { GameContext } from '../runtime/types';
|
|
4
|
+
import type { Physics2DRegistry } from './physics2d-registry';
|
|
5
|
+
|
|
6
|
+
export type Collision2DHandler = (handleA: number, handleB: number, started: boolean) => void;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Drains the Rapier 2D event queue each postPhysics and dispatches collision
|
|
10
|
+
* events — the world2d analog of `createCollisionSystem`. Events are buffered then
|
|
11
|
+
* handlers run after the drain returns (same re-entrancy guard as the 3D system:
|
|
12
|
+
* a handler must not call back into Rapier while the queue is borrowed).
|
|
13
|
+
*/
|
|
14
|
+
export function createCollision2DSystem(rapierWorld: RAPIER.World, eventQueue: RAPIER.EventQueue) {
|
|
15
|
+
const handlers: Collision2DHandler[] = [];
|
|
16
|
+
return {
|
|
17
|
+
onCollision(handler: Collision2DHandler) {
|
|
18
|
+
handlers.push(handler);
|
|
19
|
+
},
|
|
20
|
+
offCollision(handler: Collision2DHandler) {
|
|
21
|
+
const idx = handlers.indexOf(handler);
|
|
22
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
23
|
+
},
|
|
24
|
+
drain() {
|
|
25
|
+
const events: Array<[number, number, boolean]> = [];
|
|
26
|
+
eventQueue.drainCollisionEvents((h1, h2, started) => {
|
|
27
|
+
events.push([h1, h2, started]);
|
|
28
|
+
});
|
|
29
|
+
for (const [h1, h2, started] of events) {
|
|
30
|
+
for (let i = 0; i < handlers.length; i++) handlers[i]!(h1, h2, started);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
getBodyHandleFromCollider(colliderHandle: number): number | undefined {
|
|
34
|
+
const collider = rapierWorld.getCollider(colliderHandle);
|
|
35
|
+
return collider?.parent()?.handle;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type Collision2DSystem = ReturnType<typeof createCollision2DSystem>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The minimal component-manager capability `createTriggerDispatch2D` needs —
|
|
44
|
+
* `getComponents` returning instances with optional trigger callbacks, typed
|
|
45
|
+
* honestly against `NodeOf<'pixijs'>` (T7.2, D8 — this dispatch is always
|
|
46
|
+
* pixi-world-scoped) rather than the full manager type. Deliberately a
|
|
47
|
+
* structural slice: the unified `ComponentManager` (`ecs/component-manager.ts`,
|
|
48
|
+
* what production wires up since T7.3 slice 1, and the only manager left —
|
|
49
|
+
* the world2d silo's manager is deleted, T7.3 slice 2) satisfies it — this
|
|
50
|
+
* dispatcher has no dependency on which `WorldKind` a caller's manager was
|
|
51
|
+
* constructed for.
|
|
52
|
+
*/
|
|
53
|
+
export interface TriggerDispatchComponents {
|
|
54
|
+
getComponents(node: NodeOf<'pixijs'>): ReadonlyArray<{
|
|
55
|
+
onTriggerEnter?(other: NodeOf<'pixijs'>, ctx: GameContext): void;
|
|
56
|
+
onTriggerExit?(other: NodeOf<'pixijs'>, ctx: GameContext): void;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Turn Rapier 2D sensor overlaps into `onTriggerEnter`/`onTriggerExit` callbacks on
|
|
62
|
+
* the GameComponents of the two involved display objects — the 2D analog of
|
|
63
|
+
* `createTriggerDispatch`. Only sensor-involved collisions dispatch triggers.
|
|
64
|
+
*
|
|
65
|
+
* `ctx` is typed `GameContext` (the unified manager's typing) — production
|
|
66
|
+
* (`pixi-game-adapter.ts`) casts its real `World2DContext` to `GameContext`
|
|
67
|
+
* before passing it here; individual `GameComponent<'pixijs'>` subclasses
|
|
68
|
+
* that need real 2D fields off `ctx` widen their own lifecycle-method
|
|
69
|
+
* signature to `GameContext | World2DContext` and narrow back down (see
|
|
70
|
+
* `Camera2DFollow.update` in `./components-2d.ts` — the worked example of
|
|
71
|
+
* the pattern since the world2d-demo silo and its `first-party-scene.ts`
|
|
72
|
+
* were deleted, T6.1 slice 4). This function only ever forwards `ctx` — it
|
|
73
|
+
* never reads a field off it itself.
|
|
74
|
+
*/
|
|
75
|
+
export function createTriggerDispatch2D(
|
|
76
|
+
physics: Physics2DRegistry,
|
|
77
|
+
components: TriggerDispatchComponents,
|
|
78
|
+
rapierWorld: RAPIER.World,
|
|
79
|
+
ctx: GameContext,
|
|
80
|
+
): Collision2DHandler {
|
|
81
|
+
return (handleA, handleB, started) => {
|
|
82
|
+
const colA = rapierWorld.getCollider(handleA);
|
|
83
|
+
const colB = rapierWorld.getCollider(handleB);
|
|
84
|
+
if (!colA?.isSensor() && !colB?.isSensor()) return;
|
|
85
|
+
|
|
86
|
+
const objA = physics.getByColliderHandle(handleA);
|
|
87
|
+
const objB = physics.getByColliderHandle(handleB);
|
|
88
|
+
if (!objA || !objB) return;
|
|
89
|
+
|
|
90
|
+
notify(components, objA, objB, started, ctx);
|
|
91
|
+
notify(components, objB, objA, started, ctx);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function notify(
|
|
96
|
+
components: TriggerDispatchComponents,
|
|
97
|
+
self: NodeOf<'pixijs'>,
|
|
98
|
+
other: NodeOf<'pixijs'>,
|
|
99
|
+
started: boolean,
|
|
100
|
+
ctx: GameContext,
|
|
101
|
+
): void {
|
|
102
|
+
for (const inst of components.getComponents(self)) {
|
|
103
|
+
if (started) inst.onTriggerEnter?.(other, ctx);
|
|
104
|
+
else inst.onTriggerExit?.(other, ctx);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { GameComponent } from '../ecs/game-component';
|
|
3
|
+
import type { GameContext } from '../runtime/types';
|
|
4
|
+
import type { World2DContext } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Camera2DFollow — the canonical 2D camera (spec backend-2d-camera): a 2D camera is
|
|
8
|
+
* a transform on the world Container (`ctx.stage`). Attached to the entity to follow;
|
|
9
|
+
* each frame it pans/zooms `ctx.stage` so the target display stays centered. Mirrors
|
|
10
|
+
* the 3D "camera GameComponent on a camera entity" canonical-way rule.
|
|
11
|
+
*/
|
|
12
|
+
export class Camera2DFollow extends GameComponent<'pixijs'> {
|
|
13
|
+
static declaredKind = 'pixijs' as const;
|
|
14
|
+
static phase = 'preRender' as const;
|
|
15
|
+
static schema = z.object({
|
|
16
|
+
zoom: z.number().default(1).describe('Camera zoom (world Container scale)'),
|
|
17
|
+
smoothing: z.number().default(0.12).describe('Follow lerp factor 0..1 (1 = instant)'),
|
|
18
|
+
});
|
|
19
|
+
zoom = 1;
|
|
20
|
+
smoothing = 0.12;
|
|
21
|
+
|
|
22
|
+
// CONFLICT (bridged, same mechanism the deleted `GameComponent2D` alias
|
|
23
|
+
// used — T7.3 slice 2): `GameComponent<K>`'s lifecycle methods are typed
|
|
24
|
+
// `ctx: GameContext` (D8 §2 — `ctx` always means the game-level context),
|
|
25
|
+
// but this component genuinely needs `World2DContext` fields (`app`/
|
|
26
|
+
// `stage`), which share no structure with `GameContext` (neither is
|
|
27
|
+
// assignable to the other) — a plain `ctx: World2DContext` override would
|
|
28
|
+
// fail even under method bivariance (TS2416). Widening the parameter to
|
|
29
|
+
// the union is a valid (non-bivariant) override on its own — accepting a
|
|
30
|
+
// superset of the base signature is always sound — then narrowing back
|
|
31
|
+
// down via the cast below. At runtime this always receives the real
|
|
32
|
+
// `World2DContext` (`pixi-game-adapter.ts` casts it to `GameContext` only
|
|
33
|
+
// for the unified manager's internal typing).
|
|
34
|
+
update(_dt: number, gameCtx: GameContext | World2DContext): void {
|
|
35
|
+
const ctx = gameCtx as World2DContext;
|
|
36
|
+
const screen = ctx.app.screen;
|
|
37
|
+
const targetX = screen.width / 2 - this.node.x * this.zoom;
|
|
38
|
+
const targetY = screen.height / 2 - this.node.y * this.zoom;
|
|
39
|
+
const stage = ctx.stage;
|
|
40
|
+
stage.scale.set(this.zoom);
|
|
41
|
+
stage.position.x += (targetX - stage.position.x) * this.smoothing;
|
|
42
|
+
stage.position.y += (targetY - stage.position.y) * this.smoothing;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Clickable2D — federated-events picking (spec backend-2d-input-picking). The sprite
|
|
48
|
+
* must be authored `interactive: true` (loader sets eventMode='static'); on pointerdown
|
|
49
|
+
* the sprite cycles tint, proving Pixi world picking works and is independent of the
|
|
50
|
+
* React/DOM HUD layer.
|
|
51
|
+
*
|
|
52
|
+
* INPUT ROUTING RULE (resolves the headline open design question): the three surfaces
|
|
53
|
+
* are stacked DOM siblings — React/DOM HUD (top, z-index, pointer-events gated) gets the
|
|
54
|
+
* event first; if it doesn't consume it, the Pixi canvas's federated EventSystem hit-tests
|
|
55
|
+
* world2d display objects; Three's raycaster handles the world3d canvas. Because each
|
|
56
|
+
* surface owns its own canvas/DOM node, a click on the HUD does not also pick the Pixi
|
|
57
|
+
* world beneath it, and vice-versa — no cross-surface double-picking.
|
|
58
|
+
*/
|
|
59
|
+
export class Clickable2D extends GameComponent<'pixijs'> {
|
|
60
|
+
static declaredKind = 'pixijs' as const;
|
|
61
|
+
static schema = z.object({
|
|
62
|
+
tints: z
|
|
63
|
+
.array(z.string())
|
|
64
|
+
.default(['#ff6b6b', '#4ecdc4', '#ffd23f', '#a06bff'])
|
|
65
|
+
.describe('Tint cycle'),
|
|
66
|
+
});
|
|
67
|
+
tints: string[] = ['#ff6b6b', '#4ecdc4', '#ffd23f', '#a06bff'];
|
|
68
|
+
private idx = 0;
|
|
69
|
+
private handler = (): void => {
|
|
70
|
+
this.idx = (this.idx + 1) % this.tints.length;
|
|
71
|
+
(this.node as { tint?: number }).tint = Number.parseInt(
|
|
72
|
+
this.tints[this.idx]!.replace('#', ''),
|
|
73
|
+
16,
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
init(): void {
|
|
78
|
+
this.node.eventMode = 'static';
|
|
79
|
+
this.node.cursor = 'pointer';
|
|
80
|
+
this.node.on('pointerdown', this.handler);
|
|
81
|
+
}
|
|
82
|
+
update(): void {}
|
|
83
|
+
dispose(): void {
|
|
84
|
+
this.node.off('pointerdown', this.handler);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* world2d — the PixiJS 2D render surface for vgai.
|
|
3
|
+
*
|
|
4
|
+
* A peer to the Three.js world3d surface and the React UI surface (ARCHITECTURE.md
|
|
5
|
+
* §Render Surfaces). It LITERALLY reuses the engine's loop primitives (`createGameLoop`,
|
|
6
|
+
* `createSystemRunner`, `PHASE_ORDER`) and faithfully MIRRORS the entity / authored-data
|
|
7
|
+
* model as a 2D twin (`Scene2D`), instantiating entities as PixiJS display objects
|
|
8
|
+
* instead of Object3Ds, with 2D physics/collision via native Rapier 2D — built on
|
|
9
|
+
* native libraries with thin adapters. Gameplay behavior attaches via the SAME
|
|
10
|
+
* `GameComponent<'pixijs'>` (`ecs/game-component.ts`) and unified `ComponentManager`
|
|
11
|
+
* (`ecs/component-manager.ts`) every threejs world uses (T7.3).
|
|
12
|
+
*
|
|
13
|
+
* See docs/PIXI-WORLD2D-MAXIMAL-SPEC.json (FROZEN).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
AuthoringAdapter2D,
|
|
18
|
+
type EditorNode2D,
|
|
19
|
+
type Overlay2D,
|
|
20
|
+
type Override2D,
|
|
21
|
+
type Property2D,
|
|
22
|
+
type Transform2DValue,
|
|
23
|
+
} from './authoring-2d';
|
|
24
|
+
export { captureToScene2D } from './capture-to-scene2d';
|
|
25
|
+
export {
|
|
26
|
+
type Collision2DSystem,
|
|
27
|
+
createCollision2DSystem,
|
|
28
|
+
createTriggerDispatch2D,
|
|
29
|
+
} from './collision-2d';
|
|
30
|
+
export { Camera2DFollow, Clickable2D } from './components-2d';
|
|
31
|
+
export {
|
|
32
|
+
type EmbedMount2D,
|
|
33
|
+
type IframeReachableMount2D,
|
|
34
|
+
mountIngestGame2DEmbed,
|
|
35
|
+
mountIngestGame2DIframeReachable,
|
|
36
|
+
} from './ingest-iframe-2d';
|
|
37
|
+
export { type IngestGame2D, type IngestMount2D, mountIngestGame2D } from './ingest2d';
|
|
38
|
+
export {
|
|
39
|
+
createPhysics2DRegistry,
|
|
40
|
+
type Physics2DRefs,
|
|
41
|
+
type Physics2DRegistry,
|
|
42
|
+
} from './physics2d-registry';
|
|
43
|
+
export {
|
|
44
|
+
createWorld2DRuntime,
|
|
45
|
+
type MountedGame2D,
|
|
46
|
+
type PixiSceneConfig,
|
|
47
|
+
PixiSceneGameAdapter,
|
|
48
|
+
type World2DHost,
|
|
49
|
+
type World2DSession,
|
|
50
|
+
type World2DSetupFn,
|
|
51
|
+
} from './pixi-game-adapter';
|
|
52
|
+
export { createPixiSurface, type PixiSurface, type PixiSurfaceOptions } from './pixi-surface';
|
|
53
|
+
export {
|
|
54
|
+
type CapturedRuntime2D,
|
|
55
|
+
installSceneCapture2D,
|
|
56
|
+
type SceneCapture2DHandle,
|
|
57
|
+
} from './scene-capture-2d';
|
|
58
|
+
export { loadScene2DFromData, type Scene2DInstance } from './scene2d-loader';
|
|
59
|
+
export * from './schema/entity2d';
|
|
60
|
+
export * from './schema/physics2d';
|
|
61
|
+
export * from './schema/sprite';
|
|
62
|
+
export * from './schema/tilemap';
|
|
63
|
+
export * from './schema/tuples2d';
|
|
64
|
+
export { createPhysicsAdapter2D, type PhysicsAdapter2D } from './system-adapters-2d';
|
|
65
|
+
export { createTransformWriter2D } from './transform-writer-2d';
|
|
66
|
+
export type { Component2DRegistry, World2DContext } from './types';
|