@inventure71/paddockjs 1.0.0 → 2.0.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 +52 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +194 -12
- package/docs/architecture.md +119 -44
- package/docs/concepts.md +4 -2
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +299 -17
- package/docs/index.md +1 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +37 -10
- package/docs/system_specs.md +23 -10
- package/docs/training.md +272 -30
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/package.json +12 -4
- package/src/api/PaddockSimulatorController.js +4 -0
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +305 -1603
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +4 -0
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +20 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +361 -9
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observations.js +325 -40
- package/src/environment/options.js +37 -5
- package/src/environment/runtime.js +222 -21
- package/src/environment/scenarios.js +37 -8
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +65 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +100 -5
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +169 -6
- package/src/index.js +5 -0
- package/src/rendering/proceduralTrackAsset.js +20 -823
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +1 -1
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -619
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +92 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +230 -2817
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +141 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/trackLimitsSteward.js +1 -1
- package/src/simulation/rulesConfig.js +5 -323
- package/src/simulation/snapshots/raceSnapshots.js +192 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryIndex.js +945 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -1706
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -247
- package/src/simulation/vehiclePhysics.js +1 -136
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -338
- package/src/styles.css +42 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +14 -360
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +5 -4
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createCarRayMiss, estimateCarHit } from './carRays.js';
|
|
2
|
+
import { normalizeRayOptions } from './rayConfig.js';
|
|
3
|
+
import { degreesToRadians, getCarRayOrigin } from './rayGeometry.js';
|
|
4
|
+
import { rayDetectableTargetsForSnapshot } from './sensorTargets.js';
|
|
5
|
+
import { createTrackMiss, createTrackRayContext, estimateTrackHit } from './trackRays.js';
|
|
6
|
+
import { createSurfaceMiss, estimateSurfaceHits, requestedSurfaceChannels } from './surfaceRays.js';
|
|
7
|
+
import { canUseBatchTrainingRayApproximation } from './rayGuards.js';
|
|
8
|
+
|
|
9
|
+
export { buildNearbyCars } from './nearbyCars.js';
|
|
10
|
+
export { DEFAULT_RAY_ANGLES_DEGREES } from './rayDefaults.js';
|
|
11
|
+
export { normalizeRayOptions, RAY_CHANNELS, RAY_LAYOUT_PRESETS } from './rayConfig.js';
|
|
12
|
+
export { getCarRayOrigin, getCarRayVector } from './rayGeometry.js';
|
|
13
|
+
|
|
14
|
+
export function createRayBatchContext(snapshot) {
|
|
15
|
+
return {
|
|
16
|
+
rayTargets: rayDetectableTargetsForSnapshot(snapshot),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildRaySensors(car, snapshot, rayOptions = {}, batchContext = null) {
|
|
21
|
+
const normalized = normalizeRayOptions(rayOptions);
|
|
22
|
+
if (!normalized.enabled) return [];
|
|
23
|
+
if (car?.destroyed || car?.outOfRace) {
|
|
24
|
+
return buildInactiveCarRays(normalized);
|
|
25
|
+
}
|
|
26
|
+
const origin = getCarRayOrigin(car);
|
|
27
|
+
const usesTrackContext = normalized.channels.includes('roadEdge') ||
|
|
28
|
+
requestedSurfaceChannels(normalized.channels).length > 0;
|
|
29
|
+
const trackContext = !usesTrackContext
|
|
30
|
+
? null
|
|
31
|
+
: createTrackRayContext(car, snapshot, origin);
|
|
32
|
+
const carTargets = normalized.channels.includes('car')
|
|
33
|
+
? (batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot)).filter((target) => target.id !== car.id)
|
|
34
|
+
: [];
|
|
35
|
+
|
|
36
|
+
if (canUseFastBatchTrainingRays(car, snapshot, trackContext)) {
|
|
37
|
+
return buildFastBatchTrainingRays(car, snapshot, normalized, origin, carTargets, trackContext);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return normalized.rays.map((ray) => {
|
|
41
|
+
const angleDegrees = ray.angleDegrees;
|
|
42
|
+
const vector = {
|
|
43
|
+
x: Math.cos((car.heading ?? 0) + degreesToRadians(angleDegrees)),
|
|
44
|
+
y: Math.sin((car.heading ?? 0) + degreesToRadians(angleDegrees)),
|
|
45
|
+
};
|
|
46
|
+
const sharedRayQuery = {};
|
|
47
|
+
const roadEdge = normalized.channels.includes('roadEdge')
|
|
48
|
+
? estimateTrackHit(
|
|
49
|
+
car,
|
|
50
|
+
snapshot,
|
|
51
|
+
angleDegrees,
|
|
52
|
+
ray.lengthMeters,
|
|
53
|
+
trackContext,
|
|
54
|
+
{ precision: normalized.precision, sharedRayQuery },
|
|
55
|
+
)
|
|
56
|
+
: createTrackMiss(ray.lengthMeters);
|
|
57
|
+
const carHit = normalized.channels.includes('car')
|
|
58
|
+
? estimateCarHit(car, snapshot, angleDegrees, ray.lengthMeters, origin, carTargets)
|
|
59
|
+
: createCarRayMiss(ray.lengthMeters);
|
|
60
|
+
const surfaceHits = estimateSurfaceHits(
|
|
61
|
+
car,
|
|
62
|
+
snapshot,
|
|
63
|
+
ray,
|
|
64
|
+
origin,
|
|
65
|
+
vector,
|
|
66
|
+
normalized.channels,
|
|
67
|
+
trackContext,
|
|
68
|
+
{ precision: normalized.precision, sharedRayQuery },
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: ray.id,
|
|
73
|
+
angleDegrees,
|
|
74
|
+
angleRadians: degreesToRadians(angleDegrees),
|
|
75
|
+
lengthMeters: ray.lengthMeters,
|
|
76
|
+
roadEdge,
|
|
77
|
+
track: roadEdge,
|
|
78
|
+
kerb: surfaceHits.kerb ?? createSurfaceMiss(ray.lengthMeters),
|
|
79
|
+
illegalSurface: surfaceHits.illegalSurface ?? createSurfaceMiss(ray.lengthMeters),
|
|
80
|
+
car: carHit,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildInactiveCarRays(normalized) {
|
|
86
|
+
return normalized.rays.map((ray) => ({
|
|
87
|
+
id: ray.id,
|
|
88
|
+
angleDegrees: ray.angleDegrees,
|
|
89
|
+
angleRadians: degreesToRadians(ray.angleDegrees),
|
|
90
|
+
lengthMeters: ray.lengthMeters,
|
|
91
|
+
roadEdge: createTrackMiss(ray.lengthMeters),
|
|
92
|
+
track: createTrackMiss(ray.lengthMeters),
|
|
93
|
+
kerb: createSurfaceMiss(ray.lengthMeters),
|
|
94
|
+
illegalSurface: createSurfaceMiss(ray.lengthMeters),
|
|
95
|
+
car: createCarRayMiss(ray.lengthMeters),
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function canUseFastBatchTrainingRays(car, snapshot, trackContext) {
|
|
100
|
+
return car?.interaction?.profile === 'batch-training' &&
|
|
101
|
+
!car.inPitLane &&
|
|
102
|
+
!trackContext?.originState?.inPitLane &&
|
|
103
|
+
Array.isArray(snapshot.track?.samples) &&
|
|
104
|
+
snapshot.track.samples.length > 0 &&
|
|
105
|
+
trackContext?.originState &&
|
|
106
|
+
canUseBatchTrainingRayApproximation(snapshot.track, trackContext.originState);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildFastBatchTrainingRays(car, snapshot, normalized, origin, carTargets, trackContext) {
|
|
110
|
+
const channels = new Set(normalized.channels);
|
|
111
|
+
|
|
112
|
+
return normalized.rays.map((ray) => {
|
|
113
|
+
const angleRadians = degreesToRadians(ray.angleDegrees);
|
|
114
|
+
const heading = (car.heading ?? 0) + angleRadians;
|
|
115
|
+
const vector = { x: Math.cos(heading), y: Math.sin(heading) };
|
|
116
|
+
const sharedRayQuery = {};
|
|
117
|
+
const roadEdge = channels.has('roadEdge')
|
|
118
|
+
? estimateTrackHit(
|
|
119
|
+
car,
|
|
120
|
+
snapshot,
|
|
121
|
+
ray.angleDegrees,
|
|
122
|
+
ray.lengthMeters,
|
|
123
|
+
trackContext,
|
|
124
|
+
{ precision: normalized.precision, sharedRayQuery },
|
|
125
|
+
)
|
|
126
|
+
: createTrackMiss(ray.lengthMeters);
|
|
127
|
+
const carHit = channels.has('car') && carTargets.length > 0
|
|
128
|
+
? estimateCarHit(car, snapshot, ray.angleDegrees, ray.lengthMeters, origin, carTargets)
|
|
129
|
+
: createCarRayMiss(ray.lengthMeters);
|
|
130
|
+
const surfaceHits = estimateSurfaceHits(
|
|
131
|
+
car,
|
|
132
|
+
snapshot,
|
|
133
|
+
ray,
|
|
134
|
+
origin,
|
|
135
|
+
vector,
|
|
136
|
+
normalized.channels,
|
|
137
|
+
trackContext,
|
|
138
|
+
{ precision: normalized.precision, sharedRayQuery },
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
id: ray.id,
|
|
143
|
+
angleDegrees: ray.angleDegrees,
|
|
144
|
+
angleRadians,
|
|
145
|
+
lengthMeters: ray.lengthMeters,
|
|
146
|
+
roadEdge,
|
|
147
|
+
track: roadEdge,
|
|
148
|
+
kerb: channels.has('kerb')
|
|
149
|
+
? (surfaceHits.kerb ?? createSurfaceMiss(ray.lengthMeters))
|
|
150
|
+
: createSurfaceMiss(ray.lengthMeters),
|
|
151
|
+
illegalSurface: channels.has('illegalSurface')
|
|
152
|
+
? (surfaceHits.illegalSurface ?? createSurfaceMiss(ray.lengthMeters))
|
|
153
|
+
: createSurfaceMiss(ray.lengthMeters),
|
|
154
|
+
car: carHit,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { queryTrackSegmentsAlongRay } from '../../simulation/track/trackQueryIndex.js';
|
|
2
|
+
import { metersToSimUnits } from '../../simulation/units.js';
|
|
3
|
+
|
|
4
|
+
const RAY_BOUND_QUERY_MARGIN_METERS = 18;
|
|
5
|
+
|
|
6
|
+
export function findIndexedRayBoundaryHit(track, origin, vector, lengthMeters, offsets) {
|
|
7
|
+
const boundaries = findIndexedRayBoundaryDistances(track, origin, vector, lengthMeters, offsets);
|
|
8
|
+
if (!boundaries.available) return { available: false, distance: null };
|
|
9
|
+
return {
|
|
10
|
+
available: true,
|
|
11
|
+
distance: minFinite(...boundaries.distances),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findIndexedRayBoundaryDistances(track, origin, vector, lengthMeters, offsets) {
|
|
16
|
+
const finiteOffsets = offsets.filter(Number.isFinite);
|
|
17
|
+
if (finiteOffsets.length === 0) return { available: true, distances: [] };
|
|
18
|
+
const maxDistance = metersToSimUnits(lengthMeters);
|
|
19
|
+
const maxOffset = Math.max(0, ...finiteOffsets.map((offset) => Math.abs(offset)));
|
|
20
|
+
const margin = maxOffset + metersToSimUnits(RAY_BOUND_QUERY_MARGIN_METERS);
|
|
21
|
+
const segments = queryTrackSegmentsAlongRay(track, origin, vector, maxDistance, margin);
|
|
22
|
+
if (!segments) return { available: false, distances: [] };
|
|
23
|
+
const distances = finiteOffsets.map(() => Infinity);
|
|
24
|
+
|
|
25
|
+
for (const segment of segments) {
|
|
26
|
+
finiteOffsets.forEach((offset, offsetIndex) => {
|
|
27
|
+
const start = offsetSegmentStart(segment, offset);
|
|
28
|
+
const finish = offsetSegmentEnd(segment, offset);
|
|
29
|
+
const distance = raySegmentIntersectionDistance(origin, vector, start, finish, maxDistance);
|
|
30
|
+
if (distance != null && distance < distances[offsetIndex]) distances[offsetIndex] = distance;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
available: true,
|
|
36
|
+
distances: distances.map((distance) => (Number.isFinite(distance) ? distance : null)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function findIndexedTrackBandBoundaries(
|
|
41
|
+
track,
|
|
42
|
+
origin,
|
|
43
|
+
vector,
|
|
44
|
+
lengthMeters,
|
|
45
|
+
trackHalfWidth,
|
|
46
|
+
kerbOuterWidth,
|
|
47
|
+
cache = null,
|
|
48
|
+
) {
|
|
49
|
+
if (cache?.trackBandBoundaries) return cache.trackBandBoundaries;
|
|
50
|
+
const boundaries = findIndexedRayBoundaryDistances(
|
|
51
|
+
track,
|
|
52
|
+
origin,
|
|
53
|
+
vector,
|
|
54
|
+
lengthMeters,
|
|
55
|
+
[trackHalfWidth, -trackHalfWidth, kerbOuterWidth, -kerbOuterWidth],
|
|
56
|
+
);
|
|
57
|
+
const result = {
|
|
58
|
+
available: boundaries.available,
|
|
59
|
+
trackEdgeDistance: minFinite(boundaries.distances[0], boundaries.distances[1]),
|
|
60
|
+
kerbOuterDistance: minFinite(boundaries.distances[2], boundaries.distances[3]),
|
|
61
|
+
};
|
|
62
|
+
if (cache) cache.trackBandBoundaries = result;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function offsetSegmentStart(segment, offset) {
|
|
67
|
+
return {
|
|
68
|
+
x: segment.startX + segment.normalX * offset,
|
|
69
|
+
y: segment.startY + segment.normalY * offset,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function offsetSegmentEnd(segment, offset) {
|
|
74
|
+
return {
|
|
75
|
+
x: segment.endX + segment.endNormalX * offset,
|
|
76
|
+
y: segment.endY + segment.endNormalY * offset,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function raySegmentIntersectionDistance(origin, ray, start, end, maxDistance) {
|
|
81
|
+
const sx = end.x - start.x;
|
|
82
|
+
const sy = end.y - start.y;
|
|
83
|
+
const denominator = cross(ray.x, ray.y, sx, sy);
|
|
84
|
+
if (Math.abs(denominator) < 1e-9) return null;
|
|
85
|
+
|
|
86
|
+
const ox = start.x - origin.x;
|
|
87
|
+
const oy = start.y - origin.y;
|
|
88
|
+
const rayDistance = cross(ox, oy, sx, sy) / denominator;
|
|
89
|
+
const segmentAmount = cross(ox, oy, ray.x, ray.y) / denominator;
|
|
90
|
+
|
|
91
|
+
if (rayDistance < 0 || rayDistance > maxDistance) return null;
|
|
92
|
+
if (segmentAmount < -1e-6 || segmentAmount > 1 + 1e-6) return null;
|
|
93
|
+
return rayDistance;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cross(ax, ay, bx, by) {
|
|
97
|
+
return ax * by - ay * bx;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function minFinite(...values) {
|
|
101
|
+
const minimum = Math.min(...values.filter((value) => Number.isFinite(value)));
|
|
102
|
+
return Number.isFinite(minimum) ? minimum : null;
|
|
103
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
|
|
2
|
+
import { normalizeRelativeHeading } from './rayGeometry.js';
|
|
3
|
+
import { nearbyDetectableTargets } from './sensorTargets.js';
|
|
4
|
+
|
|
5
|
+
export function buildNearbyCars(car, snapshot, { maxCars = 6, radiusMeters = 150 } = {}) {
|
|
6
|
+
const limit = Math.max(0, Math.floor(maxCars));
|
|
7
|
+
if (limit === 0) return [];
|
|
8
|
+
const radius = metersToSimUnits(radiusMeters);
|
|
9
|
+
const radiusSquared = radius * radius;
|
|
10
|
+
const forwardX = Math.cos(car.heading);
|
|
11
|
+
const forwardY = Math.sin(car.heading);
|
|
12
|
+
const rightX = -Math.sin(car.heading);
|
|
13
|
+
const rightY = Math.cos(car.heading);
|
|
14
|
+
const nearest = [];
|
|
15
|
+
|
|
16
|
+
nearbyDetectableTargets(car, snapshot).forEach((other) => {
|
|
17
|
+
const dx = other.x - car.x;
|
|
18
|
+
const dy = other.y - car.y;
|
|
19
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
20
|
+
if (distanceSquared > radiusSquared) return;
|
|
21
|
+
|
|
22
|
+
const forward = forwardX * dx + forwardY * dy;
|
|
23
|
+
const right = rightX * dx + rightY * dy;
|
|
24
|
+
const entry = {
|
|
25
|
+
id: other.id,
|
|
26
|
+
relativeForwardMeters: simUnitsToMeters(forward),
|
|
27
|
+
relativeRightMeters: simUnitsToMeters(right),
|
|
28
|
+
relativeDistanceMeters: simUnitsToMeters(Math.sqrt(distanceSquared)),
|
|
29
|
+
relativeSpeedKph: other.speedKph - car.speedKph,
|
|
30
|
+
relativeHeadingRadians: normalizeRelativeHeading(other.heading - car.heading),
|
|
31
|
+
ahead: forward > 0,
|
|
32
|
+
sameLap: other.lap != null && other.lap === car.lap,
|
|
33
|
+
entityType: other.entityType,
|
|
34
|
+
distanceSquared,
|
|
35
|
+
order: other.order,
|
|
36
|
+
};
|
|
37
|
+
insertNearestCar(nearest, entry, limit);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return nearest.map(({ distanceSquared, order, ...entry }) => entry);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function insertNearestCar(nearest, entry, limit) {
|
|
44
|
+
let insertAt = nearest.findIndex((candidate) => (
|
|
45
|
+
entry.distanceSquared < candidate.distanceSquared ||
|
|
46
|
+
(entry.distanceSquared === candidate.distanceSquared && entry.order < candidate.order)
|
|
47
|
+
));
|
|
48
|
+
if (insertAt < 0) insertAt = nearest.length;
|
|
49
|
+
if (insertAt >= limit) return;
|
|
50
|
+
nearest.splice(insertAt, 0, entry);
|
|
51
|
+
if (nearest.length > limit) nearest.pop();
|
|
52
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { kphToSimSpeed, simSpeedToMetersPerSecond, simUnitsToMeters } from '../../simulation/units.js';
|
|
2
|
+
import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
|
|
3
|
+
|
|
4
|
+
export function enrichOpponentRadar(car, nearbyCars, snapshot) {
|
|
5
|
+
const targetsById = new Map([
|
|
6
|
+
...(snapshot.cars ?? []).map((entry) => [entry.id, entry]),
|
|
7
|
+
...(snapshot.replayGhosts ?? []).map((entry) => [entry.id, entry]),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
return nearbyCars.map((nearby) => {
|
|
11
|
+
const target = targetsById.get(nearby.id);
|
|
12
|
+
const derived = target ? deriveRadarFields(car, target, nearby) : fallbackRadarFields(nearby);
|
|
13
|
+
return {
|
|
14
|
+
...nearby,
|
|
15
|
+
...derived,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function deriveRadarFields(car, target, nearby) {
|
|
21
|
+
const dx = (target.x ?? 0) - (car.x ?? 0);
|
|
22
|
+
const dy = (target.y ?? 0) - (car.y ?? 0);
|
|
23
|
+
const distance = Math.max(1e-9, Math.hypot(dx, dy));
|
|
24
|
+
const selfVelocity = velocity(car);
|
|
25
|
+
const targetVelocity = velocity(target);
|
|
26
|
+
const relativeVelocity = {
|
|
27
|
+
x: targetVelocity.x - selfVelocity.x,
|
|
28
|
+
y: targetVelocity.y - selfVelocity.y,
|
|
29
|
+
};
|
|
30
|
+
const closingRate = -((relativeVelocity.x * dx + relativeVelocity.y * dy) / distance);
|
|
31
|
+
const closingRateMetersPerSecond = simSpeedToMetersPerSecond(closingRate);
|
|
32
|
+
const distanceMeters = nearby.relativeDistanceMeters ?? simUnitsToMeters(distance);
|
|
33
|
+
const longitudinalOverlap = Math.abs(nearby.relativeForwardMeters ?? 0) <= simUnitsToMeters(VEHICLE_GEOMETRY.bodyLength);
|
|
34
|
+
const lateralOverlap = Math.abs(nearby.relativeRightMeters ?? 0) <= simUnitsToMeters(VEHICLE_GEOMETRY.bodyWidth);
|
|
35
|
+
const sideOverlap = longitudinalOverlap && lateralOverlap;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
behind: !nearby.ahead,
|
|
39
|
+
closingRateMetersPerSecond,
|
|
40
|
+
timeToContactSeconds: closingRateMetersPerSecond > 0
|
|
41
|
+
? distanceMeters / closingRateMetersPerSecond
|
|
42
|
+
: null,
|
|
43
|
+
leftOverlap: sideOverlap && (nearby.relativeRightMeters ?? 0) < 0,
|
|
44
|
+
rightOverlap: sideOverlap && (nearby.relativeRightMeters ?? 0) > 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fallbackRadarFields(nearby) {
|
|
49
|
+
return {
|
|
50
|
+
behind: !nearby.ahead,
|
|
51
|
+
closingRateMetersPerSecond: 0,
|
|
52
|
+
timeToContactSeconds: null,
|
|
53
|
+
leftOverlap: false,
|
|
54
|
+
rightOverlap: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function velocity(car) {
|
|
59
|
+
const speed = Number.isFinite(car.speed) ? car.speed : kphToSimSpeed(car.speedKph ?? 0);
|
|
60
|
+
const heading = car.heading ?? car.headingRadians ?? 0;
|
|
61
|
+
return {
|
|
62
|
+
x: Math.cos(heading) * speed,
|
|
63
|
+
y: Math.sin(heading) * speed,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { DEFAULT_RAY_ANGLES_DEGREES } from './rayDefaults.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_RAY_LENGTH_METERS = 120;
|
|
4
|
+
|
|
5
|
+
export const RAY_CHANNELS = Object.freeze([
|
|
6
|
+
'roadEdge',
|
|
7
|
+
'kerb',
|
|
8
|
+
'illegalSurface',
|
|
9
|
+
'car',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const RAY_PRECISION_MODES = Object.freeze(['driver', 'debug']);
|
|
13
|
+
|
|
14
|
+
export const RAY_LAYOUT_PRESETS = Object.freeze({
|
|
15
|
+
compact: DEFAULT_RAY_ANGLES_DEGREES.map((angleDegrees) => ({ angleDegrees })),
|
|
16
|
+
'driver-front-heavy': [
|
|
17
|
+
-140, -100, -70, -50, -35, -20, -10, 0, 10, 20, 35, 50, 70, 100, 140, 180,
|
|
18
|
+
].map((angleDegrees) => ({
|
|
19
|
+
angleDegrees,
|
|
20
|
+
lengthMeters: Math.abs(angleDegrees) <= 70 ? 240 : Math.abs(angleDegrees) <= 100 ? 90 : 60,
|
|
21
|
+
})),
|
|
22
|
+
'lidar-lite': Array.from({ length: 25 }, (_, index) => ({
|
|
23
|
+
angleDegrees: -120 + index * 10,
|
|
24
|
+
lengthMeters: Math.abs(-120 + index * 10) <= 70 ? 260 : 100,
|
|
25
|
+
})),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export function normalizeRayOptions(rayOptions = {}) {
|
|
29
|
+
const defaultLengthMeters = positiveNumber(
|
|
30
|
+
rayOptions.defaultLengthMeters ?? rayOptions.lengthMeters,
|
|
31
|
+
DEFAULT_RAY_LENGTH_METERS,
|
|
32
|
+
);
|
|
33
|
+
const rays = normalizeRays(rayOptions, defaultLengthMeters);
|
|
34
|
+
const channels = normalizeRayChannels(rayOptions);
|
|
35
|
+
const precision = RAY_PRECISION_MODES.includes(rayOptions.precision) ? rayOptions.precision : 'driver';
|
|
36
|
+
const anglesDegrees = rays.map((ray) => ray.angleDegrees);
|
|
37
|
+
const lengthMeters = rays.length ? Math.max(...rays.map((ray) => ray.lengthMeters)) : defaultLengthMeters;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...rayOptions,
|
|
41
|
+
enabled: rayOptions.enabled !== false,
|
|
42
|
+
anglesDegrees,
|
|
43
|
+
lengthMeters,
|
|
44
|
+
defaultLengthMeters,
|
|
45
|
+
rays,
|
|
46
|
+
channels,
|
|
47
|
+
precision,
|
|
48
|
+
detectTrack: channels.includes('roadEdge'),
|
|
49
|
+
detectCars: channels.includes('car'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeRays(rayOptions, defaultLengthMeters) {
|
|
54
|
+
const configuredRays = Array.isArray(rayOptions.rays)
|
|
55
|
+
? rayOptions.rays
|
|
56
|
+
: resolvePresetRays(rayOptions.layout);
|
|
57
|
+
const source = configuredRays.length
|
|
58
|
+
? configuredRays
|
|
59
|
+
: normalizeAngles(rayOptions.anglesDegrees).map((angleDegrees) => ({ angleDegrees }));
|
|
60
|
+
|
|
61
|
+
return source.map((ray, index) => {
|
|
62
|
+
const angleDegrees = finiteNumber(
|
|
63
|
+
typeof ray === 'number' ? ray : ray?.angleDegrees,
|
|
64
|
+
DEFAULT_RAY_ANGLES_DEGREES[index % DEFAULT_RAY_ANGLES_DEGREES.length],
|
|
65
|
+
);
|
|
66
|
+
const lengthMeters = positiveNumber(
|
|
67
|
+
typeof ray === 'object' ? ray.lengthMeters : null,
|
|
68
|
+
defaultLengthMeters,
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
id: typeof ray === 'object' && ray.id ? String(ray.id) : `ray-${index}`,
|
|
72
|
+
angleDegrees,
|
|
73
|
+
lengthMeters,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolvePresetRays(layout) {
|
|
79
|
+
if (typeof layout !== 'string') return [];
|
|
80
|
+
return RAY_LAYOUT_PRESETS[layout] ?? [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeAngles(value) {
|
|
84
|
+
if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RAY_ANGLES_DEGREES];
|
|
85
|
+
const angles = value.map((angle) => Number(angle)).filter((angle) => Number.isFinite(angle));
|
|
86
|
+
return angles.length ? angles : [...DEFAULT_RAY_ANGLES_DEGREES];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeRayChannels(rayOptions) {
|
|
90
|
+
if (Array.isArray(rayOptions.channels) && rayOptions.channels.length) {
|
|
91
|
+
const channelSet = new Set(RAY_CHANNELS);
|
|
92
|
+
const channels = rayOptions.channels.filter((channel) => channelSet.has(channel));
|
|
93
|
+
if (channels.length) return [...new Set(channels)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const channels = [];
|
|
97
|
+
if (rayOptions.detectTrack !== false) channels.push('roadEdge');
|
|
98
|
+
if (rayOptions.detectCars !== false) channels.push('car');
|
|
99
|
+
return channels;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function positiveNumber(value, fallback) {
|
|
103
|
+
const number = Number(value);
|
|
104
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function finiteNumber(value, fallback) {
|
|
108
|
+
const number = Number(value);
|
|
109
|
+
return Number.isFinite(number) ? number : fallback;
|
|
110
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const TRACK_RAY_STEP_METERS = 1;
|
|
2
|
+
export const TRACK_RAY_REFINE_STEPS = 12;
|
|
3
|
+
export const DRIVER_RAY_REFINE_STEPS = 0;
|
|
4
|
+
export const ANALYTIC_TRACK_RAY_MAX_CURVATURE = 0.00005;
|
|
5
|
+
export const PIT_CONNECTOR_RAY_FALLBACK_METERS = 55;
|
|
6
|
+
export const DEFAULT_RAY_ANGLES_DEGREES = [-135, -60, -20, 0, 20, 60, 135, 180];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function getCarRayOrigin(car) {
|
|
2
|
+
return {
|
|
3
|
+
x: car.x,
|
|
4
|
+
y: car.y,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getCarRayVector(car, angleDegrees) {
|
|
9
|
+
const angle = car.heading + degreesToRadians(angleDegrees);
|
|
10
|
+
return {
|
|
11
|
+
x: Math.cos(angle),
|
|
12
|
+
y: Math.sin(angle),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function pointOnRay(origin, ray, distance) {
|
|
17
|
+
return {
|
|
18
|
+
x: origin.x + ray.x * distance,
|
|
19
|
+
y: origin.y + ray.y * distance,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function intersectAxisAlignedBoxRay(origin, ray, halfLength, halfWidth) {
|
|
24
|
+
let tMin = -Infinity;
|
|
25
|
+
let tMax = Infinity;
|
|
26
|
+
const xRange = intersectSlab(origin.x, ray.x, -halfLength, halfLength);
|
|
27
|
+
const yRange = intersectSlab(origin.y, ray.y, -halfWidth, halfWidth);
|
|
28
|
+
if (!xRange || !yRange) return null;
|
|
29
|
+
tMin = Math.max(tMin, xRange.min, yRange.min);
|
|
30
|
+
tMax = Math.min(tMax, xRange.max, yRange.max);
|
|
31
|
+
if (tMax < 0 || tMin > tMax) return null;
|
|
32
|
+
return Math.max(0, tMin);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function dot(a, b) {
|
|
36
|
+
return a.x * b.x + a.y * b.y;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function degreesToRadians(degrees) {
|
|
40
|
+
return (degrees * Math.PI) / 180;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeRelativeHeading(angle) {
|
|
44
|
+
let value = angle;
|
|
45
|
+
while (value > Math.PI) value -= Math.PI * 2;
|
|
46
|
+
while (value < -Math.PI) value += Math.PI * 2;
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function intersectSlab(origin, direction, min, max) {
|
|
51
|
+
if (Math.abs(direction) < 1e-9) {
|
|
52
|
+
return origin >= min && origin <= max ? { min: -Infinity, max: Infinity } : null;
|
|
53
|
+
}
|
|
54
|
+
const first = (min - origin) / direction;
|
|
55
|
+
const second = (max - origin) / direction;
|
|
56
|
+
return {
|
|
57
|
+
min: Math.min(first, second),
|
|
58
|
+
max: Math.max(first, second),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { metersToSimUnits } from '../../simulation/units.js';
|
|
2
|
+
|
|
3
|
+
export function canUseLocalStripRayApproximation(track, state) {
|
|
4
|
+
if (!track || !state || state.inPitLane) return false;
|
|
5
|
+
const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
|
|
6
|
+
return Number.isFinite(state.crossTrackError) && state.crossTrackError <= kerbOuter;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function canUseIndexedRecoveryRayApproximation(track, state) {
|
|
10
|
+
if (!track || !state || state.inPitLane) return false;
|
|
11
|
+
const recoveryBand = track.width / 2 +
|
|
12
|
+
(track.kerbWidth ?? 0) +
|
|
13
|
+
(track.gravelWidth ?? 0) +
|
|
14
|
+
(track.runoffWidth ?? 0) +
|
|
15
|
+
metersToSimUnits(64);
|
|
16
|
+
return Number.isFinite(state.crossTrackError) && state.crossTrackError <= recoveryBand;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const canUseBatchTrainingRayApproximation = canUseIndexedRecoveryRayApproximation;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNearbyDetectable,
|
|
3
|
+
isRayDetectable,
|
|
4
|
+
} from '../../simulation/participants/participantInteractions.js';
|
|
5
|
+
|
|
6
|
+
export const SENSOR_TARGET_CAR = 'car';
|
|
7
|
+
export const SENSOR_TARGET_REPLAY_GHOST = 'replayGhost';
|
|
8
|
+
|
|
9
|
+
export function rayDetectableTargets(self, snapshot) {
|
|
10
|
+
return rayDetectableTargetsForSnapshot(snapshot).filter((target) => target.id !== self.id);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function nearbyDetectableTargets(self, snapshot) {
|
|
14
|
+
return nearbyDetectableTargetsForSnapshot(snapshot).filter((target) => target.id !== self.id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function rayDetectableTargetsForSnapshot(snapshot) {
|
|
18
|
+
return [
|
|
19
|
+
...detectableCars(snapshot, isRayDetectable),
|
|
20
|
+
...detectableReplayGhosts(snapshot, 'detectableByRays'),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function nearbyDetectableTargetsForSnapshot(snapshot) {
|
|
25
|
+
return [
|
|
26
|
+
...detectableCars(snapshot, isNearbyDetectable),
|
|
27
|
+
...detectableReplayGhosts(snapshot, 'detectableAsNearby'),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function detectableCars(snapshot, predicate) {
|
|
32
|
+
return (snapshot.cars ?? [])
|
|
33
|
+
.filter((car) => predicate(car))
|
|
34
|
+
.map((car, order) => ({
|
|
35
|
+
entityType: SENSOR_TARGET_CAR,
|
|
36
|
+
id: car.id,
|
|
37
|
+
x: car.x,
|
|
38
|
+
y: car.y,
|
|
39
|
+
heading: car.heading,
|
|
40
|
+
speedKph: car.speedKph ?? 0,
|
|
41
|
+
lap: car.lap,
|
|
42
|
+
order,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function detectableReplayGhosts(snapshot, sensorFlag) {
|
|
47
|
+
const carCount = snapshot.cars?.length ?? 0;
|
|
48
|
+
return (snapshot.replayGhosts ?? [])
|
|
49
|
+
.filter((ghost) => ghost.sensors?.[sensorFlag] === true)
|
|
50
|
+
.map((ghost, index) => ({
|
|
51
|
+
entityType: SENSOR_TARGET_REPLAY_GHOST,
|
|
52
|
+
id: ghost.id,
|
|
53
|
+
x: ghost.x,
|
|
54
|
+
y: ghost.y,
|
|
55
|
+
heading: ghost.heading,
|
|
56
|
+
speedKph: ghost.speedKph ?? 0,
|
|
57
|
+
lap: null,
|
|
58
|
+
order: carCount + index,
|
|
59
|
+
}));
|
|
60
|
+
}
|