@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { applyHorizontalImpulses, applyMotionImpulses } from "../runtime/motionIntents.js";
|
|
2
|
+
import { createCharacterController } from "./characterController.js";
|
|
2
3
|
import { groundFieldFor, hasEnvironmentTerrain, sampleSlope } from "../world/terrain.js";
|
|
3
|
-
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "./movementModel.js";
|
|
4
|
+
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, MOVEMENT_TUNING, } from "./movementModel.js";
|
|
5
|
+
import { advanceFreeFlight, createFreeFlightState, resolveFlightStep, resolveFreeFlightIntentFromInput, } from "./freeFlight.js";
|
|
4
6
|
import { solidObstaclesNear } from "./solidObstacles.js";
|
|
5
7
|
import { approachYaw, steerYaw } from "./steering.js";
|
|
6
8
|
import { advanceVoxelPlayer, createVoxelPlayerBody, } from "./voxelController.js";
|
|
@@ -39,6 +41,18 @@ export function resolvePlayerMovementTuning(opts) {
|
|
|
39
41
|
...(opts.collision === undefined ? {} : { collision: opts.collision }),
|
|
40
42
|
...(opts.movement === undefined ? {} : { movement: opts.movement }),
|
|
41
43
|
...(overrides === undefined ? {} : { physics: overrides }),
|
|
44
|
+
...(opts.physics?.backend === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: {
|
|
47
|
+
controller: {
|
|
48
|
+
backend: opts.physics.backend,
|
|
49
|
+
capsule: opts.physics.controller ?? {
|
|
50
|
+
radius: DEFAULT_OBSTACLE_PLAYER_RADIUS,
|
|
51
|
+
height: 1.8,
|
|
52
|
+
stepHeight: DEFAULT_PLAYER_STEP_HEIGHT,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
42
56
|
ground: groundFieldFor(opts.world),
|
|
43
57
|
hasTerrain: hasEnvironmentTerrain(opts.world),
|
|
44
58
|
};
|
|
@@ -58,7 +72,7 @@ function storeFor(ctx) {
|
|
|
58
72
|
function stateFor(store, userId) {
|
|
59
73
|
let state = store.players.get(userId);
|
|
60
74
|
if (state === undefined) {
|
|
61
|
-
state = { heading: 0, facing: null, voxelBody: null, motion: null };
|
|
75
|
+
state = { heading: 0, facing: null, voxelBody: null, motion: null, flight: null, controller: null, controllerJumpHeld: false };
|
|
62
76
|
store.players.set(userId, state);
|
|
63
77
|
}
|
|
64
78
|
return state;
|
|
@@ -90,15 +104,41 @@ function resolveBodyFacing(state, moving, velocityX, velocityZ, fallbackYaw, tur
|
|
|
90
104
|
state.facing = next;
|
|
91
105
|
return next;
|
|
92
106
|
}
|
|
107
|
+
function flightTuningFor(movement, ctx) {
|
|
108
|
+
const cfg = movement?.flight;
|
|
109
|
+
if (cfg === undefined || cfg === false)
|
|
110
|
+
return null;
|
|
111
|
+
if (cfg === true)
|
|
112
|
+
return { mode: "creative" };
|
|
113
|
+
if (typeof cfg === "object") {
|
|
114
|
+
if (cfg.canFly !== undefined && !cfg.canFly(ctx))
|
|
115
|
+
return null;
|
|
116
|
+
return {
|
|
117
|
+
mode: cfg.mode ?? "creative",
|
|
118
|
+
speed: cfg.speed,
|
|
119
|
+
verticalSpeed: cfg.verticalSpeed,
|
|
120
|
+
sprintMultiplier: cfg.sprintMultiplier,
|
|
121
|
+
acceleration: cfg.acceleration,
|
|
122
|
+
gravity: cfg.gravity,
|
|
123
|
+
thrust: cfg.thrust,
|
|
124
|
+
alignWithLook: cfg.alignWithLook,
|
|
125
|
+
collide: cfg.collide,
|
|
126
|
+
bindings: cfg.bindings,
|
|
127
|
+
camera: cfg.camera ?? null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
93
132
|
/**
|
|
94
133
|
* Integrate one player's movement for a tick from their held-input frame and commit the pose — the single
|
|
95
134
|
* genre-agnostic controller both the shell (its local player) and a host (each connected player in `onTick`) call,
|
|
96
135
|
* so single-player and server-authoritative movement are identical. Reads the player's controlled entity, terrain,
|
|
97
136
|
* scene solids, and pending motion impulses; writes the entity pose via `setPose`. Retains heading + kinematic body
|
|
98
137
|
* per `userId` on the `ctx`. Pass `heading` to override the internally-integrated yaw (the shell owns yaw for its
|
|
99
|
-
* camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions.
|
|
138
|
+
* camera); omit it and the controller turns from the frame's `turnLeft`/`turnRight` actions. Pass `pitch` for
|
|
139
|
+
* 6DOF spectator flight when `alignWithLook` is set.
|
|
100
140
|
*/
|
|
101
|
-
export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading) {
|
|
141
|
+
export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitch) {
|
|
102
142
|
const playerId = ctx.player.possession.active(userId);
|
|
103
143
|
const player = ctx.scene.entity.get(playerId);
|
|
104
144
|
if (player === null)
|
|
@@ -141,6 +181,180 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading) {
|
|
|
141
181
|
const intent = resolveMovementIntent(keys, true, analogMove);
|
|
142
182
|
const motionBatch = ctx.player.motionFor(userId).takePending();
|
|
143
183
|
const walkSpeed = player.movement?.walkSpeed ?? DEFAULT_WALK_SPEED;
|
|
184
|
+
const flightTuning = flightTuningFor(tuning.movement, ctx);
|
|
185
|
+
if (flightTuning !== null) {
|
|
186
|
+
const value = (action) => input.analog?.[action] ?? (isDown(action) ? 1 : 0);
|
|
187
|
+
let flightIntent = resolveFreeFlightIntentFromInput(isDown, value, input.pointer, flightTuning.bindings);
|
|
188
|
+
if (tuning.movement?.canSprint !== undefined && !tuning.movement.canSprint(ctx)) {
|
|
189
|
+
flightIntent = { ...flightIntent, sprint: false };
|
|
190
|
+
}
|
|
191
|
+
let flightState = state.flight;
|
|
192
|
+
if (flightState === null) {
|
|
193
|
+
flightState = createFreeFlightState();
|
|
194
|
+
state.flight = flightState;
|
|
195
|
+
}
|
|
196
|
+
if (motionBatch !== null) {
|
|
197
|
+
flightState.vy = applyMotionImpulses(flightState.vy, motionBatch);
|
|
198
|
+
const [hx, hz] = applyHorizontalImpulses(flightState.vx, flightState.vz, motionBatch);
|
|
199
|
+
flightState.vx = hx;
|
|
200
|
+
flightState.vz = hz;
|
|
201
|
+
}
|
|
202
|
+
const normalizedFlightTuning = {
|
|
203
|
+
mode: flightTuning.mode,
|
|
204
|
+
speed: flightTuning.speed,
|
|
205
|
+
verticalSpeed: flightTuning.verticalSpeed,
|
|
206
|
+
sprintMultiplier: flightTuning.sprintMultiplier,
|
|
207
|
+
acceleration: flightTuning.acceleration,
|
|
208
|
+
gravity: flightTuning.gravity,
|
|
209
|
+
thrust: flightTuning.thrust,
|
|
210
|
+
alignWithLook: flightTuning.alignWithLook,
|
|
211
|
+
collide: flightTuning.collide,
|
|
212
|
+
bindings: flightTuning.bindings,
|
|
213
|
+
camera: flightTuning.camera,
|
|
214
|
+
};
|
|
215
|
+
if (normalizedFlightTuning.collide === undefined) {
|
|
216
|
+
normalizedFlightTuning.collide = normalizedFlightTuning.mode === "creative" || normalizedFlightTuning.mode === "hover";
|
|
217
|
+
}
|
|
218
|
+
if (normalizedFlightTuning.camera !== undefined) {
|
|
219
|
+
const cam = ctx.camera;
|
|
220
|
+
cam?.setChaseTuning(normalizedFlightTuning.camera ?? null);
|
|
221
|
+
}
|
|
222
|
+
const step = advanceFreeFlight(flightState, flightIntent, state.heading, pitch, dt, normalizedFlightTuning);
|
|
223
|
+
let stepX = step.stepX;
|
|
224
|
+
let stepY = step.stepY;
|
|
225
|
+
let stepZ = step.stepZ;
|
|
226
|
+
if (normalizedFlightTuning.collide !== false && tuning.movement?.collideObjects !== false) {
|
|
227
|
+
const obstacles = gatherMovementObstacles(ctx, player.position, stepX, stepZ);
|
|
228
|
+
const verticalResolved = resolveFlightStep(player.position, stepX, stepY, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS);
|
|
229
|
+
stepY = verticalResolved.stepY;
|
|
230
|
+
const xzResolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles, DEFAULT_OBSTACLE_PLAYER_RADIUS, 0);
|
|
231
|
+
stepX = xzResolved.stepX;
|
|
232
|
+
stepZ = xzResolved.stepZ;
|
|
233
|
+
const ground = tuning.ground.sampleHeight(player.position[0] + stepX, player.position[2] + stepZ);
|
|
234
|
+
const nextY = player.position[1] + stepY;
|
|
235
|
+
if (nextY < ground + 0.2) {
|
|
236
|
+
stepY = ground + 0.2 - player.position[1];
|
|
237
|
+
if (flightState.vy < 0)
|
|
238
|
+
flightState.vy = 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let nextX = player.position[0] + stepX;
|
|
242
|
+
let nextY = player.position[1] + stepY;
|
|
243
|
+
let nextZ = player.position[2] + stepZ;
|
|
244
|
+
if (motionBatch !== null && motionBatch.y !== null) {
|
|
245
|
+
nextY = motionBatch.y;
|
|
246
|
+
}
|
|
247
|
+
if (tuning.movement?.beforeCommit !== undefined) {
|
|
248
|
+
const frame = {
|
|
249
|
+
entityId: playerId,
|
|
250
|
+
current: player.position,
|
|
251
|
+
next: [nextX, nextY, nextZ],
|
|
252
|
+
dt,
|
|
253
|
+
ctx,
|
|
254
|
+
};
|
|
255
|
+
const replacement = tuning.movement.beforeCommit(frame);
|
|
256
|
+
if (replacement !== undefined) {
|
|
257
|
+
nextX = replacement[0];
|
|
258
|
+
nextY = replacement[1];
|
|
259
|
+
nextZ = replacement[2];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
ctx.scene.entity.setPose(playerId, {
|
|
263
|
+
position: [nextX, nextY, nextZ],
|
|
264
|
+
rotationY: resolveBodyFacing(state, flightIntent.moving, flightState.vx, flightState.vz, player.rotationY, tuning.movement?.turnSpeed, dt),
|
|
265
|
+
dt,
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (flightTuning === null && state.flight !== null) {
|
|
270
|
+
const cam = ctx.camera;
|
|
271
|
+
cam?.setChaseTuning(null);
|
|
272
|
+
}
|
|
273
|
+
if (tuning.controller !== undefined) {
|
|
274
|
+
let controller = state.controller;
|
|
275
|
+
if (controller === null) {
|
|
276
|
+
controller = createCharacterController(tuning.controller.capsule);
|
|
277
|
+
state.controller = controller;
|
|
278
|
+
controller.restore({
|
|
279
|
+
position: [player.position[0], player.position[1], player.position[2]],
|
|
280
|
+
verticalVelocity: 0,
|
|
281
|
+
grounded: true,
|
|
282
|
+
groundNormal: [0, 1, 0],
|
|
283
|
+
groundBody: null,
|
|
284
|
+
crouching: false,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const controllerState = controller.state();
|
|
288
|
+
if (Math.hypot(controllerState.position[0] - player.position[0], controllerState.position[1] - player.position[1], controllerState.position[2] - player.position[2]) > 1e-6) {
|
|
289
|
+
controller.restore({
|
|
290
|
+
...controllerState,
|
|
291
|
+
position: [player.position[0], player.position[1], player.position[2]],
|
|
292
|
+
grounded: false,
|
|
293
|
+
groundBody: null,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
// Keep the existing input feel and horizontal impulse seam; the capsule owns vertical motion and collision.
|
|
297
|
+
let horizontal = state.motion;
|
|
298
|
+
if (horizontal === null) {
|
|
299
|
+
horizontal = createPlayerMotionState();
|
|
300
|
+
state.motion = horizontal;
|
|
301
|
+
}
|
|
302
|
+
const grounded = controller.state().grounded;
|
|
303
|
+
horizontal.grounded = grounded;
|
|
304
|
+
horizontal.verticalVelocity = 0;
|
|
305
|
+
horizontal.jumpOffset = 0;
|
|
306
|
+
[horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ] = applyHorizontalImpulses(horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, motionBatch);
|
|
307
|
+
const jumpPressed = intent.jumping && !state.controllerJumpHeld;
|
|
308
|
+
const horizontalStep = advancePlayerMotion(horizontal, { ...intent, jumping: false }, forwardX, forwardZ, walkSpeed, dt, tuning.physics);
|
|
309
|
+
horizontal.grounded = grounded;
|
|
310
|
+
horizontal.verticalVelocity = 0;
|
|
311
|
+
horizontal.jumpOffset = 0;
|
|
312
|
+
state.controllerJumpHeld = intent.jumping;
|
|
313
|
+
const current = controller.state();
|
|
314
|
+
controller.restore({
|
|
315
|
+
...current,
|
|
316
|
+
verticalVelocity: applyMotionImpulses(current.verticalVelocity, motionBatch),
|
|
317
|
+
});
|
|
318
|
+
const result = controller.move(tuning.controller.backend, {
|
|
319
|
+
motion: [horizontalStep.stepX, 0, horizontalStep.stepZ],
|
|
320
|
+
dt,
|
|
321
|
+
gravity: tuning.physics?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration,
|
|
322
|
+
...(jumpPressed && !intent.crouching
|
|
323
|
+
? { jumpVelocity: tuning.physics?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity }
|
|
324
|
+
: {}),
|
|
325
|
+
crouch: intent.crouching,
|
|
326
|
+
});
|
|
327
|
+
let nextPosition = [
|
|
328
|
+
controller.state().position[0],
|
|
329
|
+
controller.state().position[1],
|
|
330
|
+
controller.state().position[2],
|
|
331
|
+
];
|
|
332
|
+
if (motionBatch !== null && motionBatch.y !== null) {
|
|
333
|
+
nextPosition[1] = motionBatch.y;
|
|
334
|
+
controller.restore({ ...controller.state(), position: nextPosition });
|
|
335
|
+
}
|
|
336
|
+
if (tuning.movement?.beforeCommit !== undefined) {
|
|
337
|
+
const frame = {
|
|
338
|
+
entityId: playerId,
|
|
339
|
+
current: player.position,
|
|
340
|
+
next: nextPosition,
|
|
341
|
+
dt,
|
|
342
|
+
ctx,
|
|
343
|
+
};
|
|
344
|
+
const replacement = tuning.movement.beforeCommit(frame);
|
|
345
|
+
if (replacement !== undefined) {
|
|
346
|
+
nextPosition = [replacement[0], replacement[1], replacement[2]];
|
|
347
|
+
controller.restore({ ...controller.state(), position: nextPosition });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
horizontal.grounded = result.grounded;
|
|
351
|
+
ctx.scene.entity.setPose(playerId, {
|
|
352
|
+
position: nextPosition,
|
|
353
|
+
rotationY: resolveBodyFacing(state, intent.moving, horizontal.horizontalVelocityX, horizontal.horizontalVelocityZ, player.rotationY, tuning.movement?.turnSpeed, dt),
|
|
354
|
+
dt,
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
144
358
|
if (tuning.collision?.voxel === true) {
|
|
145
359
|
let body = state.voxelBody;
|
|
146
360
|
if (body === null) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GameContext } from "../runtime/gameContext.js";
|
|
2
|
+
import type { InputFrame } from "../runtime/inputSnapshot.js";
|
|
3
|
+
import { type PlayerMovementTuning } from "./playerMovement.js";
|
|
4
|
+
/** Advance one authoritative player's movement using the same controller as the shell. */
|
|
5
|
+
export declare function serverStep(ctx: GameContext, userId: string, frame: InputFrame, dt: number, tuning?: PlayerMovementTuning): void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { groundFieldFor } from "../world/terrain.js";
|
|
2
|
+
import { stepPlayerMovement } from "./playerMovement.js";
|
|
3
|
+
/** Advance one authoritative player's movement using the same controller as the shell. */
|
|
4
|
+
export function serverStep(ctx, userId, frame, dt, tuning) {
|
|
5
|
+
stepPlayerMovement(ctx, userId, frame, dt, tuning ?? { ground: groundFieldFor(), hasTerrain: false });
|
|
6
|
+
}
|
|
@@ -11,8 +11,8 @@ export interface ObstacleReach {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* The slice of the scene's object store this query needs. Narrow on purpose: the scene context wires
|
|
14
|
-
* its own store here while it is still being constructed,
|
|
15
|
-
*
|
|
14
|
+
* its own store here while it is still being constructed, `ctx.scene.object` satisfies it as-is, and a
|
|
15
|
+
* pure rules package with no `GameContext` to import can satisfy it from its own object list.
|
|
16
16
|
*/
|
|
17
17
|
export interface SolidObstacleSource {
|
|
18
18
|
list(): readonly {
|
|
@@ -27,12 +27,12 @@ export interface SolidObstacleSource {
|
|
|
27
27
|
}[];
|
|
28
28
|
collidersOf(instanceId: string): Parameters<typeof resolveColliders>[0] | null;
|
|
29
29
|
}
|
|
30
|
-
/** Mutable reach cache; one per source, invalidated by object count.
|
|
30
|
+
/** Mutable reach cache; one per source, invalidated by object count. */
|
|
31
31
|
export interface ObstacleReachCache {
|
|
32
32
|
count: number;
|
|
33
33
|
value: ObstacleReach;
|
|
34
34
|
}
|
|
35
|
-
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source.
|
|
35
|
+
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source. */
|
|
36
36
|
export declare function createObstacleReachCache(): ObstacleReachCache;
|
|
37
37
|
/**
|
|
38
38
|
* Largest blocking reach in the scene, split into horizontal vs vertical and hard-capped. Cached per
|
|
@@ -41,7 +41,7 @@ export declare function createObstacleReachCache(): ObstacleReachCache;
|
|
|
41
41
|
* @internal
|
|
42
42
|
*/
|
|
43
43
|
export declare function solidObstacleReach(ctx: GameContext): ObstacleReach;
|
|
44
|
-
/** {@link solidObstacleReach} against a bare source and caller-owned cache.
|
|
44
|
+
/** {@link solidObstacleReach} against a bare source and caller-owned cache. */
|
|
45
45
|
export declare function sourceObstacleReach(source: SolidObstacleSource, cache: ObstacleReachCache): ObstacleReach;
|
|
46
46
|
/** One blocking collider as a {@link CollisionObstacle}: a compound `boxes` obstacle for a mesh collider
|
|
47
47
|
* carrying a small sub-box set (openings), else a single half-extents/offset AABB. Yaw is applied by
|
|
@@ -61,7 +61,7 @@ export declare function obstacleFromCollider(collider: ResolvedCollider, positio
|
|
|
61
61
|
* @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
|
|
62
62
|
*/
|
|
63
63
|
export declare function solidObstaclesNear(ctx: GameContext, position: EntityPosition, reachX: number, reachZ: number, height?: number): CollisionObstacle[];
|
|
64
|
-
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach.
|
|
64
|
+
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach. */
|
|
65
65
|
export declare function sourceObstaclesNear(source: SolidObstacleSource, reach: ObstacleReach, position: EntityPosition, reachX: number, reachZ: number, height?: number): CollisionObstacle[];
|
|
66
66
|
/**
|
|
67
67
|
* Slide a walker's horizontal step against the same solid geometry that stops the player, returning the
|
|
@@ -71,6 +71,11 @@ export declare function sourceObstaclesNear(source: SolidObstacleSource, reach:
|
|
|
71
71
|
* Bounded by the same broadphase as the player's gather: one `inBox` query sized to this step, never a
|
|
72
72
|
* full-world scan, so it stays affordable per NPC per tick.
|
|
73
73
|
*
|
|
74
|
+
* The sliding is already axis-separated: X and Z are resolved independently inside, so a caller that
|
|
75
|
+
* also splits its own movement by axis must hand this exactly one axis per call (`stepZ: 0`, then
|
|
76
|
+
* `stepX: 0`). Running an axis-split pass over a step this function already resolved double-cuts it,
|
|
77
|
+
* and the composition is wrong in either order.
|
|
78
|
+
*
|
|
74
79
|
* @capability walker-solid-step slide an NPC's step against the geometry that blocks the player
|
|
75
80
|
*/
|
|
76
81
|
export declare function resolveWalkerStep(ctx: GameContext, position: EntityPosition, stepX: number, stepZ: number, options?: {
|
|
@@ -80,7 +85,25 @@ export declare function resolveWalkerStep(ctx: GameContext, position: EntityPosi
|
|
|
80
85
|
stepX: number;
|
|
81
86
|
stepZ: number;
|
|
82
87
|
};
|
|
83
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* {@link resolveWalkerStep} with no `GameContext`: gather plus slide against a bare
|
|
90
|
+
* {@link SolidObstacleSource} and a caller-owned {@link ObstacleReachCache}.
|
|
91
|
+
*
|
|
92
|
+
* This is the entry point for rules that live in a pure package — no React, no three.js, no engine
|
|
93
|
+
* context to import. Hold one cache per source for the life of that source (`createObstacleReachCache()`)
|
|
94
|
+
* rather than making a fresh one per step; it is what keeps the reach scan off the per-step path.
|
|
95
|
+
* The same axis-separation note as {@link resolveWalkerStep} applies.
|
|
96
|
+
*
|
|
97
|
+
* @capability walker-solid-step-source slide a step against solid geometry from a pure rules package
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveSourceWalkerStep(source: SolidObstacleSource, cache: ObstacleReachCache, position: EntityPosition, stepX: number, stepZ: number, options?: {
|
|
100
|
+
radius?: number;
|
|
101
|
+
stepUpHeight?: number;
|
|
102
|
+
}): {
|
|
103
|
+
stepX: number;
|
|
104
|
+
stepZ: number;
|
|
105
|
+
};
|
|
106
|
+
/** {@link resolveWalkerStep} against an already-gathered obstacle set. */
|
|
84
107
|
export declare function slideStep(position: EntityPosition, stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], radius?: number, stepUpHeight?: number): {
|
|
85
108
|
stepX: number;
|
|
86
109
|
stepZ: number;
|
|
@@ -28,7 +28,7 @@ const OBSTACLE_VERTICAL_CAP = 8;
|
|
|
28
28
|
const MOVEMENT_MESH_BOX_BUDGET = 12;
|
|
29
29
|
/** Feet-to-head span a walker is tested over — matches the player's, so both agree on what a wall is. */
|
|
30
30
|
const WALKER_HEIGHT = 1.8;
|
|
31
|
-
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source.
|
|
31
|
+
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source. */
|
|
32
32
|
export function createObstacleReachCache() {
|
|
33
33
|
return { count: -1, value: { horizontal: OBSTACLE_MAX_HALF_EXTENT, vertical: OBSTACLE_VERTICAL_FLOOR } };
|
|
34
34
|
}
|
|
@@ -42,7 +42,7 @@ const reachCache = perContext(() => createObstacleReachCache());
|
|
|
42
42
|
export function solidObstacleReach(ctx) {
|
|
43
43
|
return sourceObstacleReach(ctx.scene.object, reachCache(ctx));
|
|
44
44
|
}
|
|
45
|
-
/** {@link solidObstacleReach} against a bare source and caller-owned cache.
|
|
45
|
+
/** {@link solidObstacleReach} against a bare source and caller-owned cache. */
|
|
46
46
|
export function sourceObstacleReach(source, cache) {
|
|
47
47
|
const objects = source.list();
|
|
48
48
|
if (cache.count === objects.length)
|
|
@@ -155,7 +155,7 @@ export function obstacleFromCollider(collider, position, rotationY) {
|
|
|
155
155
|
export function solidObstaclesNear(ctx, position, reachX, reachZ, height = WALKER_HEIGHT) {
|
|
156
156
|
return sourceObstaclesNear(ctx.scene.object, solidObstacleReach(ctx), position, reachX, reachZ, height);
|
|
157
157
|
}
|
|
158
|
-
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach.
|
|
158
|
+
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach. */
|
|
159
159
|
export function sourceObstaclesNear(source, reach, position, reachX, reachZ, height = WALKER_HEIGHT) {
|
|
160
160
|
const min = [
|
|
161
161
|
position[0] - reachX - reach.horizontal,
|
|
@@ -191,6 +191,11 @@ export function sourceObstaclesNear(source, reach, position, reachX, reachZ, hei
|
|
|
191
191
|
* Bounded by the same broadphase as the player's gather: one `inBox` query sized to this step, never a
|
|
192
192
|
* full-world scan, so it stays affordable per NPC per tick.
|
|
193
193
|
*
|
|
194
|
+
* The sliding is already axis-separated: X and Z are resolved independently inside, so a caller that
|
|
195
|
+
* also splits its own movement by axis must hand this exactly one axis per call (`stepZ: 0`, then
|
|
196
|
+
* `stepX: 0`). Running an axis-split pass over a step this function already resolved double-cuts it,
|
|
197
|
+
* and the composition is wrong in either order.
|
|
198
|
+
*
|
|
194
199
|
* @capability walker-solid-step slide an NPC's step against the geometry that blocks the player
|
|
195
200
|
*/
|
|
196
201
|
export function resolveWalkerStep(ctx, position, stepX, stepZ, options = {}) {
|
|
@@ -200,7 +205,25 @@ export function resolveWalkerStep(ctx, position, stepX, stepZ, options = {}) {
|
|
|
200
205
|
const obstacles = solidObstaclesNear(ctx, position, Math.abs(stepX) + radius, Math.abs(stepZ) + radius);
|
|
201
206
|
return slideStep(position, stepX, stepZ, obstacles, radius, options.stepUpHeight ?? 0);
|
|
202
207
|
}
|
|
203
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* {@link resolveWalkerStep} with no `GameContext`: gather plus slide against a bare
|
|
210
|
+
* {@link SolidObstacleSource} and a caller-owned {@link ObstacleReachCache}.
|
|
211
|
+
*
|
|
212
|
+
* This is the entry point for rules that live in a pure package — no React, no three.js, no engine
|
|
213
|
+
* context to import. Hold one cache per source for the life of that source (`createObstacleReachCache()`)
|
|
214
|
+
* rather than making a fresh one per step; it is what keeps the reach scan off the per-step path.
|
|
215
|
+
* The same axis-separation note as {@link resolveWalkerStep} applies.
|
|
216
|
+
*
|
|
217
|
+
* @capability walker-solid-step-source slide a step against solid geometry from a pure rules package
|
|
218
|
+
*/
|
|
219
|
+
export function resolveSourceWalkerStep(source, cache, position, stepX, stepZ, options = {}) {
|
|
220
|
+
if (stepX === 0 && stepZ === 0)
|
|
221
|
+
return { stepX, stepZ };
|
|
222
|
+
const radius = options.radius ?? DEFAULT_OBSTACLE_PLAYER_RADIUS;
|
|
223
|
+
const obstacles = sourceObstaclesNear(source, sourceObstacleReach(source, cache), position, Math.abs(stepX) + radius, Math.abs(stepZ) + radius);
|
|
224
|
+
return slideStep(position, stepX, stepZ, obstacles, radius, options.stepUpHeight ?? 0);
|
|
225
|
+
}
|
|
226
|
+
/** {@link resolveWalkerStep} against an already-gathered obstacle set. */
|
|
204
227
|
export function slideStep(position, stepX, stepZ, obstacles, radius = DEFAULT_OBSTACLE_PLAYER_RADIUS, stepUpHeight = 0) {
|
|
205
228
|
if (obstacles.length === 0)
|
|
206
229
|
return { stepX, stepZ };
|
|
@@ -16,4 +16,6 @@ export interface PoseSyncTuning {
|
|
|
16
16
|
export interface PoseSyncGate {
|
|
17
17
|
evaluate(pose: PlayerPose, nowMs: number): boolean;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
/** Default ten-hertz client pose gate with a five-second heartbeat. */
|
|
20
|
+
export declare const DEFAULT_POSE_SYNC_TUNING: PoseSyncTuning;
|
|
21
|
+
export declare function createPoseSyncGate(tuning?: PoseSyncTuning): PoseSyncGate;
|
|
@@ -17,7 +17,15 @@ function poseChanged(a, b, tuning) {
|
|
|
17
17
|
|| Math.abs(a.rotationPitch - b.rotationPitch) > tuning.rotationEpsilon
|
|
18
18
|
|| appearanceChanged(a.appearance, b.appearance));
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
/** Default ten-hertz client pose gate with a five-second heartbeat. */
|
|
21
|
+
export const DEFAULT_POSE_SYNC_TUNING = {
|
|
22
|
+
minIntervalMs: 100,
|
|
23
|
+
heartbeatMs: 5_000,
|
|
24
|
+
positionEpsilon: 0.01,
|
|
25
|
+
verticalEpsilon: 0.01,
|
|
26
|
+
rotationEpsilon: 0.01,
|
|
27
|
+
};
|
|
28
|
+
export function createPoseSyncGate(tuning = DEFAULT_POSE_SYNC_TUNING) {
|
|
21
29
|
let lastSentPose = null;
|
|
22
30
|
let lastSentAt = 0;
|
|
23
31
|
return {
|
|
@@ -16,6 +16,12 @@ export interface PresencePose {
|
|
|
16
16
|
export interface PresenceSession<TGameId extends string = string> {
|
|
17
17
|
homeGameId: TGameId;
|
|
18
18
|
externalId: string;
|
|
19
|
+
viewerChunkKey?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Resident identity used to suppress offline actors while their owner is online. */
|
|
22
|
+
export interface PresenceResidentRow {
|
|
23
|
+
actorExternalId: string;
|
|
24
|
+
ownerActorId?: string;
|
|
19
25
|
}
|
|
20
26
|
export interface EnsurePresenceArgs<TGameId extends string = string> {
|
|
21
27
|
gameId: TGameId;
|
|
@@ -22,6 +22,8 @@ export interface IncomingPose {
|
|
|
22
22
|
export interface PoseSyncRules {
|
|
23
23
|
/** Speed cap (units/sec) for client-authoritative movement. */
|
|
24
24
|
maxSpeed: number;
|
|
25
|
+
/** Minimum time between accepted pose writes. Defaults to 100 ms. */
|
|
26
|
+
minIntervalMs?: number;
|
|
25
27
|
/** Vertical offset clamp above floorY (e.g. peak jump height). */
|
|
26
28
|
maxVerticalOffset: number;
|
|
27
29
|
/** World-floor Y used as the base of the jump band. Defaults to 0. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Canonical client-authoritative pose-sync tuning shared by every host transport (WS, Convex). */
|
|
2
2
|
export const DEFAULT_POSE_SYNC_RULES = {
|
|
3
3
|
maxSpeed: 12,
|
|
4
|
+
minIntervalMs: 100,
|
|
4
5
|
maxVerticalOffset: 3,
|
|
5
6
|
minElapsedSec: 0.05,
|
|
6
7
|
maxElapsedSec: 0.5,
|
|
@@ -29,6 +30,16 @@ export function spawnPresenceState(origin = DEFAULT_SPAWN_ORIGIN, nowMs, rules)
|
|
|
29
30
|
}
|
|
30
31
|
/** @internal */
|
|
31
32
|
export function decidePoseSync(current, incoming, rules, nowMs, floorY) {
|
|
33
|
+
if (current.lastSeenAtMs !== undefined && nowMs - current.lastSeenAtMs < (rules.minIntervalMs ?? 100)) {
|
|
34
|
+
return {
|
|
35
|
+
position: { ...current.position },
|
|
36
|
+
rotationY: current.rotationY,
|
|
37
|
+
rotationPitch: current.rotationPitch ?? 0,
|
|
38
|
+
appearance: current.appearance,
|
|
39
|
+
changed: false,
|
|
40
|
+
refreshKeepAlive: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
32
43
|
const rawElapsedSec = Math.max(0, (nowMs - (current.lastSeenAtMs ?? nowMs)) / 1000);
|
|
33
44
|
const elapsedSec = Math.min(rules.maxElapsedSec, rawElapsedSec);
|
|
34
45
|
const maxDist = rules.maxSpeed * elapsedSec;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Vec3 } from "../world/geometry.js";
|
|
2
|
+
/** Explicit traversable connection between two navigation polygons. */
|
|
3
|
+
export interface NavMeshLink {
|
|
4
|
+
from: number;
|
|
5
|
+
to: number;
|
|
6
|
+
cost?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Serializable polygon navigation mesh data. */
|
|
9
|
+
export interface NavMeshData {
|
|
10
|
+
/** Flat xyz vertex coordinates. Polygon indices refer to triples in this array. */
|
|
11
|
+
verts: number[];
|
|
12
|
+
polys: number[][];
|
|
13
|
+
links: NavMeshLink[];
|
|
14
|
+
areas?: number[];
|
|
15
|
+
}
|
|
16
|
+
/** Neighbor relationship for one navigation polygon. */
|
|
17
|
+
export interface NavMeshAdjacency {
|
|
18
|
+
neighbors: number[];
|
|
19
|
+
cost: number;
|
|
20
|
+
}
|
|
21
|
+
/** Route points and polygons selected through a navigation mesh. */
|
|
22
|
+
export interface NavMeshPath {
|
|
23
|
+
points: Vec3[];
|
|
24
|
+
polys: number[];
|
|
25
|
+
}
|
|
26
|
+
/** Build polygon adjacency from shared edges and explicit off-mesh links. */
|
|
27
|
+
export declare function buildNavAdjacency(mesh: NavMeshData): NavMeshAdjacency[];
|
|
28
|
+
/** Return the closest point on the mesh surface, or null for an empty mesh. */
|
|
29
|
+
export declare function closestPoint(mesh: NavMeshData, point: Vec3): Vec3 | null;
|
|
30
|
+
/** A* over polygon centers, followed by deterministic visibility string-pulling. */
|
|
31
|
+
export declare function findPath(mesh: NavMeshData, from: Vec3, to: Vec3): NavMeshPath | null;
|
|
32
|
+
/** True when the segment remains over walkable polygons. */
|
|
33
|
+
export declare function raycastNav(mesh: NavMeshData, from: Vec3, to: Vec3): boolean;
|