@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,308 @@
|
|
|
1
|
+
import type RAPIER from '@dimforge/rapier2d-compat';
|
|
2
|
+
import { CompositeTilemap } from '@pixi/tilemap';
|
|
3
|
+
import {
|
|
4
|
+
AnimatedSprite,
|
|
5
|
+
Assets,
|
|
6
|
+
BlurFilter,
|
|
7
|
+
ColorMatrixFilter,
|
|
8
|
+
Container,
|
|
9
|
+
Sprite,
|
|
10
|
+
Text,
|
|
11
|
+
Texture,
|
|
12
|
+
} from 'pixi.js';
|
|
13
|
+
import type { ComponentManager } from '../ecs/component-manager';
|
|
14
|
+
import { SceneParseError } from '../scene/parse';
|
|
15
|
+
import { type Entity2D, type Scene2DFile, Scene2DSchema } from './schema/entity2d';
|
|
16
|
+
import type { SceneSprite, SceneText2D } from './schema/sprite';
|
|
17
|
+
import type { SceneTilemap } from './schema/tilemap';
|
|
18
|
+
import type { Component2DRegistry, World2DContext } from './types';
|
|
19
|
+
|
|
20
|
+
const BLEND: Record<string, 'normal' | 'add' | 'multiply' | 'screen'> = {
|
|
21
|
+
normal: 'normal',
|
|
22
|
+
add: 'add',
|
|
23
|
+
multiply: 'multiply',
|
|
24
|
+
screen: 'screen',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function hexToNum(hex: string | undefined, fallback: number): number {
|
|
28
|
+
if (!hex) return fallback;
|
|
29
|
+
return Number.parseInt(hex.replace('#', ''), 16);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Generate N asset-light frame textures (a moving accent dot) for an AnimatedSprite. */
|
|
33
|
+
function generateFrames(count: number, w: number, h: number, tint: string): Texture[] {
|
|
34
|
+
const frames: Texture[] = [];
|
|
35
|
+
for (let i = 0; i < count; i++) {
|
|
36
|
+
const canvas = document.createElement('canvas');
|
|
37
|
+
canvas.width = w;
|
|
38
|
+
canvas.height = h;
|
|
39
|
+
const g = canvas.getContext('2d')!;
|
|
40
|
+
g.fillStyle = tint;
|
|
41
|
+
g.fillRect(0, 0, w, h);
|
|
42
|
+
g.fillStyle = '#10101a';
|
|
43
|
+
const t = (i / count) * Math.PI * 2;
|
|
44
|
+
g.beginPath();
|
|
45
|
+
g.arc(
|
|
46
|
+
w / 2 + Math.cos(t) * w * 0.25,
|
|
47
|
+
h / 2 + Math.sin(t) * h * 0.25,
|
|
48
|
+
Math.min(w, h) * 0.18,
|
|
49
|
+
0,
|
|
50
|
+
Math.PI * 2,
|
|
51
|
+
);
|
|
52
|
+
g.fill();
|
|
53
|
+
frames.push(Texture.from(canvas));
|
|
54
|
+
}
|
|
55
|
+
return frames;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function applyFilter(display: Sprite | AnimatedSprite, filter: string | undefined): void {
|
|
59
|
+
if (!filter || filter === 'none') return;
|
|
60
|
+
if (filter === 'blur') display.filters = [new BlurFilter({ strength: 6 })];
|
|
61
|
+
else if (filter === 'desaturate') {
|
|
62
|
+
const cm = new ColorMatrixFilter();
|
|
63
|
+
cm.desaturate();
|
|
64
|
+
display.filters = [cm];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Instantiate a PIXI.Sprite / AnimatedSprite from authored `sprite` data. */
|
|
69
|
+
async function createSprite(data: SceneSprite): Promise<Sprite | AnimatedSprite> {
|
|
70
|
+
let display: Sprite | AnimatedSprite;
|
|
71
|
+
if (data.animationFrames && data.animationFrames > 1) {
|
|
72
|
+
const frames = generateFrames(
|
|
73
|
+
data.animationFrames,
|
|
74
|
+
data.width ?? 40,
|
|
75
|
+
data.height ?? 40,
|
|
76
|
+
data.tint ?? '#ffffff',
|
|
77
|
+
);
|
|
78
|
+
const anim = new AnimatedSprite(frames);
|
|
79
|
+
anim.animationSpeed = (data.fps ?? 8) / 60;
|
|
80
|
+
anim.play();
|
|
81
|
+
display = anim;
|
|
82
|
+
} else {
|
|
83
|
+
let texture: Texture = Texture.WHITE;
|
|
84
|
+
if (data.texture) {
|
|
85
|
+
const loaded = await Assets.load(data.texture);
|
|
86
|
+
texture =
|
|
87
|
+
data.frame && loaded?.textures?.[data.frame]
|
|
88
|
+
? loaded.textures[data.frame]
|
|
89
|
+
: (loaded as Texture);
|
|
90
|
+
}
|
|
91
|
+
display = new Sprite(texture);
|
|
92
|
+
}
|
|
93
|
+
const [ax, ay] = data.anchor ?? [0.5, 0.5];
|
|
94
|
+
display.anchor.set(ax, ay);
|
|
95
|
+
if (data.width !== undefined) display.width = data.width;
|
|
96
|
+
if (data.height !== undefined) display.height = data.height;
|
|
97
|
+
if (!data.animationFrames) display.tint = hexToNum(data.tint, 0xffffff);
|
|
98
|
+
if (data.alpha !== undefined) display.alpha = data.alpha;
|
|
99
|
+
if (data.blendMode) display.blendMode = BLEND[data.blendMode] ?? 'normal';
|
|
100
|
+
if (data.interactive) {
|
|
101
|
+
display.eventMode = 'static';
|
|
102
|
+
display.cursor = 'pointer';
|
|
103
|
+
}
|
|
104
|
+
applyFilter(display, data.filter);
|
|
105
|
+
return display;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Instantiate a @pixi/tilemap CompositeTilemap from authored `tilemap` data. */
|
|
109
|
+
function createTilemap(data: SceneTilemap): CompositeTilemap {
|
|
110
|
+
// Generate one tile texture per palette color (asset-light).
|
|
111
|
+
const textures = data.palette.map((color) => {
|
|
112
|
+
const c = document.createElement('canvas');
|
|
113
|
+
c.width = data.tileSize;
|
|
114
|
+
c.height = data.tileSize;
|
|
115
|
+
const g = c.getContext('2d')!;
|
|
116
|
+
g.fillStyle = color;
|
|
117
|
+
g.fillRect(0, 0, data.tileSize, data.tileSize);
|
|
118
|
+
g.strokeStyle = 'rgba(0,0,0,0.18)';
|
|
119
|
+
g.strokeRect(0.5, 0.5, data.tileSize - 1, data.tileSize - 1);
|
|
120
|
+
return Texture.from(c);
|
|
121
|
+
});
|
|
122
|
+
const tilemap = new CompositeTilemap();
|
|
123
|
+
for (let y = 0; y < data.tiles.length; y++) {
|
|
124
|
+
const row = data.tiles[y]!;
|
|
125
|
+
for (let x = 0; x < row.length; x++) {
|
|
126
|
+
const tex = textures[row[x]!];
|
|
127
|
+
if (tex) tilemap.tile(tex, x * data.tileSize, y * data.tileSize);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return tilemap;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Instantiate a PIXI.Text from authored `text` data (world-space label). */
|
|
134
|
+
function createText(data: SceneText2D): Text {
|
|
135
|
+
const text = new Text({
|
|
136
|
+
text: data.text,
|
|
137
|
+
style: { fontSize: data.fontSize ?? 24, fill: data.fill ?? '#ffffff' },
|
|
138
|
+
});
|
|
139
|
+
const [ax, ay] = data.anchor ?? [0.5, 0.5];
|
|
140
|
+
text.anchor.set(ax, ay);
|
|
141
|
+
return text;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Map the bodyType enum to a Rapier 2D RigidBodyDesc. */
|
|
145
|
+
function bodyDesc(rapier2d: typeof RAPIER, type: string): RAPIER.RigidBodyDesc {
|
|
146
|
+
switch (type) {
|
|
147
|
+
case 'fixed':
|
|
148
|
+
return rapier2d.RigidBodyDesc.fixed();
|
|
149
|
+
case 'kinematicVelocity':
|
|
150
|
+
return rapier2d.RigidBodyDesc.kinematicVelocityBased();
|
|
151
|
+
case 'kinematicPosition':
|
|
152
|
+
return rapier2d.RigidBodyDesc.kinematicPositionBased();
|
|
153
|
+
default:
|
|
154
|
+
return rapier2d.RigidBodyDesc.dynamic();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Create the Rapier 2D body + collider for an entity, register it, return the body. */
|
|
159
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive physics2d-descriptor builder; each branch maps an optional authored field to a Rapier setter
|
|
160
|
+
function createPhysics2D(ctx: World2DContext, entity: Entity2D, display: Container): void {
|
|
161
|
+
const p = entity.physics2d;
|
|
162
|
+
if (!p) return;
|
|
163
|
+
const { rapier2d, rapier2dWorld, physics2d } = ctx;
|
|
164
|
+
const pos = entity.transform2d?.position ?? [0, 0];
|
|
165
|
+
|
|
166
|
+
const rbDesc = bodyDesc(rapier2d, p.bodyType)
|
|
167
|
+
.setTranslation(pos[0], pos[1])
|
|
168
|
+
.setRotation(entity.transform2d?.rotation ?? 0);
|
|
169
|
+
if (p.linearDamping !== undefined) rbDesc.setLinearDamping(p.linearDamping);
|
|
170
|
+
if (p.gravityScale !== undefined) rbDesc.setGravityScale(p.gravityScale);
|
|
171
|
+
if (p.lockRotations) rbDesc.lockRotations();
|
|
172
|
+
const body = rapier2dWorld.createRigidBody(rbDesc);
|
|
173
|
+
|
|
174
|
+
let colDesc: RAPIER.ColliderDesc;
|
|
175
|
+
const c = p.collider;
|
|
176
|
+
if (c.shape === 'cuboid')
|
|
177
|
+
colDesc = rapier2d.ColliderDesc.cuboid(c.halfExtents[0], c.halfExtents[1]);
|
|
178
|
+
else if (c.shape === 'ball') colDesc = rapier2d.ColliderDesc.ball(c.radius);
|
|
179
|
+
else colDesc = rapier2d.ColliderDesc.capsule(c.halfHeight, c.radius);
|
|
180
|
+
|
|
181
|
+
if (p.density !== undefined) colDesc.setDensity(p.density);
|
|
182
|
+
if (p.friction !== undefined) colDesc.setFriction(p.friction);
|
|
183
|
+
if (p.restitution !== undefined) colDesc.setRestitution(p.restitution);
|
|
184
|
+
if (p.sensor) {
|
|
185
|
+
colDesc.setSensor(true);
|
|
186
|
+
colDesc.setActiveEvents(rapier2d.ActiveEvents.COLLISION_EVENTS);
|
|
187
|
+
} else {
|
|
188
|
+
colDesc.setActiveEvents(rapier2d.ActiveEvents.COLLISION_EVENTS);
|
|
189
|
+
}
|
|
190
|
+
const collider = rapier2dWorld.createCollider(colDesc, body);
|
|
191
|
+
physics2d.add(display, body, collider);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Hydrate the `components` field — schema.parse + Object.assign + attach (2D analog of applyComponents).
|
|
195
|
+
* `manager` is the unified `ComponentManager` (`ecs/component-manager.ts`), constructed
|
|
196
|
+
* for kind `'pixijs'` — the world2d silo manager this replaced (T7.3) is deleted. */
|
|
197
|
+
function applyComponents2D(
|
|
198
|
+
registry: Component2DRegistry,
|
|
199
|
+
display: Container,
|
|
200
|
+
components: Record<string, Record<string, unknown>> | undefined,
|
|
201
|
+
manager: ComponentManager,
|
|
202
|
+
): void {
|
|
203
|
+
if (!components) return;
|
|
204
|
+
for (const [name, data] of Object.entries(components)) {
|
|
205
|
+
const Cls = registry[name];
|
|
206
|
+
if (!Cls) throw new Error(`Unknown world2d component: "${name}"`);
|
|
207
|
+
const parsed = Cls.schema ? Cls.schema.parse(data ?? {}) : (data ?? {});
|
|
208
|
+
const instance = new Cls();
|
|
209
|
+
Object.assign(instance, parsed);
|
|
210
|
+
// T5.4: record the registry name + raw authored data — same reasoning as
|
|
211
|
+
// the 3D `applyComponents` (`scene/component-registry.ts`).
|
|
212
|
+
manager.attach(display, instance, { key: name, props: data ?? {} });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Apply a 2D transform to a display object. */
|
|
217
|
+
function applyTransform2D(display: Container, entity: Entity2D): void {
|
|
218
|
+
const t = entity.transform2d;
|
|
219
|
+
if (t?.position) display.position.set(t.position[0], t.position[1]);
|
|
220
|
+
if (t?.rotation !== undefined) display.rotation = t.rotation;
|
|
221
|
+
if (t?.scale) display.scale.set(t.scale[0], t.scale[1]);
|
|
222
|
+
const pivot = t?.pivot ?? entity.pivot;
|
|
223
|
+
if (pivot) display.pivot.set(pivot[0], pivot[1]);
|
|
224
|
+
if (entity.visible === false) display.visible = false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let _autoId = 0;
|
|
228
|
+
|
|
229
|
+
/** A loaded world2d scene instance: the root display objects + an update/dispose. */
|
|
230
|
+
export interface Scene2DInstance {
|
|
231
|
+
roots: Container[];
|
|
232
|
+
/** id → display object map for editor/lookup (the 2D analog of the loader's entity map). */
|
|
233
|
+
byId: Map<string, Container>;
|
|
234
|
+
dispose(): void;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Load a world2d scene from authored data — the 2D analog of `loadSceneFromData`.
|
|
239
|
+
* Three passes: (1) spawn all entities (recursing into children), building an
|
|
240
|
+
* id→display map and creating Rapier 2D bodies; (2) [no joints in 2D yet];
|
|
241
|
+
* (3) `componentManager.initAll()` once everything exists.
|
|
242
|
+
*
|
|
243
|
+
* Re-validates `data` even though the fetch path (`fetchScene2DFile` in
|
|
244
|
+
* `packages/editor/src/adapter-resolver.ts`) already validates before calling
|
|
245
|
+
* in — not every caller goes through that path (test-constructed
|
|
246
|
+
* `PixiSceneConfig.sceneData` literals reach here unvalidated), so a malformed
|
|
247
|
+
* payload still throws a `SceneParseError` naming the file, not a raw
|
|
248
|
+
* `ZodError` (T4.6 convention, closing §7.1-14). `sourceName` is optional
|
|
249
|
+
* because not every caller knows the originating file path; callers that do
|
|
250
|
+
* (or gain the ability to) should thread it through for a better message.
|
|
251
|
+
*/
|
|
252
|
+
export async function loadScene2DFromData(
|
|
253
|
+
data: Scene2DFile,
|
|
254
|
+
ctx: World2DContext,
|
|
255
|
+
registry: Component2DRegistry,
|
|
256
|
+
sourceName?: string,
|
|
257
|
+
): Promise<Scene2DInstance> {
|
|
258
|
+
const result = Scene2DSchema.safeParse(data);
|
|
259
|
+
if (!result.success) throw new SceneParseError(result.error.issues, sourceName);
|
|
260
|
+
const scene = result.data;
|
|
261
|
+
const byId = new Map<string, Container>();
|
|
262
|
+
const roots: Container[] = [];
|
|
263
|
+
|
|
264
|
+
async function spawn(entity: Entity2D, parent: Container): Promise<Container> {
|
|
265
|
+
let display: Container;
|
|
266
|
+
if (entity.sprite) display = await createSprite(entity.sprite);
|
|
267
|
+
else if (entity.text) display = createText(entity.text);
|
|
268
|
+
else if (entity.tilemap) display = createTilemap(entity.tilemap);
|
|
269
|
+
else display = new Container();
|
|
270
|
+
|
|
271
|
+
display.label = entity.name;
|
|
272
|
+
const id = entity.id ?? `e2d_${_autoId++}`;
|
|
273
|
+
(display as Container & { __entityId?: string }).__entityId = id;
|
|
274
|
+
byId.set(id, display);
|
|
275
|
+
|
|
276
|
+
applyTransform2D(display, entity);
|
|
277
|
+
parent.addChild(display);
|
|
278
|
+
|
|
279
|
+
// Physics bodies live in world space (flat under the world container); the
|
|
280
|
+
// transform-writer drives their pose. Components attach to the display.
|
|
281
|
+
createPhysics2D(ctx, entity, display);
|
|
282
|
+
applyComponents2D(registry, display, entity.components, ctx.components);
|
|
283
|
+
|
|
284
|
+
if (entity.children) {
|
|
285
|
+
for (const child of entity.children) await spawn(child, display);
|
|
286
|
+
}
|
|
287
|
+
return display;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (const entity of scene.entities) {
|
|
291
|
+
roots.push(await spawn(entity, ctx.stage));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await ctx.components.initAll();
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
roots,
|
|
298
|
+
byId,
|
|
299
|
+
dispose() {
|
|
300
|
+
for (const root of roots) {
|
|
301
|
+
root.removeFromParent();
|
|
302
|
+
root.destroy({ children: true });
|
|
303
|
+
}
|
|
304
|
+
roots.length = 0;
|
|
305
|
+
byId.clear();
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Physics2DSchema } from './physics2d';
|
|
3
|
+
import { SpriteSchema, Text2DSchema } from './sprite';
|
|
4
|
+
import { TilemapSchema } from './tilemap';
|
|
5
|
+
import { Transform2DSchema, Vec2Schema } from './tuples2d';
|
|
6
|
+
|
|
7
|
+
/** Component data: same shape as the 3D ComponentsSchema (scalars + scalar arrays). */
|
|
8
|
+
const ComponentScalarSchema = z.union([z.number(), z.string(), z.boolean()]);
|
|
9
|
+
const ComponentValueSchema = z.union([
|
|
10
|
+
ComponentScalarSchema,
|
|
11
|
+
z.array(z.number()),
|
|
12
|
+
z.array(z.string()),
|
|
13
|
+
z.array(z.boolean()),
|
|
14
|
+
]);
|
|
15
|
+
const ComponentDataSchema = z.record(z.string(), ComponentValueSchema);
|
|
16
|
+
const ComponentsSchema = z.record(z.string(), ComponentDataSchema);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A world2d entity — the 2D analog of the 3D `SceneEntity`. The SAME engine model
|
|
20
|
+
* (named components validated against a registry, a tree of children) reused; the
|
|
21
|
+
* sections (`sprite`/`text`/`physics2d`/`transform2d`) name engine-owned data, NOT
|
|
22
|
+
* `PIXI.Sprite`. `surface` defaults to 'world2d' for entities in a Scene2D file.
|
|
23
|
+
*/
|
|
24
|
+
const Entity2DBase = z.object({
|
|
25
|
+
id: z.string().optional().describe('Unique entity ID. Auto-generated if omitted'),
|
|
26
|
+
name: z.string().describe('Human-readable entity name (shown in hierarchy panel)'),
|
|
27
|
+
surface: z
|
|
28
|
+
.enum(['world2d', 'world3d'])
|
|
29
|
+
.optional()
|
|
30
|
+
.describe(
|
|
31
|
+
"Render surface that instantiates this entity. Default 'world2d' in a Scene2D file. The " +
|
|
32
|
+
"'world3d' value is REJECTED at parse (T4.1): declared but has no runtime reader — " +
|
|
33
|
+
"authoring surface: 'world3d' throws. Do not author it ('world2d', the default, is fine).",
|
|
34
|
+
),
|
|
35
|
+
visible: z.boolean().optional().describe('Whether the entity and its children are rendered'),
|
|
36
|
+
locked: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe(
|
|
40
|
+
'Prevent selection and editing in the scene editor. REJECTED at parse (T4.1): declared ' +
|
|
41
|
+
'but has no runtime reader — authoring this field throws. Do not author.',
|
|
42
|
+
),
|
|
43
|
+
tags: z
|
|
44
|
+
.array(z.string())
|
|
45
|
+
.optional()
|
|
46
|
+
.describe(
|
|
47
|
+
'Arbitrary string tags for runtime queries. REJECTED at parse (T4.1): declared but has no ' +
|
|
48
|
+
'runtime reader — authoring this field throws. Do not author.',
|
|
49
|
+
),
|
|
50
|
+
transform2d: Transform2DSchema.optional().describe('Local 2D transform relative to parent'),
|
|
51
|
+
pivot: Vec2Schema.optional().describe('Local-space pivot point [x, y]'),
|
|
52
|
+
sprite: SpriteSchema.optional().describe('2D sprite appearance'),
|
|
53
|
+
text: Text2DSchema.optional().describe('World-space text label'),
|
|
54
|
+
tilemap: TilemapSchema.optional().describe('2D tilemap (CompositeTilemap)'),
|
|
55
|
+
physics2d: Physics2DSchema.optional().describe('2D physics rigid body + collider'),
|
|
56
|
+
components: ComponentsSchema.optional().describe(
|
|
57
|
+
'User-defined components (validated against the world2d component registry)',
|
|
58
|
+
),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export type Entity2D = z.infer<typeof Entity2DBase> & {
|
|
62
|
+
children?: Entity2D[] | undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const Entity2DSchema: z.ZodType<Entity2D> = z.lazy(() =>
|
|
66
|
+
Entity2DBase.extend({
|
|
67
|
+
children: z.array(Entity2DSchema).optional().describe('Child entities'),
|
|
68
|
+
})
|
|
69
|
+
// T4.1: three fields are authored/typed but have no runtime reader in
|
|
70
|
+
// `scene2d-loader.ts`'s `spawn()` (unlike their 3D `SceneEntity` siblings,
|
|
71
|
+
// which ARE read — editor selection-locking for `locked`, `scene-query.ts`
|
|
72
|
+
// for `tags`):
|
|
73
|
+
// - `locked` — spawn() never checks it (no 2D editor selection-lock yet).
|
|
74
|
+
// - `tags` — never copied onto userData; no 2D equivalent of scene-query.ts.
|
|
75
|
+
// - `surface: 'world3d'` — spawn() always instantiates as world2d
|
|
76
|
+
// regardless of this field's value, so a 'world3d' entity is silently
|
|
77
|
+
// spawned into the 2D world anyway. ('world2d', the default, is a
|
|
78
|
+
// harmless no-op and is not flagged.)
|
|
79
|
+
// Loud load errors until readers exist — see T4.1.
|
|
80
|
+
.superRefine((entity, ctx) => {
|
|
81
|
+
if (entity.locked !== undefined) {
|
|
82
|
+
ctx.addIssue({
|
|
83
|
+
code: z.ZodIssueCode.custom,
|
|
84
|
+
message:
|
|
85
|
+
`world2d entity "${entity.name}": \`locked\` is authored but not implemented — ` +
|
|
86
|
+
'scene2d-loader.ts has no 2D editor selection-lock reader yet. See T4.1.',
|
|
87
|
+
path: ['locked'],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (entity.tags !== undefined) {
|
|
91
|
+
ctx.addIssue({
|
|
92
|
+
code: z.ZodIssueCode.custom,
|
|
93
|
+
message:
|
|
94
|
+
`world2d entity "${entity.name}": \`tags\` is authored but not implemented — ` +
|
|
95
|
+
'no 2D equivalent of scene-query.ts reads it yet. See T4.1.',
|
|
96
|
+
path: ['tags'],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (entity.surface === 'world3d') {
|
|
100
|
+
ctx.addIssue({
|
|
101
|
+
code: z.ZodIssueCode.custom,
|
|
102
|
+
message:
|
|
103
|
+
`world2d entity "${entity.name}": \`surface: 'world3d'\` is authored but not ` +
|
|
104
|
+
'implemented — spawn() always instantiates entities as world2d regardless of this ' +
|
|
105
|
+
'field, so a world3d entity would be silently spawned into the 2D world. See T4.1.',
|
|
106
|
+
path: ['surface'],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}),
|
|
110
|
+
) as z.ZodType<Entity2D>;
|
|
111
|
+
|
|
112
|
+
/** A world2d scene file — the 2D analog of `.vscn.json` (`.scn2d.json`). */
|
|
113
|
+
export const Scene2DSchema = z
|
|
114
|
+
.object({
|
|
115
|
+
version: z.number().optional().describe('Scene format version'),
|
|
116
|
+
name: z
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe(
|
|
120
|
+
'Scene name. REJECTED at parse (T4.1): declared but has no runtime reader — authoring ' +
|
|
121
|
+
'this field throws. Do not author.',
|
|
122
|
+
),
|
|
123
|
+
surface: z.literal('world2d').optional().describe("Render surface. Always 'world2d'"),
|
|
124
|
+
gravity: Vec2Schema.optional().describe('World gravity [x, y] in px/s^2. Default [0, 980]'),
|
|
125
|
+
background: z.string().optional().describe("Background color hex string. Default '#10101a'"),
|
|
126
|
+
entities: z.array(Entity2DSchema).describe('Root entities of the world2d scene'),
|
|
127
|
+
})
|
|
128
|
+
// T4.1: `name` is authored/typed but has no runtime reader anywhere (unlike
|
|
129
|
+
// `SceneFile.name`/`PrefabFile.name`, which the editor reads via
|
|
130
|
+
// `editor-store.ts`'s `sceneName` getter/setter) — no 2D scene-name display
|
|
131
|
+
// exists yet. Loud load error until a reader exists — see T4.1.
|
|
132
|
+
.superRefine((scene, ctx) => {
|
|
133
|
+
if (scene.name !== undefined) {
|
|
134
|
+
ctx.addIssue({
|
|
135
|
+
code: z.ZodIssueCode.custom,
|
|
136
|
+
message:
|
|
137
|
+
'`name` is authored but not implemented — no runtime reader displays/consumes a ' +
|
|
138
|
+
'world2d scene name yet (unlike SceneFile.name). See T4.1.',
|
|
139
|
+
path: ['name'],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.describe('A world2d scene (PixiJS render surface)');
|
|
144
|
+
|
|
145
|
+
export type Scene2DFile = z.infer<typeof Scene2DSchema>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Vec2Schema } from './tuples2d';
|
|
3
|
+
|
|
4
|
+
/** A single 2D collider shape (Rapier 2D). Cuboid takes 2 half-extents; ball a radius. */
|
|
5
|
+
export const Collider2DSchema = z
|
|
6
|
+
.discriminatedUnion('shape', [
|
|
7
|
+
z.object({
|
|
8
|
+
shape: z.literal('cuboid').describe('Axis-aligned box collider'),
|
|
9
|
+
halfExtents: Vec2Schema.describe('Half-width and half-height [hx, hy] in pixels'),
|
|
10
|
+
}),
|
|
11
|
+
z.object({
|
|
12
|
+
shape: z.literal('ball').describe('Circle collider'),
|
|
13
|
+
radius: z.number().describe('Radius in pixels'),
|
|
14
|
+
}),
|
|
15
|
+
z.object({
|
|
16
|
+
shape: z.literal('capsule').describe('Capsule collider (vertical)'),
|
|
17
|
+
halfHeight: z.number().describe('Half the straight segment length'),
|
|
18
|
+
radius: z.number().describe('Cap radius'),
|
|
19
|
+
}),
|
|
20
|
+
])
|
|
21
|
+
.describe('2D collider shape (Rapier 2D)');
|
|
22
|
+
|
|
23
|
+
export type SceneCollider2D = z.infer<typeof Collider2DSchema>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `physics2d` authored section — engine-owned 2D physics data, instantiated as a
|
|
27
|
+
* Rapier 2D RigidBody + Collider. The world2d analog of the 3D `physics` section.
|
|
28
|
+
*/
|
|
29
|
+
export const Physics2DSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
bodyType: z
|
|
32
|
+
.enum(['dynamic', 'fixed', 'kinematicVelocity', 'kinematicPosition'])
|
|
33
|
+
.describe('Rapier 2D rigid-body type'),
|
|
34
|
+
collider: Collider2DSchema.describe('Collider shape attached to the body'),
|
|
35
|
+
sensor: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe(
|
|
39
|
+
'If true, a sensor (trigger) collider — overlaps fire onTriggerEnter/Exit, no solid response',
|
|
40
|
+
),
|
|
41
|
+
density: z.number().optional().describe('Collider density (drives mass). Default 1'),
|
|
42
|
+
friction: z.number().optional().describe('Coefficient of friction. Default 0.5'),
|
|
43
|
+
restitution: z.number().optional().describe('Bounciness 0..1. Default 0'),
|
|
44
|
+
linearDamping: z.number().optional().describe('Linear velocity damping. Default 0'),
|
|
45
|
+
gravityScale: z.number().optional().describe('Per-body gravity multiplier. Default 1'),
|
|
46
|
+
lockRotations: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Lock the body so it cannot rotate. Default false'),
|
|
50
|
+
})
|
|
51
|
+
.describe('2D physics rigid body + collider (world2d, Rapier 2D)');
|
|
52
|
+
|
|
53
|
+
export type ScenePhysics2D = z.infer<typeof Physics2DSchema>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Vec2Schema } from './tuples2d';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `sprite` authored section — engine-owned 2D sprite data, NOT a `PIXI.Sprite`.
|
|
6
|
+
*
|
|
7
|
+
* Per ARCHITECTURE.md §Render Surfaces: authored data says `sprite`, and a runtime
|
|
8
|
+
* adapter instantiates it onto the appropriate backend (here, a `PIXI.Sprite` on
|
|
9
|
+
* the world2d surface). A 1x1 white texture is used when `texture` is omitted, so a
|
|
10
|
+
* tinted rectangle sprite needs no asset.
|
|
11
|
+
*/
|
|
12
|
+
export const SpriteSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
texture: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe(
|
|
18
|
+
'Texture asset URL (loaded via PIXI.Assets). Omit for a tintable 1x1 white texture',
|
|
19
|
+
),
|
|
20
|
+
frame: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Spritesheet frame/sub-texture name when `texture` is a spritesheet atlas'),
|
|
24
|
+
anchor: Vec2Schema.optional().describe('Normalized anchor [x, y] (0..1). Default [0.5, 0.5]'),
|
|
25
|
+
tint: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Tint color as hex string (e.g. '#ff8800'). Default white"),
|
|
29
|
+
alpha: z.number().min(0).max(1).optional().describe('Opacity 0..1. Default 1'),
|
|
30
|
+
width: z
|
|
31
|
+
.number()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Explicit display width in pixels (overrides texture size)'),
|
|
34
|
+
height: z
|
|
35
|
+
.number()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Explicit display height in pixels (overrides texture size)'),
|
|
38
|
+
blendMode: z
|
|
39
|
+
.enum(['normal', 'add', 'multiply', 'screen'])
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('PixiJS blend mode. Default normal'),
|
|
42
|
+
interactive: z
|
|
43
|
+
.boolean()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Enable federated pointer events (eventMode=static) so this sprite can be picked'),
|
|
46
|
+
filter: z
|
|
47
|
+
.enum(['none', 'blur', 'desaturate'])
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Apply a PixiJS filter (BlurFilter / ColorMatrix desaturate). Default none'),
|
|
50
|
+
animationFrames: z
|
|
51
|
+
.number()
|
|
52
|
+
.int()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('If set, build an AnimatedSprite cycling N generated tinted frames'),
|
|
55
|
+
fps: z.number().optional().describe('AnimatedSprite playback speed in frames/sec. Default 8'),
|
|
56
|
+
})
|
|
57
|
+
.describe('2D sprite appearance (world2d). Instantiated as a PIXI.Sprite or AnimatedSprite');
|
|
58
|
+
|
|
59
|
+
export type SceneSprite = z.infer<typeof SpriteSchema>;
|
|
60
|
+
|
|
61
|
+
/** `text` authored section — a world-space PIXI.Text label (world-space UI). */
|
|
62
|
+
export const Text2DSchema = z
|
|
63
|
+
.object({
|
|
64
|
+
text: z.string().describe('The string to display'),
|
|
65
|
+
fontSize: z.number().optional().describe('Font size in pixels. Default 24'),
|
|
66
|
+
fill: z.string().optional().describe("Text color as hex string. Default '#ffffff'"),
|
|
67
|
+
anchor: Vec2Schema.optional().describe('Normalized anchor [x, y] (0..1). Default [0.5, 0.5]'),
|
|
68
|
+
})
|
|
69
|
+
.describe('2D world-space text label (world2d). Instantiated as a PIXI.Text');
|
|
70
|
+
|
|
71
|
+
export type SceneText2D = z.infer<typeof Text2DSchema>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `tilemap` authored section — engine-owned 2D tilemap data (NOT a CompositeTilemap),
|
|
5
|
+
* instantiated to a `@pixi/tilemap` CompositeTilemap by the 2D spawn path. Asset-light:
|
|
6
|
+
* a `palette` of hex colors generates one tile texture each, and `tiles` is a row-major
|
|
7
|
+
* grid of palette indices. A real tileset texture/Tiled `.tmj` import is the natural
|
|
8
|
+
* extension (per the spec's optional Tiled note).
|
|
9
|
+
*/
|
|
10
|
+
export const TilemapSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
tileSize: z.number().describe('Tile width/height in pixels'),
|
|
13
|
+
palette: z
|
|
14
|
+
.array(z.string())
|
|
15
|
+
.describe('Hex colors; one generated tile texture per entry (index → color)'),
|
|
16
|
+
tiles: z
|
|
17
|
+
.array(z.array(z.number().int()))
|
|
18
|
+
.describe('Row-major grid of palette indices (each row is an array of tile indices)'),
|
|
19
|
+
})
|
|
20
|
+
.describe('2D tilemap (world2d). Instantiated as a @pixi/tilemap CompositeTilemap');
|
|
21
|
+
|
|
22
|
+
export type SceneTilemap = z.infer<typeof TilemapSchema>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** 2D vector as [x, y]. The world2d analog of the 3D Vec3. */
|
|
4
|
+
export const Vec2Schema = z.tuple([z.number(), z.number()]).describe('2D vector as [x, y]');
|
|
5
|
+
|
|
6
|
+
export type Vec2 = z.infer<typeof Vec2Schema>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 2D local transform — the world2d analog of the 3D TransformSchema.
|
|
10
|
+
*
|
|
11
|
+
* Orientation is a single scalar `rotation` (radians, about the screen Z axis),
|
|
12
|
+
* NOT a quaternion: PixiJS display objects rotate in the plane. The runtime/editor
|
|
13
|
+
* neutral `Transform` tuple (position[3]/quaternion[4]/scale[3]) maps onto this as
|
|
14
|
+
* z = 0 and rotation about Z (see PIXI-WORLD2D-MAXIMAL-SPEC model-transform2d-section).
|
|
15
|
+
*/
|
|
16
|
+
export const Transform2DSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
position: Vec2Schema.optional().describe('Local position [x, y] in world2d pixels'),
|
|
19
|
+
rotation: z.number().optional().describe('Rotation in radians about the Z axis. Default 0'),
|
|
20
|
+
scale: Vec2Schema.optional().describe('Scale per axis [x, y]. Default [1, 1]'),
|
|
21
|
+
pivot: Vec2Schema.optional().describe('Local pivot point [x, y] (center of rotation/scale)'),
|
|
22
|
+
})
|
|
23
|
+
.describe('2D local transform relative to parent (world2d surface)');
|
|
24
|
+
|
|
25
|
+
export type SceneTransform2D = z.infer<typeof Transform2DSchema>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Container } from 'pixi.js';
|
|
2
|
+
import type { Physics2DRegistry } from './physics2d-registry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PhysicsAdapter2D — the world2d analog of the 3D PhysicsAdapter (rapier-physics-adapter).
|
|
6
|
+
* Lets the editor coordinate editing of a Rapier-2D-driven entity WITHOUT owning the
|
|
7
|
+
* simulation: report transform ownership, then freeze → commit → unfreeze so a drag
|
|
8
|
+
* sticks instead of being stomped by the next physics step.
|
|
9
|
+
*/
|
|
10
|
+
export interface PhysicsAdapter2D {
|
|
11
|
+
ownerOf(display: Container): 'physics' | 'none';
|
|
12
|
+
freeze(display: Container): void;
|
|
13
|
+
commit(display: Container, position: [number, number], rotation: number): void;
|
|
14
|
+
unfreeze(display: Container): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createPhysicsAdapter2D(physics: Physics2DRegistry): PhysicsAdapter2D {
|
|
18
|
+
// Remember each frozen body's prior type so unfreeze restores it.
|
|
19
|
+
const frozen = new WeakMap<Container, number>();
|
|
20
|
+
return {
|
|
21
|
+
ownerOf(display) {
|
|
22
|
+
const refs = physics.get(display);
|
|
23
|
+
if (!refs) return 'none';
|
|
24
|
+
return refs.body.isDynamic() || refs.body.isKinematic() ? 'physics' : 'none';
|
|
25
|
+
},
|
|
26
|
+
freeze(display) {
|
|
27
|
+
const refs = physics.get(display);
|
|
28
|
+
if (!refs) return;
|
|
29
|
+
frozen.set(display, refs.body.bodyType());
|
|
30
|
+
// Kinematic-position bodies hold a pose without being driven by forces.
|
|
31
|
+
refs.body.setBodyType(2 /* KinematicPositionBased */, true);
|
|
32
|
+
},
|
|
33
|
+
commit(display, position, rotation) {
|
|
34
|
+
const refs = physics.get(display);
|
|
35
|
+
if (!refs) return;
|
|
36
|
+
refs.body.setTranslation({ x: position[0], y: position[1] }, true);
|
|
37
|
+
refs.body.setRotation(rotation, true);
|
|
38
|
+
},
|
|
39
|
+
unfreeze(display) {
|
|
40
|
+
const refs = physics.get(display);
|
|
41
|
+
if (!refs) return;
|
|
42
|
+
const prior = frozen.get(display);
|
|
43
|
+
if (prior !== undefined) {
|
|
44
|
+
refs.body.setBodyType(prior, true);
|
|
45
|
+
frozen.delete(display);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|