@inventure71/paddockjs 3.0.0 → 4.1.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 +152 -1
- package/INSTALL_AND_UPDATE.md +21 -4
- package/README.md +103 -551
- package/docs/architecture.md +28 -18
- package/docs/component_inventory.md +5 -4
- package/docs/composable-layouts.md +79 -0
- package/docs/custom_model_controller.md +4 -4
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +13 -7
- package/docs/getting-started.md +105 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +17 -4
- package/docs/rules.md +6 -2
- package/docs/system_specs.md +60 -7
- package/docs/theming.md +64 -0
- package/docs/training.md +20 -14
- package/docs/troubleshooting.md +76 -0
- package/docs/upgrading-hosts-to-3.md +42 -0
- package/examples/python/README.md +6 -0
- package/examples/python/base_policy_server.py +19 -0
- package/package.json +21 -5
- package/src/api/PaddockSimulatorController.js +78 -22
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +24 -3
- package/src/app/readouts/timingTowerRenderer.js +203 -42
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/config/defaultOptions.js +8 -1
- package/src/config/themeOptions.js +5 -0
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/episode.js +11 -1
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/runtime.js +41 -0
- package/src/environment/sensors/carRays.js +48 -37
- package/src/environment/sensors/index.js +501 -97
- package/src/environment/sensors/indexedRayBands.js +134 -46
- package/src/environment/sensors/rayBandTrace.js +1171 -0
- package/src/environment/sensors/rayChannels.js +108 -0
- package/src/environment/sensors/rayConfig.js +71 -25
- package/src/environment/sensors/rayGeometry.js +23 -17
- package/src/environment/sensors/sensorTargets.js +67 -1
- package/src/environment/sensors/surfaceRays.js +130 -89
- package/src/environment/sensors/trackRays.js +115 -1
- package/src/index.d.ts +43 -0
- package/src/index.js +27 -2
- package/src/placeholder/index.d.ts +11 -0
- package/src/placeholder/index.js +61 -0
- package/src/placeholder.css +72 -0
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +77 -42
- package/src/simulation/pit/pitState.js +1 -0
- package/src/simulation/pit/pitTireService.js +7 -3
- package/src/simulation/race/raceFinish.js +25 -8
- package/src/simulation/race/raceOrder.js +189 -32
- package/src/simulation/race/raceProgress.js +129 -37
- package/src/simulation/race/raceSetup.js +2 -0
- package/src/simulation/race/raceStep.js +25 -13
- package/src/simulation/raceSimulation.js +20 -17
- package/src/simulation/rules/collisionSteward.js +40 -23
- package/src/simulation/rules/rulesReview.js +77 -56
- package/src/simulation/snapshots/raceSnapshots.js +37 -0
- package/src/simulation/timing/drsTiming.js +143 -30
- package/src/simulation/timing/gapEstimation.js +25 -3
- package/src/simulation/timing/lapTelemetry.js +115 -36
- package/src/simulation/timing/raceTiming.js +5 -1
- package/src/simulation/timing/sectorPerformance.js +104 -26
- package/src/simulation/timing/timingHistory.js +229 -14
- package/src/simulation/timing/timingLines.js +65 -7
- package/src/simulation/track/pitLaneLayout.js +23 -0
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +67 -11
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1334 -100
- package/src/simulation/track/trackQueryPitIndex.js +138 -14
- package/src/simulation/track/trackQueryScratch.js +7 -0
- package/src/simulation/track/trackQueryStats.js +25 -0
- package/src/simulation/track/trackSnapshotJson.js +139 -0
- package/src/simulation/track/trackStatePolicy.js +249 -1
- package/src/simulation/vehicle/contactResolution.js +96 -59
- package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
- package/src/simulation/vehicle/pitWheelSurface.js +81 -28
- package/src/simulation/vehicle/runoffResponse.js +54 -29
- package/src/simulation/vehicle/vehicleGeometry.js +245 -51
- package/src/simulation/vehicle/vehiclePhysics.js +42 -27
- package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
- package/src/simulation/vehicle/vehicleState.js +6 -0
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/styles.css +497 -497
- package/src/ui/raceControlsTemplate.js +1 -0
- package/src/ui/telemetryDrawerTemplate.js +1 -0
- package/src/ui/telemetryTemplates.js +1 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
distanceToNextLimiterSegment,
|
|
7
7
|
nearestDistanceOnRoute,
|
|
8
8
|
routeLimiterActiveAt,
|
|
9
|
-
|
|
9
|
+
sampleRouteInto,
|
|
10
10
|
} from './pitRouting.js';
|
|
11
11
|
import { syncPitCarKinematics } from './pitKinematics.js';
|
|
12
12
|
import { completePitPenaltyService } from './pitPenaltyService.js';
|
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
pointDistance,
|
|
27
27
|
} from './pitServiceConstants.js';
|
|
28
28
|
|
|
29
|
+
function samplePitStopRoute(stop, route, distanceAlong) {
|
|
30
|
+
const target = stop.routeSample ?? (stop.routeSample = { x: 0, y: 0, heading: 0, limiterActive: false });
|
|
31
|
+
return sampleRouteInto(target, route, distanceAlong);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export function applyPitRoutePosition(sim, car, delta) {
|
|
30
35
|
const stop = car.pitStop;
|
|
31
36
|
if (!stop?.route) return false;
|
|
@@ -33,7 +38,7 @@ export function applyPitRoutePosition(sim, car, delta) {
|
|
|
33
38
|
const route = stop.route;
|
|
34
39
|
if (stop.phase === 'queue-release') {
|
|
35
40
|
const nextProgress = Math.min(route.length, (stop.routeProgress ?? 0) + PIT_QUEUE_RELEASE_SPEED * delta);
|
|
36
|
-
const point =
|
|
41
|
+
const point = samplePitStopRoute(stop, route, nextProgress);
|
|
37
42
|
if (!point) return false;
|
|
38
43
|
const previousHeading = car.heading;
|
|
39
44
|
car.previousX = car.x;
|
|
@@ -63,7 +68,7 @@ export function applyPitRoutePosition(sim, car, delta) {
|
|
|
63
68
|
return route.length - stop.routeProgress <= PIT_QUEUE_RELEASE_FINISH_DISTANCE;
|
|
64
69
|
}
|
|
65
70
|
stop.routeProgress = clamp(nearestDistanceOnRoute(route, car, stop.routeProgress ?? 0), 0, route.length);
|
|
66
|
-
const routeEnd = route.points.
|
|
71
|
+
const routeEnd = route.points[route.points.length - 1];
|
|
67
72
|
const limiterActive = routeLimiterActiveAt(route, stop.routeProgress);
|
|
68
73
|
let targetSpeed = limiterActive
|
|
69
74
|
? Math.min(speedLimit, VEHICLE_LIMITS.maxSpeed)
|
|
@@ -93,7 +98,7 @@ export function applyPitRoutePosition(sim, car, delta) {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
const nextProgress = Math.min(route.length, stop.routeProgress + Math.max(0, targetSpeed) * delta);
|
|
96
|
-
const point =
|
|
101
|
+
const point = samplePitStopRoute(stop, route, nextProgress);
|
|
97
102
|
if (!point) return false;
|
|
98
103
|
const previousSpeed = car.speed;
|
|
99
104
|
const previousHeading = car.heading;
|
|
@@ -128,18 +128,17 @@ export function pointDistance(first, second) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
export function createRoute(points) {
|
|
131
|
-
const routePoints =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}, []);
|
|
131
|
+
const routePoints = [];
|
|
132
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
133
|
+
const point = routePoint(points[index]);
|
|
134
|
+
if (!point) continue;
|
|
135
|
+
const previous = routePoints.length > 0 ? routePoints[routePoints.length - 1] : null;
|
|
136
|
+
if (!sameRoutePoint(previous, point)) {
|
|
137
|
+
routePoints.push(point);
|
|
138
|
+
} else if (point.limiterActive) {
|
|
139
|
+
previous.limiterActive = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
143
142
|
const segments = [];
|
|
144
143
|
let totalLength = 0;
|
|
145
144
|
|
|
@@ -176,6 +175,12 @@ export function createRoute(points) {
|
|
|
176
175
|
};
|
|
177
176
|
}
|
|
178
177
|
|
|
178
|
+
function recordRouteBenchmarkStat(route, key, amount = 1) {
|
|
179
|
+
const stats = route?.runtimeBenchmarkStats;
|
|
180
|
+
if (!stats) return;
|
|
181
|
+
stats[key] = (stats[key] ?? 0) + amount;
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
function findRouteSegment(route, distanceAlong, endPadding = 0) {
|
|
180
185
|
if (!route?.segments?.length) return null;
|
|
181
186
|
const clampedDistance = clamp(distanceAlong, 0, route.length);
|
|
@@ -205,35 +210,62 @@ function findRouteSegment(route, distanceAlong, endPadding = 0) {
|
|
|
205
210
|
clampedDistance < segment.startDistance - 0.001 ||
|
|
206
211
|
clampedDistance > segment.endDistance - endPadding
|
|
207
212
|
) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
recordRouteBenchmarkStat(route, 'segmentFallbackScans');
|
|
214
|
+
segment = segments[segments.length - 1];
|
|
215
|
+
index = segments.length - 1;
|
|
216
|
+
for (let candidateIndex = 0; candidateIndex < segments.length; candidateIndex += 1) {
|
|
217
|
+
const candidate = segments[candidateIndex];
|
|
218
|
+
if (
|
|
219
|
+
clampedDistance >= candidate.startDistance - 0.001 &&
|
|
220
|
+
clampedDistance <= candidate.endDistance - endPadding
|
|
221
|
+
) {
|
|
222
|
+
segment = candidate;
|
|
223
|
+
index = candidateIndex;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
route.segmentCursor = Math.max(0, index);
|
|
216
230
|
return segment;
|
|
217
231
|
}
|
|
218
232
|
|
|
219
|
-
|
|
220
|
-
if (!route?.segments?.length)
|
|
233
|
+
function writeRouteSample(target, route, distanceAlong, statKey) {
|
|
234
|
+
if (!route?.segments?.length) {
|
|
235
|
+
const point = route?.points?.[0] ?? null;
|
|
236
|
+
if (!point || !target) return point;
|
|
237
|
+
target.x = point.x;
|
|
238
|
+
target.y = point.y;
|
|
239
|
+
target.heading = point.heading;
|
|
240
|
+
target.limiterActive = Boolean(point.limiterActive);
|
|
241
|
+
return target;
|
|
242
|
+
}
|
|
243
|
+
const output = target ?? {};
|
|
221
244
|
const clampedDistance = clamp(distanceAlong, 0, route.length);
|
|
222
|
-
const segment = findRouteSegment(route, clampedDistance) ?? route.segments.
|
|
245
|
+
const segment = findRouteSegment(route, clampedDistance) ?? route.segments[route.segments.length - 1];
|
|
223
246
|
const amount = clamp((clampedDistance - segment.startDistance) / segment.length, 0, 1);
|
|
224
247
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
output.x = segment.start.x + (segment.end.x - segment.start.x) * amount;
|
|
249
|
+
output.y = segment.start.y + (segment.end.y - segment.start.y) * amount;
|
|
250
|
+
output.heading = segment.heading;
|
|
251
|
+
output.limiterActive = segment.limiterActive;
|
|
252
|
+
recordRouteBenchmarkStat(route, statKey);
|
|
253
|
+
return output;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function sampleRouteInto(target, route, distanceAlong) {
|
|
257
|
+
return writeRouteSample(target, route, distanceAlong, 'sampleRouteIntoCalls');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function sampleRoute(route, distanceAlong) {
|
|
261
|
+
if (!route?.segments?.length) return route?.points?.[0] ?? null;
|
|
262
|
+
return writeRouteSample({}, route, distanceAlong, 'sampleRouteAllocations');
|
|
231
263
|
}
|
|
232
264
|
|
|
233
265
|
export function routeLimiterActiveAt(route, distanceAlong) {
|
|
234
266
|
if (!route?.segments?.length) return false;
|
|
235
267
|
const clampedDistance = clamp(distanceAlong, 0, route.length);
|
|
236
|
-
const segment = findRouteSegment(route, clampedDistance, 0.001) ?? route.segments.
|
|
268
|
+
const segment = findRouteSegment(route, clampedDistance, 0.001) ?? route.segments[route.segments.length - 1];
|
|
237
269
|
return Boolean(segment?.limiterActive);
|
|
238
270
|
}
|
|
239
271
|
|
|
@@ -251,7 +283,7 @@ function easeInOut(value) {
|
|
|
251
283
|
return amount * amount * (3 - 2 * amount);
|
|
252
284
|
}
|
|
253
285
|
|
|
254
|
-
function
|
|
286
|
+
function projectPositionToSegmentInto(target, position, segment) {
|
|
255
287
|
const dx = segment.end.x - segment.start.x;
|
|
256
288
|
const dy = segment.end.y - segment.start.y;
|
|
257
289
|
const lengthSquared = dx * dx + dy * dy;
|
|
@@ -260,26 +292,29 @@ function projectPositionToSegment(position, segment) {
|
|
|
260
292
|
: 0;
|
|
261
293
|
const x = segment.start.x + dx * amount;
|
|
262
294
|
const y = segment.start.y + dy * amount;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return
|
|
266
|
-
distanceAlong: segment.startDistance + segment.length * amount,
|
|
267
|
-
distanceSquared,
|
|
268
|
-
};
|
|
295
|
+
target.distanceAlong = segment.startDistance + segment.length * amount;
|
|
296
|
+
target.distanceSquared = (position.x - x) ** 2 + (position.y - y) ** 2;
|
|
297
|
+
return target;
|
|
269
298
|
}
|
|
270
299
|
|
|
271
300
|
export function nearestDistanceOnRoute(route, position, previousDistance = 0) {
|
|
272
301
|
if (!route?.segments?.length) return 0;
|
|
273
|
-
let
|
|
302
|
+
let bestDistanceAlong = previousDistance;
|
|
303
|
+
let bestDistanceSquared = Infinity;
|
|
274
304
|
const backtrackAllowance = metersToSimUnits(90);
|
|
305
|
+
const projection = route._projectionScratch ?? (route._projectionScratch = { distanceAlong: 0, distanceSquared: 0 });
|
|
306
|
+
|
|
307
|
+
for (let index = 0; index < route.segments.length; index += 1) {
|
|
308
|
+
const segment = route.segments[index];
|
|
309
|
+
if (segment.endDistance < previousDistance - backtrackAllowance) continue;
|
|
310
|
+
projectPositionToSegmentInto(projection, position, segment);
|
|
311
|
+
if (projection.distanceSquared < bestDistanceSquared) {
|
|
312
|
+
bestDistanceSquared = projection.distanceSquared;
|
|
313
|
+
bestDistanceAlong = projection.distanceAlong;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
275
316
|
|
|
276
|
-
route.
|
|
277
|
-
if (segment.endDistance < previousDistance - backtrackAllowance) return;
|
|
278
|
-
const projected = projectPositionToSegment(position, segment);
|
|
279
|
-
if (!best || projected.distanceSquared < best.distanceSquared) best = projected;
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
return clamp(best?.distanceAlong ?? previousDistance, 0, route.length);
|
|
317
|
+
return clamp(bestDistanceAlong, 0, route.length);
|
|
283
318
|
}
|
|
284
319
|
|
|
285
320
|
export function createPitApproachPoints(track, car, pitLane, entryRaceDistance) {
|
|
@@ -103,6 +103,7 @@ export function initializePitStops({ cars, pitLane, pitStops, totalLaps, trackLe
|
|
|
103
103
|
serviceProfile: null,
|
|
104
104
|
queueingForService: false,
|
|
105
105
|
route: null,
|
|
106
|
+
routeSample: null,
|
|
106
107
|
routeProgress: 0,
|
|
107
108
|
routeStartRaceDistance: null,
|
|
108
109
|
routeEndRaceDistance: null,
|
|
@@ -51,13 +51,17 @@ export function completePitService(sim, car) {
|
|
|
51
51
|
|
|
52
52
|
const driveLaneOffset = pitDriveLaneOffset(pitLane);
|
|
53
53
|
const boxReleaseDistance = Math.min(pitLane.mainLane.length, box.distanceAlongLane + PIT_BOX_APPROACH_DISTANCE);
|
|
54
|
-
const
|
|
54
|
+
const routePoints = [
|
|
55
55
|
routePoint(box.center, pitLane.mainLane.heading, { limiterActive: true }),
|
|
56
56
|
routePoint(box.laneTarget, pitLane.mainLane.heading, { limiterActive: true }),
|
|
57
57
|
routePoint(pitMainLanePointAt(pitLane, boxReleaseDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
58
58
|
routePoint(pitMainLanePointAt(pitLane, pitLane.mainLane.length, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
59
|
-
|
|
60
|
-
]
|
|
59
|
+
];
|
|
60
|
+
const exitCenterline = pitLane.exit.roadCenterline ?? [];
|
|
61
|
+
for (let index = 0; index < exitCenterline.length; index += 1) {
|
|
62
|
+
routePoints.push(routePoint(exitCenterline[index]));
|
|
63
|
+
}
|
|
64
|
+
const route = createRoute(routePoints);
|
|
61
65
|
stop.status = 'exiting';
|
|
62
66
|
stop.phase = 'exit';
|
|
63
67
|
stop.route = route;
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { isRaceDnf } from './retirements.js';
|
|
2
2
|
|
|
3
|
-
export function evaluateRaceFinishForSimulation(sim) {
|
|
3
|
+
export function evaluateRaceFinishForSimulation(sim, orderedCars = sim.orderedCars()) {
|
|
4
4
|
if (sim.raceControl.finished) return;
|
|
5
5
|
if (sim.raceControl.mode === 'pre-start') return;
|
|
6
6
|
|
|
7
|
-
const ordered =
|
|
8
|
-
|
|
7
|
+
const ordered = orderedCars;
|
|
8
|
+
if (!ordered.length) return;
|
|
9
|
+
|
|
10
|
+
let allFinishedOrDnf = true;
|
|
11
|
+
let newlyFinishedCount = 0;
|
|
12
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
13
|
+
const car = ordered[index];
|
|
14
|
+
if (isRaceDnf(car) || car.finished) continue;
|
|
15
|
+
if (car.raceDistance < sim.finishDistance) {
|
|
16
|
+
allFinishedOrDnf = false;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
9
19
|
|
|
10
|
-
newlyFinished.forEach((car) => {
|
|
11
20
|
car.finished = true;
|
|
12
21
|
car.finishTime = sim.time;
|
|
13
22
|
car.finishRank = sim.raceControl.finishOrder.length + 1;
|
|
@@ -27,9 +36,17 @@ export function evaluateRaceFinishForSimulation(sim) {
|
|
|
27
36
|
winnerId: sim.raceControl.winnerId,
|
|
28
37
|
});
|
|
29
38
|
sim.reviewTireRequirement(car);
|
|
30
|
-
|
|
39
|
+
newlyFinishedCount += 1;
|
|
40
|
+
}
|
|
31
41
|
|
|
32
|
-
if (!
|
|
42
|
+
if (!allFinishedOrDnf) {
|
|
43
|
+
if (!newlyFinishedCount && sim.runtimeBenchmarkStats) {
|
|
44
|
+
sim.runtimeBenchmarkStats.finishEvalNoFinishFastPathCalls = (
|
|
45
|
+
sim.runtimeBenchmarkStats.finishEvalNoFinishFastPathCalls ?? 0
|
|
46
|
+
) + 1;
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
33
50
|
|
|
34
51
|
sim.applyOutstandingServicePenalties();
|
|
35
52
|
const classification = sim.buildClassificationFromFinishOrder();
|
|
@@ -41,8 +58,8 @@ export function evaluateRaceFinishForSimulation(sim) {
|
|
|
41
58
|
sim.raceControl.classification = classification;
|
|
42
59
|
sim.raceControl.frozenOrder = classification.map((entry) => entry.id);
|
|
43
60
|
sim.safetyCar.deployed = true;
|
|
44
|
-
const
|
|
45
|
-
const safetyCarProgress = (
|
|
61
|
+
const safetyCarLeader = sim.cars.find((car) => car.id === sim.raceControl.winnerId) ?? ordered[0];
|
|
62
|
+
const safetyCarProgress = (safetyCarLeader?.raceDistance ?? 0) + sim.rules.safetyCarLeadDistance;
|
|
46
63
|
if (sim.safetyCar.progress < safetyCarProgress) {
|
|
47
64
|
sim.moveSafetyCarTo(safetyCarProgress);
|
|
48
65
|
}
|
|
@@ -1,45 +1,194 @@
|
|
|
1
1
|
import { clamp } from '../simMath.js';
|
|
2
2
|
import { affectsRaceOrder } from '../participants/participantInteractions.js';
|
|
3
3
|
import { distanceForward } from './raceDistance.js';
|
|
4
|
-
import {
|
|
4
|
+
import { compareDnfOrder, isRaceDnf } from './retirements.js';
|
|
5
5
|
|
|
6
6
|
function canAppearInRaceOrder(car) {
|
|
7
7
|
return car?.interaction?.affectsRaceOrder !== false;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
10
|
+
function partitionRaceOrderParticipants(cars) {
|
|
11
|
+
const live = [];
|
|
12
|
+
const dnf = [];
|
|
13
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
14
|
+
const car = cars[index];
|
|
15
|
+
if (!canAppearInRaceOrder(car)) continue;
|
|
16
|
+
if (isRaceDnf(car)) dnf.push(car);
|
|
17
|
+
else live.push(car);
|
|
18
|
+
}
|
|
19
|
+
dnf.sort(compareDnfOrder);
|
|
20
|
+
return { live, dnf };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sortLiveRaceOrderInPlace(cars) {
|
|
24
|
+
cars.sort((a, b) => {
|
|
14
25
|
const delta = b.raceDistance - a.raceDistance;
|
|
15
26
|
return delta === 0 ? a.index - b.index : delta;
|
|
16
27
|
});
|
|
17
|
-
|
|
28
|
+
return cars;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function appendCars(target, source) {
|
|
32
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
33
|
+
target.push(source[index]);
|
|
34
|
+
}
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectLiveRaceOrderedCars(orderedCars, scratch) {
|
|
39
|
+
const physicalCars = scratch?.physicalCars ?? [];
|
|
40
|
+
physicalCars.length = 0;
|
|
41
|
+
for (let index = 0; index < orderedCars.length; index += 1) {
|
|
42
|
+
const car = orderedCars[index];
|
|
43
|
+
if (!affectsRaceOrder(car) || isRaceDnf(car)) continue;
|
|
44
|
+
physicalCars.push(car);
|
|
45
|
+
}
|
|
46
|
+
if (scratch) scratch.physicalCars = physicalCars;
|
|
47
|
+
return physicalCars;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sortPhysicalCarsInTrackOrder(sim, physicalCars) {
|
|
51
|
+
physicalCars.sort((a, b) => {
|
|
52
|
+
const progressDelta = (b.progress ?? b.raceDistance ?? 0) - (a.progress ?? a.raceDistance ?? 0);
|
|
53
|
+
if (progressDelta !== 0) return progressDelta;
|
|
54
|
+
const raceDistanceDelta = (b.raceDistance ?? 0) - (a.raceDistance ?? 0);
|
|
55
|
+
return raceDistanceDelta === 0 ? a.index - b.index : raceDistanceDelta;
|
|
56
|
+
});
|
|
57
|
+
return physicalCars;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function canUseRaceOrderAsPhysicalOrder(sim, physicalCars) {
|
|
61
|
+
if (physicalCars.length < 2) return true;
|
|
62
|
+
const leaderDistance = physicalCars[0]?.raceDistance;
|
|
63
|
+
const tailDistance = physicalCars[physicalCars.length - 1]?.raceDistance;
|
|
64
|
+
return Number.isFinite(leaderDistance) &&
|
|
65
|
+
Number.isFinite(tailDistance) &&
|
|
66
|
+
leaderDistance - tailDistance < sim.track.length - 1e-6;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildFullFieldDrsReferenceArrayForSimulation(sim, orderedCars, references) {
|
|
70
|
+
if (orderedCars.length !== sim.cars.length || !orderedCars.length) return false;
|
|
71
|
+
let wrappedEligibleCar = null;
|
|
72
|
+
for (let index = orderedCars.length - 1; index >= 0; index -= 1) {
|
|
73
|
+
const car = orderedCars[index];
|
|
74
|
+
if (!affectsRaceOrder(car) || isRaceDnf(car)) return false;
|
|
75
|
+
if (!car.finished && !sim.isCarInActivePitStop(car) && wrappedEligibleCar == null) {
|
|
76
|
+
wrappedEligibleCar = car;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!canUseRaceOrderAsPhysicalOrder(sim, orderedCars)) return false;
|
|
80
|
+
|
|
81
|
+
const trackLength = sim.track.length;
|
|
82
|
+
let previousEligibleCar = wrappedEligibleCar;
|
|
83
|
+
for (let index = 0; index < orderedCars.length; index += 1) {
|
|
84
|
+
const car = orderedCars[index];
|
|
85
|
+
const candidate = previousEligibleCar === car ? null : previousEligibleCar;
|
|
86
|
+
if (candidate) {
|
|
87
|
+
const delta = distanceForward(
|
|
88
|
+
car.progress ?? car.raceDistance ?? 0,
|
|
89
|
+
candidate.progress ?? candidate.raceDistance ?? 0,
|
|
90
|
+
trackLength,
|
|
91
|
+
);
|
|
92
|
+
references[car.index] = delta > 0 && delta <= trackLength / 2 ? candidate : null;
|
|
93
|
+
} else {
|
|
94
|
+
references[car.index] = null;
|
|
95
|
+
}
|
|
96
|
+
if (!car.finished && !sim.isCarInActivePitStop(car)) previousEligibleCar = car;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildDrsReferenceArrayForSimulation(sim, orderedCars = orderedCarsForSimulation(sim)) {
|
|
102
|
+
const scratch = sim._drsReferenceScratch ??= {};
|
|
103
|
+
const references = scratch.references ?? [];
|
|
104
|
+
references.length = sim.cars.length;
|
|
105
|
+
if (buildFullFieldDrsReferenceArrayForSimulation(sim, orderedCars, references)) {
|
|
106
|
+
scratch.references = references;
|
|
107
|
+
if (sim?.runtimeBenchmarkStats) {
|
|
108
|
+
sim.runtimeBenchmarkStats.drsReferenceScratchBuilds = (sim.runtimeBenchmarkStats.drsReferenceScratchBuilds ?? 0) + 1;
|
|
109
|
+
sim.runtimeBenchmarkStats.drsReferenceFullFieldFastPathCalls = (sim.runtimeBenchmarkStats.drsReferenceFullFieldFastPathCalls ?? 0) + 1;
|
|
110
|
+
sim.runtimeBenchmarkStats.drsReferenceOrderedFastPathCalls = (sim.runtimeBenchmarkStats.drsReferenceOrderedFastPathCalls ?? 0) + 1;
|
|
111
|
+
}
|
|
112
|
+
return references;
|
|
113
|
+
}
|
|
114
|
+
const physicalCars = collectLiveRaceOrderedCars(orderedCars, scratch);
|
|
115
|
+
for (let index = 0; index < references.length; index += 1) {
|
|
116
|
+
references[index] = null;
|
|
117
|
+
}
|
|
118
|
+
if (!physicalCars.length) return references;
|
|
119
|
+
if (canUseRaceOrderAsPhysicalOrder(sim, physicalCars)) {
|
|
120
|
+
if (sim?.runtimeBenchmarkStats) {
|
|
121
|
+
sim.runtimeBenchmarkStats.drsReferenceOrderedFastPathCalls = (sim.runtimeBenchmarkStats.drsReferenceOrderedFastPathCalls ?? 0) + 1;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
sortPhysicalCarsInTrackOrder(sim, physicalCars);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const trackLength = sim.track.length;
|
|
128
|
+
let wrappedEligibleCar = null;
|
|
129
|
+
for (let index = physicalCars.length - 1; index >= 0; index -= 1) {
|
|
130
|
+
const car = physicalCars[index];
|
|
131
|
+
if (!car.finished && !sim.isCarInActivePitStop(car)) {
|
|
132
|
+
wrappedEligibleCar = car;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let previousEligibleCar = wrappedEligibleCar;
|
|
138
|
+
for (let index = 0; index < physicalCars.length; index += 1) {
|
|
139
|
+
const car = physicalCars[index];
|
|
140
|
+
const candidate = previousEligibleCar === car ? null : previousEligibleCar;
|
|
141
|
+
if (candidate) {
|
|
142
|
+
const delta = distanceForward(
|
|
143
|
+
car.progress ?? car.raceDistance ?? 0,
|
|
144
|
+
candidate.progress ?? candidate.raceDistance ?? 0,
|
|
145
|
+
trackLength,
|
|
146
|
+
);
|
|
147
|
+
references[car.index] = delta > 0 && delta <= trackLength / 2 ? candidate : null;
|
|
148
|
+
} else {
|
|
149
|
+
references[car.index] = null;
|
|
150
|
+
}
|
|
151
|
+
if (!car.finished && !sim.isCarInActivePitStop(car)) previousEligibleCar = car;
|
|
152
|
+
}
|
|
153
|
+
scratch.references = references;
|
|
154
|
+
if (sim?.runtimeBenchmarkStats) {
|
|
155
|
+
sim.runtimeBenchmarkStats.drsReferenceScratchBuilds = (sim.runtimeBenchmarkStats.drsReferenceScratchBuilds ?? 0) + 1;
|
|
156
|
+
}
|
|
157
|
+
return references;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function orderedCarsForSimulation(sim) {
|
|
161
|
+
const participants = partitionRaceOrderParticipants(sim.cars);
|
|
162
|
+
const raceCars = participants.live;
|
|
163
|
+
const dnfCars = participants.dnf;
|
|
18
164
|
|
|
19
165
|
if (sim.raceControl.finished && sim.raceControl.classification?.length) {
|
|
166
|
+
const byId = new Map(sim.cars.map((car) => [car.id, car]));
|
|
20
167
|
const classified = sim.raceControl.classification.map((entry) => byId.get(entry.id)).filter(Boolean);
|
|
21
168
|
const classifiedIds = new Set(classified.map((car) => car.id));
|
|
22
169
|
return [
|
|
23
170
|
...classified,
|
|
24
|
-
...
|
|
171
|
+
...sortLiveRaceOrderInPlace(raceCars.filter((car) => !classifiedIds.has(car.id))),
|
|
25
172
|
...dnfCars.filter((car) => !classifiedIds.has(car.id)),
|
|
26
173
|
];
|
|
27
174
|
}
|
|
28
175
|
|
|
29
176
|
if (sim.raceControl.finishOrder?.length) {
|
|
177
|
+
const byId = new Map(sim.cars.map((car) => [car.id, car]));
|
|
30
178
|
const finished = sim.raceControl.finishOrder.map((id) => byId.get(id)).filter(Boolean);
|
|
31
179
|
const finishedIds = new Set(finished.map((car) => car.id));
|
|
32
|
-
const missedFinished =
|
|
33
|
-
.filter((car) =>
|
|
180
|
+
const missedFinished = raceCars
|
|
181
|
+
.filter((car) => car.finished && !finishedIds.has(car.id))
|
|
34
182
|
.sort((a, b) => {
|
|
35
183
|
const delta = (a.finishRank ?? Infinity) - (b.finishRank ?? Infinity);
|
|
36
184
|
return delta === 0 ? a.index - b.index : delta;
|
|
37
185
|
});
|
|
38
|
-
const running =
|
|
186
|
+
const running = sortLiveRaceOrderInPlace(raceCars.filter((car) => !finishedIds.has(car.id) && !car.finished));
|
|
39
187
|
return [...finished, ...missedFinished, ...running, ...dnfCars.filter((car) => !finishedIds.has(car.id))];
|
|
40
188
|
}
|
|
41
189
|
|
|
42
190
|
if (sim.safetyCar.deployed && sim.raceControl.frozenOrder?.length) {
|
|
191
|
+
const byId = new Map(sim.cars.map((car) => [car.id, car]));
|
|
43
192
|
const frozen = sim.raceControl.frozenOrder.map((id) => byId.get(id)).filter(Boolean);
|
|
44
193
|
const activeFrozen = frozen.filter((car) => !isRaceDnf(car));
|
|
45
194
|
const activeFrozenIds = new Set(activeFrozen.map((car) => car.id));
|
|
@@ -49,9 +198,11 @@ export function orderedCarsForSimulation(sim) {
|
|
|
49
198
|
];
|
|
50
199
|
}
|
|
51
200
|
|
|
52
|
-
|
|
201
|
+
sortLiveRaceOrderInPlace(raceCars);
|
|
202
|
+
return appendCars(raceCars, dnfCars);
|
|
53
203
|
}
|
|
54
204
|
|
|
205
|
+
|
|
55
206
|
export function raceOrderCarsForSimulation(sim) {
|
|
56
207
|
return sim.cars.filter((car) => affectsRaceOrder(car) && !isRaceDnf(car));
|
|
57
208
|
}
|
|
@@ -68,14 +219,36 @@ export function driverRaceContextForSimulation(sim, orderedCars = orderedCarsFor
|
|
|
68
219
|
};
|
|
69
220
|
}
|
|
70
221
|
|
|
71
|
-
export function
|
|
222
|
+
export function buildDrsReferenceByCarId(sim, orderedCars = orderedCarsForSimulation(sim)) {
|
|
223
|
+
const referencesByIndex = buildDrsReferenceArrayForSimulation(sim, orderedCars);
|
|
224
|
+
const references = new Map();
|
|
225
|
+
for (let index = 0; index < orderedCars.length; index += 1) {
|
|
226
|
+
const car = orderedCars[index];
|
|
227
|
+
if (!affectsRaceOrder(car) || isRaceDnf(car)) continue;
|
|
228
|
+
references.set(car.id, referencesByIndex[car.index] ?? null);
|
|
229
|
+
}
|
|
230
|
+
return references;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function assignDrsReferenceCarsForSimulation(sim, orderedCars = orderedCarsForSimulation(sim)) {
|
|
234
|
+
return buildDrsReferenceArrayForSimulation(sim, orderedCars);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function computeAggressionForSimulation(
|
|
238
|
+
sim,
|
|
239
|
+
car,
|
|
240
|
+
orderIndex = Math.max(0, (car.rank ?? 1) - 1),
|
|
241
|
+
fieldDepth = null,
|
|
242
|
+
) {
|
|
72
243
|
const personality = car.personality ?? { baseAggression: 0.5, riskTolerance: 0.5, patience: 0.5 };
|
|
73
244
|
if (sim.safetyCar.deployed || car.canAttack === false) {
|
|
74
245
|
return clamp(personality.baseAggression * 0.62, 0.08, 0.62);
|
|
75
246
|
}
|
|
76
247
|
|
|
77
|
-
const
|
|
78
|
-
|
|
248
|
+
const resolvedFieldDepth = Number.isFinite(fieldDepth)
|
|
249
|
+
? Math.max(1, fieldDepth)
|
|
250
|
+
: Math.max(1, raceOrderCarsForSimulation(sim).length - 1);
|
|
251
|
+
const positionPressure = clamp(orderIndex / resolvedFieldDepth, 0, 1);
|
|
79
252
|
const gapPressure = Number.isFinite(car.gapAhead) ? clamp((230 - car.gapAhead) / 230, 0, 1) : 0;
|
|
80
253
|
const tireConfidence = clamp(((car.tireEnergy ?? 100) - 42) / 58, 0, 1);
|
|
81
254
|
const patienceDamping = (1 - gapPressure) * personality.patience * 0.08;
|
|
@@ -92,22 +265,6 @@ export function computeAggressionForSimulation(sim, car, orderIndex = Math.max(0
|
|
|
92
265
|
}
|
|
93
266
|
|
|
94
267
|
export function getDrsReferenceCarForSimulation(sim, car) {
|
|
95
|
-
|
|
96
|
-
sim.
|
|
97
|
-
if (
|
|
98
|
-
candidate === car ||
|
|
99
|
-
!affectsRaceOrder(candidate) ||
|
|
100
|
-
isRaceDnf(candidate) ||
|
|
101
|
-
candidate.finished ||
|
|
102
|
-
sim.isCarInActivePitStop(candidate)
|
|
103
|
-
) return;
|
|
104
|
-
const delta = distanceForward(
|
|
105
|
-
car.progress ?? car.raceDistance ?? 0,
|
|
106
|
-
candidate.progress ?? candidate.raceDistance ?? 0,
|
|
107
|
-
sim.track.length,
|
|
108
|
-
);
|
|
109
|
-
if (delta <= 0 || delta > sim.track.length / 2) return;
|
|
110
|
-
if (!closest || delta < closest.delta) closest = { car: candidate, delta };
|
|
111
|
-
});
|
|
112
|
-
return closest?.car ?? null;
|
|
268
|
+
if (!car) return null;
|
|
269
|
+
return buildDrsReferenceArrayForSimulation(sim)[car.index] ?? null;
|
|
113
270
|
}
|