@jgengine/core 0.18.0 → 0.18.1
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/CHANGELOG.md +114 -2
- package/dist/ai/decisionGraph.d.ts +52 -0
- package/dist/ai/decisionGraph.js +82 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/anim/animGraph.d.ts +144 -0
- package/dist/anim/animGraph.js +286 -0
- package/dist/anim/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +26 -0
- package/dist/anim/locomotionGraph.js +33 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +10 -0
- package/dist/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +89 -0
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +16 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +16 -0
- package/dist/game/cameraConfig.d.ts +8 -0
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +8 -1
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +97 -3
- package/dist/game/systemRuntime.js +6 -1
- package/dist/input/actionBindings.d.ts +2 -1
- package/dist/input/actionBindings.js +10 -8
- package/dist/input/actionContexts.d.ts +21 -0
- package/dist/input/actionContexts.js +56 -0
- package/dist/input/gamepadModel.d.ts +45 -0
- package/dist/input/gamepadModel.js +69 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +85 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- package/dist/movement/playerMovement.d.ts +10 -2
- package/dist/movement/playerMovement.js +218 -4
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +30 -7
- package/dist/movement/solidObstacles.js +27 -4
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navMesh.d.ts +33 -0
- package/dist/nav/navMesh.js +164 -0
- package/dist/physics/flightDynamics.d.ts +8 -0
- package/dist/physics/kinematicVehicle.d.ts +6 -1
- package/dist/physics/physicsBackend.d.ts +191 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +117 -0
- package/dist/physics/physicsWorldBackend.d.ts +44 -0
- package/dist/physics/physicsWorldBackend.js +485 -0
- package/dist/physics/worldColliders.d.ts +13 -0
- package/dist/physics/worldColliders.js +96 -0
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +11 -5
- package/dist/render/lookPreset.js +29 -4
- package/dist/render/postProcessing.d.ts +10 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/commandInput.d.ts +15 -0
- package/dist/runtime/commandInput.js +13 -0
- package/dist/runtime/commandRunner.d.ts +12 -7
- package/dist/runtime/commandRunner.js +7 -5
- package/dist/runtime/gameContext.js +11 -1
- package/dist/runtime/gameContextTypes.d.ts +14 -4
- package/dist/runtime/gameRuntime.d.ts +20 -4
- package/dist/runtime/gameRuntime.js +17 -4
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +8 -0
- package/dist/runtime/inputSnapshot.js +1 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +32 -2
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +19 -1
- package/dist/scene/behaviorRuntime.js +55 -4
- package/dist/scene/behaviors.d.ts +8 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- package/dist/sensor/perception.d.ts +60 -0
- package/dist/sensor/perception.js +142 -0
- package/dist/settings/graphicsProfile.d.ts +19 -0
- package/dist/settings/graphicsProfile.js +36 -0
- package/dist/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -0
- package/dist/world/buildings.d.ts +12 -0
- package/dist/world/buildings.js +43 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +132 -0
- package/dist/world/cityKind.js +315 -15
- package/dist/world/features.d.ts +27 -1
- package/dist/world/features.js +7 -2
- package/dist/world/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -0
- package/dist/world.d.ts +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +94 -19
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-gameplay/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +40 -18
- package/skills/jgengine-gameplay/capabilities.md +12 -0
- package/skills/jgengine-world/SKILL.md +6 -0
- package/skills/jgengine-world/api.md +312 -12
- package/skills/jgengine-world/capabilities.md +102 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { IDENTITY_QUAT, isIdentityQuat, shapeHalfExtents, } from "./physicsBackend.js";
|
|
2
|
+
import { PhysicsWorld } from "./physicsWorld.js";
|
|
3
|
+
const ALL_LAYERS = 0xffffffff;
|
|
4
|
+
const CAPABILITIES = {
|
|
5
|
+
rotation: false,
|
|
6
|
+
shapes: ["box", "sphere", "capsule", "convex", "trimesh", "heightfield"],
|
|
7
|
+
ccd: false,
|
|
8
|
+
joints: ["fixed", "hinge", "ball", "distance", "spring"],
|
|
9
|
+
};
|
|
10
|
+
function slabRay(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ) {
|
|
11
|
+
let tMin = 0;
|
|
12
|
+
let tMax = Number.POSITIVE_INFINITY;
|
|
13
|
+
let axis = -1;
|
|
14
|
+
let sign = 0;
|
|
15
|
+
const origin = [ox, oy, oz];
|
|
16
|
+
const dir = [dx, dy, dz];
|
|
17
|
+
const mins = [minX, minY, minZ];
|
|
18
|
+
const maxs = [maxX, maxY, maxZ];
|
|
19
|
+
for (let i = 0; i < 3; i += 1) {
|
|
20
|
+
const o = origin[i];
|
|
21
|
+
const d = dir[i];
|
|
22
|
+
if (Math.abs(d) < 1e-12) {
|
|
23
|
+
if (o < mins[i] || o > maxs[i])
|
|
24
|
+
return null;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const inv = 1 / d;
|
|
28
|
+
let t1 = (mins[i] - o) * inv;
|
|
29
|
+
let t2 = (maxs[i] - o) * inv;
|
|
30
|
+
let entrySign = -1;
|
|
31
|
+
if (t1 > t2) {
|
|
32
|
+
const tmp = t1;
|
|
33
|
+
t1 = t2;
|
|
34
|
+
t2 = tmp;
|
|
35
|
+
entrySign = 1;
|
|
36
|
+
}
|
|
37
|
+
// `>=` so a shape resting exactly on a face still reports the contact at t=0 (grounding needs it).
|
|
38
|
+
if (t1 >= tMin) {
|
|
39
|
+
tMin = t1;
|
|
40
|
+
axis = i;
|
|
41
|
+
sign = entrySign;
|
|
42
|
+
}
|
|
43
|
+
tMax = Math.min(tMax, t2);
|
|
44
|
+
if (tMin > tMax)
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { t: tMin, axis, sign };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `PhysicsWorld` behind the {@link PhysicsBackend} interface: zero dependencies, translation-only. Capsules,
|
|
51
|
+
* convex hulls, meshes, and heightfields collide as their bounding boxes; rotation, angular velocity, CCD, and
|
|
52
|
+
* hinge limits are accepted and ignored (see {@link PhysicsBackend.capabilities}). Reach for `@jgengine/rapier`
|
|
53
|
+
* when a game needs those for real; the seam is the same.
|
|
54
|
+
*
|
|
55
|
+
* @capability physics-world-backend the zero-dependency PhysicsWorld solver exposed through the PhysicsBackend seam
|
|
56
|
+
*/
|
|
57
|
+
export function createPhysicsWorldBackend(options) {
|
|
58
|
+
const world = new PhysicsWorld({
|
|
59
|
+
capacity: options.capacity,
|
|
60
|
+
bounds: options.bounds,
|
|
61
|
+
...(options.gravity === undefined ? {} : { gravity: options.gravity[1] }),
|
|
62
|
+
...(options.fixedDt === undefined ? {} : { fixedDt: options.fixedDt }),
|
|
63
|
+
...(options.maxSubsteps === undefined ? {} : { maxSubsteps: options.maxSubsteps }),
|
|
64
|
+
...(options.precision === undefined ? {} : { precision: options.precision }),
|
|
65
|
+
...(options.friction === undefined ? {} : { friction: options.friction }),
|
|
66
|
+
...(options.restitution === undefined ? {} : { restitution: options.restitution }),
|
|
67
|
+
});
|
|
68
|
+
const records = new Map();
|
|
69
|
+
const handleOfIndex = new Map();
|
|
70
|
+
const joints = new Map();
|
|
71
|
+
const masses = new Map();
|
|
72
|
+
let nextHandle = 1;
|
|
73
|
+
let nextJoint = 1;
|
|
74
|
+
let warnedRotation = false;
|
|
75
|
+
let warnedShape = false;
|
|
76
|
+
let contactListener = null;
|
|
77
|
+
const contactEvent = {
|
|
78
|
+
a: 0,
|
|
79
|
+
b: 0,
|
|
80
|
+
normal: [0, 0, 0],
|
|
81
|
+
approachSpeed: 0,
|
|
82
|
+
impulse: 0,
|
|
83
|
+
};
|
|
84
|
+
const warn = options.warn ?? true;
|
|
85
|
+
function noteRotation(rotation) {
|
|
86
|
+
if (warnedRotation || !warn || isIdentityQuat(rotation))
|
|
87
|
+
return;
|
|
88
|
+
warnedRotation = true;
|
|
89
|
+
console.warn("[jgengine:physics] PhysicsWorld backend ignores rotation; use @jgengine/rapier for rotating bodies.");
|
|
90
|
+
}
|
|
91
|
+
function noteShape(shape) {
|
|
92
|
+
if (warnedShape || !warn || shape.kind === "box" || shape.kind === "sphere")
|
|
93
|
+
return;
|
|
94
|
+
warnedShape = true;
|
|
95
|
+
console.warn(`[jgengine:physics] PhysicsWorld backend collides "${shape.kind}" shapes as their bounding box.`);
|
|
96
|
+
}
|
|
97
|
+
function record(handle) {
|
|
98
|
+
const rec = records.get(handle);
|
|
99
|
+
if (rec === undefined)
|
|
100
|
+
throw new Error(`physics: unknown body handle ${handle}`);
|
|
101
|
+
return rec;
|
|
102
|
+
}
|
|
103
|
+
function insert(handle, desc, sleeping) {
|
|
104
|
+
const kind = desc.kind ?? "dynamic";
|
|
105
|
+
const half = shapeHalfExtents(desc.shape);
|
|
106
|
+
const index = world.addBody(desc.shape.kind === "sphere"
|
|
107
|
+
? {
|
|
108
|
+
shape: "sphere",
|
|
109
|
+
radius: desc.shape.radius,
|
|
110
|
+
position: desc.position,
|
|
111
|
+
...(desc.velocity === undefined ? {} : { velocity: desc.velocity }),
|
|
112
|
+
mass: desc.mass ?? 1,
|
|
113
|
+
static: kind !== "dynamic",
|
|
114
|
+
asleep: sleeping,
|
|
115
|
+
}
|
|
116
|
+
: {
|
|
117
|
+
shape: "box",
|
|
118
|
+
halfExtents: half,
|
|
119
|
+
position: desc.position,
|
|
120
|
+
...(desc.velocity === undefined ? {} : { velocity: desc.velocity }),
|
|
121
|
+
mass: desc.mass ?? 1,
|
|
122
|
+
static: kind !== "dynamic",
|
|
123
|
+
asleep: sleeping,
|
|
124
|
+
});
|
|
125
|
+
records.set(handle, {
|
|
126
|
+
index,
|
|
127
|
+
shape: desc.shape,
|
|
128
|
+
kind,
|
|
129
|
+
layers: desc.layers ?? 1,
|
|
130
|
+
mask: desc.mask ?? ALL_LAYERS,
|
|
131
|
+
userData: desc.userData,
|
|
132
|
+
half,
|
|
133
|
+
kinematicVelocity: [0, 0, 0],
|
|
134
|
+
kinematicTarget: null,
|
|
135
|
+
});
|
|
136
|
+
handleOfIndex.set(index, handle);
|
|
137
|
+
masses.set(handle, desc.mass ?? 1);
|
|
138
|
+
}
|
|
139
|
+
function addJointToWorld(desc) {
|
|
140
|
+
const a = record(desc.bodyA).index;
|
|
141
|
+
const base = {
|
|
142
|
+
bodyA: a,
|
|
143
|
+
...(desc.bodyB === undefined ? {} : { bodyB: record(desc.bodyB).index }),
|
|
144
|
+
...(desc.anchorA === undefined ? {} : { anchorA: desc.anchorA }),
|
|
145
|
+
...(desc.anchorB === undefined ? {} : { anchorB: desc.anchorB }),
|
|
146
|
+
...(desc.restLength === undefined ? {} : { restLength: desc.restLength }),
|
|
147
|
+
...(desc.stiffness === undefined ? {} : { stiffness: desc.stiffness }),
|
|
148
|
+
...(desc.damping === undefined ? {} : { damping: desc.damping }),
|
|
149
|
+
...(desc.axis === undefined ? {} : { axis: desc.axis }),
|
|
150
|
+
};
|
|
151
|
+
switch (desc.kind) {
|
|
152
|
+
case "hinge":
|
|
153
|
+
return world.hingeJoint(base);
|
|
154
|
+
case "distance":
|
|
155
|
+
return world.distanceJoint(base);
|
|
156
|
+
case "spring":
|
|
157
|
+
return world.springJoint(base);
|
|
158
|
+
default:
|
|
159
|
+
return world.fixedJoint(base);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function bodyMatches(rec, mask, exclude, handle) {
|
|
163
|
+
if (exclude === handle)
|
|
164
|
+
return false;
|
|
165
|
+
if (mask !== undefined && (rec.layers & mask) === 0)
|
|
166
|
+
return false;
|
|
167
|
+
return world.isAlive(rec.index);
|
|
168
|
+
}
|
|
169
|
+
world.onCollision((event) => {
|
|
170
|
+
if (contactListener === null)
|
|
171
|
+
return;
|
|
172
|
+
const a = handleOfIndex.get(event.a);
|
|
173
|
+
const b = handleOfIndex.get(event.b);
|
|
174
|
+
if (a === undefined || b === undefined)
|
|
175
|
+
return;
|
|
176
|
+
contactEvent.a = a;
|
|
177
|
+
contactEvent.b = b;
|
|
178
|
+
contactEvent.normal[0] = event.nx;
|
|
179
|
+
contactEvent.normal[1] = event.ny;
|
|
180
|
+
contactEvent.normal[2] = event.nz;
|
|
181
|
+
contactEvent.approachSpeed = event.approachSpeed;
|
|
182
|
+
contactEvent.impulse = event.impulse;
|
|
183
|
+
contactListener(contactEvent);
|
|
184
|
+
});
|
|
185
|
+
const backend = {
|
|
186
|
+
name: "physics-world",
|
|
187
|
+
capabilities: CAPABILITIES,
|
|
188
|
+
addBody(desc) {
|
|
189
|
+
noteRotation(desc.rotation);
|
|
190
|
+
noteShape(desc.shape);
|
|
191
|
+
const handle = nextHandle;
|
|
192
|
+
nextHandle += 1;
|
|
193
|
+
insert(handle, desc, desc.asleep === true);
|
|
194
|
+
return handle;
|
|
195
|
+
},
|
|
196
|
+
removeBody(handle) {
|
|
197
|
+
const rec = records.get(handle);
|
|
198
|
+
if (rec === undefined)
|
|
199
|
+
return;
|
|
200
|
+
for (const [jointHandle, joint] of joints) {
|
|
201
|
+
if (joint.desc.bodyA === handle || joint.desc.bodyB === handle) {
|
|
202
|
+
world.removeJoint(joint.id);
|
|
203
|
+
joints.delete(jointHandle);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
world.removeBody(rec.index);
|
|
207
|
+
handleOfIndex.delete(rec.index);
|
|
208
|
+
records.delete(handle);
|
|
209
|
+
masses.delete(handle);
|
|
210
|
+
},
|
|
211
|
+
hasBody: (handle) => records.has(handle),
|
|
212
|
+
body(handle, out) {
|
|
213
|
+
const rec = records.get(handle);
|
|
214
|
+
if (rec === undefined)
|
|
215
|
+
return null;
|
|
216
|
+
const i = rec.index;
|
|
217
|
+
const velocity = rec.kind === "kinematic" ? rec.kinematicVelocity : [world.velX[i], world.velY[i], world.velZ[i]];
|
|
218
|
+
const state = out ?? {
|
|
219
|
+
position: [0, 0, 0],
|
|
220
|
+
rotation: IDENTITY_QUAT,
|
|
221
|
+
velocity: [0, 0, 0],
|
|
222
|
+
angularVelocity: [0, 0, 0],
|
|
223
|
+
sleeping: false,
|
|
224
|
+
};
|
|
225
|
+
state.position = [world.posX[i], world.posY[i], world.posZ[i]];
|
|
226
|
+
state.rotation = IDENTITY_QUAT;
|
|
227
|
+
state.velocity = velocity;
|
|
228
|
+
state.angularVelocity = [0, 0, 0];
|
|
229
|
+
state.sleeping = world.isSleeping(i);
|
|
230
|
+
return state;
|
|
231
|
+
},
|
|
232
|
+
userDataOf: (handle) => records.get(handle)?.userData,
|
|
233
|
+
setPosition(handle, position) {
|
|
234
|
+
world.setPosition(record(handle).index, position[0], position[1], position[2]);
|
|
235
|
+
},
|
|
236
|
+
setRotation(_handle, rotation) {
|
|
237
|
+
noteRotation(rotation);
|
|
238
|
+
},
|
|
239
|
+
setVelocity(handle, velocity) {
|
|
240
|
+
world.setVelocity(record(handle).index, velocity[0], velocity[1], velocity[2]);
|
|
241
|
+
},
|
|
242
|
+
setAngularVelocity() { },
|
|
243
|
+
applyImpulse(handle, impulse) {
|
|
244
|
+
const rec = record(handle);
|
|
245
|
+
if (rec.kind !== "dynamic")
|
|
246
|
+
return;
|
|
247
|
+
const i = rec.index;
|
|
248
|
+
const invMass = world.invMass[i];
|
|
249
|
+
world.setVelocity(i, world.velX[i] + impulse[0] * invMass, world.velY[i] + impulse[1] * invMass, world.velZ[i] + impulse[2] * invMass);
|
|
250
|
+
},
|
|
251
|
+
setKinematicTarget(handle, position) {
|
|
252
|
+
const rec = record(handle);
|
|
253
|
+
rec.kinematicTarget = [position[0], position[1], position[2]];
|
|
254
|
+
},
|
|
255
|
+
wake(handle) {
|
|
256
|
+
world.wake(record(handle).index);
|
|
257
|
+
},
|
|
258
|
+
addJoint(desc) {
|
|
259
|
+
const handle = nextJoint;
|
|
260
|
+
nextJoint += 1;
|
|
261
|
+
joints.set(handle, { id: addJointToWorld(desc), desc });
|
|
262
|
+
return handle;
|
|
263
|
+
},
|
|
264
|
+
removeJoint(handle) {
|
|
265
|
+
const joint = joints.get(handle);
|
|
266
|
+
if (joint === undefined)
|
|
267
|
+
return;
|
|
268
|
+
world.removeJoint(joint.id);
|
|
269
|
+
joints.delete(handle);
|
|
270
|
+
},
|
|
271
|
+
step(dt) {
|
|
272
|
+
if (!(dt > 0))
|
|
273
|
+
return;
|
|
274
|
+
for (const rec of records.values()) {
|
|
275
|
+
if (rec.kind !== "kinematic")
|
|
276
|
+
continue;
|
|
277
|
+
const target = rec.kinematicTarget;
|
|
278
|
+
const i = rec.index;
|
|
279
|
+
if (target === null) {
|
|
280
|
+
rec.kinematicVelocity[0] = 0;
|
|
281
|
+
rec.kinematicVelocity[1] = 0;
|
|
282
|
+
rec.kinematicVelocity[2] = 0;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
rec.kinematicVelocity[0] = (target[0] - world.posX[i]) / dt;
|
|
286
|
+
rec.kinematicVelocity[1] = (target[1] - world.posY[i]) / dt;
|
|
287
|
+
rec.kinematicVelocity[2] = (target[2] - world.posZ[i]) / dt;
|
|
288
|
+
world.setPosition(i, target[0], target[1], target[2]);
|
|
289
|
+
rec.kinematicTarget = null;
|
|
290
|
+
}
|
|
291
|
+
world.step(dt);
|
|
292
|
+
},
|
|
293
|
+
raycast(desc) {
|
|
294
|
+
const len = Math.hypot(desc.direction[0], desc.direction[1], desc.direction[2]);
|
|
295
|
+
if (!(len > 0))
|
|
296
|
+
return null;
|
|
297
|
+
const dx = desc.direction[0] / len;
|
|
298
|
+
const dy = desc.direction[1] / len;
|
|
299
|
+
const dz = desc.direction[2] / len;
|
|
300
|
+
let best = null;
|
|
301
|
+
for (const [handle, rec] of records) {
|
|
302
|
+
if (!bodyMatches(rec, desc.mask, desc.exclude, handle))
|
|
303
|
+
continue;
|
|
304
|
+
const i = rec.index;
|
|
305
|
+
const px = world.posX[i];
|
|
306
|
+
const py = world.posY[i];
|
|
307
|
+
const pz = world.posZ[i];
|
|
308
|
+
let t;
|
|
309
|
+
let normal;
|
|
310
|
+
if (rec.shape.kind === "sphere") {
|
|
311
|
+
const r = rec.shape.radius;
|
|
312
|
+
const ox = desc.origin[0] - px;
|
|
313
|
+
const oy = desc.origin[1] - py;
|
|
314
|
+
const oz = desc.origin[2] - pz;
|
|
315
|
+
const b = ox * dx + oy * dy + oz * dz;
|
|
316
|
+
const c = ox * ox + oy * oy + oz * oz - r * r;
|
|
317
|
+
const disc = b * b - c;
|
|
318
|
+
if (disc < 0)
|
|
319
|
+
continue;
|
|
320
|
+
t = -b - Math.sqrt(disc);
|
|
321
|
+
if (t < 0)
|
|
322
|
+
continue;
|
|
323
|
+
const hx = ox + dx * t;
|
|
324
|
+
const hy = oy + dy * t;
|
|
325
|
+
const hz = oz + dz * t;
|
|
326
|
+
normal = [hx / r, hy / r, hz / r];
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
const hit = slabRay(desc.origin[0], desc.origin[1], desc.origin[2], dx, dy, dz, px - rec.half[0], py - rec.half[1], pz - rec.half[2], px + rec.half[0], py + rec.half[1], pz + rec.half[2]);
|
|
330
|
+
if (hit === null || hit.axis < 0)
|
|
331
|
+
continue;
|
|
332
|
+
t = hit.t;
|
|
333
|
+
normal = [hit.axis === 0 ? hit.sign : 0, hit.axis === 1 ? hit.sign : 0, hit.axis === 2 ? hit.sign : 0];
|
|
334
|
+
}
|
|
335
|
+
if (t > desc.maxDistance)
|
|
336
|
+
continue;
|
|
337
|
+
if (best !== null && t >= best.distance)
|
|
338
|
+
continue;
|
|
339
|
+
best = {
|
|
340
|
+
body: handle,
|
|
341
|
+
distance: t,
|
|
342
|
+
point: [desc.origin[0] + dx * t, desc.origin[1] + dy * t, desc.origin[2] + dz * t],
|
|
343
|
+
normal,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return best;
|
|
347
|
+
},
|
|
348
|
+
shapecast(desc) {
|
|
349
|
+
noteRotation(desc.rotation);
|
|
350
|
+
const castHalf = shapeHalfExtents(desc.shape);
|
|
351
|
+
const [mx, my, mz] = desc.motion;
|
|
352
|
+
const motionLen = Math.hypot(mx, my, mz);
|
|
353
|
+
let best = null;
|
|
354
|
+
for (const [handle, rec] of records) {
|
|
355
|
+
if (!bodyMatches(rec, desc.mask, desc.exclude, handle))
|
|
356
|
+
continue;
|
|
357
|
+
const i = rec.index;
|
|
358
|
+
const minX = world.posX[i] - rec.half[0] - castHalf[0];
|
|
359
|
+
const minY = world.posY[i] - rec.half[1] - castHalf[1];
|
|
360
|
+
const minZ = world.posZ[i] - rec.half[2] - castHalf[2];
|
|
361
|
+
const maxX = world.posX[i] + rec.half[0] + castHalf[0];
|
|
362
|
+
const maxY = world.posY[i] + rec.half[1] + castHalf[1];
|
|
363
|
+
const maxZ = world.posZ[i] + rec.half[2] + castHalf[2];
|
|
364
|
+
const [ox, oy, oz] = desc.position;
|
|
365
|
+
const inside = ox > minX && ox < maxX && oy > minY && oy < maxY && oz > minZ && oz < maxZ;
|
|
366
|
+
let toi;
|
|
367
|
+
let normal;
|
|
368
|
+
if (inside) {
|
|
369
|
+
const pens = [ox - minX, maxX - ox, oy - minY, maxY - oy, oz - minZ, maxZ - oz];
|
|
370
|
+
let least = 0;
|
|
371
|
+
for (let k = 1; k < 6; k += 1)
|
|
372
|
+
if (pens[k] < pens[least])
|
|
373
|
+
least = k;
|
|
374
|
+
toi = 0;
|
|
375
|
+
normal = [
|
|
376
|
+
least === 0 ? -1 : least === 1 ? 1 : 0,
|
|
377
|
+
least === 2 ? -1 : least === 3 ? 1 : 0,
|
|
378
|
+
least === 4 ? -1 : least === 5 ? 1 : 0,
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
if (!(motionLen > 0))
|
|
383
|
+
continue;
|
|
384
|
+
const hit = slabRay(ox, oy, oz, mx, my, mz, minX, minY, minZ, maxX, maxY, maxZ);
|
|
385
|
+
if (hit === null || hit.axis < 0 || hit.t > 1)
|
|
386
|
+
continue;
|
|
387
|
+
toi = hit.t;
|
|
388
|
+
normal = [hit.axis === 0 ? hit.sign : 0, hit.axis === 1 ? hit.sign : 0, hit.axis === 2 ? hit.sign : 0];
|
|
389
|
+
}
|
|
390
|
+
if (best !== null && toi >= best.toi)
|
|
391
|
+
continue;
|
|
392
|
+
best = {
|
|
393
|
+
body: handle,
|
|
394
|
+
toi,
|
|
395
|
+
distance: toi * motionLen,
|
|
396
|
+
point: [ox + mx * toi - normal[0] * castHalf[0], oy + my * toi - normal[1] * castHalf[1], oz + mz * toi - normal[2] * castHalf[2]],
|
|
397
|
+
normal,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return best;
|
|
401
|
+
},
|
|
402
|
+
overlap(desc) {
|
|
403
|
+
const half = shapeHalfExtents(desc.shape);
|
|
404
|
+
const out = [];
|
|
405
|
+
for (const [handle, rec] of records) {
|
|
406
|
+
if (!bodyMatches(rec, desc.mask, desc.exclude, handle))
|
|
407
|
+
continue;
|
|
408
|
+
const i = rec.index;
|
|
409
|
+
if (Math.abs(world.posX[i] - desc.position[0]) >= rec.half[0] + half[0])
|
|
410
|
+
continue;
|
|
411
|
+
if (Math.abs(world.posY[i] - desc.position[1]) >= rec.half[1] + half[1])
|
|
412
|
+
continue;
|
|
413
|
+
if (Math.abs(world.posZ[i] - desc.position[2]) >= rec.half[2] + half[2])
|
|
414
|
+
continue;
|
|
415
|
+
out.push(handle);
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
},
|
|
419
|
+
onContact(listener) {
|
|
420
|
+
contactListener = listener;
|
|
421
|
+
},
|
|
422
|
+
retune() {
|
|
423
|
+
// PhysicsWorld fixes its solver knobs at construction; retune is a no-op here and a real call on native backends.
|
|
424
|
+
},
|
|
425
|
+
snapshot() {
|
|
426
|
+
const bodies = [];
|
|
427
|
+
for (const [handle, rec] of records) {
|
|
428
|
+
const i = rec.index;
|
|
429
|
+
bodies.push({
|
|
430
|
+
handle,
|
|
431
|
+
shape: rec.shape,
|
|
432
|
+
kind: rec.kind,
|
|
433
|
+
layers: rec.layers,
|
|
434
|
+
mask: rec.mask,
|
|
435
|
+
userData: rec.userData,
|
|
436
|
+
mass: masses.get(handle) ?? 1,
|
|
437
|
+
position: [world.posX[i], world.posY[i], world.posZ[i]],
|
|
438
|
+
velocity: [world.velX[i], world.velY[i], world.velZ[i]],
|
|
439
|
+
sleeping: world.isSleeping(i),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
nextHandle,
|
|
444
|
+
nextJoint,
|
|
445
|
+
bodies,
|
|
446
|
+
joints: Array.from(joints, ([handle, joint]) => ({ handle, desc: joint.desc })),
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
restore(state) {
|
|
450
|
+
const saved = state;
|
|
451
|
+
world.clear();
|
|
452
|
+
records.clear();
|
|
453
|
+
handleOfIndex.clear();
|
|
454
|
+
joints.clear();
|
|
455
|
+
masses.clear();
|
|
456
|
+
nextHandle = saved.nextHandle;
|
|
457
|
+
nextJoint = saved.nextJoint;
|
|
458
|
+
for (const body of saved.bodies) {
|
|
459
|
+
insert(body.handle, {
|
|
460
|
+
shape: body.shape,
|
|
461
|
+
kind: body.kind,
|
|
462
|
+
position: body.position,
|
|
463
|
+
velocity: body.velocity,
|
|
464
|
+
mass: body.mass,
|
|
465
|
+
layers: body.layers,
|
|
466
|
+
mask: body.mask,
|
|
467
|
+
userData: body.userData,
|
|
468
|
+
}, body.sleeping);
|
|
469
|
+
}
|
|
470
|
+
for (const joint of saved.joints) {
|
|
471
|
+
joints.set(joint.handle, { id: addJointToWorld(joint.desc), desc: joint.desc });
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
dispose() {
|
|
475
|
+
world.onCollision(null);
|
|
476
|
+
contactListener = null;
|
|
477
|
+
world.clear();
|
|
478
|
+
records.clear();
|
|
479
|
+
handleOfIndex.clear();
|
|
480
|
+
joints.clear();
|
|
481
|
+
masses.clear();
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
return backend;
|
|
485
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GameContext } from "../runtime/gameContextTypes.js";
|
|
2
|
+
import type { PhysicsBackend } from "./physicsBackend.js";
|
|
3
|
+
/** Handle for synchronizing authored static world collision with a physics backend. */
|
|
4
|
+
export interface WorldColliderSync {
|
|
5
|
+
/** Rebuild static terrain and object bodies from the current context. */
|
|
6
|
+
sync(): void;
|
|
7
|
+
/** Remove all bodies owned by this synchronizer and stop reacting to scene changes. */
|
|
8
|
+
dispose(): void;
|
|
9
|
+
}
|
|
10
|
+
/** Mirrors static scene-object physical colliders and the context's ground field into a physics backend.
|
|
11
|
+
* @capability world-physics-colliders Synchronize terrain and authored static colliders with a physics backend.
|
|
12
|
+
*/
|
|
13
|
+
export declare function syncWorldColliders(backend: PhysicsBackend, ctx: GameContext): WorldColliderSync;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { colliderWorldCenter, resolveColliders } from "../scene/colliders.js";
|
|
2
|
+
const TERRAIN_SEGMENTS = 32;
|
|
3
|
+
function bodyShape(shape) {
|
|
4
|
+
if (shape.kind === "sphere")
|
|
5
|
+
return { kind: "sphere", radius: shape.radius };
|
|
6
|
+
if (shape.kind === "aabb")
|
|
7
|
+
return { kind: "box", halfExtents: shape.halfExtents };
|
|
8
|
+
return { kind: "trimesh", vertices: shape.mesh.positions, indices: shape.mesh.indices };
|
|
9
|
+
}
|
|
10
|
+
function bodyPosition(shape, position, rotationY) {
|
|
11
|
+
return colliderWorldCenter({ name: "body", purpose: "physical", shape, damageEligible: false, blocks: true }, position, rotationY);
|
|
12
|
+
}
|
|
13
|
+
/** Mirrors static scene-object physical colliders and the context's ground field into a physics backend.
|
|
14
|
+
* @capability world-physics-colliders Synchronize terrain and authored static colliders with a physics backend.
|
|
15
|
+
*/
|
|
16
|
+
export function syncWorldColliders(backend, ctx) {
|
|
17
|
+
const objectBodies = new Map();
|
|
18
|
+
let terrainBody = null;
|
|
19
|
+
let disposed = false;
|
|
20
|
+
function rebuildTerrain() {
|
|
21
|
+
if (terrainBody !== null)
|
|
22
|
+
backend.removeBody(terrainBody);
|
|
23
|
+
terrainBody = null;
|
|
24
|
+
const bounds = ctx.world.ground.bounds;
|
|
25
|
+
if (bounds === undefined)
|
|
26
|
+
return;
|
|
27
|
+
const columns = TERRAIN_SEGMENTS + 1;
|
|
28
|
+
const rows = TERRAIN_SEGMENTS + 1;
|
|
29
|
+
const heights = new Float32Array(columns * rows);
|
|
30
|
+
for (let row = 0; row < rows; row += 1) {
|
|
31
|
+
const z = -bounds.d / 2 + (row / TERRAIN_SEGMENTS) * bounds.d;
|
|
32
|
+
for (let column = 0; column < columns; column += 1) {
|
|
33
|
+
const x = -bounds.w / 2 + (column / TERRAIN_SEGMENTS) * bounds.w;
|
|
34
|
+
heights[row * columns + column] = ctx.world.ground.sampleHeight(x, z);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
terrainBody = backend.addBody({
|
|
38
|
+
shape: { kind: "heightfield", rows, columns, heights, scale: [bounds.w / TERRAIN_SEGMENTS, 1, bounds.d / TERRAIN_SEGMENTS] },
|
|
39
|
+
position: [0, 0, 0],
|
|
40
|
+
kind: "static",
|
|
41
|
+
userData: { kind: "terrain" },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function syncObject(objectId) {
|
|
45
|
+
const old = objectBodies.get(objectId);
|
|
46
|
+
if (old !== undefined) {
|
|
47
|
+
backend.removeBody(old);
|
|
48
|
+
objectBodies.delete(objectId);
|
|
49
|
+
}
|
|
50
|
+
const object = ctx.scene.object.get(objectId);
|
|
51
|
+
if (object === null)
|
|
52
|
+
return;
|
|
53
|
+
const body = resolveColliders(ctx.scene.object.collidersOf(objectId)).find((collider) => collider.purpose === "physical");
|
|
54
|
+
if (body === undefined)
|
|
55
|
+
return;
|
|
56
|
+
const handle = backend.addBody({
|
|
57
|
+
shape: bodyShape(body.shape),
|
|
58
|
+
position: bodyPosition(body.shape, object.position, object.rotationY),
|
|
59
|
+
rotation: [0, Math.sin(object.rotationY / 2), 0, Math.cos(object.rotationY / 2)],
|
|
60
|
+
kind: "static",
|
|
61
|
+
userData: { kind: "object", instanceId: object.instanceId, catalogId: object.catalogId },
|
|
62
|
+
});
|
|
63
|
+
objectBodies.set(objectId, handle);
|
|
64
|
+
}
|
|
65
|
+
function sync() {
|
|
66
|
+
if (disposed)
|
|
67
|
+
return;
|
|
68
|
+
rebuildTerrain();
|
|
69
|
+
const ids = new Set(ctx.scene.object.ids());
|
|
70
|
+
for (const [objectId, handle] of objectBodies) {
|
|
71
|
+
if (!ids.has(objectId)) {
|
|
72
|
+
backend.removeBody(handle);
|
|
73
|
+
objectBodies.delete(objectId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const objectId of ids)
|
|
77
|
+
syncObject(objectId);
|
|
78
|
+
}
|
|
79
|
+
const unsubscribe = ctx.scene.object.subscribe(sync);
|
|
80
|
+
sync();
|
|
81
|
+
return {
|
|
82
|
+
sync,
|
|
83
|
+
dispose() {
|
|
84
|
+
if (disposed)
|
|
85
|
+
return;
|
|
86
|
+
disposed = true;
|
|
87
|
+
unsubscribe();
|
|
88
|
+
if (terrainBody !== null)
|
|
89
|
+
backend.removeBody(terrainBody);
|
|
90
|
+
terrainBody = null;
|
|
91
|
+
for (const handle of objectBodies.values())
|
|
92
|
+
backend.removeBody(handle);
|
|
93
|
+
objectBodies.clear();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/random/rng.d.ts
CHANGED
|
@@ -20,10 +20,21 @@ export declare function randomSeedFrom(seed: number): RandomSeed;
|
|
|
20
20
|
* instead of closing over a generator.
|
|
21
21
|
*/
|
|
22
22
|
export declare function stepRandomSeed(seed: RandomSeed): readonly [value: number, next: RandomSeed];
|
|
23
|
+
/** A {@link seededRng} stream: callable like any `() => number`, with its cursor readable and restorable for snapshots and replay. */
|
|
24
|
+
export interface SeededRng {
|
|
25
|
+
(): number;
|
|
26
|
+
/** The current cursor; feed it back to {@link restore} (or a fresh stream's `restore`) to resume the exact sequence. */
|
|
27
|
+
state(): number;
|
|
28
|
+
restore(cursor: number): void;
|
|
29
|
+
}
|
|
23
30
|
/** Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
24
31
|
*
|
|
25
32
|
* @capability seeded-random injected deterministic randomness for game logic — never `Math.random` in a simulation
|
|
26
33
|
*/
|
|
27
|
-
export declare function seededRng(seed: string | number):
|
|
34
|
+
export declare function seededRng(seed: string | number): SeededRng;
|
|
35
|
+
/** The cursor of a seeded stream, or `null` for a generator that carries no state (an injected closure, `Math.random`). */
|
|
36
|
+
export declare function rngStateOf(rng: () => number): number | null;
|
|
37
|
+
/** Restore a seeded stream's cursor; a no-op for generators that carry no state. */
|
|
38
|
+
export declare function restoreRng(rng: () => number, cursor: number): void;
|
|
28
39
|
/** Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. */
|
|
29
40
|
export declare function seededStreams(seed: string | number): (stream: string) => () => number;
|
package/dist/random/rng.js
CHANGED
|
@@ -35,11 +35,28 @@ export function stepRandomSeed(seed) {
|
|
|
35
35
|
*/
|
|
36
36
|
export function seededRng(seed) {
|
|
37
37
|
let cursor = randomSeedFrom(hashSeed(seed));
|
|
38
|
-
|
|
39
|
-
const [value,
|
|
40
|
-
cursor =
|
|
38
|
+
const next = () => {
|
|
39
|
+
const [value, following] = stepRandomSeed(cursor);
|
|
40
|
+
cursor = following;
|
|
41
41
|
return value;
|
|
42
42
|
};
|
|
43
|
+
const rng = next;
|
|
44
|
+
rng.state = () => cursor;
|
|
45
|
+
rng.restore = (value) => {
|
|
46
|
+
cursor = randomSeedFrom(value);
|
|
47
|
+
};
|
|
48
|
+
return rng;
|
|
49
|
+
}
|
|
50
|
+
/** The cursor of a seeded stream, or `null` for a generator that carries no state (an injected closure, `Math.random`). */
|
|
51
|
+
export function rngStateOf(rng) {
|
|
52
|
+
const candidate = rng;
|
|
53
|
+
return typeof candidate.state === "function" ? candidate.state() : null;
|
|
54
|
+
}
|
|
55
|
+
/** Restore a seeded stream's cursor; a no-op for generators that carry no state. */
|
|
56
|
+
export function restoreRng(rng, cursor) {
|
|
57
|
+
const candidate = rng;
|
|
58
|
+
if (typeof candidate.restore === "function")
|
|
59
|
+
candidate.restore(cursor);
|
|
43
60
|
}
|
|
44
61
|
/** Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. */
|
|
45
62
|
export function seededStreams(seed) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A declarative source for image-based scene lighting. */
|
|
2
|
+
export type EnvironmentSource =
|
|
3
|
+
/** Procedural two-color sky probe, preserving the engine default. */
|
|
4
|
+
{
|
|
5
|
+
kind: "gradient";
|
|
6
|
+
sky: string;
|
|
7
|
+
ground: string;
|
|
8
|
+
sun?: string;
|
|
9
|
+
intensity?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Equirectangular HDR or EXR environment map loaded from `url`. */
|
|
12
|
+
| {
|
|
13
|
+
kind: "hdri";
|
|
14
|
+
url: string;
|
|
15
|
+
rotation?: number;
|
|
16
|
+
intensity?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Six faces of a cube environment map, in three.js cube-loader order. */
|
|
19
|
+
| {
|
|
20
|
+
kind: "cube";
|
|
21
|
+
urls: [string, string, string, string, string, string];
|
|
22
|
+
intensity?: number;
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|