@inventure71/paddockjs 4.0.0 → 4.2.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 +48 -0
- package/INSTALL_AND_UPDATE.md +14 -1
- package/README.md +33 -1
- package/docs/architecture.md +3 -1
- package/docs/component_inventory.md +5 -4
- package/docs/composable-layouts.md +2 -0
- package/docs/data_contract.md +5 -1
- package/docs/getting-started.md +33 -0
- package/docs/index.md +4 -2
- package/docs/loading.md +141 -0
- package/docs/system_specs.md +15 -2
- package/docs/testing-reliability-guide.md +62 -0
- package/package.json +16 -9
- package/src/data/index.d.ts +64 -1
- package/src/environment/index.d.ts +75 -12
- package/src/index.d.ts +72 -6
- 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 +7 -9
- package/src/simulation/driver/driverController.js +2 -2
- package/src/simulation/driver/edgeRecovery.js +2 -2
- package/src/simulation/driver/racingControls.js +35 -25
- package/src/simulation/driver/racingLinePlan.js +6 -5
- package/src/simulation/driver/rejoinControls.js +3 -3
- package/src/simulation/driver/trafficScan.js +48 -22
- package/src/simulation/participants/participantInteractions.js +24 -15
- package/src/simulation/race/raceStep.js +19 -10
- package/src/simulation/race/safetyCar.js +2 -1
- package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
- package/src/simulation/rules/rulesReview.js +19 -9
- package/src/simulation/rules/trackLimitsSteward.js +2 -2
- package/src/simulation/timing/sectorPerformance.js +11 -3
- package/src/simulation/track/spatialQueries.js +80 -11
- package/src/simulation/track/trackModel.js +10 -1
- package/src/simulation/vehicle/contactResolution.js +4 -4
- package/src/simulation/vehicle/vehicleGeometry.js +87 -49
- package/src/simulation/vehicle/vehiclePhysics.js +7 -2
- package/src/simulation/vehicle/vehicleSnapshots.js +5 -2
- package/src/simulation/vehicle/vehicleState.js +3 -2
- package/src/ui/raceCanvasTemplate.js +3 -2
- package/src/ui/raceControlsTemplate.js +1 -0
- package/src/ui/telemetryDrawerTemplate.js +1 -0
- package/src/ui/telemetryTemplates.js +1 -0
- package/examples/train-basic-policy.mjs +0 -276
- package/examples/trainingData.mjs +0 -133
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { clamp, normalizeAngle } from '../simMath.js';
|
|
2
2
|
import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../units.js';
|
|
3
|
-
import {
|
|
4
|
-
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
3
|
+
import { pointAtInto, sampleHeadingAt, sampleHeadingCurvatureAtInto } from '../track/trackModel.js';
|
|
4
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
5
5
|
import { BRAKING_LOOKAHEAD_MAX_DISTANCE, CURVATURE_LOOKAHEAD_SAMPLES, EDGE_RECOVERY_MIN_LOOKAHEAD, LOOKAHEAD_BASE_DISTANCE, LOOKAHEAD_MAX_DISTANCE, OVERTAKE_LATERAL_MIN, PREVIEW_CURVATURE_CAP, TRAFFIC_GAP_AHEAD, TRAFFIC_SIDE_GAP } from './driverControlConstants.js';
|
|
6
6
|
import { createDriverInput } from './driverInput.js';
|
|
7
7
|
import { angleToPoint } from './driverMath.js';
|
|
@@ -10,15 +10,14 @@ import { calculateActualOverlapPenalty, calculatePlannedTrafficPenalty, planRaci
|
|
|
10
10
|
import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
|
|
11
11
|
|
|
12
12
|
export function decideRacingControls(car, orderIndex, race) {
|
|
13
|
-
if (race.physicsMode
|
|
13
|
+
if (isSimulatorPhysicsMode(race.physicsMode)) {
|
|
14
14
|
return decideSimulatorRacingControls(car, orderIndex, race);
|
|
15
15
|
}
|
|
16
16
|
return decideArcadeRacingControls(car, orderIndex, race);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
simulatorMode: false,
|
|
19
|
+
const ARCADE_CONTROL_PROFILE = {
|
|
20
|
+
simulatorMode: false,
|
|
22
21
|
baseLookaheadSpeedFactor: (aggression) => 1.62 - aggression * 0.12,
|
|
23
22
|
previewLookaheadSpeedFactor: 3.15,
|
|
24
23
|
edgeLookaheadPenaltyMeters: 18,
|
|
@@ -73,12 +72,14 @@ export function decideArcadeRacingControls(car, orderIndex, race) {
|
|
|
73
72
|
recoveryHeadingThrottleStart: 0.38,
|
|
74
73
|
throttleResponseKph: 16,
|
|
75
74
|
throttleScale: () => 1,
|
|
76
|
-
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function decideArcadeRacingControls(car, orderIndex, race) {
|
|
78
|
+
return decideRacingControlsForMode(car, orderIndex, race, ARCADE_CONTROL_PROFILE);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
simulatorMode: true,
|
|
81
|
+
const SIMULATOR_CONTROL_PROFILE = {
|
|
82
|
+
simulatorMode: true,
|
|
82
83
|
baseLookaheadSpeedFactor: (aggression) => 2.45 - aggression * 0.06,
|
|
83
84
|
previewLookaheadSpeedFactor: 6.15,
|
|
84
85
|
edgeLookaheadPenaltyMeters: 34,
|
|
@@ -141,7 +142,10 @@ export function decideSimulatorRacingControls(car, orderIndex, race) {
|
|
|
141
142
|
0,
|
|
142
143
|
1,
|
|
143
144
|
),
|
|
144
|
-
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export function decideSimulatorRacingControls(car, orderIndex, race) {
|
|
148
|
+
return decideRacingControlsForMode(car, orderIndex, race, SIMULATOR_CONTROL_PROFILE);
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
function decideRacingControlsForMode(car, orderIndex, race, profile) {
|
|
@@ -170,7 +174,7 @@ function decideRacingControlsForMode(car, orderIndex, race, profile) {
|
|
|
170
174
|
EDGE_RECOVERY_MIN_LOOKAHEAD,
|
|
171
175
|
LOOKAHEAD_MAX_DISTANCE,
|
|
172
176
|
);
|
|
173
|
-
const targetBase =
|
|
177
|
+
const targetBase = pointAtInto(race.track, car.progress + lookahead, TARGET_BASE_SCRATCH);
|
|
174
178
|
const curvature = Math.max(
|
|
175
179
|
car.trackState.curvature,
|
|
176
180
|
targetBase.curvature,
|
|
@@ -189,8 +193,9 @@ function decideRacingControlsForMode(car, orderIndex, race, profile) {
|
|
|
189
193
|
-edgeGuard.recoveryOffset,
|
|
190
194
|
edgeGuard.recoveryOffset,
|
|
191
195
|
);
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
TARGET_POINT_SCRATCH.x = targetBase.x + targetBase.normalX * targetOffset;
|
|
197
|
+
TARGET_POINT_SCRATCH.y = targetBase.y + targetBase.normalY * targetOffset;
|
|
198
|
+
const angleError = angleToPoint(car, TARGET_POINT_SCRATCH);
|
|
194
199
|
const headingError = normalizeAngle(targetBase.heading - car.heading);
|
|
195
200
|
const lateralError = car.trackState?.signedOffset ?? 0;
|
|
196
201
|
const outwardDriftAngle = simulatorMode &&
|
|
@@ -363,15 +368,20 @@ function decideRacingControlsForMode(car, orderIndex, race, profile) {
|
|
|
363
368
|
.controls();
|
|
364
369
|
}
|
|
365
370
|
|
|
371
|
+
const LOOKAHEAD_SAMPLE_SCRATCH = { heading: 0, curvature: 0 };
|
|
372
|
+
const TARGET_BASE_SCRATCH = { x: 0, y: 0, heading: 0, normalX: 0, normalY: 0, curvature: 0, distance: 0 };
|
|
373
|
+
const TARGET_POINT_SCRATCH = { x: 0, y: 0 };
|
|
374
|
+
|
|
366
375
|
export function maxLookaheadCurvature(track, progress, lookahead) {
|
|
367
376
|
let maximum = 0;
|
|
368
|
-
|
|
377
|
+
const sample = LOOKAHEAD_SAMPLE_SCRATCH;
|
|
378
|
+
let previousHeading = sampleHeadingCurvatureAtInto(track, progress, sample).heading;
|
|
379
|
+
const segmentDistance = Math.max(1, lookahead / CURVATURE_LOOKAHEAD_SAMPLES);
|
|
369
380
|
for (let index = 1; index <= CURVATURE_LOOKAHEAD_SAMPLES; index += 1) {
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const segmentCurvature = Math.abs(normalizeAngle(sample.heading - previous.heading)) / segmentDistance;
|
|
381
|
+
sampleHeadingCurvatureAtInto(track, progress + lookahead * (index / CURVATURE_LOOKAHEAD_SAMPLES), sample);
|
|
382
|
+
const segmentCurvature = Math.abs(normalizeAngle(sample.heading - previousHeading)) / segmentDistance;
|
|
373
383
|
maximum = Math.max(maximum, Math.abs(sample.curvature ?? 0), Math.min(segmentCurvature, PREVIEW_CURVATURE_CAP));
|
|
374
|
-
|
|
384
|
+
previousHeading = sample.heading;
|
|
375
385
|
}
|
|
376
386
|
return maximum;
|
|
377
387
|
}
|
|
@@ -380,16 +390,16 @@ export function calculateRacingLineOffset(car, race, lookahead, curvature, edgeG
|
|
|
380
390
|
const cornerStrength = clamp((curvature - 0.00012) / 0.00095, 0, 1);
|
|
381
391
|
if (cornerStrength <= 0) return 0;
|
|
382
392
|
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const signedEntry = normalizeAngle(
|
|
387
|
-
const signedExit = normalizeAngle(
|
|
393
|
+
const entryHeading = sampleHeadingAt(race.track, car.progress - lookahead * 0.35);
|
|
394
|
+
const apexHeading = sampleHeadingAt(race.track, car.progress + lookahead * 0.55);
|
|
395
|
+
const exitHeading = sampleHeadingAt(race.track, car.progress + lookahead * 1.25);
|
|
396
|
+
const signedEntry = normalizeAngle(apexHeading - entryHeading);
|
|
397
|
+
const signedExit = normalizeAngle(exitHeading - apexHeading);
|
|
388
398
|
const turnDirection = Math.sign(Math.abs(signedEntry) > Math.abs(signedExit) ? signedEntry : signedExit);
|
|
389
399
|
if (turnDirection === 0) return 0;
|
|
390
400
|
|
|
391
401
|
const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
|
|
392
|
-
const simulatorMode = race.physicsMode
|
|
402
|
+
const simulatorMode = isSimulatorPhysicsMode(race.physicsMode);
|
|
393
403
|
const safeEdge = race.track.width / 2 - VEHICLE_LIMITS.carWidth *
|
|
394
404
|
(simulatorMode ? 2.05 - aggression * 0.08 : 1.15 - aggression * 0.12);
|
|
395
405
|
const apexOffset = turnDirection * safeEdge *
|
|
@@ -18,15 +18,16 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
18
18
|
let bestOffset = currentOffset;
|
|
19
19
|
let bestScore = -Infinity;
|
|
20
20
|
|
|
21
|
-
LANE_OFFSETS.
|
|
22
|
-
const offset = clamp(
|
|
21
|
+
for (let laneIndex = 0; laneIndex < LANE_OFFSETS.length; laneIndex += 1) {
|
|
22
|
+
const offset = clamp(LANE_OFFSETS[laneIndex], -trackLimit, trackLimit);
|
|
23
23
|
const edgeClearance = trackLimit - Math.abs(offset);
|
|
24
24
|
let score = 80;
|
|
25
25
|
score -= Math.abs(offset - preferred) * 0.18;
|
|
26
26
|
score -= Math.abs(offset - currentOffset) * (0.09 - aggression * 0.025);
|
|
27
27
|
score -= Math.max(0, LANE_EDGE_CLEARANCE_TARGET - edgeClearance) * (0.92 - aggression * 0.42);
|
|
28
28
|
|
|
29
|
-
traffic.
|
|
29
|
+
for (let trafficIndex = 0; trafficIndex < traffic.length; trafficIndex += 1) {
|
|
30
|
+
const entry = traffic[trafficIndex];
|
|
30
31
|
const lateral = Math.abs(entry.signedOffset - offset);
|
|
31
32
|
if (entry.gap > 0 && entry.gap < TRAFFIC_GAP_AHEAD) {
|
|
32
33
|
const overlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
|
|
@@ -38,7 +39,7 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
38
39
|
const sideOverlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
|
|
39
40
|
score -= sideOverlapRisk * (TRAFFIC_SIDE_OVERLAP + entry.gap) * 0.052 * (1 - riskTolerance * 0.22);
|
|
40
41
|
}
|
|
41
|
-
}
|
|
42
|
+
}
|
|
42
43
|
|
|
43
44
|
if (ahead && car.gapAhead < TRAFFIC_GAP_AHEAD) {
|
|
44
45
|
const passSide = attackPlan?.targetOffset ?? clamp(
|
|
@@ -73,7 +74,7 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
73
74
|
bestScore = score;
|
|
74
75
|
bestOffset = offset;
|
|
75
76
|
}
|
|
76
|
-
}
|
|
77
|
+
}
|
|
77
78
|
|
|
78
79
|
const laneChangeRate = metersToSimUnits(0.95 + car.racecraft * 0.32 + aggression * 0.78);
|
|
79
80
|
car.desiredOffset = currentOffset + clamp(bestOffset - currentOffset, -laneChangeRate, laneChangeRate);
|
|
@@ -4,11 +4,11 @@ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
|
|
|
4
4
|
import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
|
|
5
5
|
import { angleToPoint } from './driverMath.js';
|
|
6
6
|
import { createDriverInput } from './driverInput.js';
|
|
7
|
-
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
7
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
8
8
|
import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
|
|
9
9
|
|
|
10
10
|
export function decideRejoinControls(car, race) {
|
|
11
|
-
if (race.physicsMode
|
|
11
|
+
if (isSimulatorPhysicsMode(race.physicsMode)) {
|
|
12
12
|
return decideSimulatorRejoinControls(car, race);
|
|
13
13
|
}
|
|
14
14
|
return decideArcadeRejoinControls(car, race);
|
|
@@ -103,7 +103,7 @@ export function decideSimulatorRejoinControls(car, race) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function decideRejoinControlsForMode(car, race, profile) {
|
|
106
|
-
const simulatorMode = race.physicsMode
|
|
106
|
+
const simulatorMode = isSimulatorPhysicsMode(race.physicsMode);
|
|
107
107
|
const lookahead = clamp(car.speed * (profile.simulatorMode ? 0.58 : 0.72) + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
|
|
108
108
|
const edgeMotion = analyzeTrackEdgeMotion(car, race);
|
|
109
109
|
const lowSpeedOffTrack = !car.trackState.onTrack && car.speed < kphToSimSpeed(24);
|
|
@@ -2,25 +2,43 @@ import { metersToSimUnits } from '../units.js';
|
|
|
2
2
|
import { DEFEND_REAR_GAP, TRAFFIC_GAP_AHEAD, TRAFFIC_SIDE_GAP, TRAFFIC_REAR_WINDOW } from './driverControlConstants.js';
|
|
3
3
|
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
4
4
|
|
|
5
|
+
const TRAFFIC_REAR_SCAN_WINDOW = metersToSimUnits(39);
|
|
6
|
+
const TRAFFIC_FORWARD_SCAN_WINDOW = metersToSimUnits(134);
|
|
7
|
+
const TRAFFIC_BESIDE_WINDOW = metersToSimUnits(28);
|
|
8
|
+
|
|
9
|
+
// Entries are reused across calls: drivers consume the scan synchronously
|
|
10
|
+
// within a single control decision, so a module-level pool avoids re-allocating
|
|
11
|
+
// the candidate list for every car on every step.
|
|
12
|
+
const TRAFFIC_SCRATCH = [];
|
|
13
|
+
const TRAFFIC_ENTRY_POOL = [];
|
|
14
|
+
|
|
5
15
|
export function scanNearbyTraffic(car, race) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
const traffic = TRAFFIC_SCRATCH;
|
|
17
|
+
traffic.length = 0;
|
|
18
|
+
const cars = race.cars;
|
|
19
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
20
|
+
const other = cars[index];
|
|
21
|
+
if (other === car) continue;
|
|
22
|
+
const gap = other.raceDistance - car.raceDistance;
|
|
23
|
+
if (gap <= -TRAFFIC_REAR_SCAN_WINDOW || gap >= TRAFFIC_FORWARD_SCAN_WINDOW) continue;
|
|
24
|
+
const entry = TRAFFIC_ENTRY_POOL[traffic.length] ??= { car: null, gap: 0, signedOffset: 0, speed: 0 };
|
|
25
|
+
entry.car = other;
|
|
26
|
+
entry.gap = gap;
|
|
27
|
+
entry.signedOffset = other.trackState?.signedOffset ?? 0;
|
|
28
|
+
entry.speed = other.speed;
|
|
29
|
+
traffic.push(entry);
|
|
30
|
+
}
|
|
31
|
+
return traffic;
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
export function findDefensiveThreat(traffic) {
|
|
18
35
|
let closest = null;
|
|
19
36
|
|
|
20
|
-
traffic.
|
|
21
|
-
|
|
37
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
38
|
+
const entry = traffic[index];
|
|
39
|
+
if (entry.gap >= -VEHICLE_LIMITS.carLength * 1.1 || entry.gap < -DEFEND_REAR_GAP) continue;
|
|
22
40
|
if (!closest || entry.gap > closest.gap) closest = entry;
|
|
23
|
-
}
|
|
41
|
+
}
|
|
24
42
|
|
|
25
43
|
return closest;
|
|
26
44
|
}
|
|
@@ -28,24 +46,32 @@ export function findDefensiveThreat(traffic) {
|
|
|
28
46
|
export function findLaneTrafficAhead(traffic, offset, maxDistance) {
|
|
29
47
|
let closest = null;
|
|
30
48
|
|
|
31
|
-
traffic.
|
|
32
|
-
|
|
33
|
-
if (
|
|
49
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
50
|
+
const entry = traffic[index];
|
|
51
|
+
if (entry.gap <= 0 || entry.gap > maxDistance) continue;
|
|
52
|
+
if (Math.abs(entry.signedOffset - offset) > TRAFFIC_SIDE_GAP) continue;
|
|
34
53
|
if (!closest || entry.gap < closest.gap) closest = entry;
|
|
35
|
-
}
|
|
54
|
+
}
|
|
36
55
|
|
|
37
56
|
return closest;
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
export function findLaneTrafficBeside(traffic, offset) {
|
|
41
60
|
let closest = null;
|
|
61
|
+
let closestLateral = 0;
|
|
62
|
+
let closestRisk = -Infinity;
|
|
42
63
|
|
|
43
|
-
traffic.
|
|
64
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
65
|
+
const entry = traffic[index];
|
|
44
66
|
const lateral = Math.abs(entry.signedOffset - offset);
|
|
45
|
-
if (Math.abs(entry.gap) >
|
|
46
|
-
const risk = (
|
|
47
|
-
if (!closest || risk >
|
|
48
|
-
|
|
67
|
+
if (Math.abs(entry.gap) > TRAFFIC_BESIDE_WINDOW || lateral > TRAFFIC_SIDE_GAP) continue;
|
|
68
|
+
const risk = (TRAFFIC_BESIDE_WINDOW - Math.abs(entry.gap)) + (TRAFFIC_SIDE_GAP - lateral);
|
|
69
|
+
if (!closest || risk > closestRisk) {
|
|
70
|
+
closest = entry;
|
|
71
|
+
closestLateral = lateral;
|
|
72
|
+
closestRisk = risk;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
49
75
|
|
|
50
|
-
return closest;
|
|
76
|
+
return closest ? { ...closest, lateral: closestLateral, risk: closestRisk } : null;
|
|
51
77
|
}
|
|
@@ -41,14 +41,6 @@ export const PARTICIPANT_INTERACTION_PROFILES = Object.freeze({
|
|
|
41
41
|
}),
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
const INTERACTION_FLAGS = Object.freeze([
|
|
45
|
-
'collidable',
|
|
46
|
-
'detectableByRays',
|
|
47
|
-
'detectableAsNearby',
|
|
48
|
-
'blocksPitLane',
|
|
49
|
-
'affectsRaceOrder',
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
44
|
export function normalizeParticipantInteractions(options = {}) {
|
|
53
45
|
const defaultProfile = resolveProfileName(options.defaultProfile);
|
|
54
46
|
const defaultInteraction = { ...PARTICIPANT_INTERACTION_PROFILES[defaultProfile] };
|
|
@@ -73,11 +65,15 @@ export function attachParticipantInteractions(cars = [], options = {}) {
|
|
|
73
65
|
|
|
74
66
|
export function resolveInteraction(override = {}, fallbackProfile = 'normal') {
|
|
75
67
|
const profile = resolveProfileName(override?.profile ?? fallbackProfile);
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
const base = PARTICIPANT_INTERACTION_PROFILES[profile];
|
|
69
|
+
return {
|
|
70
|
+
profile,
|
|
71
|
+
collidable: override?.collidable != null ? Boolean(override.collidable) : base.collidable,
|
|
72
|
+
detectableByRays: override?.detectableByRays != null ? Boolean(override.detectableByRays) : base.detectableByRays,
|
|
73
|
+
detectableAsNearby: override?.detectableAsNearby != null ? Boolean(override.detectableAsNearby) : base.detectableAsNearby,
|
|
74
|
+
blocksPitLane: override?.blocksPitLane != null ? Boolean(override.blocksPitLane) : base.blocksPitLane,
|
|
75
|
+
affectsRaceOrder: override?.affectsRaceOrder != null ? Boolean(override.affectsRaceOrder) : base.affectsRaceOrder,
|
|
76
|
+
};
|
|
81
77
|
}
|
|
82
78
|
|
|
83
79
|
export function canCollide(first, second) {
|
|
@@ -110,8 +106,21 @@ export function affectsRaceOrder(car) {
|
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
export function serializeParticipantInteraction(interaction) {
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
return resolveInteraction(interaction ?? {}, interaction?.profile ?? 'normal');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const SERIALIZED_INTERACTION_SOURCE = Symbol('serializedInteractionSource');
|
|
113
|
+
const SERIALIZED_INTERACTION_VALUE = Symbol('serializedInteractionValue');
|
|
114
|
+
|
|
115
|
+
// Render snapshots are pooled and rebuilt every frame; the interaction config
|
|
116
|
+
// only changes when a new object is attached, so cache the serialized form per
|
|
117
|
+
// car keyed on the interaction reference.
|
|
118
|
+
export function serializeParticipantInteractionCached(car) {
|
|
119
|
+
if (car[SERIALIZED_INTERACTION_SOURCE] !== car.interaction || !car[SERIALIZED_INTERACTION_VALUE]) {
|
|
120
|
+
car[SERIALIZED_INTERACTION_SOURCE] = car.interaction;
|
|
121
|
+
car[SERIALIZED_INTERACTION_VALUE] = serializeParticipantInteraction(car.interaction);
|
|
122
|
+
}
|
|
123
|
+
return car[SERIALIZED_INTERACTION_VALUE];
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
function resolveProfileName(profile) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decideDriverControls } from '../driverController.js';
|
|
2
2
|
import { clamp } from '../simMath.js';
|
|
3
3
|
import { updateReplayGhosts } from '../replay/replayGhosts.js';
|
|
4
|
-
import { integrateVehiclePhysics } from '../vehicle/vehiclePhysics.js';
|
|
4
|
+
import { integrateVehiclePhysics, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
5
5
|
import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
6
6
|
import { applyRedFlagHoldForSimulation } from './redFlag.js';
|
|
7
7
|
import { freezeRetiredCar, updateStalledDnfForSimulation } from './stalledDnf.js';
|
|
@@ -38,19 +38,28 @@ export function runRaceStep(simulation, dt) {
|
|
|
38
38
|
|
|
39
39
|
const orderedCars = simulation.orderedCars();
|
|
40
40
|
const raceContext = simulation.driverRaceContext(orderedCars);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
for (let index = 0; index < simulation.cars.length; index += 1) {
|
|
42
|
+
simulation.cars[index]._driveOrderIndex = -1;
|
|
43
|
+
}
|
|
44
|
+
for (let index = 0; index < orderedCars.length; index += 1) {
|
|
45
|
+
orderedCars[index]._driveOrderIndex = index;
|
|
46
|
+
}
|
|
47
|
+
let driveCars = orderedCars;
|
|
48
|
+
if (orderedCars.length !== simulation.cars.length) {
|
|
49
|
+
driveCars = simulation._driveCarsScratch ??= [];
|
|
50
|
+
driveCars.length = 0;
|
|
51
|
+
driveCars.push(...orderedCars);
|
|
52
|
+
for (let index = 0; index < simulation.cars.length; index += 1) {
|
|
53
|
+
const car = simulation.cars[index];
|
|
54
|
+
if (car._driveOrderIndex === -1) driveCars.push(car);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
48
57
|
driveCars.forEach((car) => {
|
|
49
58
|
if (car.destroyed || car.outOfRace) {
|
|
50
59
|
freezeRetiredCar(car, { physicsMode: simulation.physicsMode });
|
|
51
60
|
return;
|
|
52
61
|
}
|
|
53
|
-
const orderIndex =
|
|
62
|
+
const orderIndex = car._driveOrderIndex >= 0 ? car._driveOrderIndex : undefined;
|
|
54
63
|
car.previousX = car.x;
|
|
55
64
|
car.previousY = car.y;
|
|
56
65
|
car.previousHeading = car.heading;
|
|
@@ -66,7 +75,7 @@ export function runRaceStep(simulation, dt) {
|
|
|
66
75
|
throttle: controls.throttle ?? 0,
|
|
67
76
|
brake: controls.brake ?? 0,
|
|
68
77
|
};
|
|
69
|
-
if (simulation.physicsMode
|
|
78
|
+
if (isSimulatorPhysicsMode(simulation.physicsMode)) {
|
|
70
79
|
measureRuntimePhase(simulation, 'prePhysicsWheelSurface', () => {
|
|
71
80
|
applyWheelSurfaceState(car, simulation.track);
|
|
72
81
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pointAt } from '../track/trackModel.js';
|
|
2
2
|
import { kphToSimSpeed } from '../units.js';
|
|
3
3
|
import { freezeVehicleMotion, syncVehicleVelocityToHeading } from '../vehicle/vehicleKinematics.js';
|
|
4
|
+
import { isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
4
5
|
|
|
5
6
|
export function setSafetyCarState(sim, deployed) {
|
|
6
7
|
const next = Boolean(deployed);
|
|
@@ -82,7 +83,7 @@ export function setRedFlagState(sim, deployed) {
|
|
|
82
83
|
});
|
|
83
84
|
} else if (!car.finished) {
|
|
84
85
|
car.speed = Math.max(car.speed, kphToSimSpeed(60));
|
|
85
|
-
if (sim.physicsMode
|
|
86
|
+
if (isSimulatorPhysicsMode(sim.physicsMode)) syncVehicleVelocityToHeading(car);
|
|
86
87
|
car.brake = 0;
|
|
87
88
|
car.throttle = Math.max(car.throttle ?? 0, 0.35);
|
|
88
89
|
}
|
|
@@ -20,7 +20,7 @@ export function calculatePitLaneSpeedingReview({ car, rule, stewardState }) {
|
|
|
20
20
|
|
|
21
21
|
if (!isViolation) {
|
|
22
22
|
return {
|
|
23
|
-
nextState: { ...current, active: false },
|
|
23
|
+
nextState: current.active === false ? current : { ...current, active: false },
|
|
24
24
|
event: null,
|
|
25
25
|
penalty: null,
|
|
26
26
|
};
|
|
@@ -129,7 +129,9 @@ export function reviewTrackLimitsForSimulation(sim) {
|
|
|
129
129
|
sim.cars.forEach((car) => {
|
|
130
130
|
if (isLegallyInsidePitLaneForTrackLimits(car)) {
|
|
131
131
|
const current = sim.stewardState.trackLimits[car.id];
|
|
132
|
-
|
|
132
|
+
if (!current || current.active !== false) {
|
|
133
|
+
sim.stewardState.trackLimits[car.id] = { ...(current ?? { violations: 0 }), active: false };
|
|
134
|
+
}
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
135
137
|
|
|
@@ -146,18 +148,26 @@ export function reviewTrackLimitsForSimulation(sim) {
|
|
|
146
148
|
});
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
const PIT_SPEED_REVIEW_SCRATCH = {
|
|
152
|
+
car: { id: null, speedKph: 0, trackState: null, pitLanePart: null },
|
|
153
|
+
rule: null,
|
|
154
|
+
stewardState: null,
|
|
155
|
+
};
|
|
156
|
+
|
|
149
157
|
export function reviewPitLaneSpeedingForSimulation(sim) {
|
|
150
158
|
const rule = getPenaltyRule(sim.rules, 'pitLaneSpeeding');
|
|
151
159
|
sim.cars.forEach((car) => {
|
|
152
160
|
const currentState = sim.stewardState.pitLaneSpeeding[car.id];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
// The steward only reads the speed and pit-lane location, so feed it a
|
|
162
|
+
// pooled view instead of spreading the whole car every step.
|
|
163
|
+
const reviewCar = PIT_SPEED_REVIEW_SCRATCH.car;
|
|
164
|
+
reviewCar.id = car.id;
|
|
165
|
+
reviewCar.speedKph = simSpeedToKph(car.speed);
|
|
166
|
+
reviewCar.trackState = car.trackState;
|
|
167
|
+
reviewCar.pitLanePart = car.pitLanePart;
|
|
168
|
+
PIT_SPEED_REVIEW_SCRATCH.rule = rule;
|
|
169
|
+
PIT_SPEED_REVIEW_SCRATCH.stewardState = currentState;
|
|
170
|
+
const review = calculatePitLaneSpeedingReview(PIT_SPEED_REVIEW_SCRATCH);
|
|
161
171
|
sim.stewardState.pitLaneSpeeding[car.id] = review.nextState;
|
|
162
172
|
if (review.event) sim.events.unshift({ ...review.event, at: sim.time });
|
|
163
173
|
if (review.penalty) sim.recordPenalty(review.penalty);
|
|
@@ -22,7 +22,7 @@ export function calculateTrackLimitReview({ car, rule, track, stewardState }) {
|
|
|
22
22
|
const wheelStates = wheelSurfaceState.wheels;
|
|
23
23
|
if (wheelStates.some((wheelState) => wheelState.inPitLane)) {
|
|
24
24
|
return {
|
|
25
|
-
nextState: { ...current, active: false },
|
|
25
|
+
nextState: current.active === false ? current : { ...current, active: false },
|
|
26
26
|
event: null,
|
|
27
27
|
penalty: null,
|
|
28
28
|
};
|
|
@@ -34,7 +34,7 @@ export function calculateTrackLimitReview({ car, rule, track, stewardState }) {
|
|
|
34
34
|
|
|
35
35
|
if (!isViolation) {
|
|
36
36
|
return {
|
|
37
|
-
nextState: { ...current, active: false },
|
|
37
|
+
nextState: current.active === false ? current : { ...current, active: false },
|
|
38
38
|
event: null,
|
|
39
39
|
penalty: null,
|
|
40
40
|
};
|
|
@@ -5,7 +5,7 @@ const OVERALL_BEST_1 = '_sectorPerformanceOverallBest1';
|
|
|
5
5
|
const OVERALL_BEST_2 = '_sectorPerformanceOverallBest2';
|
|
6
6
|
|
|
7
7
|
export function createEmptySectorTimes() {
|
|
8
|
-
return Array
|
|
8
|
+
return new Array(TELEMETRY_SECTOR_COUNT).fill(null);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function createEmptySectorPerformance() {
|
|
@@ -17,11 +17,19 @@ export function createEmptySectorPerformance() {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function serializeSectorTimes(values) {
|
|
20
|
-
|
|
20
|
+
const result = new Array(TELEMETRY_SECTOR_COUNT);
|
|
21
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
22
|
+
result[index] = finiteOrNull(values?.[index]);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export function serializeSectorPerformance(values) {
|
|
24
|
-
|
|
28
|
+
const result = new Array(TELEMETRY_SECTOR_COUNT);
|
|
29
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
30
|
+
result[index] = values?.[index] ?? null;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export function updateSectorPerformance(cars, stats = null) {
|
|
@@ -6,27 +6,59 @@ import { queryNearestTrackProjection, queryNearestTrackProjectionInto } from './
|
|
|
6
6
|
import { recordStat } from './trackQueryStats.js';
|
|
7
7
|
|
|
8
8
|
export function pointAt(track, distanceAlong) {
|
|
9
|
+
return pointAtInto(track, distanceAlong, {});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function pointAtInto(track, distanceAlong, target) {
|
|
9
13
|
const wrapped = wrapDistance(distanceAlong, track.length);
|
|
10
|
-
const low =
|
|
14
|
+
const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
|
|
11
15
|
|
|
12
16
|
const next = track.samples[low] ?? track.samples[0];
|
|
13
17
|
const previous = track.samples[Math.max(0, low - 1)] ?? next;
|
|
14
18
|
const span = Math.max(1, next.distance - previous.distance);
|
|
15
19
|
const amount = clamp((wrapped - previous.distance) / span, 0, 1);
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
21
|
+
target.x = previous.x + (next.x - previous.x) * amount;
|
|
22
|
+
target.y = previous.y + (next.y - previous.y) * amount;
|
|
23
|
+
target.heading = previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
|
|
24
|
+
target.normalX = previous.normalX + (next.normalX - previous.normalX) * amount;
|
|
25
|
+
target.normalY = previous.normalY + (next.normalY - previous.normalY) * amount;
|
|
26
|
+
target.curvature = previous.curvature + (next.curvature - previous.curvature) * amount;
|
|
27
|
+
target.distance = wrapped;
|
|
28
|
+
return target;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
export function
|
|
31
|
+
export function sampleHeadingCurvatureAtInto(track, distanceAlong, target) {
|
|
29
32
|
const wrapped = wrapDistance(distanceAlong, track.length);
|
|
33
|
+
const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
|
|
34
|
+
|
|
35
|
+
const next = track.samples[low] ?? track.samples[0];
|
|
36
|
+
const previous = track.samples[Math.max(0, low - 1)] ?? next;
|
|
37
|
+
const span = Math.max(1, next.distance - previous.distance);
|
|
38
|
+
const amount = clamp((wrapped - previous.distance) / span, 0, 1);
|
|
39
|
+
|
|
40
|
+
target.heading = previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
|
|
41
|
+
target.curvature = previous.curvature + (next.curvature - previous.curvature) * amount;
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sampleHeadingAt(track, distanceAlong) {
|
|
46
|
+
const wrapped = wrapDistance(distanceAlong, track.length);
|
|
47
|
+
const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
|
|
48
|
+
|
|
49
|
+
const next = track.samples[low] ?? track.samples[0];
|
|
50
|
+
const previous = track.samples[Math.max(0, low - 1)] ?? next;
|
|
51
|
+
const span = Math.max(1, next.distance - previous.distance);
|
|
52
|
+
const amount = clamp((wrapped - previous.distance) / span, 0, 1);
|
|
53
|
+
|
|
54
|
+
return previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sampleIndexAtDistance(track, distanceAlong) {
|
|
58
|
+
return binarySampleIndexAtDistance(track, wrapDistance(distanceAlong, track.length));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function binarySampleIndexAtDistance(track, wrapped) {
|
|
30
62
|
let low = 0;
|
|
31
63
|
let high = track.samples.length - 1;
|
|
32
64
|
|
|
@@ -39,6 +71,43 @@ export function sampleIndexAtDistance(track, distanceAlong) {
|
|
|
39
71
|
return low;
|
|
40
72
|
}
|
|
41
73
|
|
|
74
|
+
// Uniform-bin index over sample distances: table[bin] holds the smallest
|
|
75
|
+
// sample index whose distance reaches the bin start, so a lookup plus a short
|
|
76
|
+
// forward walk returns exactly what the binary search would, in O(1). Built
|
|
77
|
+
// eagerly when the track query index is attached; lookups on sample arrays
|
|
78
|
+
// without a prepared table fall back to the binary search.
|
|
79
|
+
const SAMPLE_INDEX_LUTS = new WeakMap();
|
|
80
|
+
|
|
81
|
+
export function prepareSampleIndexLut(track) {
|
|
82
|
+
const samples = track?.samples;
|
|
83
|
+
if (!Array.isArray(samples) || samples.length < 2 || !Number.isFinite(track.length) || track.length <= 0) return;
|
|
84
|
+
if (SAMPLE_INDEX_LUTS.has(samples)) return;
|
|
85
|
+
const lastIndex = samples.length - 1;
|
|
86
|
+
const bins = Math.max(1, lastIndex * 2);
|
|
87
|
+
const table = new Int32Array(bins);
|
|
88
|
+
const binSize = track.length / bins;
|
|
89
|
+
let index = 0;
|
|
90
|
+
for (let bin = 0; bin < bins; bin += 1) {
|
|
91
|
+
const start = bin * binSize;
|
|
92
|
+
while (index < lastIndex && samples[index].distance < start) index += 1;
|
|
93
|
+
table[bin] = index;
|
|
94
|
+
}
|
|
95
|
+
SAMPLE_INDEX_LUTS.set(samples, { bins, binSize, table });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sampleIndexAtDistanceUnwrapped(track, wrapped) {
|
|
99
|
+
const samples = track.samples;
|
|
100
|
+
const lut = Number.isFinite(wrapped) ? SAMPLE_INDEX_LUTS.get(samples) : null;
|
|
101
|
+
if (!lut) return binarySampleIndexAtDistance(track, wrapped);
|
|
102
|
+
const lastIndex = samples.length - 1;
|
|
103
|
+
let bin = Math.floor(wrapped / lut.binSize);
|
|
104
|
+
if (bin < 0) bin = 0;
|
|
105
|
+
else if (bin >= lut.bins) bin = lut.bins - 1;
|
|
106
|
+
let index = lut.table[bin];
|
|
107
|
+
while (index < lastIndex && samples[index].distance < wrapped) index += 1;
|
|
108
|
+
return index;
|
|
109
|
+
}
|
|
110
|
+
|
|
42
111
|
export function nearestSampleInRange(track, position, startIndex, endIndex) {
|
|
43
112
|
const sampleCount = track.samples.length - 1;
|
|
44
113
|
let best = null;
|