@jgengine/core 0.7.0 → 0.9.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 +63 -0
- package/README.md +2 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +14 -2
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +22 -2
- package/dist/combat/abilityKit.js +72 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +64 -6
- package/dist/combat/projectiles.js +232 -74
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +40 -0
- package/dist/combat/shotOrigin.js +57 -0
- package/dist/combat/telegraph.d.ts +30 -1
- package/dist/combat/telegraph.js +61 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/recipe.js +4 -1
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +205 -0
- package/dist/devtools/devtools.js +832 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +68 -0
- package/dist/game/chat.js +173 -0
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +35 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +286 -6
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +44 -1
- package/dist/game/race.js +116 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +89 -0
- package/dist/input/touchScheme.js +153 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +59 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +17 -2
- package/dist/multiplayer/presenceModel.js +30 -4
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +10 -0
- package/dist/nav/navGrid.js +48 -5
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +43 -5
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +54 -8
- package/dist/physics/physicsWorld.js +400 -79
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +18 -18
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +15 -0
- package/dist/runtime/cameraDirector.js +31 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +65 -2
- package/dist/runtime/gameContext.js +280 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +30 -1
- package/dist/runtime/hostPersistence.js +70 -5
- package/dist/runtime/inputSnapshot.d.ts +12 -0
- package/dist/runtime/inputSnapshot.js +17 -0
- package/dist/runtime/motionIntents.d.ts +33 -0
- package/dist/runtime/motionIntents.js +43 -0
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +30 -5
- package/dist/scene/entityStore.js +83 -13
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +29 -0
- package/dist/scene/objectQuery.js +140 -0
- package/dist/scene/objectStore.d.ts +13 -0
- package/dist/scene/objectStore.js +130 -6
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +17 -1
- package/dist/scene/spatial.js +140 -17
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +198 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +61 -0
- package/dist/world/environmentSummary.js +138 -0
- package/dist/world/features.d.ts +138 -9
- package/dist/world/features.js +73 -2
- package/dist/world/gridInstances.d.ts +12 -0
- package/dist/world/gridInstances.js +160 -0
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +62 -1
- package/dist/world/terrain.js +253 -9
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +4983 -0
- package/package.json +14 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yaw-frame steering math. The engine's heading convention is
|
|
3
|
+
* `forward = (sin yaw, cos yaw)` on the XZ plane with +Y up, which makes a
|
|
4
|
+
* positive yaw increment rotate counterclockwise seen from above — a turn to
|
|
5
|
+
* the entity's LEFT on screen. Every hand-written `heading += steer * rate * dt`
|
|
6
|
+
* therefore turns the wrong way; integrate steering through `steerYaw` instead.
|
|
7
|
+
*/
|
|
8
|
+
export type YawVectorXZ = readonly [number, number];
|
|
9
|
+
/** XZ forward direction of a yaw (`rotationY`): `(sin yaw, cos yaw)`. */
|
|
10
|
+
export declare function yawForward(yaw: number): YawVectorXZ;
|
|
11
|
+
/** XZ screen-right of a yaw — `forward × up` with up = +Y: `(-cos yaw, sin yaw)`. */
|
|
12
|
+
export declare function yawRight(yaw: number): YawVectorXZ;
|
|
13
|
+
/**
|
|
14
|
+
* Integrate one steering step. `steerRight` is the signed steer input
|
|
15
|
+
* (+1 = turn right, matching `DRIVE_AXIS_BINDINGS`' KeyD/ArrowRight),
|
|
16
|
+
* `turnRatePerSecond` is radians per second at full lock. Steering right
|
|
17
|
+
* decreases yaw in the engine frame; this helper owns that sign so game
|
|
18
|
+
* code never re-derives it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function steerYaw(yaw: number, steerRight: number, turnRatePerSecond: number, dt: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* Signed steer-right input that closes on `desiredYaw` when integrated
|
|
23
|
+
* through `steerYaw` — the seek half every AI driver and homing script
|
|
24
|
+
* needs. Magnitude is the wrapped radian error in (−π, π]; scale and clamp
|
|
25
|
+
* it into [-1, 1] at the call site.
|
|
26
|
+
*/
|
|
27
|
+
export declare function steerToward(yaw: number, desiredYaw: number): number;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yaw-frame steering math. The engine's heading convention is
|
|
3
|
+
* `forward = (sin yaw, cos yaw)` on the XZ plane with +Y up, which makes a
|
|
4
|
+
* positive yaw increment rotate counterclockwise seen from above — a turn to
|
|
5
|
+
* the entity's LEFT on screen. Every hand-written `heading += steer * rate * dt`
|
|
6
|
+
* therefore turns the wrong way; integrate steering through `steerYaw` instead.
|
|
7
|
+
*/
|
|
8
|
+
/** XZ forward direction of a yaw (`rotationY`): `(sin yaw, cos yaw)`. */
|
|
9
|
+
export function yawForward(yaw) {
|
|
10
|
+
return [Math.sin(yaw), Math.cos(yaw)];
|
|
11
|
+
}
|
|
12
|
+
/** XZ screen-right of a yaw — `forward × up` with up = +Y: `(-cos yaw, sin yaw)`. */
|
|
13
|
+
export function yawRight(yaw) {
|
|
14
|
+
return [-Math.cos(yaw), Math.sin(yaw)];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Integrate one steering step. `steerRight` is the signed steer input
|
|
18
|
+
* (+1 = turn right, matching `DRIVE_AXIS_BINDINGS`' KeyD/ArrowRight),
|
|
19
|
+
* `turnRatePerSecond` is radians per second at full lock. Steering right
|
|
20
|
+
* decreases yaw in the engine frame; this helper owns that sign so game
|
|
21
|
+
* code never re-derives it.
|
|
22
|
+
*/
|
|
23
|
+
export function steerYaw(yaw, steerRight, turnRatePerSecond, dt) {
|
|
24
|
+
return yaw - steerRight * turnRatePerSecond * dt;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Signed steer-right input that closes on `desiredYaw` when integrated
|
|
28
|
+
* through `steerYaw` — the seek half every AI driver and homing script
|
|
29
|
+
* needs. Magnitude is the wrapped radian error in (−π, π]; scale and clamp
|
|
30
|
+
* it into [-1, 1] at the call site.
|
|
31
|
+
*/
|
|
32
|
+
export function steerToward(yaw, desiredYaw) {
|
|
33
|
+
let delta = (desiredYaw - yaw) % TWO_PI;
|
|
34
|
+
if (delta > Math.PI)
|
|
35
|
+
delta -= TWO_PI;
|
|
36
|
+
if (delta <= -Math.PI)
|
|
37
|
+
delta += TWO_PI;
|
|
38
|
+
return -delta;
|
|
39
|
+
}
|
|
40
|
+
const TWO_PI = Math.PI * 2;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voxel character controller (pure model).
|
|
3
|
+
*
|
|
4
|
+
* The base first-person controller in `movementModel.ts` assumes flat ground at
|
|
5
|
+
* y=0 — it integrates gravity into a `jumpOffset` and never asks the world how
|
|
6
|
+
* tall the ground is. That is fine for a plaza demo but wrong for any game where
|
|
7
|
+
* the player walks on placed blocks and digs down into the world: they float on
|
|
8
|
+
* an invisible floor, clip through walls, and can never descend.
|
|
9
|
+
*
|
|
10
|
+
* This module resolves the player as an axis-aligned box against a set of solid
|
|
11
|
+
* unit cells (a `SolidQuery`), so the same held-keys → intent pipeline drives a
|
|
12
|
+
* body that stands on blocks, falls into holes, is stopped by walls, and steps
|
|
13
|
+
* up single-block ledges. Keeping it renderer-free means the shell can drive it
|
|
14
|
+
* from live scene objects while the descent/landing/step-up behaviour stays unit
|
|
15
|
+
* testable without three.js.
|
|
16
|
+
*
|
|
17
|
+
* Cell convention matches the shell's object rendering and the voxel raycast:
|
|
18
|
+
* cell (x,y,z) is the solid box X∈[x-0.5,x+0.5], Y∈[y,y+1], Z∈[z-0.5,z+0.5]. A
|
|
19
|
+
* block at cell y therefore has its top face at world y+1, so a player standing
|
|
20
|
+
* on the surface layer (cell y=-1) rests with their feet at y=0.
|
|
21
|
+
*/
|
|
22
|
+
import { type MovementIntent, type MovementTuningOverrides } from "./movementModel.js";
|
|
23
|
+
export type SolidQuery = (x: number, y: number, z: number) => boolean;
|
|
24
|
+
/** Footprint + height of the player box, in world units. */
|
|
25
|
+
export interface VoxelPlayerDims {
|
|
26
|
+
/** Half the box width on both horizontal axes. */
|
|
27
|
+
halfWidth: number;
|
|
28
|
+
/** Box height measured up from the feet. */
|
|
29
|
+
height: number;
|
|
30
|
+
/** Tallest ledge the player walks up without jumping. */
|
|
31
|
+
stepHeight: number;
|
|
32
|
+
}
|
|
33
|
+
export declare const DEFAULT_VOXEL_DIMS: VoxelPlayerDims;
|
|
34
|
+
/** Mutable kinematic state carried between frames by the voxel controller. */
|
|
35
|
+
export interface VoxelPlayerBody {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
z: number;
|
|
39
|
+
velocityX: number;
|
|
40
|
+
velocityY: number;
|
|
41
|
+
velocityZ: number;
|
|
42
|
+
grounded: boolean;
|
|
43
|
+
jumpHeld: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function createVoxelPlayerBody(x: number, y: number, z: number): VoxelPlayerBody;
|
|
46
|
+
/** Samples the terrain floor height at a point; unset means flat ground plays no part in support. */
|
|
47
|
+
export type GroundHeightQuery = (x: number, z: number) => number;
|
|
48
|
+
/**
|
|
49
|
+
* Advance one frame of voxel-collided player kinematics. Mutates `body`
|
|
50
|
+
* (position, velocity, grounded) in place.
|
|
51
|
+
*
|
|
52
|
+
* `forwardX`/`forwardZ` are the camera heading projected onto the ground plane
|
|
53
|
+
* (need not be normalized). Pulling this out of the render loop is what lets us
|
|
54
|
+
* assert "dig the block underfoot and the player drops a level" in a unit test.
|
|
55
|
+
*
|
|
56
|
+
* `groundHeight`, when supplied, is an additional solid floor sampled at the
|
|
57
|
+
* body's (x, z): the player never sinks below it and standing on it counts as
|
|
58
|
+
* supported (resets jump state like landing on a solid cell). Where terrain and
|
|
59
|
+
* a solid cell's top disagree, the higher of the two wins.
|
|
60
|
+
*/
|
|
61
|
+
export declare function advanceVoxelPlayer(body: VoxelPlayerBody, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, isSolid: SolidQuery, dims?: VoxelPlayerDims, tuning?: MovementTuningOverrides, groundHeight?: GroundHeightQuery): void;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voxel character controller (pure model).
|
|
3
|
+
*
|
|
4
|
+
* The base first-person controller in `movementModel.ts` assumes flat ground at
|
|
5
|
+
* y=0 — it integrates gravity into a `jumpOffset` and never asks the world how
|
|
6
|
+
* tall the ground is. That is fine for a plaza demo but wrong for any game where
|
|
7
|
+
* the player walks on placed blocks and digs down into the world: they float on
|
|
8
|
+
* an invisible floor, clip through walls, and can never descend.
|
|
9
|
+
*
|
|
10
|
+
* This module resolves the player as an axis-aligned box against a set of solid
|
|
11
|
+
* unit cells (a `SolidQuery`), so the same held-keys → intent pipeline drives a
|
|
12
|
+
* body that stands on blocks, falls into holes, is stopped by walls, and steps
|
|
13
|
+
* up single-block ledges. Keeping it renderer-free means the shell can drive it
|
|
14
|
+
* from live scene objects while the descent/landing/step-up behaviour stays unit
|
|
15
|
+
* testable without three.js.
|
|
16
|
+
*
|
|
17
|
+
* Cell convention matches the shell's object rendering and the voxel raycast:
|
|
18
|
+
* cell (x,y,z) is the solid box X∈[x-0.5,x+0.5], Y∈[y,y+1], Z∈[z-0.5,z+0.5]. A
|
|
19
|
+
* block at cell y therefore has its top face at world y+1, so a player standing
|
|
20
|
+
* on the surface layer (cell y=-1) rests with their feet at y=0.
|
|
21
|
+
*/
|
|
22
|
+
import { MOVEMENT_TUNING } from "./movementModel.js";
|
|
23
|
+
export const DEFAULT_VOXEL_DIMS = {
|
|
24
|
+
halfWidth: 0.3,
|
|
25
|
+
height: 1.8,
|
|
26
|
+
stepHeight: 0.6,
|
|
27
|
+
};
|
|
28
|
+
export function createVoxelPlayerBody(x, y, z) {
|
|
29
|
+
return { x, y, z, velocityX: 0, velocityY: 0, velocityZ: 0, grounded: true, jumpHeld: false };
|
|
30
|
+
}
|
|
31
|
+
const SUPPORT_EPSILON = 0.02;
|
|
32
|
+
const PENETRATION_EPSILON = 1e-4;
|
|
33
|
+
function cellX(worldX) {
|
|
34
|
+
return Math.floor(worldX + 0.5);
|
|
35
|
+
}
|
|
36
|
+
function cellZ(worldZ) {
|
|
37
|
+
return Math.floor(worldZ + 0.5);
|
|
38
|
+
}
|
|
39
|
+
/** True if the player box centred at (x, y, z) overlaps any solid cell. */
|
|
40
|
+
function overlapsSolid(x, y, z, isSolid, dims) {
|
|
41
|
+
const minCellX = cellX(x - dims.halfWidth);
|
|
42
|
+
const maxCellX = cellX(x + dims.halfWidth);
|
|
43
|
+
const minCellZ = cellZ(z - dims.halfWidth);
|
|
44
|
+
const maxCellZ = cellZ(z + dims.halfWidth);
|
|
45
|
+
const minCellY = Math.floor(y + PENETRATION_EPSILON);
|
|
46
|
+
const maxCellY = Math.floor(y + dims.height - PENETRATION_EPSILON);
|
|
47
|
+
for (let cx = minCellX; cx <= maxCellX; cx += 1) {
|
|
48
|
+
for (let cz = minCellZ; cz <= maxCellZ; cz += 1) {
|
|
49
|
+
for (let cy = minCellY; cy <= maxCellY; cy += 1) {
|
|
50
|
+
if (isSolid(cx, cy, cz))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/** True if a solid cell sits directly beneath the feet, or terrain supports them (i.e. the player is supported). */
|
|
58
|
+
function hasSupport(x, z, y, isSolid, dims, groundHeight) {
|
|
59
|
+
const cy = Math.floor(y - SUPPORT_EPSILON);
|
|
60
|
+
const minCellX = cellX(x - dims.halfWidth);
|
|
61
|
+
const maxCellX = cellX(x + dims.halfWidth);
|
|
62
|
+
const minCellZ = cellZ(z - dims.halfWidth);
|
|
63
|
+
const maxCellZ = cellZ(z + dims.halfWidth);
|
|
64
|
+
for (let cx = minCellX; cx <= maxCellX; cx += 1) {
|
|
65
|
+
for (let cz = minCellZ; cz <= maxCellZ; cz += 1) {
|
|
66
|
+
if (isSolid(cx, cy, cz))
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (groundHeight !== undefined && y <= groundHeight(x, z) + SUPPORT_EPSILON)
|
|
71
|
+
return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Highest solid-cell top under the footprint at (x, z) within the vertical
|
|
76
|
+
* window [fromY, toY]. Returns null when nothing supports the feet there — used
|
|
77
|
+
* both to snap a fall onto a block and to raise a step-up.
|
|
78
|
+
*/
|
|
79
|
+
function supportTopWithin(x, z, fromY, toY, isSolid, dims) {
|
|
80
|
+
const minCellX = cellX(x - dims.halfWidth);
|
|
81
|
+
const maxCellX = cellX(x + dims.halfWidth);
|
|
82
|
+
const minCellZ = cellZ(z - dims.halfWidth);
|
|
83
|
+
const maxCellZ = cellZ(z + dims.halfWidth);
|
|
84
|
+
const lowCellY = Math.floor(fromY - PENETRATION_EPSILON);
|
|
85
|
+
const highCellY = Math.floor(toY - PENETRATION_EPSILON);
|
|
86
|
+
let best = null;
|
|
87
|
+
for (let cx = minCellX; cx <= maxCellX; cx += 1) {
|
|
88
|
+
for (let cz = minCellZ; cz <= maxCellZ; cz += 1) {
|
|
89
|
+
for (let cy = lowCellY; cy <= highCellY; cy += 1) {
|
|
90
|
+
if (!isSolid(cx, cy, cz))
|
|
91
|
+
continue;
|
|
92
|
+
const top = cy + 1;
|
|
93
|
+
if (best === null || top > best)
|
|
94
|
+
best = top;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return best;
|
|
99
|
+
}
|
|
100
|
+
function targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed) {
|
|
101
|
+
if (!intent.moving)
|
|
102
|
+
return { x: 0, z: 0 };
|
|
103
|
+
let fx = forwardX;
|
|
104
|
+
let fz = forwardZ;
|
|
105
|
+
const lengthSq = fx * fx + fz * fz;
|
|
106
|
+
if (lengthSq < 1e-6) {
|
|
107
|
+
fx = 0;
|
|
108
|
+
fz = -1;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const inv = 1 / Math.sqrt(lengthSq);
|
|
112
|
+
fx *= inv;
|
|
113
|
+
fz *= inv;
|
|
114
|
+
}
|
|
115
|
+
const rightX = -fz;
|
|
116
|
+
const rightZ = fx;
|
|
117
|
+
let vx = fx * intent.forward + rightX * intent.right;
|
|
118
|
+
let vz = fz * intent.forward + rightZ * intent.right;
|
|
119
|
+
const magSq = vx * vx + vz * vz;
|
|
120
|
+
if (magSq < 1e-6)
|
|
121
|
+
return { x: 0, z: 0 };
|
|
122
|
+
const speedMultiplier = intent.crouching
|
|
123
|
+
? MOVEMENT_TUNING.crouchSpeedMultiplier
|
|
124
|
+
: intent.running
|
|
125
|
+
? MOVEMENT_TUNING.runSpeedMultiplier
|
|
126
|
+
: 1;
|
|
127
|
+
const targetSpeed = baseSpeed * MOVEMENT_TUNING.walkSpeedMultiplier * speedMultiplier;
|
|
128
|
+
const scale = targetSpeed / Math.sqrt(magSq);
|
|
129
|
+
vx *= scale;
|
|
130
|
+
vz *= scale;
|
|
131
|
+
return { x: vx, z: vz };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Move the body along one horizontal axis, resolving collisions. Returns the
|
|
135
|
+
* component of the step that survived (0 when a wall stopped the move). When the
|
|
136
|
+
* move is blocked but the obstacle is a short ledge, the body steps up onto it.
|
|
137
|
+
*/
|
|
138
|
+
function moveAxis(body, axis, delta, isSolid, dims) {
|
|
139
|
+
if (delta === 0)
|
|
140
|
+
return 0;
|
|
141
|
+
const nextX = axis === "x" ? body.x + delta : body.x;
|
|
142
|
+
const nextZ = axis === "z" ? body.z + delta : body.z;
|
|
143
|
+
if (!overlapsSolid(nextX, body.y, nextZ, isSolid, dims)) {
|
|
144
|
+
body.x = nextX;
|
|
145
|
+
body.z = nextZ;
|
|
146
|
+
return delta;
|
|
147
|
+
}
|
|
148
|
+
if (body.grounded) {
|
|
149
|
+
const raised = supportTopWithin(nextX, nextZ, body.y, body.y + dims.stepHeight, isSolid, dims);
|
|
150
|
+
if (raised !== null &&
|
|
151
|
+
raised - body.y <= dims.stepHeight + PENETRATION_EPSILON &&
|
|
152
|
+
raised >= body.y &&
|
|
153
|
+
!overlapsSolid(nextX, raised, nextZ, isSolid, dims)) {
|
|
154
|
+
body.x = nextX;
|
|
155
|
+
body.z = nextZ;
|
|
156
|
+
body.y = raised;
|
|
157
|
+
return delta;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Advance one frame of voxel-collided player kinematics. Mutates `body`
|
|
164
|
+
* (position, velocity, grounded) in place.
|
|
165
|
+
*
|
|
166
|
+
* `forwardX`/`forwardZ` are the camera heading projected onto the ground plane
|
|
167
|
+
* (need not be normalized). Pulling this out of the render loop is what lets us
|
|
168
|
+
* assert "dig the block underfoot and the player drops a level" in a unit test.
|
|
169
|
+
*
|
|
170
|
+
* `groundHeight`, when supplied, is an additional solid floor sampled at the
|
|
171
|
+
* body's (x, z): the player never sinks below it and standing on it counts as
|
|
172
|
+
* supported (resets jump state like landing on a solid cell). Where terrain and
|
|
173
|
+
* a solid cell's top disagree, the higher of the two wins.
|
|
174
|
+
*/
|
|
175
|
+
export function advanceVoxelPlayer(body, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, isSolid, dims = DEFAULT_VOXEL_DIMS, tuning, groundHeight) {
|
|
176
|
+
const dt = Math.min(rawDeltaSeconds, MOVEMENT_TUNING.maxFrameSeconds);
|
|
177
|
+
const gravityAcceleration = tuning?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration;
|
|
178
|
+
const jumpVelocity = tuning?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity;
|
|
179
|
+
const target = targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed);
|
|
180
|
+
const acceleration = body.grounded
|
|
181
|
+
? MOVEMENT_TUNING.groundAcceleration
|
|
182
|
+
: MOVEMENT_TUNING.airAcceleration;
|
|
183
|
+
const accelerationBlend = 1 - Math.exp(-acceleration * dt);
|
|
184
|
+
body.velocityX += (target.x - body.velocityX) * accelerationBlend;
|
|
185
|
+
body.velocityZ += (target.z - body.velocityZ) * accelerationBlend;
|
|
186
|
+
if (!intent.moving && body.grounded) {
|
|
187
|
+
const friction = Math.exp(-MOVEMENT_TUNING.groundFriction * dt);
|
|
188
|
+
body.velocityX *= friction;
|
|
189
|
+
body.velocityZ *= friction;
|
|
190
|
+
}
|
|
191
|
+
if (moveAxis(body, "x", body.velocityX * dt, isSolid, dims) === 0)
|
|
192
|
+
body.velocityX = 0;
|
|
193
|
+
if (moveAxis(body, "z", body.velocityZ * dt, isSolid, dims) === 0)
|
|
194
|
+
body.velocityZ = 0;
|
|
195
|
+
const supported = body.grounded && hasSupport(body.x, body.z, body.y, isSolid, dims, groundHeight);
|
|
196
|
+
body.grounded = supported;
|
|
197
|
+
const jumpPressed = intent.jumping;
|
|
198
|
+
if (jumpPressed && !body.jumpHeld && body.grounded && !intent.crouching) {
|
|
199
|
+
body.velocityY = jumpVelocity;
|
|
200
|
+
body.grounded = false;
|
|
201
|
+
}
|
|
202
|
+
body.jumpHeld = jumpPressed;
|
|
203
|
+
if (!body.grounded) {
|
|
204
|
+
body.velocityY -= gravityAcceleration * dt;
|
|
205
|
+
const nextY = body.y + body.velocityY * dt;
|
|
206
|
+
if (body.velocityY <= 0) {
|
|
207
|
+
const terrainFloor = groundHeight?.(body.x, body.z);
|
|
208
|
+
const solidBlocked = overlapsSolid(body.x, nextY, body.z, isSolid, dims);
|
|
209
|
+
const terrainBlocked = terrainFloor !== undefined && nextY <= terrainFloor;
|
|
210
|
+
if (solidBlocked || terrainBlocked) {
|
|
211
|
+
const solidTop = solidBlocked
|
|
212
|
+
? (supportTopWithin(body.x, body.z, nextY, body.y + dims.height, isSolid, dims) ?? Math.floor(body.y))
|
|
213
|
+
: undefined;
|
|
214
|
+
body.y = Math.max(solidTop ?? -Infinity, terrainFloor ?? -Infinity);
|
|
215
|
+
body.velocityY = 0;
|
|
216
|
+
body.grounded = true;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
body.y = nextY;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (overlapsSolid(body.x, nextY, body.z, isSolid, dims)) {
|
|
223
|
+
body.velocityY = 0;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
body.y = nextY;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ChatMessage } from "../game/chat.js";
|
|
2
|
+
export interface ChatSendArgs {
|
|
3
|
+
channelId: string;
|
|
4
|
+
body: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatSendOutcome {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
reason?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ChatActions {
|
|
11
|
+
sendMessage(args: ChatSendArgs): Promise<ChatSendOutcome>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Backend seam for remote text chat, mirroring PresenceTransport: the use*
|
|
15
|
+
* members are called as React hooks by consumers, so a mounted transport must
|
|
16
|
+
* never change identity — remount the subtree to switch backends. useMessages
|
|
17
|
+
* returns undefined while the subscription is loading and the channel's recent
|
|
18
|
+
* history once live.
|
|
19
|
+
*/
|
|
20
|
+
export interface ChatTransport {
|
|
21
|
+
useMessages(channelId: string | "skip"): readonly ChatMessage[] | undefined;
|
|
22
|
+
useActions(): ChatActions;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Callback seam for backends that cannot host React hooks (e.g. the ws
|
|
26
|
+
* client): subscribe delivers the channel's recent history on every change;
|
|
27
|
+
* send resolves with the host's verdict. @jgengine/react's chatTransportFromSync
|
|
28
|
+
* lifts a ChatSync into a ChatTransport.
|
|
29
|
+
*/
|
|
30
|
+
export interface ChatSync {
|
|
31
|
+
subscribe(channelId: string, onChange: (messages: readonly ChatMessage[]) => void): () => void;
|
|
32
|
+
send(channelId: string, body: string): Promise<ChatSendOutcome>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createLocalChatTransport(options?: {
|
|
35
|
+
userId?: string;
|
|
36
|
+
historyLimit?: number;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
}): {
|
|
39
|
+
transport: ChatTransport;
|
|
40
|
+
sync: ChatSync;
|
|
41
|
+
actions: ChatActions;
|
|
42
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function createLocalChatTransport(options) {
|
|
2
|
+
const userId = options?.userId ?? "local";
|
|
3
|
+
const historyLimit = options?.historyLimit ?? 100;
|
|
4
|
+
const now = options?.now ?? Date.now;
|
|
5
|
+
const messagesByChannel = new Map();
|
|
6
|
+
const listeners = new Map();
|
|
7
|
+
let counter = 0;
|
|
8
|
+
function snapshot(channelId) {
|
|
9
|
+
return (messagesByChannel.get(channelId) ?? []).slice();
|
|
10
|
+
}
|
|
11
|
+
const sync = {
|
|
12
|
+
subscribe(channelId, onChange) {
|
|
13
|
+
let set = listeners.get(channelId);
|
|
14
|
+
if (set === undefined) {
|
|
15
|
+
set = new Set();
|
|
16
|
+
listeners.set(channelId, set);
|
|
17
|
+
}
|
|
18
|
+
set.add(onChange);
|
|
19
|
+
onChange(snapshot(channelId));
|
|
20
|
+
return () => {
|
|
21
|
+
set.delete(onChange);
|
|
22
|
+
if (set.size === 0)
|
|
23
|
+
listeners.delete(channelId);
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
send(channelId, body) {
|
|
27
|
+
const trimmed = body.trim();
|
|
28
|
+
if (trimmed.length === 0)
|
|
29
|
+
return Promise.resolve({ ok: false, reason: "empty message" });
|
|
30
|
+
counter += 1;
|
|
31
|
+
const message = {
|
|
32
|
+
id: `local_${counter}`,
|
|
33
|
+
channelId,
|
|
34
|
+
fromUserId: userId,
|
|
35
|
+
body: trimmed,
|
|
36
|
+
at: now(),
|
|
37
|
+
};
|
|
38
|
+
const ring = messagesByChannel.get(channelId) ?? [];
|
|
39
|
+
ring.push(message);
|
|
40
|
+
if (ring.length > historyLimit)
|
|
41
|
+
ring.splice(0, ring.length - historyLimit);
|
|
42
|
+
messagesByChannel.set(channelId, ring);
|
|
43
|
+
const current = snapshot(channelId);
|
|
44
|
+
for (const listener of listeners.get(channelId) ?? [])
|
|
45
|
+
listener(current);
|
|
46
|
+
return Promise.resolve({ ok: true });
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const actions = {
|
|
50
|
+
sendMessage: (args) => sync.send(args.channelId, args.body),
|
|
51
|
+
};
|
|
52
|
+
const transport = {
|
|
53
|
+
useMessages: (channelId) => (channelId === "skip" ? undefined : snapshot(channelId)),
|
|
54
|
+
useActions: () => actions,
|
|
55
|
+
};
|
|
56
|
+
return { transport, sync, actions };
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type FeedWriteGate = {
|
|
2
|
+
allowedActions: readonly string[];
|
|
3
|
+
};
|
|
4
|
+
export declare function createFeedWriteGate(allowedActions?: readonly string[]): FeedWriteGate;
|
|
5
|
+
export declare function validateFeedWrite(gate: FeedWriteGate | undefined, action: string): {
|
|
6
|
+
ok: true;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createFeedWriteGate(allowedActions = []) {
|
|
2
|
+
return { allowedActions: [...allowedActions] };
|
|
3
|
+
}
|
|
4
|
+
export function validateFeedWrite(gate, action) {
|
|
5
|
+
if (gate === undefined || gate.allowedActions.length === 0) {
|
|
6
|
+
return { ok: false, reason: "client feed writes are disabled" };
|
|
7
|
+
}
|
|
8
|
+
if (!gate.allowedActions.includes(action)) {
|
|
9
|
+
return { ok: false, reason: `feed action not allowed: ${action}` };
|
|
10
|
+
}
|
|
11
|
+
return { ok: true };
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AuthSession {
|
|
2
|
+
userId: string;
|
|
3
|
+
displayName?: string;
|
|
4
|
+
avatarUrl?: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
isNew?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface PlayerIdentity {
|
|
9
|
+
userId: string;
|
|
10
|
+
isNew: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function sessionPlayer(session: AuthSession): PlayerIdentity;
|
|
13
|
+
export declare function resolveGuestSession(seed?: string): AuthSession;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function sessionPlayer(session) {
|
|
2
|
+
return { userId: session.userId, isNew: session.isNew ?? false };
|
|
3
|
+
}
|
|
4
|
+
const DEFAULT_GUEST_SEED = "local";
|
|
5
|
+
export function resolveGuestSession(seed) {
|
|
6
|
+
const normalized = seed === undefined || seed.trim().length === 0 ? DEFAULT_GUEST_SEED : seed.trim();
|
|
7
|
+
const hash = fnv1a(normalized);
|
|
8
|
+
return {
|
|
9
|
+
userId: `guest_${hash}`,
|
|
10
|
+
displayName: `Guest ${hash.slice(0, 4).toUpperCase()}`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function fnv1a(input) {
|
|
14
|
+
let hash = 0x811c9dc5;
|
|
15
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
16
|
+
hash ^= input.charCodeAt(index);
|
|
17
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
18
|
+
}
|
|
19
|
+
return hash.toString(36);
|
|
20
|
+
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
function appearanceChanged(a, b) {
|
|
2
|
+
if (a === b)
|
|
3
|
+
return false;
|
|
4
|
+
if (a === undefined || b === undefined)
|
|
5
|
+
return true;
|
|
6
|
+
const aKeys = Object.keys(a);
|
|
7
|
+
const bKeys = Object.keys(b);
|
|
8
|
+
if (aKeys.length !== bKeys.length)
|
|
9
|
+
return true;
|
|
10
|
+
return aKeys.some((key) => a[key] !== b[key]);
|
|
11
|
+
}
|
|
1
12
|
function poseChanged(a, b, tuning) {
|
|
2
13
|
return (Math.abs(a.x - b.x) > tuning.positionEpsilon
|
|
3
14
|
|| Math.abs(a.y - b.y) > tuning.verticalEpsilon
|
|
4
15
|
|| Math.abs(a.z - b.z) > tuning.positionEpsilon
|
|
5
16
|
|| Math.abs(a.rotationY - b.rotationY) > tuning.rotationEpsilon
|
|
6
|
-
|| Math.abs(a.rotationPitch - b.rotationPitch) > tuning.rotationEpsilon
|
|
17
|
+
|| Math.abs(a.rotationPitch - b.rotationPitch) > tuning.rotationEpsilon
|
|
18
|
+
|| appearanceChanged(a.appearance, b.appearance));
|
|
7
19
|
}
|
|
8
20
|
export function createPoseSyncGate(tuning) {
|
|
9
21
|
let lastSentPose = null;
|
|
@@ -7,6 +7,7 @@ export interface PresencePoseState {
|
|
|
7
7
|
rotationY: number;
|
|
8
8
|
rotationPitch?: number;
|
|
9
9
|
lastSeenAtMs?: number;
|
|
10
|
+
appearance?: Record<string, string>;
|
|
10
11
|
}
|
|
11
12
|
export interface IncomingPose {
|
|
12
13
|
position: {
|
|
@@ -16,12 +17,15 @@ export interface IncomingPose {
|
|
|
16
17
|
};
|
|
17
18
|
rotationY?: number;
|
|
18
19
|
rotationPitch?: number;
|
|
20
|
+
appearance?: Record<string, string>;
|
|
19
21
|
}
|
|
20
22
|
export interface PoseSyncRules {
|
|
21
23
|
/** Speed cap (units/sec) for client-authoritative movement. */
|
|
22
24
|
maxSpeed: number;
|
|
23
|
-
/** Vertical offset clamp (e.g. peak jump height). */
|
|
25
|
+
/** Vertical offset clamp above floorY (e.g. peak jump height). */
|
|
24
26
|
maxVerticalOffset: number;
|
|
27
|
+
/** World-floor Y used as the base of the jump band. Defaults to 0. */
|
|
28
|
+
floorY?: number;
|
|
25
29
|
/** Elapsed-time clamp so stale or bursty clients cannot teleport. */
|
|
26
30
|
minElapsedSec: number;
|
|
27
31
|
maxElapsedSec: number;
|
|
@@ -36,12 +40,23 @@ export interface PoseSyncDecision {
|
|
|
36
40
|
};
|
|
37
41
|
rotationY: number;
|
|
38
42
|
rotationPitch: number;
|
|
43
|
+
appearance?: Record<string, string>;
|
|
39
44
|
/** True when the pose differs and a pose write is needed. */
|
|
40
45
|
changed: boolean;
|
|
41
46
|
/** True when only the keep-alive stamp should be written. */
|
|
42
47
|
refreshKeepAlive: boolean;
|
|
43
48
|
}
|
|
44
|
-
export declare
|
|
49
|
+
export declare const DEFAULT_SPAWN_ORIGIN: {
|
|
50
|
+
readonly x: 0;
|
|
51
|
+
readonly y: 0;
|
|
52
|
+
readonly z: 0;
|
|
53
|
+
};
|
|
54
|
+
export declare function spawnPresenceState(origin: {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
z: number;
|
|
58
|
+
} | undefined, nowMs: number, rules: Pick<PoseSyncRules, "maxElapsedSec">): PresencePoseState;
|
|
59
|
+
export declare function decidePoseSync(current: PresencePoseState, incoming: IncomingPose, rules: PoseSyncRules, nowMs: number, floorY?: number): PoseSyncDecision;
|
|
45
60
|
export declare function shouldRefreshKeepAlive(lastSeenAtMs: number | undefined, nowMs: number, rules: Pick<PoseSyncRules, "keepAliveRefreshMs">): boolean;
|
|
46
61
|
export declare function shouldPersistWorldSnapshot(lastSavedAtMs: number | undefined, nowMs: number, intervalMs: number): boolean;
|
|
47
62
|
export interface ActivePresenceResolution<T> {
|
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
function appearanceEqual(a, b) {
|
|
2
|
+
if (a === b)
|
|
3
|
+
return true;
|
|
4
|
+
if (a === undefined || b === undefined)
|
|
5
|
+
return false;
|
|
6
|
+
const aKeys = Object.keys(a);
|
|
7
|
+
const bKeys = Object.keys(b);
|
|
8
|
+
if (aKeys.length !== bKeys.length)
|
|
9
|
+
return false;
|
|
10
|
+
return aKeys.every((key) => a[key] === b[key]);
|
|
11
|
+
}
|
|
12
|
+
export const DEFAULT_SPAWN_ORIGIN = { x: 0, y: 0, z: 0 };
|
|
13
|
+
export function spawnPresenceState(origin = DEFAULT_SPAWN_ORIGIN, nowMs, rules) {
|
|
14
|
+
return {
|
|
15
|
+
position: { x: origin.x, y: origin.y, z: origin.z },
|
|
16
|
+
rotationY: 0,
|
|
17
|
+
rotationPitch: 0,
|
|
18
|
+
lastSeenAtMs: nowMs - rules.maxElapsedSec * 1000,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function decidePoseSync(current, incoming, rules, nowMs, floorY) {
|
|
22
|
+
const rawElapsedSec = Math.max(0, (nowMs - (current.lastSeenAtMs ?? nowMs)) / 1000);
|
|
23
|
+
const elapsedSec = Math.min(rules.maxElapsedSec, rawElapsedSec);
|
|
3
24
|
const maxDist = rules.maxSpeed * elapsedSec;
|
|
4
25
|
const maxDistSq = maxDist * maxDist;
|
|
5
26
|
let targetX = incoming.position.x;
|
|
@@ -12,19 +33,24 @@ export function decidePoseSync(current, incoming, rules, nowMs) {
|
|
|
12
33
|
targetX = current.position.x + dx * scale;
|
|
13
34
|
targetZ = current.position.z + dz * scale;
|
|
14
35
|
}
|
|
36
|
+
const floor = floorY ?? rules.floorY ?? 0;
|
|
37
|
+
const ceiling = floor + rules.maxVerticalOffset;
|
|
15
38
|
const nextY = incoming.position.y === undefined
|
|
16
39
|
? current.position.y
|
|
17
|
-
: Math.max(
|
|
40
|
+
: Math.max(floor, Math.min(ceiling, incoming.position.y));
|
|
18
41
|
const nextRotationY = incoming.rotationY ?? current.rotationY;
|
|
19
42
|
const nextRotationPitch = incoming.rotationPitch ?? current.rotationPitch ?? 0;
|
|
43
|
+
const nextAppearance = incoming.appearance ?? current.appearance;
|
|
44
|
+
const appearanceChanged = incoming.appearance !== undefined && !appearanceEqual(incoming.appearance, current.appearance);
|
|
20
45
|
const moved = targetX !== current.position.x || targetZ !== current.position.z;
|
|
21
46
|
const jumped = nextY !== current.position.y;
|
|
22
47
|
const rotated = nextRotationY !== current.rotationY || nextRotationPitch !== (current.rotationPitch ?? 0);
|
|
23
|
-
const changed = moved || jumped || rotated;
|
|
48
|
+
const changed = moved || jumped || rotated || appearanceChanged;
|
|
24
49
|
return {
|
|
25
50
|
position: { x: targetX, y: nextY, z: targetZ },
|
|
26
51
|
rotationY: nextRotationY,
|
|
27
52
|
rotationPitch: nextRotationPitch,
|
|
53
|
+
appearance: nextAppearance,
|
|
28
54
|
changed,
|
|
29
55
|
refreshKeepAlive: !changed && shouldRefreshKeepAlive(current.lastSeenAtMs, nowMs, rules),
|
|
30
56
|
};
|