@inventure71/paddockjs 0.3.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 +91 -0
- package/INSTALL_AND_UPDATE.md +5 -2
- package/README.md +328 -13
- package/docs/architecture.md +150 -41
- package/docs/concepts.md +22 -3
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +474 -34
- package/docs/data_model.md +1 -1
- package/docs/index.md +1 -0
- package/docs/learnings.md +23 -1
- package/docs/rules.md +169 -21
- package/docs/system_specs.md +56 -20
- package/docs/training.md +369 -25
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +15 -4
- package/src/api/PaddockSimulatorController.js +36 -2
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +615 -940
- 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 +14 -1
- 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 +24 -4
- package/src/environment/actions.js +38 -7
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +206 -0
- package/src/environment/index.d.ts +638 -6
- package/src/environment/index.js +14 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +33 -0
- package/src/environment/observations.js +500 -43
- package/src/environment/options.js +41 -2
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +260 -24
- package/src/environment/scenarios.js +249 -0
- 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 -257
- package/src/environment/specs.js +188 -9
- package/src/environment/workerProtocol.js +64 -0
- package/src/index.d.ts +533 -7
- package/src/index.js +26 -0
- package/src/rendering/proceduralTrackAsset.js +20 -510
- 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 +214 -0
- 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 -253
- 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 +386 -1012
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/collisionSteward.js +69 -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/penaltyLedger.js +153 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -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/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +34 -0
- 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 -628
- package/src/simulation/units.js +6 -4
- 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 -0
- package/src/simulation/vehiclePhysics.js +1 -104
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -0
- package/src/styles.css +342 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +20 -426
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +6 -5
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/templateUtils.js +23 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -1,253 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const LANE_OFFSETS = [-78, -52, -26, 0, 26, 52, 78];
|
|
6
|
-
|
|
7
|
-
function angleToPoint(car, target) {
|
|
8
|
-
const angle = Math.atan2(target.y - car.y, target.x - car.x);
|
|
9
|
-
return normalizeAngle(angle - car.heading);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function buildDriverPersonality(driver, index, racecraft, random) {
|
|
13
|
-
const numberSeed = Number(driver.driverNumber ?? index + 1);
|
|
14
|
-
const numberBias = (((Number.isFinite(numberSeed) ? numberSeed : index + 1) % 11) - 5) * 0.014;
|
|
15
|
-
const baseAggression = clamp(
|
|
16
|
-
driver.personality?.aggression ?? 0.36 + racecraft * 0.34 + numberBias + seededRange(random, -0.045, 0.045),
|
|
17
|
-
0.18,
|
|
18
|
-
0.88,
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
baseAggression,
|
|
23
|
-
riskTolerance: clamp(
|
|
24
|
-
driver.personality?.riskTolerance ?? baseAggression * 0.72 + racecraft * 0.24 + seededRange(random, -0.04, 0.04),
|
|
25
|
-
0.12,
|
|
26
|
-
0.95,
|
|
27
|
-
),
|
|
28
|
-
patience: clamp(
|
|
29
|
-
driver.personality?.patience ?? 0.72 - baseAggression * 0.36 + racecraft * 0.12 + seededRange(random, -0.05, 0.05),
|
|
30
|
-
0.18,
|
|
31
|
-
0.9,
|
|
32
|
-
),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function createDriverInput() {
|
|
37
|
-
const state = {
|
|
38
|
-
steering: 0,
|
|
39
|
-
throttle: 0,
|
|
40
|
-
brake: 0,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
steer(amount) {
|
|
45
|
-
state.steering = clamp(amount, -VEHICLE_LIMITS.maxSteer, VEHICLE_LIMITS.maxSteer);
|
|
46
|
-
return this;
|
|
47
|
-
},
|
|
48
|
-
accelerate(amount) {
|
|
49
|
-
state.throttle = clamp(amount, 0, 1);
|
|
50
|
-
return this;
|
|
51
|
-
},
|
|
52
|
-
brake(amount) {
|
|
53
|
-
state.brake = clamp(amount, 0, 1);
|
|
54
|
-
return this;
|
|
55
|
-
},
|
|
56
|
-
controls() {
|
|
57
|
-
return { ...state };
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function decideDriverControls({ car, orderIndex, race }) {
|
|
63
|
-
if (car.manualControls) return car.manualControls;
|
|
64
|
-
|
|
65
|
-
if (car.gridLocked) {
|
|
66
|
-
return createDriverInput().brake(1).controls();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (race.safetyCar.deployed) {
|
|
70
|
-
return decideSafetyCarControls(car, orderIndex, race);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!car.trackState.onTrack) {
|
|
74
|
-
return decideRejoinControls(car, race);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return decideRacingControls(car, orderIndex, race);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function decideRacingControls(car, orderIndex, race) {
|
|
81
|
-
const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
|
|
82
|
-
const lookahead = clamp(car.speed * (1.12 - aggression * 0.08) + 160, 160, 360);
|
|
83
|
-
const targetBase = pointAt(race.track, car.progress + lookahead);
|
|
84
|
-
const lanePlan = planRacingLine(car, orderIndex, race);
|
|
85
|
-
const recoveryBias = car.trackState.crossTrackError > race.track.width * 0.46 ? 0 : 1;
|
|
86
|
-
const target = offsetTrackPoint(targetBase, lanePlan.offset * recoveryBias);
|
|
87
|
-
const angleError = angleToPoint(car, target);
|
|
88
|
-
const curvature = Math.max(car.trackState.curvature, targetBase.curvature);
|
|
89
|
-
const gripBudget = 54 + car.racecraft * 11 + (car.tireEnergy ?? 100) * 0.05 + aggression * 4.5;
|
|
90
|
-
const cornerTarget = clamp(
|
|
91
|
-
Math.sqrt(gripBudget / Math.max(curvature, 0.0001)) + (car.pace - 1) * 20 + aggression * 7,
|
|
92
|
-
72,
|
|
93
|
-
168,
|
|
94
|
-
);
|
|
95
|
-
const edgeTolerance = race.track.width * (0.36 + aggression * 0.1);
|
|
96
|
-
const edgePenalty = Math.max(0, car.trackState.crossTrackError - edgeTolerance) * (0.16 - aggression * 0.05);
|
|
97
|
-
const trafficPenalty = Math.max(
|
|
98
|
-
lanePlan.sameLaneAhead ? clamp((230 - lanePlan.sameLaneAhead.gap) * 0.16, 0, 32) : 0,
|
|
99
|
-
lanePlan.sideRisk ? clamp((44 - lanePlan.sideRisk.lateral) * 0.42, 0, 16) : 0,
|
|
100
|
-
) * (1 - aggression * 0.28);
|
|
101
|
-
const desiredSpeed = clamp(
|
|
102
|
-
(car.drsActive ? cornerTarget + 22 : cornerTarget) - edgePenalty - trafficPenalty,
|
|
103
|
-
58,
|
|
104
|
-
VEHICLE_LIMITS.maxSpeed,
|
|
105
|
-
);
|
|
106
|
-
const speedError = desiredSpeed - car.speed;
|
|
107
|
-
|
|
108
|
-
return createDriverInput()
|
|
109
|
-
.steer(angleError * (0.82 + car.racecraft * 0.1))
|
|
110
|
-
.accelerate(speedError > 1 ? clamp(speedError / 16, 0.1 + aggression * 0.08, 1) : 0)
|
|
111
|
-
.brake(speedError < -2 ? clamp(Math.abs(speedError) / (22 + aggression * 8), 0, 1) : 0)
|
|
112
|
-
.controls();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function decideRejoinControls(car, race) {
|
|
116
|
-
const lookahead = clamp(car.speed * 0.72 + 118, 118, 220);
|
|
117
|
-
const targetBase = pointAt(race.track, car.progress + lookahead);
|
|
118
|
-
const target = offsetTrackPoint(targetBase, 0);
|
|
119
|
-
const angleError = angleToPoint(car, target);
|
|
120
|
-
const distanceFromRoad = Math.max(0, car.trackState.crossTrackError - race.track.width / 2);
|
|
121
|
-
const surfaceTargetSpeed = car.trackState.surface === 'gravel'
|
|
122
|
-
? 54
|
|
123
|
-
: car.trackState.surface === 'grass'
|
|
124
|
-
? 44
|
|
125
|
-
: 30;
|
|
126
|
-
const desiredSpeed = clamp(surfaceTargetSpeed - distanceFromRoad * 0.045, 20, surfaceTargetSpeed);
|
|
127
|
-
const speedError = desiredSpeed - car.speed;
|
|
128
|
-
const alignment = clamp(1 - Math.abs(angleError) / Math.PI, 0.28, 1);
|
|
129
|
-
const lowSpeedRecovery = car.speed < 18 ? 0.16 : 0;
|
|
130
|
-
const throttleLimit = car.trackState.surface === 'gravel' ? 0.62 : 0.48;
|
|
131
|
-
|
|
132
|
-
return createDriverInput()
|
|
133
|
-
.steer(angleError * 0.92)
|
|
134
|
-
.accelerate(speedError > 0 ? clamp((speedError / 24) * alignment + lowSpeedRecovery, 0.08, throttleLimit) : lowSpeedRecovery)
|
|
135
|
-
.brake(speedError < -5 ? clamp(Math.abs(speedError) / 30, 0.04, 0.74) : 0)
|
|
136
|
-
.controls();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function decideSafetyCarControls(car, orderIndex, race) {
|
|
140
|
-
const queueSlot = race.safetyCar.progress - race.rules.safetyCarLeadDistance - orderIndex * race.rules.safetyCarGap;
|
|
141
|
-
const lookahead = clamp(car.speed * 0.8 + 74, 84, 148);
|
|
142
|
-
const targetBase = pointAt(race.track, car.progress + lookahead);
|
|
143
|
-
const target = offsetTrackPoint(targetBase, 0);
|
|
144
|
-
const slotError = queueSlot - car.raceDistance;
|
|
145
|
-
const desiredSpeed = clamp(
|
|
146
|
-
race.rules.safetyCarSpeed + slotError * 0.24,
|
|
147
|
-
22,
|
|
148
|
-
race.rules.safetyCarSpeed + 32,
|
|
149
|
-
);
|
|
150
|
-
const speedError = desiredSpeed - car.speed;
|
|
151
|
-
|
|
152
|
-
return createDriverInput()
|
|
153
|
-
.steer(angleToPoint(car, target) * 1.04)
|
|
154
|
-
.accelerate(speedError > 1 ? clamp(speedError / 16, 0, 0.5) : 0)
|
|
155
|
-
.brake(speedError < -0.5 ? clamp(Math.abs(speedError) / 14, 0, 1) : 0)
|
|
156
|
-
.controls();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function planRacingLine(car, orderIndex, race) {
|
|
160
|
-
const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
|
|
161
|
-
const riskTolerance = car.personality?.riskTolerance ?? aggression;
|
|
162
|
-
const trackLimit = race.track.width / 2 - VEHICLE_LIMITS.carWidth * clamp(1.22 - aggression * 0.34, 0.84, 1.22);
|
|
163
|
-
const preferred = Math.sin((car.index / Math.max(1, race.cars.length)) * TWO_PI) * 26;
|
|
164
|
-
const currentOffset = clamp(car.desiredOffset ?? preferred, -trackLimit, trackLimit);
|
|
165
|
-
const ahead = race.orderedCars[orderIndex - 1];
|
|
166
|
-
const traffic = scanNearbyTraffic(car, race);
|
|
167
|
-
|
|
168
|
-
let bestOffset = currentOffset;
|
|
169
|
-
let bestScore = -Infinity;
|
|
170
|
-
|
|
171
|
-
LANE_OFFSETS.forEach((rawOffset) => {
|
|
172
|
-
const offset = clamp(rawOffset, -trackLimit, trackLimit);
|
|
173
|
-
const edgeClearance = trackLimit - Math.abs(offset);
|
|
174
|
-
let score = 80;
|
|
175
|
-
score -= Math.abs(offset - preferred) * 0.18;
|
|
176
|
-
score -= Math.abs(offset - currentOffset) * (0.09 - aggression * 0.025);
|
|
177
|
-
score -= Math.max(0, 18 - edgeClearance) * (0.92 - aggression * 0.42);
|
|
178
|
-
|
|
179
|
-
traffic.forEach((entry) => {
|
|
180
|
-
const lateral = Math.abs(entry.signedOffset - offset);
|
|
181
|
-
if (entry.gap > 0 && entry.gap < 260) {
|
|
182
|
-
const overlapRisk = clamp(58 - lateral, 0, 58);
|
|
183
|
-
score -= overlapRisk * (260 - entry.gap) * 0.038 * (1 - riskTolerance * 0.34);
|
|
184
|
-
if (entry.gap < 190 + aggression * 70 && lateral > 34 - aggression * 8) {
|
|
185
|
-
score += Math.min(30 + aggression * 18, lateral - (28 - aggression * 6)) * (0.62 + aggression * 0.82);
|
|
186
|
-
}
|
|
187
|
-
} else if (entry.gap <= 0 && entry.gap > -74) {
|
|
188
|
-
const sideOverlapRisk = clamp(52 - lateral, 0, 52);
|
|
189
|
-
score -= sideOverlapRisk * (74 + entry.gap) * 0.052 * (1 - riskTolerance * 0.22);
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
if (ahead && car.gapAhead < 230) {
|
|
194
|
-
const side = car.index % 2 === 0 ? -1 : 1;
|
|
195
|
-
const passSide = clamp(
|
|
196
|
-
(ahead.trackState.signedOffset * -0.65) + side * (48 + aggression * 42),
|
|
197
|
-
-trackLimit,
|
|
198
|
-
trackLimit,
|
|
199
|
-
);
|
|
200
|
-
score -= Math.abs(offset - passSide) * (0.09 + aggression * 0.08);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (score > bestScore) {
|
|
204
|
-
bestScore = score;
|
|
205
|
-
bestOffset = offset;
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const laneChangeRate = 0.72 + car.racecraft * 0.3 + aggression * 0.58;
|
|
210
|
-
car.desiredOffset = currentOffset + clamp(bestOffset - currentOffset, -laneChangeRate, laneChangeRate);
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
offset: car.desiredOffset,
|
|
214
|
-
sameLaneAhead: findLaneTrafficAhead(traffic, car.desiredOffset, 230),
|
|
215
|
-
sideRisk: findLaneTrafficBeside(traffic, car.desiredOffset),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function scanNearbyTraffic(car, race) {
|
|
220
|
-
return race.cars
|
|
221
|
-
.filter((other) => other !== car)
|
|
222
|
-
.map((other) => ({
|
|
223
|
-
car: other,
|
|
224
|
-
gap: other.raceDistance - car.raceDistance,
|
|
225
|
-
signedOffset: other.trackState?.signedOffset ?? 0,
|
|
226
|
-
}))
|
|
227
|
-
.filter((entry) => entry.gap > -82 && entry.gap < 280);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function findLaneTrafficAhead(traffic, offset, maxDistance) {
|
|
231
|
-
let closest = null;
|
|
232
|
-
|
|
233
|
-
traffic.forEach((entry) => {
|
|
234
|
-
if (entry.gap <= 0 || entry.gap > maxDistance) return;
|
|
235
|
-
if (Math.abs(entry.signedOffset - offset) > 42) return;
|
|
236
|
-
if (!closest || entry.gap < closest.gap) closest = entry;
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
return closest;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function findLaneTrafficBeside(traffic, offset) {
|
|
243
|
-
let closest = null;
|
|
244
|
-
|
|
245
|
-
traffic.forEach((entry) => {
|
|
246
|
-
const lateral = Math.abs(entry.signedOffset - offset);
|
|
247
|
-
if (Math.abs(entry.gap) > 58 || lateral > 44) return;
|
|
248
|
-
const risk = (58 - Math.abs(entry.gap)) + (44 - lateral);
|
|
249
|
-
if (!closest || risk > closest.risk) closest = { ...entry, lateral, risk };
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
return closest;
|
|
253
|
-
}
|
|
1
|
+
export { decideDriverControls } from './driver/driverController.js';
|
|
2
|
+
export { createDriverInput } from './driver/driverInput.js';
|
|
3
|
+
export { buildDriverPersonality } from './driver/driverPersonality.js';
|
|
4
|
+
export { planRacingLine } from './driver/racingLinePlan.js';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const PARTICIPANT_INTERACTION_PROFILES = Object.freeze({
|
|
2
|
+
normal: Object.freeze({
|
|
3
|
+
profile: 'normal',
|
|
4
|
+
collidable: true,
|
|
5
|
+
detectableByRays: true,
|
|
6
|
+
detectableAsNearby: true,
|
|
7
|
+
blocksPitLane: true,
|
|
8
|
+
affectsRaceOrder: true,
|
|
9
|
+
}),
|
|
10
|
+
'isolated-training': Object.freeze({
|
|
11
|
+
profile: 'isolated-training',
|
|
12
|
+
collidable: false,
|
|
13
|
+
detectableByRays: false,
|
|
14
|
+
detectableAsNearby: false,
|
|
15
|
+
blocksPitLane: false,
|
|
16
|
+
affectsRaceOrder: true,
|
|
17
|
+
}),
|
|
18
|
+
'batch-training': Object.freeze({
|
|
19
|
+
profile: 'batch-training',
|
|
20
|
+
collidable: false,
|
|
21
|
+
detectableByRays: false,
|
|
22
|
+
detectableAsNearby: false,
|
|
23
|
+
blocksPitLane: false,
|
|
24
|
+
affectsRaceOrder: false,
|
|
25
|
+
}),
|
|
26
|
+
'phantom-race': Object.freeze({
|
|
27
|
+
profile: 'phantom-race',
|
|
28
|
+
collidable: false,
|
|
29
|
+
detectableByRays: true,
|
|
30
|
+
detectableAsNearby: true,
|
|
31
|
+
blocksPitLane: false,
|
|
32
|
+
affectsRaceOrder: true,
|
|
33
|
+
}),
|
|
34
|
+
'time-trial-overlay': Object.freeze({
|
|
35
|
+
profile: 'time-trial-overlay',
|
|
36
|
+
collidable: false,
|
|
37
|
+
detectableByRays: false,
|
|
38
|
+
detectableAsNearby: false,
|
|
39
|
+
blocksPitLane: false,
|
|
40
|
+
affectsRaceOrder: false,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const INTERACTION_FLAGS = Object.freeze([
|
|
45
|
+
'collidable',
|
|
46
|
+
'detectableByRays',
|
|
47
|
+
'detectableAsNearby',
|
|
48
|
+
'blocksPitLane',
|
|
49
|
+
'affectsRaceOrder',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export function normalizeParticipantInteractions(options = {}) {
|
|
53
|
+
const defaultProfile = resolveProfileName(options.defaultProfile);
|
|
54
|
+
const defaultInteraction = { ...PARTICIPANT_INTERACTION_PROFILES[defaultProfile] };
|
|
55
|
+
const drivers = {};
|
|
56
|
+
Object.entries(options.drivers ?? {}).forEach(([driverId, override]) => {
|
|
57
|
+
drivers[driverId] = resolveInteraction(override, defaultProfile);
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
defaultProfile,
|
|
61
|
+
defaultInteraction,
|
|
62
|
+
drivers,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function attachParticipantInteractions(cars = [], options = {}) {
|
|
67
|
+
const normalized = normalizeParticipantInteractions(options);
|
|
68
|
+
cars.forEach((car) => {
|
|
69
|
+
car.interaction = normalized.drivers[car.id] ?? { ...normalized.defaultInteraction };
|
|
70
|
+
});
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function resolveInteraction(override = {}, fallbackProfile = 'normal') {
|
|
75
|
+
const profile = resolveProfileName(override?.profile ?? fallbackProfile);
|
|
76
|
+
const resolved = { ...PARTICIPANT_INTERACTION_PROFILES[profile] };
|
|
77
|
+
INTERACTION_FLAGS.forEach((flag) => {
|
|
78
|
+
if (override?.[flag] != null) resolved[flag] = Boolean(override[flag]);
|
|
79
|
+
});
|
|
80
|
+
return resolved;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function canCollide(first, second) {
|
|
84
|
+
return isCollidable(first) && isCollidable(second);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isCollidable(car) {
|
|
88
|
+
if (car?.destroyed || car?.outOfRace) return false;
|
|
89
|
+
return car?.interaction?.collidable !== false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isRayDetectable(car) {
|
|
93
|
+
if (car?.destroyed || car?.outOfRace) return false;
|
|
94
|
+
return car?.interaction?.detectableByRays !== false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isNearbyDetectable(car) {
|
|
98
|
+
if (car?.destroyed || car?.outOfRace) return false;
|
|
99
|
+
return car?.interaction?.detectableAsNearby !== false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function blocksPitLane(car) {
|
|
103
|
+
if (car?.destroyed || car?.outOfRace) return false;
|
|
104
|
+
return car?.interaction?.blocksPitLane !== false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function affectsRaceOrder(car) {
|
|
108
|
+
if (car?.destroyed || car?.outOfRace) return false;
|
|
109
|
+
return car?.interaction?.affectsRaceOrder !== false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function serializeParticipantInteraction(interaction) {
|
|
113
|
+
const normalized = resolveInteraction(interaction ?? {}, interaction?.profile ?? 'normal');
|
|
114
|
+
return { ...normalized };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveProfileName(profile) {
|
|
118
|
+
const key = String(profile ?? 'normal');
|
|
119
|
+
return PARTICIPANT_INTERACTION_PROFILES[key] ? key : 'normal';
|
|
120
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { pitLaneStatusSnapshot } from './pitSnapshots.js';
|
|
2
|
+
import {
|
|
3
|
+
firstDifferentCompound,
|
|
4
|
+
normalizePitIntent,
|
|
5
|
+
shouldStartPitStopForSimulation,
|
|
6
|
+
updateAutomaticPitIntentForSimulation,
|
|
7
|
+
} from './pitIntent.js';
|
|
8
|
+
import {
|
|
9
|
+
advancePitService,
|
|
10
|
+
applyPitRoutePosition,
|
|
11
|
+
beginPitQueue,
|
|
12
|
+
beginPitService,
|
|
13
|
+
finishPitExit,
|
|
14
|
+
getPitBoxRaceDistance,
|
|
15
|
+
getPitStopBox,
|
|
16
|
+
isPitServiceBusy,
|
|
17
|
+
isPitServiceQueueOccupied,
|
|
18
|
+
releasePitQueue,
|
|
19
|
+
} from './pitService.js';
|
|
20
|
+
import {
|
|
21
|
+
createPitApproachPoints,
|
|
22
|
+
createRoute,
|
|
23
|
+
pitDriveLaneOffset,
|
|
24
|
+
pitMainLanePointAt,
|
|
25
|
+
routePoint,
|
|
26
|
+
} from './pitRouting.js';
|
|
27
|
+
import { metersToSimUnits } from '../units.js';
|
|
28
|
+
import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
29
|
+
|
|
30
|
+
const PIT_ENTRY_APPROACH_DISTANCE = metersToSimUnits(250);
|
|
31
|
+
const PIT_BOX_APPROACH_DISTANCE = metersToSimUnits(34);
|
|
32
|
+
const PIT_SERVICE_QUEUE_FALLBACK_GAP = metersToSimUnits(48);
|
|
33
|
+
const PIT_ENTRY_LATE_THRESHOLD_DISTANCE = metersToSimUnits(90);
|
|
34
|
+
|
|
35
|
+
export function isCarInActivePitStop(sim, car) {
|
|
36
|
+
return Boolean(car?.pitStop && car.pitStop.status !== 'pending' && car.pitStop.status !== 'completed');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function schedulePitStopAtNextEntry(sim, car, stop) {
|
|
40
|
+
const pitLane = sim.track.pitLane;
|
|
41
|
+
if (!stop || !pitLane?.entry || !Number.isFinite(sim.track.length) || sim.track.length <= 0) return false;
|
|
42
|
+
const raceDistance = car.raceDistance ?? 0;
|
|
43
|
+
const entryOffset = pitLane.entry.distanceFromStart ?? 0;
|
|
44
|
+
let lapBase = Math.floor((raceDistance - entryOffset) / sim.track.length) * sim.track.length;
|
|
45
|
+
let entryRaceDistance = lapBase + entryOffset;
|
|
46
|
+
|
|
47
|
+
while (raceDistance > entryRaceDistance + PIT_ENTRY_LATE_THRESHOLD_DISTANCE) {
|
|
48
|
+
lapBase += sim.track.length;
|
|
49
|
+
entryRaceDistance = lapBase + entryOffset;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
stop.lapBase = lapBase;
|
|
53
|
+
stop.entryRaceDistance = entryRaceDistance;
|
|
54
|
+
stop.plannedRaceDistance = entryRaceDistance - PIT_ENTRY_APPROACH_DISTANCE;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function rearmCompletedPitStop(sim, car, stop) {
|
|
59
|
+
if (!stop || stop.status !== 'completed') return false;
|
|
60
|
+
stop.status = 'pending';
|
|
61
|
+
stop.phase = null;
|
|
62
|
+
stop.route = null;
|
|
63
|
+
stop.routeProgress = 0;
|
|
64
|
+
stop.routeStartRaceDistance = null;
|
|
65
|
+
stop.routeEndRaceDistance = null;
|
|
66
|
+
stop.serviceRemaining = 0;
|
|
67
|
+
stop.penaltyServiceRemaining = 0;
|
|
68
|
+
stop.penaltyServiceTotal = 0;
|
|
69
|
+
stop.servingPenaltyIds = [];
|
|
70
|
+
stop.queueingForService = false;
|
|
71
|
+
stop.serviceProfile = null;
|
|
72
|
+
stop.targetTire = stop.targetTire ?? firstDifferentCompound(car.tire, sim.rules.modules?.tireStrategy?.compounds);
|
|
73
|
+
return schedulePitStopAtNextEntry(sim, car, stop);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function canStartPitStop(sim, car) {
|
|
77
|
+
const stop = car.pitStop;
|
|
78
|
+
const pitStops = sim.rules.modules?.pitStops;
|
|
79
|
+
if (!stop || !pitStops?.enabled) return false;
|
|
80
|
+
const active = sim.cars.filter((candidate) => candidate !== car && isCarInActivePitStop(sim, candidate));
|
|
81
|
+
const maxConcurrentPitLaneCars = Math.max(1, Math.floor(pitStops.maxConcurrentPitLaneCars ?? 3));
|
|
82
|
+
if (active.length >= maxConcurrentPitLaneCars) return false;
|
|
83
|
+
|
|
84
|
+
if (!pitStops.doubleStacking && stop.teamId) {
|
|
85
|
+
const box = getPitStopBox(sim, stop);
|
|
86
|
+
if (isPitServiceQueueOccupied(sim, car, box)) return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const minimumGap = Math.max(0, pitStops.minimumPitLaneGap ?? 0);
|
|
90
|
+
const candidateDistance = car.raceDistance ?? 0;
|
|
91
|
+
return active.every((candidate) => {
|
|
92
|
+
const gap = (candidate.raceDistance ?? 0) - candidateDistance;
|
|
93
|
+
return gap < 0 || gap >= minimumGap;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function shouldStartPitStop(sim, car) {
|
|
98
|
+
return shouldStartPitStopForSimulation(sim, car);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function updateAutomaticPitIntent(sim, car) {
|
|
102
|
+
updateAutomaticPitIntentForSimulation(sim, car);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isPitLaneOpenForStops(sim) {
|
|
106
|
+
return pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops).open;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function pitEntryLateThresholdDistance() {
|
|
110
|
+
return PIT_ENTRY_LATE_THRESHOLD_DISTANCE;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function startPitStop(sim, car) {
|
|
114
|
+
const stop = car.pitStop;
|
|
115
|
+
const box = getPitStopBox(sim, stop);
|
|
116
|
+
const pitLane = sim.track.pitLane;
|
|
117
|
+
if (!stop || !box || !pitLane?.enabled) return false;
|
|
118
|
+
|
|
119
|
+
car.gridLocked = false;
|
|
120
|
+
car.drsActive = false;
|
|
121
|
+
car.drsEligible = false;
|
|
122
|
+
car.drsZoneId = null;
|
|
123
|
+
car.drsZoneEnabled = false;
|
|
124
|
+
car.canAttack = false;
|
|
125
|
+
stop.targetTire = stop.targetTire ?? firstDifferentCompound(car.tire, sim.rules.modules?.tireStrategy?.compounds);
|
|
126
|
+
|
|
127
|
+
const currentState = applyWheelSurfaceState(car, sim.track).representativeState;
|
|
128
|
+
sim.events.unshift({
|
|
129
|
+
type: 'pit-entry',
|
|
130
|
+
at: sim.time,
|
|
131
|
+
carId: car.id,
|
|
132
|
+
boxId: box.id,
|
|
133
|
+
teamId: box.teamId ?? null,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (currentState.surface === 'pit-box' && currentState.pitBoxId === box.id) {
|
|
137
|
+
beginPitService(sim, car, box);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const hasServiceQueue = Boolean(box.queuePoint);
|
|
142
|
+
const shouldStageForService = hasServiceQueue;
|
|
143
|
+
const queueDistanceAlongLane = box.queueDistanceAlongLane ??
|
|
144
|
+
Math.max(0, box.distanceAlongLane - PIT_SERVICE_QUEUE_FALLBACK_GAP);
|
|
145
|
+
const stopDistanceAlongLane = shouldStageForService
|
|
146
|
+
? queueDistanceAlongLane
|
|
147
|
+
: box.distanceAlongLane;
|
|
148
|
+
const serviceTarget = shouldStageForService ? box.queuePoint : box.center;
|
|
149
|
+
const driveLaneOffset = pitDriveLaneOffset(pitLane);
|
|
150
|
+
const boxApproachDistance = Math.max(
|
|
151
|
+
0,
|
|
152
|
+
(hasServiceQueue ? queueDistanceAlongLane : stopDistanceAlongLane) - PIT_BOX_APPROACH_DISTANCE,
|
|
153
|
+
);
|
|
154
|
+
const route = createRoute([
|
|
155
|
+
...createPitApproachPoints(sim.track, car, pitLane, stop.entryRaceDistance),
|
|
156
|
+
...(pitLane.entry.roadCenterline ?? []).map((point) => routePoint(point)),
|
|
157
|
+
routePoint(pitMainLanePointAt(pitLane, 0, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
158
|
+
routePoint(pitMainLanePointAt(pitLane, boxApproachDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
159
|
+
routePoint(serviceTarget, pitLane.mainLane.heading, { limiterActive: true }),
|
|
160
|
+
]);
|
|
161
|
+
stop.status = 'entering';
|
|
162
|
+
stop.phase = 'entry';
|
|
163
|
+
stop.queueingForService = shouldStageForService;
|
|
164
|
+
stop.route = route;
|
|
165
|
+
stop.routeProgress = 0;
|
|
166
|
+
stop.routeStartRaceDistance = car.raceDistance ?? stop.plannedRaceDistance;
|
|
167
|
+
stop.routeEndRaceDistance = getPitBoxRaceDistance(sim, stop, {
|
|
168
|
+
...box,
|
|
169
|
+
distanceAlongLane: stopDistanceAlongLane,
|
|
170
|
+
});
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function advancePitStopCar(sim, car, delta) {
|
|
175
|
+
const pitStops = sim.rules.modules?.pitStops;
|
|
176
|
+
const stop = car.pitStop;
|
|
177
|
+
if (!pitStops?.enabled || !stop) return false;
|
|
178
|
+
if (stop.status === 'pending' || stop.status === 'completed') updateAutomaticPitIntent(sim, car);
|
|
179
|
+
if (stop.status === 'completed') return false;
|
|
180
|
+
if (stop.status === 'pending' && !shouldStartPitStop(sim, car)) return false;
|
|
181
|
+
if (stop.status === 'pending') startPitStop(sim, car);
|
|
182
|
+
|
|
183
|
+
if (stop.status === 'entering') {
|
|
184
|
+
const finishedRoute = applyPitRoutePosition(sim, car, delta);
|
|
185
|
+
const box = getPitStopBox(sim, stop);
|
|
186
|
+
if (finishedRoute && box) {
|
|
187
|
+
if (stop.queueingForService) {
|
|
188
|
+
if (!isPitServiceBusy(sim, car, box) && !isPitServiceQueueOccupied(sim, car, box)) {
|
|
189
|
+
releasePitQueue(sim, car, box, { fromCurrent: true });
|
|
190
|
+
} else {
|
|
191
|
+
beginPitQueue(sim, car, box);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else beginPitService(sim, car, box);
|
|
195
|
+
}
|
|
196
|
+
car.contactCooldown = Math.max(0, car.contactCooldown - delta);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (stop.status === 'queued') {
|
|
201
|
+
const box = getPitStopBox(sim, stop);
|
|
202
|
+
if (!box) return true;
|
|
203
|
+
car.previousX = car.x;
|
|
204
|
+
car.previousY = car.y;
|
|
205
|
+
car.previousHeading = car.heading;
|
|
206
|
+
car.x = box.queuePoint.x;
|
|
207
|
+
car.y = box.queuePoint.y;
|
|
208
|
+
car.heading = sim.track.pitLane.mainLane.heading;
|
|
209
|
+
car.speed = 0;
|
|
210
|
+
car.throttle = 0;
|
|
211
|
+
car.brake = 1;
|
|
212
|
+
applyWheelSurfaceState(car, sim.track);
|
|
213
|
+
car.progress = car.trackState.distance;
|
|
214
|
+
car.raceDistance = getPitBoxRaceDistance(sim, stop, {
|
|
215
|
+
...box,
|
|
216
|
+
distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
|
|
217
|
+
});
|
|
218
|
+
if (!isPitServiceBusy(sim, car, box)) releasePitQueue(sim, car, box);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (stop.status === 'servicing') {
|
|
223
|
+
return advancePitService(sim, car, delta);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (stop.status === 'exiting') {
|
|
227
|
+
const finishedRoute = applyPitRoutePosition(sim, car, delta);
|
|
228
|
+
if (finishedRoute) finishPitExit(sim, car);
|
|
229
|
+
car.contactCooldown = Math.max(0, car.contactCooldown - delta);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const PIT_INTENT_NONE = 0;
|
|
2
|
+
export const PIT_INTENT_IF_FREE = 1;
|
|
3
|
+
export const PIT_INTENT_COMMITTED = 2;
|
|
4
|
+
|
|
5
|
+
export function firstDifferentCompound(currentTire, compounds) {
|
|
6
|
+
const available = Array.isArray(compounds) && compounds.length ? compounds : ['S', 'M', 'H'];
|
|
7
|
+
return available.find((compound) => compound !== currentTire) ?? currentTire;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizePitCompound(value, compounds) {
|
|
11
|
+
if (value == null || value === '') return null;
|
|
12
|
+
const available = Array.isArray(compounds) && compounds.length ? compounds : ['S', 'M', 'H'];
|
|
13
|
+
return available.includes(value) ? value : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizePitIntent(value) {
|
|
17
|
+
const number = Number(value);
|
|
18
|
+
if (!Number.isInteger(number) || number < 0 || number > 2) return null;
|
|
19
|
+
return number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setPitIntentForSimulation(sim, id, intent, targetCompound = undefined) {
|
|
23
|
+
const request = intent && typeof intent === 'object'
|
|
24
|
+
? intent
|
|
25
|
+
: { intent, targetCompound };
|
|
26
|
+
const nextIntent = normalizePitIntent(request.intent ?? request.pitIntent);
|
|
27
|
+
if (nextIntent == null) return false;
|
|
28
|
+
const car = sim.cars.find((item) => item.id === id);
|
|
29
|
+
const stop = car?.pitStop;
|
|
30
|
+
const pitStops = sim.rules.modules?.pitStops;
|
|
31
|
+
if (!car || !stop || !pitStops?.enabled || !sim.track.pitLane?.enabled) return false;
|
|
32
|
+
if (sim.isCarInActivePitStop(car)) return false;
|
|
33
|
+
|
|
34
|
+
const compoundRequest = request.targetCompound ??
|
|
35
|
+
request.compound ??
|
|
36
|
+
request.pitCompound ??
|
|
37
|
+
request.pitTargetCompound ??
|
|
38
|
+
request.targetTire;
|
|
39
|
+
const hasCompoundRequest = compoundRequest != null && compoundRequest !== '';
|
|
40
|
+
if (hasCompoundRequest) {
|
|
41
|
+
const targetTire = normalizePitCompound(compoundRequest, sim.rules.modules?.tireStrategy?.compounds);
|
|
42
|
+
if (!targetTire) return false;
|
|
43
|
+
stop.targetTire = targetTire;
|
|
44
|
+
} else if (nextIntent !== 0 && (!stop.targetTire || stop.status === 'completed')) {
|
|
45
|
+
stop.targetTire = firstDifferentCompound(car.tire, sim.rules.modules?.tireStrategy?.compounds);
|
|
46
|
+
} else if (nextIntent === 0) {
|
|
47
|
+
stop.targetTire = firstDifferentCompound(car.tire, sim.rules.modules?.tireStrategy?.compounds);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
stop.intent = nextIntent;
|
|
51
|
+
if (stop.status === 'pending' && nextIntent !== 0) {
|
|
52
|
+
sim.schedulePitStopAtNextEntry(car, stop);
|
|
53
|
+
} else if (stop.status === 'completed' && nextIntent !== 0) {
|
|
54
|
+
sim.rearmCompletedPitStop(car, stop);
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function shouldStartPitStopForSimulation(sim, car) {
|
|
60
|
+
const stop = car.pitStop;
|
|
61
|
+
if (!stop || stop.status !== 'pending' || car.finished) return false;
|
|
62
|
+
if (!sim.isPitLaneOpenForStops()) return false;
|
|
63
|
+
const intent = normalizePitIntent(stop.intent) ?? 0;
|
|
64
|
+
if (intent === 0) return false;
|
|
65
|
+
const raceDistance = car.raceDistance ?? 0;
|
|
66
|
+
if (raceDistance > stop.entryRaceDistance + sim.pitEntryLateThresholdDistance()) {
|
|
67
|
+
sim.schedulePitStopAtNextEntry(car, stop);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return raceDistance >= stop.plannedRaceDistance &&
|
|
71
|
+
(intent === 2 || sim.canStartPitStop(car));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function updateAutomaticPitIntentForSimulation(sim, car) {
|
|
75
|
+
const stop = car.pitStop;
|
|
76
|
+
const pitStops = sim.rules.modules?.pitStops;
|
|
77
|
+
if (!stop || !['pending', 'completed'].includes(stop.status) || !pitStops?.enabled || car.finished) return;
|
|
78
|
+
if (car.automaticPitIntentEnabled === false) return;
|
|
79
|
+
const tireEnergy = Number(car.tireEnergy ?? 100);
|
|
80
|
+
const requestThreshold = Number(pitStops.tirePitRequestThresholdPercent ?? 50);
|
|
81
|
+
const commitThreshold = Number(pitStops.tirePitCommitThresholdPercent ?? 30);
|
|
82
|
+
if (!Number.isFinite(tireEnergy)) return;
|
|
83
|
+
|
|
84
|
+
let nextIntent = 0;
|
|
85
|
+
if (tireEnergy < commitThreshold) nextIntent = 2;
|
|
86
|
+
else if (tireEnergy < requestThreshold) nextIntent = 1;
|
|
87
|
+
if (nextIntent === 0 || nextIntent <= (normalizePitIntent(stop.intent) ?? 0)) return;
|
|
88
|
+
|
|
89
|
+
stop.intent = nextIntent;
|
|
90
|
+
if (stop.status === 'completed') sim.rearmCompletedPitStop(car, stop);
|
|
91
|
+
else sim.schedulePitStopAtNextEntry(car, stop);
|
|
92
|
+
}
|