@jgengine/core 0.18.1 → 0.19.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/CHANGELOG.md +158 -7
- package/dist/ai/decisionGraph.d.ts +48 -4
- package/dist/ai/decisionGraph.js +153 -23
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +11 -0
- package/dist/anim/animGraph.js +128 -0
- package/dist/anim/boneTexture.d.ts +55 -0
- package/dist/anim/boneTexture.js +43 -0
- package/dist/anim/footPlacement.d.ts +57 -0
- package/dist/anim/footPlacement.js +67 -0
- package/dist/anim/locomotionGraph.d.ts +9 -0
- package/dist/anim/locomotionGraph.js +29 -0
- package/dist/audio/audioFalloff.d.ts +2 -0
- package/dist/audio/doppler.d.ts +21 -0
- package/dist/audio/doppler.js +32 -0
- package/dist/audio/engineLayers.d.ts +83 -0
- package/dist/audio/engineLayers.js +115 -0
- package/dist/combat/abilityKit.d.ts +1 -0
- package/dist/combat/abilityKit.js +1 -0
- package/dist/combat/death.d.ts +7 -0
- package/dist/combat/shotOrigin.d.ts +6 -0
- package/dist/combat/shotOrigin.js +6 -0
- package/dist/combat/weaponFeedback.d.ts +36 -0
- package/dist/combat/weaponFeedback.js +42 -0
- package/dist/combat/weaponHandling.d.ts +161 -0
- package/dist/combat/weaponHandling.js +202 -0
- package/dist/combat/weaponPresentation.d.ts +115 -0
- package/dist/combat/weaponPresentation.js +96 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/editor/document.js +3 -1
- package/dist/editor/environment.js +8 -0
- package/dist/editor/types.d.ts +4 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +51 -1
- package/dist/game/events.d.ts +6 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/playableGame.d.ts +84 -17
- package/dist/game/toasts.d.ts +8 -0
- package/dist/game/toasts.js +7 -0
- package/dist/game/vfxInstance.d.ts +9 -0
- package/dist/game/vfxInstance.js +12 -1
- package/dist/game/viewports.d.ts +43 -0
- package/dist/game/viewports.js +46 -0
- package/dist/game/worldItem.d.ts +5 -1
- package/dist/game/worldItem.js +0 -1
- package/dist/input/actionBindings.d.ts +15 -1
- package/dist/input/actionBindings.js +14 -3
- package/dist/input/actionContexts.d.ts +25 -1
- package/dist/input/actionContexts.js +82 -25
- package/dist/input/axisInput.d.ts +4 -0
- package/dist/input/axisInput.js +4 -0
- package/dist/input/axisShaper.d.ts +73 -0
- package/dist/input/axisShaper.js +105 -0
- package/dist/input/gamepadModel.d.ts +36 -2
- package/dist/input/gamepadModel.js +69 -30
- package/dist/input/haptics.d.ts +50 -0
- package/dist/input/haptics.js +74 -0
- package/dist/input/inputBuffer.d.ts +40 -0
- package/dist/input/inputBuffer.js +59 -0
- package/dist/input/lookChannel.d.ts +10 -0
- package/dist/input/lookChannel.js +6 -0
- package/dist/input/pointer.d.ts +2 -0
- package/dist/input/pointer.js +4 -0
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +112 -1
- package/dist/movement/glideModel.d.ts +3 -0
- package/dist/movement/glideModel.js +6 -0
- package/dist/movement/grappleSwing.d.ts +2 -0
- package/dist/movement/grappleSwing.js +5 -1
- package/dist/movement/movementModel.d.ts +39 -0
- package/dist/movement/movementModel.js +62 -19
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +21 -2
- package/dist/movement/playerMovement.js +89 -6
- package/dist/movement/solidObstacles.d.ts +9 -1
- package/dist/movement/solidObstacles.js +44 -2
- package/dist/movement/voxelController.js +7 -7
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/boatDynamics.d.ts +116 -0
- package/dist/physics/boatDynamics.js +160 -0
- package/dist/physics/drivableVehicle.d.ts +22 -8
- package/dist/physics/drivableVehicle.js +4 -2
- package/dist/physics/flightDynamics.d.ts +21 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +32 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +12 -0
- package/dist/physics/physicsBackendConformance.js +34 -0
- package/dist/physics/physicsWorldBackend.d.ts +1 -0
- package/dist/physics/physicsWorldBackend.js +33 -0
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +374 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/worldColliders.d.ts +1 -1
- package/dist/physics/worldColliders.js +35 -3
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/render/lookPreset.d.ts +9 -2
- package/dist/render/lookPreset.js +29 -2
- package/dist/render/postProcessing.d.ts +30 -0
- package/dist/render/postProcessing.js +1 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +37 -0
- package/dist/runtime/commandInput.js +58 -0
- package/dist/runtime/commandRunner.d.ts +27 -0
- package/dist/runtime/commandRunner.js +31 -3
- package/dist/runtime/context/combat.d.ts +1 -0
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/deathLoot.d.ts +2 -0
- package/dist/runtime/context/deathLoot.js +1 -0
- package/dist/runtime/gameContext.js +7 -0
- package/dist/runtime/gameContextTypes.d.ts +19 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/inputSnapshot.d.ts +6 -0
- package/dist/runtime/inputSnapshot.js +10 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/scene/behaviorRuntime.d.ts +12 -2
- package/dist/scene/behaviorRuntime.js +30 -14
- package/dist/scene/behaviors.d.ts +5 -1
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/vehicleSeat.d.ts +5 -1
- package/dist/scene/vehicleSeat.js +5 -1
- package/dist/sensor/concealment.d.ts +6 -0
- package/dist/sensor/concealment.js +8 -0
- package/dist/sensor/freezeMonitor.d.ts +6 -0
- package/dist/sensor/freezeMonitor.js +8 -0
- package/dist/sensor/perception.d.ts +11 -1
- package/dist/sensor/perception.js +57 -10
- package/dist/sensor/recordingBuffer.d.ts +3 -0
- package/dist/sensor/recordingBuffer.js +8 -0
- package/dist/session/roundState.d.ts +4 -0
- package/dist/session/roundState.js +11 -0
- package/dist/settings/frameRateLimit.d.ts +16 -0
- package/dist/settings/frameRateLimit.js +37 -0
- package/dist/settings/graphicsProfile.d.ts +2 -0
- package/dist/settings/graphicsProfile.js +12 -0
- package/dist/settings/graphicsSettings.d.ts +40 -0
- package/dist/settings/graphicsSettings.js +81 -0
- package/dist/settings/settingsModel.d.ts +9 -1
- package/dist/settings/settingsModel.js +10 -0
- package/dist/stats/eventMeter.d.ts +1 -1
- package/dist/stats/eventMeter.js +1 -1
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/visibility/simulationCulling.d.ts +8 -1
- package/dist/visibility/simulationCulling.js +7 -0
- package/dist/visibility/spatialIndex.d.ts +21 -0
- package/dist/visibility/spatialIndex.js +16 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +3 -1
- package/dist/world/buildings.d.ts +34 -2
- package/dist/world/buildings.js +11 -1
- package/dist/world/cityKind.d.ts +7 -0
- package/dist/world/cityKind.js +52 -1
- package/dist/world/features.d.ts +29 -2
- package/dist/world/features.js +6 -0
- package/dist/world/footprintGrid.d.ts +7 -0
- package/dist/world/footprintGrid.js +16 -0
- package/dist/world/interiors.d.ts +1 -0
- package/dist/world/interiors.js +5 -3
- package/dist/world/lod.d.ts +6 -0
- package/dist/world/lod.js +8 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/worldSolids.d.ts +84 -0
- package/dist/world/worldSolids.js +240 -0
- package/dist/world.d.ts +2 -2
- package/dist/world.js +1 -1
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +7 -5
- package/skills/jgengine/api.md +118 -89
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-combat/SKILL.md +5 -0
- package/skills/jgengine-combat/api.md +138 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-gameplay/api.md +462 -436
- package/skills/jgengine-gameplay/capabilities.md +24 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +15 -3
- package/skills/jgengine-world/api.md +873 -626
- package/skills/jgengine-world/capabilities.md +130 -6
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
|
@@ -2,11 +2,14 @@ import type { PhysicsConfig } from "../game/defineGame.js";
|
|
|
2
2
|
import type { PlayerMovementConfig, VoxelCollisionConfig } from "../game/playableGame.js";
|
|
3
3
|
import type { GameContext } from "../runtime/gameContext.js";
|
|
4
4
|
import type { InputFrame } from "../runtime/inputSnapshot.js";
|
|
5
|
-
import { type
|
|
5
|
+
import { type InputBufferSnapshot } from "../input/inputBuffer.js";
|
|
6
|
+
import { type CharacterControllerConfig, type CharacterControllerState } from "./characterController.js";
|
|
6
7
|
import { type TerrainField } from "../world/terrain.js";
|
|
7
8
|
import type { WorldFeature } from "../world/features.js";
|
|
8
9
|
import type { PhysicsBackend } from "../physics/physicsBackend.js";
|
|
9
|
-
import { type MovementTuningOverrides } from "./movementModel.js";
|
|
10
|
+
import { type MovementTuningOverrides, type PlayerMotionState } from "./movementModel.js";
|
|
11
|
+
import { type FreeFlightState } from "./freeFlight.js";
|
|
12
|
+
import { type VoxelPlayerBody } from "./voxelController.js";
|
|
10
13
|
/** The resolved, per-world movement configuration {@link stepPlayerMovement} integrates against — the same inputs the shell FrameDriver used to read piecemeal, gathered into one struct so single-player and host movement run identical math. */
|
|
11
14
|
export interface PlayerMovementTuning {
|
|
12
15
|
collision?: VoxelCollisionConfig;
|
|
@@ -35,6 +38,22 @@ export declare function resolvePlayerMovementTuning(opts: {
|
|
|
35
38
|
}): PlayerMovementTuning;
|
|
36
39
|
/** One player's current heading (radians), integrated by {@link stepPlayerMovement} — the shell reads it back into its camera/aim yaw. */
|
|
37
40
|
export declare function playerMovementHeading(ctx: GameContext, userId: string): number;
|
|
41
|
+
/** One player's serializable movement state: heading, facing, velocities, jump latch and controller capsule. The entity pose lives in the entity store. */
|
|
42
|
+
export interface PlayerMovementSnapshot {
|
|
43
|
+
heading: number;
|
|
44
|
+
facing: number | null;
|
|
45
|
+
voxelBody: VoxelPlayerBody | null;
|
|
46
|
+
motion: PlayerMotionState | null;
|
|
47
|
+
flight: FreeFlightState | null;
|
|
48
|
+
controller: CharacterControllerState | null;
|
|
49
|
+
controllerJumpHeld: boolean;
|
|
50
|
+
/** Buffered jump presses; absent in snapshots taken before jump buffering existed. */
|
|
51
|
+
jumpBuffer?: InputBufferSnapshot | null;
|
|
52
|
+
}
|
|
53
|
+
/** Copy one player's movement state for prediction, rollback or a save; `null` when the player has not moved yet. */
|
|
54
|
+
export declare function snapshotPlayerMovement(ctx: GameContext, userId: string): PlayerMovementSnapshot | null;
|
|
55
|
+
/** Put a player's movement state back to a {@link snapshotPlayerMovement} copy, so the next {@link stepPlayerMovement} replays from there. */
|
|
56
|
+
export declare function restorePlayerMovement(ctx: GameContext, userId: string, snapshot: PlayerMovementSnapshot): void;
|
|
38
57
|
/** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
|
|
39
58
|
export declare function forgetPlayerMovement(ctx: GameContext, userId: string): void;
|
|
40
59
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { createInputBuffer } from "../input/inputBuffer.js";
|
|
1
2
|
import { applyHorizontalImpulses, applyMotionImpulses } from "../runtime/motionIntents.js";
|
|
2
|
-
import { createCharacterController } from "./characterController.js";
|
|
3
|
+
import { createCharacterController, } from "./characterController.js";
|
|
3
4
|
import { groundFieldFor, hasEnvironmentTerrain, sampleSlope } from "../world/terrain.js";
|
|
4
5
|
import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, DEFAULT_OBSTACLE_PLAYER_RADIUS, obstacleSupportHeight, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, MOVEMENT_TUNING, } from "./movementModel.js";
|
|
5
6
|
import { advanceFreeFlight, createFreeFlightState, resolveFlightStep, resolveFreeFlightIntentFromInput, } from "./freeFlight.js";
|
|
@@ -36,7 +37,24 @@ export function resolvePhysicsTuning(physics) {
|
|
|
36
37
|
export function resolvePlayerMovementTuning(opts) {
|
|
37
38
|
const physics = resolvePhysicsTuning(opts.physics);
|
|
38
39
|
const backpedal = opts.movement?.backpedalMult;
|
|
39
|
-
const
|
|
40
|
+
const feel = opts.movement?.feel;
|
|
41
|
+
const feelOverrides = {
|
|
42
|
+
...(backpedal === undefined ? {} : { backpedalSpeedMultiplier: backpedal }),
|
|
43
|
+
...(feel?.groundAcceleration === undefined ? {} : { groundAcceleration: feel.groundAcceleration }),
|
|
44
|
+
...(feel?.airAcceleration === undefined ? {} : { airAcceleration: feel.airAcceleration }),
|
|
45
|
+
...(feel?.groundFriction === undefined ? {} : { groundFriction: feel.groundFriction }),
|
|
46
|
+
...(feel?.runMultiplier === undefined ? {} : { runSpeedMultiplier: feel.runMultiplier }),
|
|
47
|
+
...(feel?.crouchMultiplier === undefined ? {} : { crouchSpeedMultiplier: feel.crouchMultiplier }),
|
|
48
|
+
...(feel?.jumpBufferMs === undefined ? {} : { jumpBufferMs: feel.jumpBufferMs }),
|
|
49
|
+
...(feel?.coyoteMs === undefined ? {} : { coyoteMs: feel.coyoteMs }),
|
|
50
|
+
...(feel?.jumpCutFactor === undefined ? {} : { jumpCutFactor: feel.jumpCutFactor }),
|
|
51
|
+
...(feel?.apexGravityScale === undefined ? {} : { apexGravityScale: feel.apexGravityScale }),
|
|
52
|
+
...(feel?.apexSpeed === undefined ? {} : { apexSpeed: feel.apexSpeed }),
|
|
53
|
+
...(feel?.fallGravityScale === undefined ? {} : { fallGravityScale: feel.fallGravityScale }),
|
|
54
|
+
...(feel?.landingRecoveryMs === undefined ? {} : { landingRecoveryMs: feel.landingRecoveryMs }),
|
|
55
|
+
...(feel?.landingSpeedScale === undefined ? {} : { landingSpeedScale: feel.landingSpeedScale }),
|
|
56
|
+
};
|
|
57
|
+
const overrides = Object.keys(feelOverrides).length === 0 ? physics : { ...(physics ?? {}), ...feelOverrides };
|
|
40
58
|
return {
|
|
41
59
|
...(opts.collision === undefined ? {} : { collision: opts.collision }),
|
|
42
60
|
...(opts.movement === undefined ? {} : { movement: opts.movement }),
|
|
@@ -72,7 +90,17 @@ function storeFor(ctx) {
|
|
|
72
90
|
function stateFor(store, userId) {
|
|
73
91
|
let state = store.players.get(userId);
|
|
74
92
|
if (state === undefined) {
|
|
75
|
-
state = {
|
|
93
|
+
state = {
|
|
94
|
+
heading: 0,
|
|
95
|
+
facing: null,
|
|
96
|
+
voxelBody: null,
|
|
97
|
+
motion: null,
|
|
98
|
+
flight: null,
|
|
99
|
+
controller: null,
|
|
100
|
+
controllerJumpHeld: false,
|
|
101
|
+
jumpBuffer: null,
|
|
102
|
+
pendingController: null,
|
|
103
|
+
};
|
|
76
104
|
store.players.set(userId, state);
|
|
77
105
|
}
|
|
78
106
|
return state;
|
|
@@ -81,6 +109,54 @@ function stateFor(store, userId) {
|
|
|
81
109
|
export function playerMovementHeading(ctx, userId) {
|
|
82
110
|
return stores.get(ctx)?.players.get(userId)?.heading ?? 0;
|
|
83
111
|
}
|
|
112
|
+
function copyController(state) {
|
|
113
|
+
return { ...state, position: [...state.position], groundNormal: [...state.groundNormal] };
|
|
114
|
+
}
|
|
115
|
+
/** Copy one player's movement state for prediction, rollback or a save; `null` when the player has not moved yet. */
|
|
116
|
+
export function snapshotPlayerMovement(ctx, userId) {
|
|
117
|
+
const state = stores.get(ctx)?.players.get(userId);
|
|
118
|
+
if (state === undefined)
|
|
119
|
+
return null;
|
|
120
|
+
const controller = state.controller?.snapshot() ?? state.pendingController;
|
|
121
|
+
return {
|
|
122
|
+
heading: state.heading,
|
|
123
|
+
facing: state.facing,
|
|
124
|
+
voxelBody: state.voxelBody === null ? null : { ...state.voxelBody },
|
|
125
|
+
motion: state.motion === null ? null : { ...state.motion },
|
|
126
|
+
flight: state.flight === null ? null : { ...state.flight },
|
|
127
|
+
controller: controller === null ? null : copyController(controller),
|
|
128
|
+
controllerJumpHeld: state.controllerJumpHeld,
|
|
129
|
+
jumpBuffer: state.jumpBuffer?.snapshot() ?? null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Put a player's movement state back to a {@link snapshotPlayerMovement} copy, so the next {@link stepPlayerMovement} replays from there. */
|
|
133
|
+
export function restorePlayerMovement(ctx, userId, snapshot) {
|
|
134
|
+
const state = stateFor(storeFor(ctx), userId);
|
|
135
|
+
state.heading = snapshot.heading;
|
|
136
|
+
state.facing = snapshot.facing;
|
|
137
|
+
state.voxelBody = snapshot.voxelBody === null ? null : { ...snapshot.voxelBody };
|
|
138
|
+
state.motion = snapshot.motion === null ? null : { ...snapshot.motion };
|
|
139
|
+
state.flight = snapshot.flight === null ? null : { ...snapshot.flight };
|
|
140
|
+
state.controllerJumpHeld = snapshot.controllerJumpHeld;
|
|
141
|
+
const jumpBuffer = snapshot.jumpBuffer ?? null;
|
|
142
|
+
if (jumpBuffer === null) {
|
|
143
|
+
state.jumpBuffer = null;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
state.jumpBuffer ??= createInputBuffer({ windowMs: jumpBuffer.windowMs });
|
|
147
|
+
state.jumpBuffer.restore(jumpBuffer);
|
|
148
|
+
}
|
|
149
|
+
const controller = snapshot.controller === null ? null : copyController(snapshot.controller);
|
|
150
|
+
if (state.controller !== null && controller !== null) {
|
|
151
|
+
state.controller.restore(controller);
|
|
152
|
+
state.pendingController = null;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (controller === null)
|
|
156
|
+
state.controller = null;
|
|
157
|
+
state.pendingController = controller;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
84
160
|
/** Drop a player's retained movement state (heading + kinematic body) — call on leave so a rejoin starts fresh instead of resuming stale velocity. */
|
|
85
161
|
export function forgetPlayerMovement(ctx, userId) {
|
|
86
162
|
stores.get(ctx)?.players.delete(userId);
|
|
@@ -275,7 +351,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
275
351
|
if (controller === null) {
|
|
276
352
|
controller = createCharacterController(tuning.controller.capsule);
|
|
277
353
|
state.controller = controller;
|
|
278
|
-
controller.restore({
|
|
354
|
+
controller.restore(state.pendingController ?? {
|
|
279
355
|
position: [player.position[0], player.position[1], player.position[2]],
|
|
280
356
|
verticalVelocity: 0,
|
|
281
357
|
grounded: true,
|
|
@@ -283,6 +359,7 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
283
359
|
groundBody: null,
|
|
284
360
|
crouching: false,
|
|
285
361
|
});
|
|
362
|
+
state.pendingController = null;
|
|
286
363
|
}
|
|
287
364
|
const controllerState = controller.state();
|
|
288
365
|
if (Math.hypot(controllerState.position[0] - player.position[0], controllerState.position[1] - player.position[1], controllerState.position[2] - player.position[2]) > 1e-6) {
|
|
@@ -401,9 +478,15 @@ export function stepPlayerMovement(ctx, userId, input, dt, tuning, heading, pitc
|
|
|
401
478
|
const submerged = swimEnabled &&
|
|
402
479
|
waterLevel !== undefined &&
|
|
403
480
|
tuning.ground.sampleHeight(player.position[0], player.position[2]) < waterLevel;
|
|
481
|
+
const jumpBufferMs = tuning.physics?.jumpBufferMs ?? 0;
|
|
482
|
+
let jumpBuffer = state.jumpBuffer;
|
|
483
|
+
if (jumpBuffer === null) {
|
|
484
|
+
jumpBuffer = createInputBuffer({ windowMs: jumpBufferMs });
|
|
485
|
+
state.jumpBuffer = jumpBuffer;
|
|
486
|
+
}
|
|
404
487
|
const motionOptions = submerged
|
|
405
|
-
? { speedScale: swimSpeedMultiplier, floating: true }
|
|
406
|
-
:
|
|
488
|
+
? { speedScale: swimSpeedMultiplier, floating: true, buffer: jumpBuffer }
|
|
489
|
+
: { buffer: jumpBuffer };
|
|
407
490
|
const prevJumpOffset = motion.jumpOffset;
|
|
408
491
|
const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, walkSpeed, dt, tuning.physics, motionOptions);
|
|
409
492
|
// Airborne means "not resting on the surface below": any jump/impulse height before or after this
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GameContext } from "../runtime/gameContext.js";
|
|
2
2
|
import { resolveColliders, type ResolvedCollider } from "../scene/colliders.js";
|
|
3
3
|
import type { EntityPosition } from "../scene/entityStore.js";
|
|
4
|
+
import type { WorldSolid } from "../world/worldSolids.js";
|
|
4
5
|
import { type CollisionObstacle } from "./movementModel.js";
|
|
5
6
|
/** Largest blocking-collider extent in the scene, split so tower height cannot inflate the XZ query. */
|
|
6
7
|
export interface ObstacleReach {
|
|
@@ -49,9 +50,16 @@ export declare function sourceObstacleReach(source: SolidObstacleSource, cache:
|
|
|
49
50
|
* never under-covers.
|
|
50
51
|
* @internal */
|
|
51
52
|
export declare function obstacleFromCollider(collider: ResolvedCollider, position: EntityPosition, rotationY: number): CollisionObstacle;
|
|
53
|
+
/**
|
|
54
|
+
* A {@link WorldSolid} as a {@link CollisionObstacle}. Axis-aligned solids are one box; a yawed solid is
|
|
55
|
+
* cut into strips along its long axis, each fitted with an AABB, so a turned building does not block
|
|
56
|
+
* the street beside it the way a single enclosing box would.
|
|
57
|
+
* @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
|
|
58
|
+
*/
|
|
59
|
+
export declare function obstacleFromSolid(solid: WorldSolid): CollisionObstacle;
|
|
52
60
|
/**
|
|
53
61
|
* Every blocking physical obstacle overlapping the box `position` ± `reachX`/`reachZ`, read from the
|
|
54
|
-
* scene's resolved colliders.
|
|
62
|
+
* scene's resolved colliders and `ctx.world.solids`.
|
|
55
63
|
*
|
|
56
64
|
* This is the engine's single answer to "what is solid here". The player resolver, a walking NPC, and
|
|
57
65
|
* anything else that has to stop at a wall read it rather than deriving their own set — three
|
|
@@ -26,6 +26,10 @@ const OBSTACLE_VERTICAL_CAP = 8;
|
|
|
26
26
|
* multiply narrowphase cost without changing the blocked footprint.
|
|
27
27
|
*/
|
|
28
28
|
const MOVEMENT_MESH_BOX_BUDGET = 12;
|
|
29
|
+
/** Most AABB strips an oriented world solid is sliced into, bounding the corner overhang a yawed box leaves. */
|
|
30
|
+
const SOLID_MAX_STRIPS = 8;
|
|
31
|
+
/** Yaw within this of a quarter turn is treated as axis-aligned (radians). */
|
|
32
|
+
const AXIS_ALIGNED_EPSILON = 1e-4;
|
|
29
33
|
/** Feet-to-head span a walker is tested over — matches the player's, so both agree on what a wall is. */
|
|
30
34
|
const WALKER_HEIGHT = 1.8;
|
|
31
35
|
/** A fresh {@link ObstacleReachCache} for a caller that owns its own source. */
|
|
@@ -141,9 +145,40 @@ export function obstacleFromCollider(collider, position, rotationY) {
|
|
|
141
145
|
halfExtents: [Math.abs(hx * cos) + Math.abs(hz * sin), hy, Math.abs(hx * sin) + Math.abs(hz * cos)],
|
|
142
146
|
};
|
|
143
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* A {@link WorldSolid} as a {@link CollisionObstacle}. Axis-aligned solids are one box; a yawed solid is
|
|
150
|
+
* cut into strips along its long axis, each fitted with an AABB, so a turned building does not block
|
|
151
|
+
* the street beside it the way a single enclosing box would.
|
|
152
|
+
* @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
|
|
153
|
+
*/
|
|
154
|
+
export function obstacleFromSolid(solid) {
|
|
155
|
+
const [hx, hy, hz] = solid.halfExtents;
|
|
156
|
+
const yaw = solid.rotationY ?? 0;
|
|
157
|
+
const quarter = yaw / (Math.PI / 2);
|
|
158
|
+
const nearest = Math.round(quarter);
|
|
159
|
+
if (Math.abs(quarter - nearest) < AXIS_ALIGNED_EPSILON) {
|
|
160
|
+
const swapped = Math.abs(nearest) % 2 === 1;
|
|
161
|
+
return { position: solid.center, halfExtents: swapped ? [hz, hy, hx] : [hx, hy, hz] };
|
|
162
|
+
}
|
|
163
|
+
const alongX = hx >= hz;
|
|
164
|
+
const long = alongX ? hx : hz;
|
|
165
|
+
const short = Math.max(alongX ? hz : hx, 1e-6);
|
|
166
|
+
const strips = Math.min(SOLID_MAX_STRIPS, Math.max(1, Math.ceil(long / short)));
|
|
167
|
+
const cos = Math.cos(yaw);
|
|
168
|
+
const sin = Math.sin(yaw);
|
|
169
|
+
const boxes = [];
|
|
170
|
+
for (let i = 0; i < strips; i += 1) {
|
|
171
|
+
const from = -long + (2 * long * i) / strips;
|
|
172
|
+
const to = from + (2 * long) / strips;
|
|
173
|
+
const boxMin = alongX ? [from, -hy, -hz] : [-hx, -hy, from];
|
|
174
|
+
const boxMax = alongX ? [to, hy, hz] : [hx, hy, to];
|
|
175
|
+
boxes.push(yawExpandLocalBox(boxMin, boxMax, cos, sin));
|
|
176
|
+
}
|
|
177
|
+
return { position: solid.center, boxes };
|
|
178
|
+
}
|
|
144
179
|
/**
|
|
145
180
|
* Every blocking physical obstacle overlapping the box `position` ± `reachX`/`reachZ`, read from the
|
|
146
|
-
* scene's resolved colliders.
|
|
181
|
+
* scene's resolved colliders and `ctx.world.solids`.
|
|
147
182
|
*
|
|
148
183
|
* This is the engine's single answer to "what is solid here". The player resolver, a walking NPC, and
|
|
149
184
|
* anything else that has to stop at a wall read it rather than deriving their own set — three
|
|
@@ -153,7 +188,14 @@ export function obstacleFromCollider(collider, position, rotationY) {
|
|
|
153
188
|
* @capability solid-obstacles the one blocking-geometry query the player resolver and NPC movers share
|
|
154
189
|
*/
|
|
155
190
|
export function solidObstaclesNear(ctx, position, reachX, reachZ, height = WALKER_HEIGHT) {
|
|
156
|
-
|
|
191
|
+
const obstacles = sourceObstaclesNear(ctx.scene.object, solidObstacleReach(ctx), position, reachX, reachZ, height);
|
|
192
|
+
const solids = ctx.world.solids;
|
|
193
|
+
if (solids.count() === 0)
|
|
194
|
+
return obstacles;
|
|
195
|
+
for (const solid of solids.inBox([position[0] - reachX, position[1] - height, position[2] - reachZ], [position[0] + reachX, position[1] + 2 * height, position[2] + reachZ])) {
|
|
196
|
+
obstacles.push(obstacleFromSolid(solid));
|
|
197
|
+
}
|
|
198
|
+
return obstacles;
|
|
157
199
|
}
|
|
158
200
|
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach. */
|
|
159
201
|
export function sourceObstaclesNear(source, reach, position, reachX, reachZ, height = WALKER_HEIGHT) {
|
|
@@ -97,7 +97,7 @@ function supportTopWithin(x, z, fromY, toY, isSolid, dims) {
|
|
|
97
97
|
}
|
|
98
98
|
return best;
|
|
99
99
|
}
|
|
100
|
-
function targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed) {
|
|
100
|
+
function targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed, tuning) {
|
|
101
101
|
if (!intent.moving)
|
|
102
102
|
return { x: 0, z: 0 };
|
|
103
103
|
let fx = forwardX;
|
|
@@ -120,9 +120,9 @@ function targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed) {
|
|
|
120
120
|
if (magSq < 1e-6)
|
|
121
121
|
return { x: 0, z: 0 };
|
|
122
122
|
const speedMultiplier = intent.crouching
|
|
123
|
-
? MOVEMENT_TUNING.crouchSpeedMultiplier
|
|
123
|
+
? (tuning?.crouchSpeedMultiplier ?? MOVEMENT_TUNING.crouchSpeedMultiplier)
|
|
124
124
|
: intent.running
|
|
125
|
-
? MOVEMENT_TUNING.runSpeedMultiplier
|
|
125
|
+
? (tuning?.runSpeedMultiplier ?? MOVEMENT_TUNING.runSpeedMultiplier)
|
|
126
126
|
: 1;
|
|
127
127
|
const targetSpeed = baseSpeed * MOVEMENT_TUNING.walkSpeedMultiplier * speedMultiplier;
|
|
128
128
|
const scale = targetSpeed / Math.sqrt(magSq);
|
|
@@ -176,15 +176,15 @@ export function advanceVoxelPlayer(body, intent, forwardX, forwardZ, baseSpeed,
|
|
|
176
176
|
const dt = Math.min(rawDeltaSeconds, MOVEMENT_TUNING.maxFrameSeconds);
|
|
177
177
|
const gravityAcceleration = tuning?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration;
|
|
178
178
|
const jumpVelocity = tuning?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity;
|
|
179
|
-
const target = targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed);
|
|
179
|
+
const target = targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed, tuning);
|
|
180
180
|
const acceleration = body.grounded
|
|
181
|
-
? MOVEMENT_TUNING.groundAcceleration
|
|
182
|
-
: MOVEMENT_TUNING.airAcceleration;
|
|
181
|
+
? (tuning?.groundAcceleration ?? MOVEMENT_TUNING.groundAcceleration)
|
|
182
|
+
: (tuning?.airAcceleration ?? MOVEMENT_TUNING.airAcceleration);
|
|
183
183
|
const accelerationBlend = 1 - Math.exp(-acceleration * dt);
|
|
184
184
|
body.velocityX += (target.x - body.velocityX) * accelerationBlend;
|
|
185
185
|
body.velocityZ += (target.z - body.velocityZ) * accelerationBlend;
|
|
186
186
|
if (!intent.moving && body.grounded) {
|
|
187
|
-
const friction = Math.exp(-MOVEMENT_TUNING.groundFriction * dt);
|
|
187
|
+
const friction = Math.exp(-(tuning?.groundFriction ?? MOVEMENT_TUNING.groundFriction) * dt);
|
|
188
188
|
body.velocityX *= friction;
|
|
189
189
|
body.velocityZ *= friction;
|
|
190
190
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { EnvironmentWorldFeature } from "../world/features.js";
|
|
2
2
|
import type { Aabb } from "../world/geometry.js";
|
|
3
|
+
import { type WorldSolids } from "../world/worldSolids.js";
|
|
3
4
|
export interface NavObstacleGrid {
|
|
4
5
|
blockAabb(aabb: Aabb): void;
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* number of buildings blocked.
|
|
8
|
+
* Blocks the footprint of every solid building an environment feature's `structures` generate — the
|
|
9
|
+
* same set `ctx.world.solids` holds for it. Returns the number of buildings blocked.
|
|
10
10
|
*/
|
|
11
11
|
export declare function populateNavGridFromEnvironment(grid: NavObstacleGrid, world: EnvironmentWorldFeature): number;
|
|
12
|
+
/**
|
|
13
|
+
* Blocks the XZ footprint of every solid in `solids` (usually `ctx.world.solids`) on `grid`, so NPC
|
|
14
|
+
* paths avoid exactly what movement and physics collide with. Returns the number of solids blocked.
|
|
15
|
+
* @capability nav-from-solids block world solids on a nav grid
|
|
16
|
+
*/
|
|
17
|
+
export declare function populateNavGridFromSolids(grid: NavObstacleGrid, solids: Pick<WorldSolids, "all">): number;
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { structureSolids, worldSolidFootprint } from "../world/worldSolids.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* number of buildings blocked.
|
|
3
|
+
* Blocks the footprint of every solid building an environment feature's `structures` generate — the
|
|
4
|
+
* same set `ctx.world.solids` holds for it. Returns the number of buildings blocked.
|
|
6
5
|
*/
|
|
7
6
|
export function populateNavGridFromEnvironment(grid, world) {
|
|
8
|
-
|
|
9
|
-
for (const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
const solids = structureSolids(world);
|
|
8
|
+
for (const solid of solids)
|
|
9
|
+
grid.blockAabb(worldSolidFootprint(solid));
|
|
10
|
+
return solids.length;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Blocks the XZ footprint of every solid in `solids` (usually `ctx.world.solids`) on `grid`, so NPC
|
|
14
|
+
* paths avoid exactly what movement and physics collide with. Returns the number of solids blocked.
|
|
15
|
+
* @capability nav-from-solids block world solids on a nav grid
|
|
16
|
+
*/
|
|
17
|
+
export function populateNavGridFromSolids(grid, solids) {
|
|
18
|
+
const all = solids.all();
|
|
19
|
+
for (const solid of all)
|
|
20
|
+
grid.blockAabb(worldSolidFootprint(solid));
|
|
21
|
+
return all.length;
|
|
16
22
|
}
|
package/dist/nav/navMesh.d.ts
CHANGED
|
@@ -4,13 +4,19 @@ export interface NavMeshLink {
|
|
|
4
4
|
from: number;
|
|
5
5
|
to: number;
|
|
6
6
|
cost?: number;
|
|
7
|
+
/** Where the link leaves `from`; defaults to that polygon's center. */
|
|
8
|
+
start?: Vec3;
|
|
9
|
+
/** Where the link lands in `to`; defaults to that polygon's center. */
|
|
10
|
+
end?: Vec3;
|
|
7
11
|
}
|
|
8
|
-
/** Serializable polygon navigation mesh data. */
|
|
12
|
+
/** Serializable polygon navigation mesh data. Treat it as immutable once queried; queries cache derived geometry per object. */
|
|
9
13
|
export interface NavMeshData {
|
|
10
14
|
/** Flat xyz vertex coordinates. Polygon indices refer to triples in this array. */
|
|
11
15
|
verts: number[];
|
|
16
|
+
/** Convex polygons as vertex index lists. */
|
|
12
17
|
polys: number[][];
|
|
13
18
|
links: NavMeshLink[];
|
|
19
|
+
/** Area id per polygon, priced by {@link NavMeshQueryOptions.areaCosts}. */
|
|
14
20
|
areas?: number[];
|
|
15
21
|
}
|
|
16
22
|
/** Neighbor relationship for one navigation polygon. */
|
|
@@ -22,12 +28,47 @@ export interface NavMeshAdjacency {
|
|
|
22
28
|
export interface NavMeshPath {
|
|
23
29
|
points: Vec3[];
|
|
24
30
|
polys: number[];
|
|
31
|
+
/** True when the search budget ran out and the route ends at the polygon nearest the goal. */
|
|
32
|
+
partial?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Tunable pricing and bounds for a {@link NavMeshQuery}. */
|
|
35
|
+
export interface NavMeshQueryOptions {
|
|
36
|
+
/** Cost multiplier per area id; unlisted areas cost 1, and a negative or non-finite cost blocks the area. */
|
|
37
|
+
areaCosts?: Readonly<Record<number, number>>;
|
|
38
|
+
/** Most polygons one path search may expand before returning a partial path. Default 2048. */
|
|
39
|
+
maxNodes?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Serializable tuning of a {@link NavMeshQuery}. */
|
|
42
|
+
export interface NavMeshQuerySnapshot {
|
|
43
|
+
areaCosts: Record<number, number>;
|
|
44
|
+
maxNodes: number;
|
|
45
|
+
}
|
|
46
|
+
/** Cached, allocation-light queries over one {@link NavMeshData}. */
|
|
47
|
+
export interface NavMeshQuery {
|
|
48
|
+
readonly mesh: NavMeshData;
|
|
49
|
+
/** Polygon nearest the point in 3D, so stacked floors resolve by height; null for an empty mesh. */
|
|
50
|
+
findPolygon(point: Vec3): number | null;
|
|
51
|
+
/** Closest point on the mesh surface, or null for an empty mesh. */
|
|
52
|
+
closestPoint(point: Vec3): Vec3 | null;
|
|
53
|
+
/** Cheapest polygon corridor by A*, straightened with a portal funnel. */
|
|
54
|
+
findPath(from: Vec3, to: Vec3): NavMeshPath | null;
|
|
55
|
+
/** True when the straight segment stays on walkable, unblocked polygons. */
|
|
56
|
+
raycast(from: Vec3, to: Vec3): boolean;
|
|
57
|
+
retune(patch: NavMeshQueryOptions): void;
|
|
58
|
+
snapshot(): NavMeshQuerySnapshot;
|
|
59
|
+
restore(next: NavMeshQuerySnapshot): void;
|
|
25
60
|
}
|
|
26
61
|
/** Build polygon adjacency from shared edges and explicit off-mesh links. */
|
|
27
62
|
export declare function buildNavAdjacency(mesh: NavMeshData): NavMeshAdjacency[];
|
|
63
|
+
/**
|
|
64
|
+
* Create a cached query over a nav mesh: height-aware polygon lookup through a uniform grid, binary-heap A*
|
|
65
|
+
* priced by area costs and bounded by `maxNodes`, portal-funnel path straightening, and edge-walking raycasts.
|
|
66
|
+
* @capability navmesh-query route, snap, and raycast on a polygon navmesh with retunable area costs and a bounded search
|
|
67
|
+
*/
|
|
68
|
+
export declare function createNavMeshQuery(mesh: NavMeshData, options?: NavMeshQueryOptions): NavMeshQuery;
|
|
28
69
|
/** Return the closest point on the mesh surface, or null for an empty mesh. */
|
|
29
70
|
export declare function closestPoint(mesh: NavMeshData, point: Vec3): Vec3 | null;
|
|
30
|
-
/** A* over polygon
|
|
71
|
+
/** A* over polygon portals with default area costs, straightened by a portal funnel. Use {@link createNavMeshQuery} to price areas. */
|
|
31
72
|
export declare function findPath(mesh: NavMeshData, from: Vec3, to: Vec3): NavMeshPath | null;
|
|
32
73
|
/** True when the segment remains over walkable polygons. */
|
|
33
74
|
export declare function raycastNav(mesh: NavMeshData, from: Vec3, to: Vec3): boolean;
|