@jgengine/core 0.6.0 → 0.8.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 +214 -2
- package/README.md +2 -0
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +61 -0
- package/dist/ai/spawnDirector.js +162 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +64 -0
- package/dist/cards/cardPile.js +175 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +47 -0
- package/dist/combat/abilityKit.js +139 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +12 -1
- package/dist/combat/effects.js +31 -19
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +31 -0
- package/dist/combat/telegraph.js +51 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- 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 +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +87 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +516 -8
- 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/race.d.ts +100 -0
- package/dist/game/race.js +236 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +69 -0
- package/dist/game/social.js +138 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +95 -0
- package/dist/game/worldItem.js +138 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +69 -0
- package/dist/input/pointer.js +65 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +115 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +71 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- 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/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -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 +56 -0
- package/dist/nav/navGrid.js +265 -0
- package/dist/nav/pathFollow.d.ts +31 -0
- package/dist/nav/pathFollow.js +69 -0
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +124 -2
- package/dist/physics/physicsWorld.js +493 -41
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +143 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +99 -0
- package/dist/physics/vehicleBody.js +233 -0
- 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/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +107 -3
- package/dist/runtime/gameContext.js +253 -7
- package/dist/runtime/hostPersistence.d.ts +39 -0
- package/dist/runtime/hostPersistence.js +54 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +9 -1
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- 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/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +79 -0
- package/dist/session/roundState.js +158 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +68 -0
- package/dist/tactics/tacticalGrid.js +261 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +60 -0
- package/dist/turn/turnLoop.js +223 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +69 -0
- package/dist/world/placementController.js +153 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
package/dist/game/social.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const DEFAULT_EMOTE_RADIUS = 20;
|
|
1
2
|
const DEFAULT_INVITE_TTL_MS = 60_000;
|
|
2
3
|
export function createSocial(deps) {
|
|
3
4
|
const now = deps.now ?? Date.now;
|
|
@@ -71,12 +72,25 @@ export function createSocial(deps) {
|
|
|
71
72
|
friendRequests.delete(requestId);
|
|
72
73
|
}
|
|
73
74
|
},
|
|
75
|
+
decline(userId, requestId) {
|
|
76
|
+
const request = friendRequests.get(requestId);
|
|
77
|
+
if (request !== undefined && request.to === userId)
|
|
78
|
+
friendRequests.delete(requestId);
|
|
79
|
+
},
|
|
74
80
|
list(userId) {
|
|
75
81
|
return Array.from(friendships.get(userId) ?? [], (friendUserId) => ({
|
|
76
82
|
userId: friendUserId,
|
|
77
83
|
online: deps.presence?.(friendUserId).online ?? false,
|
|
78
84
|
}));
|
|
79
85
|
},
|
|
86
|
+
requestsFor(userId) {
|
|
87
|
+
const entries = [];
|
|
88
|
+
for (const [requestId, request] of friendRequests) {
|
|
89
|
+
if (request.to === userId)
|
|
90
|
+
entries.push({ requestId, fromUserId: request.from });
|
|
91
|
+
}
|
|
92
|
+
return entries;
|
|
93
|
+
},
|
|
80
94
|
snapshot(userId) {
|
|
81
95
|
return {
|
|
82
96
|
friends: Array.from(friendships.get(userId) ?? []),
|
|
@@ -211,9 +225,131 @@ export function createSocial(deps) {
|
|
|
211
225
|
role: member === current.leader ? "leader" : "member",
|
|
212
226
|
}));
|
|
213
227
|
},
|
|
228
|
+
decline(userId, inviteId) {
|
|
229
|
+
const invite = partyInvites.get(inviteId);
|
|
230
|
+
if (invite !== undefined && invite.to === userId)
|
|
231
|
+
partyInvites.delete(inviteId);
|
|
232
|
+
},
|
|
214
233
|
membersOf(userId) {
|
|
215
234
|
return partyOf(userId)?.members.slice() ?? [];
|
|
216
235
|
},
|
|
236
|
+
invitesFor(userId) {
|
|
237
|
+
const ttlMs = partyConfig?.inviteTtlMs ?? DEFAULT_INVITE_TTL_MS;
|
|
238
|
+
const cutoff = now() - ttlMs;
|
|
239
|
+
const entries = [];
|
|
240
|
+
for (const [inviteId, invite] of partyInvites) {
|
|
241
|
+
if (invite.createdAt < cutoff) {
|
|
242
|
+
partyInvites.delete(inviteId);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (invite.to === userId) {
|
|
246
|
+
entries.push({ inviteId, fromUserId: invite.from, createdAt: invite.createdAt });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return entries;
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const worldInviteTtlMs = deps.worldInviteTtlMs ?? DEFAULT_INVITE_TTL_MS;
|
|
253
|
+
const worldInviteEntries = new Map();
|
|
254
|
+
let worldInviteCounter = 0;
|
|
255
|
+
function pruneWorldInvites() {
|
|
256
|
+
const cutoff = now() - worldInviteTtlMs;
|
|
257
|
+
for (const [id, entry] of worldInviteEntries) {
|
|
258
|
+
if (entry.createdAt < cutoff)
|
|
259
|
+
worldInviteEntries.delete(id);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function canWorldInvite(fromUserId, toUserId) {
|
|
263
|
+
if (fromUserId === toUserId)
|
|
264
|
+
return { reason: "cannot invite yourself" };
|
|
265
|
+
if (blocked.get(toUserId)?.has(fromUserId) || blocked.get(fromUserId)?.has(toUserId)) {
|
|
266
|
+
return { reason: "blocked" };
|
|
267
|
+
}
|
|
268
|
+
pruneWorldInvites();
|
|
269
|
+
for (const entry of worldInviteEntries.values()) {
|
|
270
|
+
if (entry.fromUserId === fromUserId && entry.toUserId === toUserId) {
|
|
271
|
+
return { reason: "invite already pending" };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const worldInvites = {
|
|
277
|
+
canInvite: canWorldInvite,
|
|
278
|
+
invite(fromUserId, toUserId, target) {
|
|
279
|
+
const denied = canWorldInvite(fromUserId, toUserId);
|
|
280
|
+
if (denied !== null)
|
|
281
|
+
return denied;
|
|
282
|
+
worldInviteCounter += 1;
|
|
283
|
+
const inviteId = `winv_${worldInviteCounter}`;
|
|
284
|
+
const entry = {
|
|
285
|
+
id: inviteId,
|
|
286
|
+
fromUserId,
|
|
287
|
+
toUserId,
|
|
288
|
+
serverId: target.serverId,
|
|
289
|
+
createdAt: now(),
|
|
290
|
+
};
|
|
291
|
+
if (target.joinCode !== undefined)
|
|
292
|
+
entry.joinCode = target.joinCode;
|
|
293
|
+
worldInviteEntries.set(inviteId, entry);
|
|
294
|
+
const invited = {
|
|
295
|
+
inviteId,
|
|
296
|
+
fromUserId,
|
|
297
|
+
toUserId,
|
|
298
|
+
serverId: target.serverId,
|
|
299
|
+
};
|
|
300
|
+
if (target.joinCode !== undefined)
|
|
301
|
+
invited.joinCode = target.joinCode;
|
|
302
|
+
events.emit("social.world.invited", invited);
|
|
303
|
+
return { inviteId };
|
|
304
|
+
},
|
|
305
|
+
accept(userId, inviteId) {
|
|
306
|
+
const entry = worldInviteEntries.get(inviteId);
|
|
307
|
+
if (entry === undefined)
|
|
308
|
+
return { reason: `unknown invite "${inviteId}"` };
|
|
309
|
+
if (entry.toUserId !== userId)
|
|
310
|
+
return { reason: "invite is not addressed to this user" };
|
|
311
|
+
if (now() - entry.createdAt > worldInviteTtlMs) {
|
|
312
|
+
worldInviteEntries.delete(inviteId);
|
|
313
|
+
return { reason: "invite expired" };
|
|
314
|
+
}
|
|
315
|
+
worldInviteEntries.delete(inviteId);
|
|
316
|
+
const target = { serverId: entry.serverId };
|
|
317
|
+
if (entry.joinCode !== undefined)
|
|
318
|
+
target.joinCode = entry.joinCode;
|
|
319
|
+
const accepted = {
|
|
320
|
+
inviteId,
|
|
321
|
+
userId,
|
|
322
|
+
fromUserId: entry.fromUserId,
|
|
323
|
+
serverId: entry.serverId,
|
|
324
|
+
};
|
|
325
|
+
if (entry.joinCode !== undefined)
|
|
326
|
+
accepted.joinCode = entry.joinCode;
|
|
327
|
+
events.emit("social.world.accepted", accepted);
|
|
328
|
+
return { target };
|
|
329
|
+
},
|
|
330
|
+
decline(userId, inviteId) {
|
|
331
|
+
const entry = worldInviteEntries.get(inviteId);
|
|
332
|
+
if (entry !== undefined && entry.toUserId === userId)
|
|
333
|
+
worldInviteEntries.delete(inviteId);
|
|
334
|
+
},
|
|
335
|
+
listFor(userId) {
|
|
336
|
+
pruneWorldInvites();
|
|
337
|
+
return Array.from(worldInviteEntries.values()).filter((entry) => entry.toUserId === userId);
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
const emotes = {
|
|
341
|
+
play(fromUserId, emoteId, radius) {
|
|
342
|
+
const emoteDeps = deps.emotes;
|
|
343
|
+
if (emoteDeps === undefined)
|
|
344
|
+
return { reason: "emotes not configured" };
|
|
345
|
+
const origin = emoteDeps.entities.get(fromUserId);
|
|
346
|
+
if (origin === null)
|
|
347
|
+
return { reason: `entity "${fromUserId}" is not spawned` };
|
|
348
|
+
const recipients = emoteDeps.spatial.inRadius(origin.position, radius ?? DEFAULT_EMOTE_RADIUS, (id) => id !== fromUserId && emoteDeps.entities.get(id)?.role === "player");
|
|
349
|
+
const result = { from: fromUserId, emoteId, at: origin.position, recipients };
|
|
350
|
+
events.emit("emote.played", result);
|
|
351
|
+
return result;
|
|
352
|
+
},
|
|
217
353
|
};
|
|
218
354
|
return {
|
|
219
355
|
friends,
|
|
@@ -223,5 +359,7 @@ export function createSocial(deps) {
|
|
|
223
359
|
return deps.presence?.(userId) ?? { online: false };
|
|
224
360
|
},
|
|
225
361
|
},
|
|
362
|
+
emotes,
|
|
363
|
+
worldInvites,
|
|
226
364
|
};
|
|
227
365
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntityPose } from "../scene/entityStore.js";
|
|
2
|
+
export interface SpawnPointPose {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
z: number;
|
|
6
|
+
rotationY?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface RespawnTarget {
|
|
9
|
+
setPose(id: string, pose: EntityPose): boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SpawnPoints {
|
|
12
|
+
record(id: string, pose: SpawnPointPose): void;
|
|
13
|
+
get(id: string): SpawnPointPose | undefined;
|
|
14
|
+
list(): readonly (SpawnPointPose & {
|
|
15
|
+
id: string;
|
|
16
|
+
})[];
|
|
17
|
+
respawn(entities: RespawnTarget, entityId: string, spawnId: string): boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function createSpawnPoints(): SpawnPoints;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createSpawnPoints() {
|
|
2
|
+
const points = new Map();
|
|
3
|
+
return {
|
|
4
|
+
record(id, pose) {
|
|
5
|
+
points.set(id, { ...pose });
|
|
6
|
+
},
|
|
7
|
+
get(id) {
|
|
8
|
+
return points.get(id);
|
|
9
|
+
},
|
|
10
|
+
list() {
|
|
11
|
+
return Array.from(points, ([id, pose]) => ({ id, ...pose }));
|
|
12
|
+
},
|
|
13
|
+
respawn(entities, entityId, spawnId) {
|
|
14
|
+
const pose = points.get(spawnId);
|
|
15
|
+
if (pose === undefined)
|
|
16
|
+
return false;
|
|
17
|
+
return entities.setPose(entityId, {
|
|
18
|
+
position: { x: pose.x, y: pose.y, z: pose.z },
|
|
19
|
+
...(pose.rotationY === undefined ? {} : { rotationY: pose.rotationY }),
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
import { type LootFilterItem, type LootFilterRule } from "./lootFilter.js";
|
|
3
|
+
import type { Drop } from "./lootTable.js";
|
|
4
|
+
/** Scene-entity catalog name every dropped-item instance spawns under (see the three buckets: worldItem is an entity, never an inventory item or object). */
|
|
5
|
+
export declare const WORLD_ITEM_ENTITY_NAME = "world_item";
|
|
6
|
+
export declare const DEFAULT_RARITY = "common";
|
|
7
|
+
export declare const DEFAULT_PICKUP_RADIUS = 2;
|
|
8
|
+
export interface ScatterOptions {
|
|
9
|
+
radius: number;
|
|
10
|
+
minRadius?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_SCATTER: ScatterOptions;
|
|
14
|
+
/** Random offset within an annulus `[minRadius, radius]` around the origin — the on-death scatter impulse. */
|
|
15
|
+
export declare function scatterOffset(rng: () => number, options?: ScatterOptions): EntityPosition;
|
|
16
|
+
export declare function scatterPosition(origin: EntityPosition, rng: () => number, options?: ScatterOptions): EntityPosition;
|
|
17
|
+
export interface WorldItemRecord {
|
|
18
|
+
instanceId: string;
|
|
19
|
+
itemId: string;
|
|
20
|
+
rarity: string;
|
|
21
|
+
baseType: string;
|
|
22
|
+
count: number;
|
|
23
|
+
affixTier?: number;
|
|
24
|
+
source?: string;
|
|
25
|
+
droppedAt: number;
|
|
26
|
+
}
|
|
27
|
+
export interface WorldItemSpawnInput {
|
|
28
|
+
itemId: string;
|
|
29
|
+
position: EntityPosition;
|
|
30
|
+
rarity?: string;
|
|
31
|
+
baseType?: string;
|
|
32
|
+
count?: number;
|
|
33
|
+
affixTier?: number;
|
|
34
|
+
source?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface WorldItemStoreDeps {
|
|
37
|
+
spawnEntity(position: EntityPosition): string;
|
|
38
|
+
despawnEntity(instanceId: string): boolean;
|
|
39
|
+
resolvePosition(instanceId: string): EntityPosition | undefined;
|
|
40
|
+
now?(): number;
|
|
41
|
+
}
|
|
42
|
+
export interface WorldItemStore {
|
|
43
|
+
spawn(input: WorldItemSpawnInput): WorldItemRecord;
|
|
44
|
+
get(instanceId: string): WorldItemRecord | null;
|
|
45
|
+
list(): readonly WorldItemRecord[];
|
|
46
|
+
nearestInRadius(from: EntityPosition, radius: number, filter?: (record: WorldItemRecord) => boolean): string | null;
|
|
47
|
+
/** Detaches the record for a pickup: caller is expected to grant its contents (e.g. `loot.grantToPlayer`) itself. */
|
|
48
|
+
take(instanceId: string): WorldItemRecord | null;
|
|
49
|
+
/** Consumes/destroys the record with no grant implied (decay, an explosion, a cleanup sweep) — same despawn path as `take`, leaves no orphaned record. */
|
|
50
|
+
remove(instanceId: string): WorldItemRecord | null;
|
|
51
|
+
}
|
|
52
|
+
/** Pure pickup-radius + nearest-item selection, usable without a live store (click-to-grab, proximity prompts). */
|
|
53
|
+
export declare function selectNearestWorldItem(candidates: readonly {
|
|
54
|
+
instanceId: string;
|
|
55
|
+
position: EntityPosition;
|
|
56
|
+
}[], from: EntityPosition, radius: number): string | null;
|
|
57
|
+
export declare function createWorldItemStore(deps: WorldItemStoreDeps): WorldItemStore;
|
|
58
|
+
export interface ResolveDeathDropsOptions {
|
|
59
|
+
mode: "grant" | "world";
|
|
60
|
+
origin: EntityPosition;
|
|
61
|
+
resolveRarity(itemId: string): string;
|
|
62
|
+
resolveBaseType?(itemId: string): string;
|
|
63
|
+
scatter?: ScatterOptions;
|
|
64
|
+
rng?(): number;
|
|
65
|
+
source?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ResolvedDeathDrops {
|
|
68
|
+
worldSpawns: WorldItemSpawnInput[];
|
|
69
|
+
grants: Drop[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Splits rolled death drops between direct grants and scattered ground items.
|
|
73
|
+
* `mode: "grant"` is the legacy behavior (loot straight to inventory); `mode:
|
|
74
|
+
* "world"` routes item drops through a scatter impulse and leaves currency
|
|
75
|
+
* drops granting directly (coins fly to the killer, gear hits the ground).
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveDeathDrops(drops: readonly Drop[], options: ResolveDeathDropsOptions): ResolvedDeathDrops;
|
|
78
|
+
export interface RarityStyle {
|
|
79
|
+
color?: string;
|
|
80
|
+
beam?: boolean;
|
|
81
|
+
label?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface WorldItemPresentation {
|
|
84
|
+
hidden: boolean;
|
|
85
|
+
color?: string;
|
|
86
|
+
beam: boolean;
|
|
87
|
+
label?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Composes the baseline rarity→beam/color/label render binding (#32, catalog
|
|
91
|
+
* data the game supplies) with the loot-filter rule overrides (#33). A
|
|
92
|
+
* matching rule wins field-by-field; unmatched fields fall back to the
|
|
93
|
+
* rarity's baseline style.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveWorldItemPresentation(item: LootFilterItem, rarityStyle: Record<string, RarityStyle> | undefined, rules: readonly LootFilterRule[] | undefined): WorldItemPresentation;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { distanceBetween } from "../scene/spatial.js";
|
|
2
|
+
import { evaluateLootFilter } from "./lootFilter.js";
|
|
3
|
+
/** Scene-entity catalog name every dropped-item instance spawns under (see the three buckets: worldItem is an entity, never an inventory item or object). */
|
|
4
|
+
export const WORLD_ITEM_ENTITY_NAME = "world_item";
|
|
5
|
+
export const DEFAULT_RARITY = "common";
|
|
6
|
+
export const DEFAULT_PICKUP_RADIUS = 2;
|
|
7
|
+
export const DEFAULT_SCATTER = { radius: 1.5, minRadius: 0.4, height: 0 };
|
|
8
|
+
/** Random offset within an annulus `[minRadius, radius]` around the origin — the on-death scatter impulse. */
|
|
9
|
+
export function scatterOffset(rng, options = DEFAULT_SCATTER) {
|
|
10
|
+
const angle = rng() * Math.PI * 2;
|
|
11
|
+
const min = options.minRadius ?? 0;
|
|
12
|
+
const span = Math.max(options.radius - min, 0);
|
|
13
|
+
const distance = min + rng() * span;
|
|
14
|
+
return [Math.cos(angle) * distance, options.height ?? 0, Math.sin(angle) * distance];
|
|
15
|
+
}
|
|
16
|
+
export function scatterPosition(origin, rng, options = DEFAULT_SCATTER) {
|
|
17
|
+
const offset = scatterOffset(rng, options);
|
|
18
|
+
return [origin[0] + offset[0], origin[1] + offset[1], origin[2] + offset[2]];
|
|
19
|
+
}
|
|
20
|
+
/** Pure pickup-radius + nearest-item selection, usable without a live store (click-to-grab, proximity prompts). */
|
|
21
|
+
export function selectNearestWorldItem(candidates, from, radius) {
|
|
22
|
+
let bestId = null;
|
|
23
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
const distance = distanceBetween(from, candidate.position);
|
|
26
|
+
if (distance <= radius && distance < bestDistance) {
|
|
27
|
+
bestDistance = distance;
|
|
28
|
+
bestId = candidate.instanceId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return bestId;
|
|
32
|
+
}
|
|
33
|
+
export function createWorldItemStore(deps) {
|
|
34
|
+
const records = new Map();
|
|
35
|
+
const now = deps.now ?? Date.now;
|
|
36
|
+
function removeRecord(instanceId) {
|
|
37
|
+
const record = records.get(instanceId);
|
|
38
|
+
if (record === undefined)
|
|
39
|
+
return null;
|
|
40
|
+
records.delete(instanceId);
|
|
41
|
+
deps.despawnEntity(instanceId);
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
spawn(input) {
|
|
46
|
+
const instanceId = deps.spawnEntity(input.position);
|
|
47
|
+
const record = {
|
|
48
|
+
instanceId,
|
|
49
|
+
itemId: input.itemId,
|
|
50
|
+
rarity: input.rarity ?? DEFAULT_RARITY,
|
|
51
|
+
baseType: input.baseType ?? input.itemId,
|
|
52
|
+
count: input.count ?? 1,
|
|
53
|
+
droppedAt: now(),
|
|
54
|
+
};
|
|
55
|
+
if (input.affixTier !== undefined)
|
|
56
|
+
record.affixTier = input.affixTier;
|
|
57
|
+
if (input.source !== undefined)
|
|
58
|
+
record.source = input.source;
|
|
59
|
+
records.set(instanceId, record);
|
|
60
|
+
return record;
|
|
61
|
+
},
|
|
62
|
+
get(instanceId) {
|
|
63
|
+
return records.get(instanceId) ?? null;
|
|
64
|
+
},
|
|
65
|
+
list() {
|
|
66
|
+
return [...records.values()];
|
|
67
|
+
},
|
|
68
|
+
nearestInRadius(from, radius, filter) {
|
|
69
|
+
const candidates = [];
|
|
70
|
+
for (const record of records.values()) {
|
|
71
|
+
if (filter !== undefined && !filter(record))
|
|
72
|
+
continue;
|
|
73
|
+
const position = deps.resolvePosition(record.instanceId);
|
|
74
|
+
if (position !== undefined)
|
|
75
|
+
candidates.push({ instanceId: record.instanceId, position });
|
|
76
|
+
}
|
|
77
|
+
return selectNearestWorldItem(candidates, from, radius);
|
|
78
|
+
},
|
|
79
|
+
take(instanceId) {
|
|
80
|
+
return removeRecord(instanceId);
|
|
81
|
+
},
|
|
82
|
+
remove(instanceId) {
|
|
83
|
+
return removeRecord(instanceId);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Splits rolled death drops between direct grants and scattered ground items.
|
|
89
|
+
* `mode: "grant"` is the legacy behavior (loot straight to inventory); `mode:
|
|
90
|
+
* "world"` routes item drops through a scatter impulse and leaves currency
|
|
91
|
+
* drops granting directly (coins fly to the killer, gear hits the ground).
|
|
92
|
+
*/
|
|
93
|
+
export function resolveDeathDrops(drops, options) {
|
|
94
|
+
if (options.mode !== "world")
|
|
95
|
+
return { worldSpawns: [], grants: [...drops] };
|
|
96
|
+
const rng = options.rng ?? Math.random;
|
|
97
|
+
const worldSpawns = [];
|
|
98
|
+
const grants = [];
|
|
99
|
+
for (const drop of drops) {
|
|
100
|
+
if (drop.item === undefined) {
|
|
101
|
+
grants.push(drop);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const spawn = {
|
|
105
|
+
itemId: drop.item,
|
|
106
|
+
count: drop.count,
|
|
107
|
+
rarity: options.resolveRarity(drop.item),
|
|
108
|
+
position: scatterPosition(options.origin, rng, options.scatter),
|
|
109
|
+
};
|
|
110
|
+
if (options.resolveBaseType !== undefined)
|
|
111
|
+
spawn.baseType = options.resolveBaseType(drop.item);
|
|
112
|
+
if (options.source !== undefined)
|
|
113
|
+
spawn.source = options.source;
|
|
114
|
+
worldSpawns.push(spawn);
|
|
115
|
+
}
|
|
116
|
+
return { worldSpawns, grants };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Composes the baseline rarity→beam/color/label render binding (#32, catalog
|
|
120
|
+
* data the game supplies) with the loot-filter rule overrides (#33). A
|
|
121
|
+
* matching rule wins field-by-field; unmatched fields fall back to the
|
|
122
|
+
* rarity's baseline style.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveWorldItemPresentation(item, rarityStyle, rules) {
|
|
125
|
+
const base = rarityStyle?.[item.rarity];
|
|
126
|
+
const override = rules === undefined || rules.length === 0 ? {} : evaluateLootFilter(rules, item);
|
|
127
|
+
const presentation = {
|
|
128
|
+
hidden: override.hidden ?? false,
|
|
129
|
+
beam: override.beam ?? base?.beam ?? false,
|
|
130
|
+
};
|
|
131
|
+
const color = override.color ?? base?.color;
|
|
132
|
+
if (color !== undefined)
|
|
133
|
+
presentation.color = color;
|
|
134
|
+
const label = override.label ?? base?.label;
|
|
135
|
+
if (label !== undefined)
|
|
136
|
+
presentation.label = label;
|
|
137
|
+
return presentation;
|
|
138
|
+
}
|
|
@@ -27,8 +27,18 @@ export type ActionStateBindingMap<TAction extends string, TCode extends string =
|
|
|
27
27
|
export type ActionCodes<TCode extends string = string> = readonly TCode[] | {
|
|
28
28
|
hold?: readonly TCode[];
|
|
29
29
|
toggle?: readonly TCode[];
|
|
30
|
+
repeatMs?: number;
|
|
30
31
|
};
|
|
31
32
|
export type ActionCodesMap<TAction extends string = string, TCode extends string = string> = Record<TAction, ActionCodes<TCode>>;
|
|
33
|
+
export declare function actionRepeatMs(codes: ActionCodes | undefined): number | undefined;
|
|
34
|
+
export interface ShouldDispatchActionInput {
|
|
35
|
+
pressed: boolean;
|
|
36
|
+
down: boolean;
|
|
37
|
+
repeatMs: number | undefined;
|
|
38
|
+
lastFiredAt: number | null;
|
|
39
|
+
now: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function shouldDispatchAction({ pressed, down, repeatMs, lastFiredAt, now }: ShouldDispatchActionInput): boolean;
|
|
32
42
|
export declare function toActionStateBindingMap<TAction extends string, TCode extends string>(map: ActionCodesMap<TAction, TCode>): ActionStateBindingMap<TAction, TCode>;
|
|
33
43
|
export declare function hotbarSlotBindings(count: number, options?: {
|
|
34
44
|
action?: (slot: number) => string;
|
|
@@ -27,6 +27,20 @@ export function normalizeKeyCode(code) {
|
|
|
27
27
|
return "Control";
|
|
28
28
|
return code;
|
|
29
29
|
}
|
|
30
|
+
export function actionRepeatMs(codes) {
|
|
31
|
+
if (codes === undefined || Array.isArray(codes))
|
|
32
|
+
return undefined;
|
|
33
|
+
return codes.repeatMs;
|
|
34
|
+
}
|
|
35
|
+
export function shouldDispatchAction({ pressed, down, repeatMs, lastFiredAt, now }) {
|
|
36
|
+
if (pressed)
|
|
37
|
+
return true;
|
|
38
|
+
if (repeatMs === undefined || repeatMs <= 0)
|
|
39
|
+
return false;
|
|
40
|
+
if (!down || lastFiredAt === null)
|
|
41
|
+
return false;
|
|
42
|
+
return now - lastFiredAt >= repeatMs;
|
|
43
|
+
}
|
|
30
44
|
function toBindings(codes) {
|
|
31
45
|
return codes.map((code) => ({ primary: code, secondary: null }));
|
|
32
46
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface AxisInput {
|
|
2
|
+
throttle: number;
|
|
3
|
+
brake: number;
|
|
4
|
+
steer: number;
|
|
5
|
+
handbrake: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const NEUTRAL_AXIS: AxisInput;
|
|
8
|
+
export type AxisName = keyof AxisInput;
|
|
9
|
+
export interface AxisBinding {
|
|
10
|
+
positive: readonly string[];
|
|
11
|
+
negative?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export type AxisBindingMap = Record<AxisName, AxisBinding>;
|
|
14
|
+
export interface AxisChannelConfig {
|
|
15
|
+
bindings: AxisBindingMap;
|
|
16
|
+
smoothing?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface AxisRange {
|
|
19
|
+
min: number;
|
|
20
|
+
max: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const AXIS_RANGE: Record<AxisName, AxisRange>;
|
|
23
|
+
export declare function clampAxis(value: number, range: AxisRange): number;
|
|
24
|
+
export declare function rampToward(current: number, target: number, rate: number, dt: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* Analog control channel — distinct from the digital action bindings. Throttle/brake/steer/handbrake
|
|
27
|
+
* are continuous values ramped from held keys (a keyboard feels like a pedal) or driven directly from
|
|
28
|
+
* a gamepad axis via `setAnalog`. `sample(dt, isDown)` folds the current held-key state into the
|
|
29
|
+
* smoothed value; a `setAnalog` override replaces the digital target for that axis until cleared.
|
|
30
|
+
*/
|
|
31
|
+
export declare class AxisChannel {
|
|
32
|
+
private readonly bindings;
|
|
33
|
+
private readonly smoothing;
|
|
34
|
+
private readonly current;
|
|
35
|
+
private readonly overrides;
|
|
36
|
+
constructor(config: AxisChannelConfig);
|
|
37
|
+
setAnalog(axis: AxisName, value: number): void;
|
|
38
|
+
clearAnalog(axis: AxisName): void;
|
|
39
|
+
reset(): void;
|
|
40
|
+
get value(): AxisInput;
|
|
41
|
+
sample(dt: number, isDown: (code: string) => boolean): AxisInput;
|
|
42
|
+
}
|
|
43
|
+
export declare const DRIVE_AXIS_BINDINGS: AxisBindingMap;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const NEUTRAL_AXIS = { throttle: 0, brake: 0, steer: 0, handbrake: 0 };
|
|
2
|
+
export const AXIS_RANGE = {
|
|
3
|
+
throttle: { min: 0, max: 1 },
|
|
4
|
+
brake: { min: 0, max: 1 },
|
|
5
|
+
steer: { min: -1, max: 1 },
|
|
6
|
+
handbrake: { min: 0, max: 1 },
|
|
7
|
+
};
|
|
8
|
+
export function clampAxis(value, range) {
|
|
9
|
+
return value < range.min ? range.min : value > range.max ? range.max : value;
|
|
10
|
+
}
|
|
11
|
+
export function rampToward(current, target, rate, dt) {
|
|
12
|
+
if (!Number.isFinite(rate))
|
|
13
|
+
return target;
|
|
14
|
+
const delta = target - current;
|
|
15
|
+
const step = rate * dt;
|
|
16
|
+
if (Math.abs(delta) <= step)
|
|
17
|
+
return target;
|
|
18
|
+
return current + Math.sign(delta) * step;
|
|
19
|
+
}
|
|
20
|
+
function digitalTarget(binding, isDown) {
|
|
21
|
+
const positive = binding.positive.some(isDown) ? 1 : 0;
|
|
22
|
+
const negative = binding.negative !== undefined && binding.negative.some(isDown) ? 1 : 0;
|
|
23
|
+
return positive - negative;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Analog control channel — distinct from the digital action bindings. Throttle/brake/steer/handbrake
|
|
27
|
+
* are continuous values ramped from held keys (a keyboard feels like a pedal) or driven directly from
|
|
28
|
+
* a gamepad axis via `setAnalog`. `sample(dt, isDown)` folds the current held-key state into the
|
|
29
|
+
* smoothed value; a `setAnalog` override replaces the digital target for that axis until cleared.
|
|
30
|
+
*/
|
|
31
|
+
export class AxisChannel {
|
|
32
|
+
bindings;
|
|
33
|
+
smoothing;
|
|
34
|
+
current = { ...NEUTRAL_AXIS };
|
|
35
|
+
overrides = {};
|
|
36
|
+
constructor(config) {
|
|
37
|
+
this.bindings = config.bindings;
|
|
38
|
+
this.smoothing = config.smoothing ?? 6;
|
|
39
|
+
}
|
|
40
|
+
setAnalog(axis, value) {
|
|
41
|
+
this.overrides[axis] = clampAxis(value, AXIS_RANGE[axis]);
|
|
42
|
+
}
|
|
43
|
+
clearAnalog(axis) {
|
|
44
|
+
delete this.overrides[axis];
|
|
45
|
+
}
|
|
46
|
+
reset() {
|
|
47
|
+
this.current.throttle = 0;
|
|
48
|
+
this.current.brake = 0;
|
|
49
|
+
this.current.steer = 0;
|
|
50
|
+
this.current.handbrake = 0;
|
|
51
|
+
}
|
|
52
|
+
get value() {
|
|
53
|
+
return this.current;
|
|
54
|
+
}
|
|
55
|
+
sample(dt, isDown) {
|
|
56
|
+
for (const axis of Object.keys(this.bindings)) {
|
|
57
|
+
const range = AXIS_RANGE[axis];
|
|
58
|
+
const override = this.overrides[axis];
|
|
59
|
+
const target = override !== undefined ? override : clampAxis(digitalTarget(this.bindings[axis], isDown), range);
|
|
60
|
+
this.current[axis] = clampAxis(rampToward(this.current[axis], target, this.smoothing, dt), range);
|
|
61
|
+
}
|
|
62
|
+
return this.current;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export const DRIVE_AXIS_BINDINGS = {
|
|
66
|
+
throttle: { positive: ["KeyW", "ArrowUp"] },
|
|
67
|
+
brake: { positive: ["KeyS", "ArrowDown"] },
|
|
68
|
+
steer: { positive: ["KeyD", "ArrowRight"], negative: ["KeyA", "ArrowLeft"] },
|
|
69
|
+
handbrake: { positive: ["Space"] },
|
|
70
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-finger play-surface gesture recognizer for touch controls: taps,
|
|
3
|
+
* directional flick swipes, and continuous step drags all resolve to action
|
|
4
|
+
* names from a TouchGestureBindings declaration. Drag steps stream out of
|
|
5
|
+
* move() as the finger travels; taps and swipes resolve on end(). Timestamps
|
|
6
|
+
* are injected so the state machine stays pure; the capture layer owns touch
|
|
7
|
+
* identifiers and pointer capture.
|
|
8
|
+
*/
|
|
9
|
+
import type { TouchGestureBindings } from "./touchScheme.js";
|
|
10
|
+
export interface GestureSurfaceTuning {
|
|
11
|
+
/** A finger that moves less than this (px) and lifts quickly is a tap. */
|
|
12
|
+
tapMoveThresholdPx: number;
|
|
13
|
+
tapMaxMs: number;
|
|
14
|
+
/** Minimum travel (px) for a lift to register as a swipe. */
|
|
15
|
+
swipeMinPx: number;
|
|
16
|
+
/** Minimum lift velocity (px/ms) for a swipe; slower travel is a drag that ends quietly. */
|
|
17
|
+
swipeMinVelocity: number;
|
|
18
|
+
/** Default travel (px) per repeated drag action when the binding sets none. */
|
|
19
|
+
dragStepPx: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_GESTURE_TUNING: GestureSurfaceTuning;
|
|
22
|
+
export interface GestureSurfaceTracker {
|
|
23
|
+
begin(x: number, y: number, nowMs: number): void;
|
|
24
|
+
/** Drag-step actions fired by this movement, in travel order. */
|
|
25
|
+
move(x: number, y: number): readonly string[];
|
|
26
|
+
/** Tap or swipe action resolved by lifting the finger, if any. */
|
|
27
|
+
end(x: number, y: number, nowMs: number): readonly string[];
|
|
28
|
+
cancel(): void;
|
|
29
|
+
isActive(): boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function createGestureSurfaceTracker(bindings: TouchGestureBindings, tuning?: GestureSurfaceTuning): GestureSurfaceTracker;
|