@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,1146 +1,520 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from './
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
2
|
+
applyUnservedServicePenalty,
|
|
3
|
+
cancelPenaltyRecord,
|
|
4
|
+
createPenaltyEvent,
|
|
5
|
+
createPenaltyRecord,
|
|
6
|
+
servePenaltyRecord,
|
|
7
|
+
} from './rules/penaltyLedger.js';
|
|
8
|
+
import { createPenaltyCancelledEvent, createPenaltyServedEvent } from './rules/penaltyEvents.js';
|
|
9
|
+
import { buildPenaltyStatsByDriver, getPenaltyStats } from './rules/penaltyStats.js';
|
|
10
|
+
import {
|
|
11
|
+
reviewCollisionForSimulation,
|
|
12
|
+
reviewPitLaneSpeedingForSimulation,
|
|
13
|
+
reviewTireRequirementForSimulation,
|
|
14
|
+
reviewTrackLimitsForSimulation,
|
|
15
|
+
} from './rules/rulesReview.js';
|
|
16
|
+
import { DEFAULT_RULES } from './rulesConfig.js';
|
|
17
|
+
import { applyExternalCarState } from './vehicle/vehicleState.js';
|
|
18
|
+
import {
|
|
19
|
+
applyContactVelocityResponse as applyContactVelocityResponseForSimulation,
|
|
20
|
+
resolveCollisionsForSimulation,
|
|
21
|
+
} from './vehicle/contactResolution.js';
|
|
22
|
+
import { applyRunoffResponseForSimulation } from './vehicle/runoffResponse.js';
|
|
23
|
+
import { resetLapTelemetry, resetTimingHistory, resetTimingLineCrossings } from './timing/raceTiming.js';
|
|
24
|
+
import { updateDrsLatch as updateDrsLatchState } from './timing/drsTiming.js';
|
|
25
|
+
import {
|
|
26
|
+
PIT_INTENT_NONE,
|
|
27
|
+
setPitIntentForSimulation,
|
|
28
|
+
} from './pit/pitIntent.js';
|
|
29
|
+
import { assignPitLaneTeams as assignPitLaneTeamsState, initializePitStops as initializePitStopsState } from './pit/pitState.js';
|
|
30
|
+
import {
|
|
31
|
+
applyPitRoutePosition as applyPitRoutePositionForSimulation,
|
|
32
|
+
beginPitPenaltyService as beginPitPenaltyServiceForSimulation,
|
|
33
|
+
beginPitQueue as beginPitQueueForSimulation,
|
|
34
|
+
beginPitService as beginPitServiceForSimulation,
|
|
35
|
+
beginTireService as beginTireServiceForSimulation,
|
|
36
|
+
calculatePitServiceProfile as calculatePitServiceProfileForSimulation,
|
|
37
|
+
completePitPenaltyService as completePitPenaltyServiceForSimulation,
|
|
38
|
+
completePitService as completePitServiceForSimulation,
|
|
39
|
+
finishPitExit as finishPitExitForSimulation,
|
|
40
|
+
getPitBoxRaceDistance as getPitBoxRaceDistanceForSimulation,
|
|
41
|
+
getPitServicePenalties as getPitServicePenaltiesForSimulation,
|
|
42
|
+
getPitStopBox as getPitStopBoxForSimulation,
|
|
43
|
+
isPitServiceAreaOccupied as isPitServiceAreaOccupiedForSimulation,
|
|
44
|
+
isPitServiceBusy as isPitServiceBusyForSimulation,
|
|
45
|
+
isPitServiceQueueOccupied as isPitServiceQueueOccupiedForSimulation,
|
|
46
|
+
releasePitQueue as releasePitQueueForSimulation,
|
|
47
|
+
} from './pit/pitService.js';
|
|
48
|
+
import {
|
|
49
|
+
advancePitStopCar as advancePitStopCarForSimulation,
|
|
50
|
+
canStartPitStop as canStartPitStopForSimulation,
|
|
51
|
+
isCarInActivePitStop as isCarInActivePitStopForSimulation,
|
|
52
|
+
isPitLaneOpenForStops as isPitLaneOpenForStopsForSimulation,
|
|
53
|
+
pitEntryLateThresholdDistance as pitEntryLateThresholdDistanceForSimulation,
|
|
54
|
+
rearmCompletedPitStop as rearmCompletedPitStopForSimulation,
|
|
55
|
+
schedulePitStopAtNextEntry as schedulePitStopAtNextEntryForSimulation,
|
|
56
|
+
shouldStartPitStop as shouldStartPitStopForSimulation,
|
|
57
|
+
startPitStop as startPitStopForSimulation,
|
|
58
|
+
updateAutomaticPitIntent as updateAutomaticPitIntentForSimulation,
|
|
59
|
+
} from './pit/pitFlow.js';
|
|
60
|
+
import { nearestDistanceOnRoute } from './pit/pitRouting.js';
|
|
61
|
+
import {
|
|
62
|
+
moveSafetyCarTo as moveSafetyCarToState,
|
|
63
|
+
setRedFlagState,
|
|
64
|
+
setSafetyCarState,
|
|
65
|
+
updateSafetyCarState,
|
|
66
|
+
} from './race/safetyCar.js';
|
|
67
|
+
import {
|
|
68
|
+
holdGridCars as holdGridCarsForSimulation,
|
|
69
|
+
releaseRaceStart as releaseRaceStartForSimulation,
|
|
70
|
+
setPitLaneOpenState,
|
|
71
|
+
updateStartSequence as updateStartSequenceForSimulation,
|
|
72
|
+
} from './race/raceLifecycle.js';
|
|
73
|
+
import {
|
|
74
|
+
applyClassificationConsequences as applyClassificationConsequencesState,
|
|
75
|
+
applyOutstandingServicePenalties as applyOutstandingServicePenaltiesState,
|
|
76
|
+
buildClassification as buildClassificationState,
|
|
77
|
+
buildClassificationFromFinishOrder as buildClassificationFromFinishOrderState,
|
|
78
|
+
} from './race/classification.js';
|
|
79
|
+
import {
|
|
80
|
+
computeAggressionForSimulation,
|
|
81
|
+
driverRaceContextForSimulation,
|
|
82
|
+
getDrsReferenceCarForSimulation,
|
|
83
|
+
orderedCarsForSimulation,
|
|
84
|
+
} from './race/raceOrder.js';
|
|
85
|
+
import {
|
|
86
|
+
computeLapForDistance,
|
|
87
|
+
finishDistanceForRace,
|
|
88
|
+
progressDelta,
|
|
89
|
+
} from './race/raceDistance.js';
|
|
90
|
+
import { recalculateRaceStateForSimulation } from './race/raceProgress.js';
|
|
91
|
+
import { evaluateRaceFinishForSimulation } from './race/raceFinish.js';
|
|
92
|
+
import { applyGridDropForSimulation } from './race/gridPenalties.js';
|
|
93
|
+
import { initializeRaceSimulation, normalizePitIntentForRace } from './race/raceSetup.js';
|
|
94
|
+
import { runRaceStep } from './race/raceStep.js';
|
|
95
|
+
import {
|
|
96
|
+
createVehicleSnapshotDependencies,
|
|
97
|
+
getRaceWinnerSnapshot as getRaceWinnerSnapshotForSimulation,
|
|
98
|
+
snapshotRace,
|
|
99
|
+
snapshotRaceObservation,
|
|
100
|
+
snapshotRaceRender,
|
|
101
|
+
snapshotRaceTraining,
|
|
102
|
+
} from './snapshots/raceSnapshots.js';
|
|
103
|
+
import {
|
|
104
|
+
disableWarmupOptions,
|
|
105
|
+
runWarmupWithGuard,
|
|
106
|
+
withWarmupSurfaceOptions,
|
|
107
|
+
} from './warmup/runtimeWarmup.js';
|
|
41
108
|
|
|
42
|
-
|
|
43
|
-
return wrapProgress(to - from, length);
|
|
44
|
-
}
|
|
109
|
+
export const FIXED_STEP = 1 / 60;
|
|
45
110
|
|
|
46
|
-
|
|
47
|
-
const travelled = distanceForward(previous, current, length);
|
|
48
|
-
if (travelled <= 0 || travelled > length / 2) return false;
|
|
49
|
-
const targetOffset = distanceForward(previous, target, length);
|
|
50
|
-
return targetOffset > 0 && targetOffset <= travelled + 0.001;
|
|
51
|
-
}
|
|
111
|
+
export { DEFAULT_RULES };
|
|
52
112
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (zone.end - zone.start >= track.length) return true;
|
|
58
|
-
return end >= start
|
|
59
|
-
? wrapped >= start && wrapped <= end
|
|
60
|
-
: wrapped >= start || wrapped <= end;
|
|
61
|
-
}
|
|
113
|
+
export class F1RaceSimulation {
|
|
114
|
+
constructor(options = {}) {
|
|
115
|
+
initializeRaceSimulation(this, options);
|
|
116
|
+
}
|
|
62
117
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
118
|
+
assignPitLaneTeams() {
|
|
119
|
+
assignPitLaneTeamsState({ cars: this.cars, pitLane: this.track.pitLane });
|
|
120
|
+
}
|
|
66
121
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
122
|
+
initializePitStops() {
|
|
123
|
+
initializePitStopsState({
|
|
124
|
+
cars: this.cars,
|
|
125
|
+
pitLane: this.track.pitLane,
|
|
126
|
+
pitStops: this.rules.modules?.pitStops,
|
|
127
|
+
totalLaps: this.totalLaps,
|
|
128
|
+
trackLength: this.track.length,
|
|
129
|
+
tireCompounds: this.rules.modules?.tireStrategy?.compounds,
|
|
130
|
+
PIT_INTENT_NONE,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
74
133
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
134
|
+
setSafetyCar(deployed) {
|
|
135
|
+
setSafetyCarState(this, deployed);
|
|
136
|
+
}
|
|
78
137
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Math.floor(positiveDistance / track.length),
|
|
83
|
-
Math.max(0, totalLaps - 1),
|
|
84
|
-
);
|
|
85
|
-
const lapProgress = positiveDistance >= track.length * totalLaps
|
|
86
|
-
? track.length
|
|
87
|
-
: positiveDistance - lapIndex * track.length;
|
|
88
|
-
const sectorLength = getSectorLength(track);
|
|
89
|
-
const sectorIndex = Math.min(TELEMETRY_SECTOR_COUNT - 1, Math.floor(lapProgress / sectorLength));
|
|
90
|
-
const sectorStart = sectorIndex * sectorLength;
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
completedLaps: Math.floor(positiveDistance / track.length),
|
|
94
|
-
currentLap: Math.max(1, lapIndex + 1),
|
|
95
|
-
currentSector: sectorIndex + 1,
|
|
96
|
-
currentSectorProgress: clamp((lapProgress - sectorStart) / sectorLength, 0, 1),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
138
|
+
setRedFlag(deployed) {
|
|
139
|
+
setRedFlagState(this, deployed);
|
|
140
|
+
}
|
|
99
141
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
...position,
|
|
104
|
-
currentLapStartedAt: currentTime,
|
|
105
|
-
currentSectorStartedAt: currentTime,
|
|
106
|
-
lastUpdatedAt: currentTime,
|
|
107
|
-
currentLapTime: 0,
|
|
108
|
-
currentSectorElapsed: 0,
|
|
109
|
-
currentSectors: createEmptySectorTimes(),
|
|
110
|
-
lastLapTime: null,
|
|
111
|
-
bestLapTime: null,
|
|
112
|
-
lastSectors: createEmptySectorTimes(),
|
|
113
|
-
bestSectors: createEmptySectorTimes(),
|
|
114
|
-
sectorPerformance: createEmptySectorPerformance(),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
142
|
+
setPitLaneOpen(open) {
|
|
143
|
+
setPitLaneOpenState(this, open);
|
|
144
|
+
}
|
|
117
145
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
146
|
+
setCarState(id, partial) {
|
|
147
|
+
this.setCarStates({ [id]: partial });
|
|
148
|
+
}
|
|
121
149
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
150
|
+
setCarStates(partialsById = {}) {
|
|
151
|
+
const context = {
|
|
152
|
+
cars: this.cars,
|
|
153
|
+
computeLap: (raceDistance) => this.computeLap(raceDistance),
|
|
154
|
+
nearestDistanceOnRoute,
|
|
155
|
+
progressDelta,
|
|
156
|
+
raceControl: this.raceControl,
|
|
157
|
+
releaseRaceStart: () => this.releaseRaceStart(),
|
|
158
|
+
resetLapTelemetry,
|
|
159
|
+
resetTimingHistory,
|
|
160
|
+
resetTimingLineCrossings,
|
|
161
|
+
time: this.time,
|
|
162
|
+
totalLaps: this.totalLaps,
|
|
163
|
+
track: this.track,
|
|
164
|
+
};
|
|
165
|
+
let applied = false;
|
|
166
|
+
Object.entries(partialsById).forEach(([id, partial]) => {
|
|
167
|
+
const car = this.cars.find((item) => item.id === id);
|
|
168
|
+
if (!car) return;
|
|
169
|
+
applyExternalCarState(car, partial, context);
|
|
170
|
+
applied = true;
|
|
171
|
+
});
|
|
172
|
+
if (!applied) return;
|
|
173
|
+
this.recalculateRaceState({ updateDrs: false });
|
|
174
|
+
this.evaluateRaceFinish();
|
|
175
|
+
}
|
|
125
176
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
177
|
+
setCarControls(id, controls) {
|
|
178
|
+
const car = this.cars.find((item) => item.id === id);
|
|
179
|
+
if (!car) return;
|
|
180
|
+
car.manualControls = controls;
|
|
181
|
+
}
|
|
129
182
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
currentLapTime: finiteOrNull(telemetry.currentLapTime),
|
|
135
|
-
currentSectorElapsed: finiteOrNull(telemetry.currentSectorElapsed),
|
|
136
|
-
currentSectorProgress: finiteOrNull(telemetry.currentSectorProgress),
|
|
137
|
-
currentSectors: serializeSectorTimes(telemetry.currentSectors),
|
|
138
|
-
lastLapTime: finiteOrNull(telemetry.lastLapTime),
|
|
139
|
-
bestLapTime: finiteOrNull(telemetry.bestLapTime),
|
|
140
|
-
lastSectors: serializeSectorTimes(telemetry.lastSectors),
|
|
141
|
-
bestSectors: serializeSectorTimes(telemetry.bestSectors),
|
|
142
|
-
sectorPerformance: {
|
|
143
|
-
current: serializeSectorPerformance(telemetry.sectorPerformance?.current),
|
|
144
|
-
last: serializeSectorPerformance(telemetry.sectorPerformance?.last),
|
|
145
|
-
best: serializeSectorPerformance(telemetry.sectorPerformance?.best),
|
|
146
|
-
},
|
|
147
|
-
completedLaps: telemetry.completedLaps,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
183
|
+
clearCarControls(id) {
|
|
184
|
+
const car = this.cars.find((item) => item.id === id);
|
|
185
|
+
if (car) car.manualControls = null;
|
|
186
|
+
}
|
|
150
187
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
188
|
+
setAutomaticPitIntentEnabled(id, enabled) {
|
|
189
|
+
const car = this.cars.find((item) => item.id === id);
|
|
190
|
+
if (!car) return false;
|
|
191
|
+
car.automaticPitIntentEnabled = Boolean(enabled);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
154
194
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
telemetry.bestSectors[sectorIndex] = sectorTime;
|
|
195
|
+
getPitIntent(id) {
|
|
196
|
+
const car = this.cars.find((item) => item.id === id);
|
|
197
|
+
return normalizePitIntentForRace(car);
|
|
159
198
|
}
|
|
160
|
-
}
|
|
161
199
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
telemetry.currentSectorProgress = position.currentSectorProgress;
|
|
167
|
-
telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(position.completedLaps, totalLaps));
|
|
168
|
-
telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
|
|
169
|
-
telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
|
|
170
|
-
}
|
|
200
|
+
getPitTargetCompound(id) {
|
|
201
|
+
const car = this.cars.find((item) => item.id === id);
|
|
202
|
+
return car?.pitStop?.targetTire ?? null;
|
|
203
|
+
}
|
|
171
204
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (Number.isFinite(personalBest) && Math.abs(value - personalBest) <= 1e-6) return 'personal-best';
|
|
176
|
-
return 'slower';
|
|
177
|
-
}
|
|
205
|
+
setPitIntent(id, intent, targetCompound = undefined) {
|
|
206
|
+
return setPitIntentForSimulation(this, id, intent, targetCompound);
|
|
207
|
+
}
|
|
178
208
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
car.lapTelemetry?.bestSectors?.forEach((time, index) => {
|
|
183
|
-
if (!Number.isFinite(time)) return;
|
|
184
|
-
const previousBest = overallBestSectors[index];
|
|
185
|
-
if (!Number.isFinite(previousBest) || time < previousBest) overallBestSectors[index] = time;
|
|
186
|
-
});
|
|
187
|
-
});
|
|
209
|
+
isCarInActivePitStop(car) {
|
|
210
|
+
return isCarInActivePitStopForSimulation(this, car);
|
|
211
|
+
}
|
|
188
212
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
telemetry.sectorPerformance = {
|
|
193
|
-
current: telemetry.currentSectors.map((time, index) => (
|
|
194
|
-
classifySectorPerformance(time, telemetry.bestSectors[index], overallBestSectors[index])
|
|
195
|
-
)),
|
|
196
|
-
last: telemetry.lastSectors.map((time, index) => (
|
|
197
|
-
classifySectorPerformance(time, telemetry.bestSectors[index], overallBestSectors[index])
|
|
198
|
-
)),
|
|
199
|
-
best: telemetry.bestSectors.map((time, index) => (
|
|
200
|
-
classifySectorPerformance(time, telemetry.bestSectors[index], overallBestSectors[index])
|
|
201
|
-
)),
|
|
202
|
-
};
|
|
203
|
-
});
|
|
204
|
-
}
|
|
213
|
+
schedulePitStopAtNextEntry(car, stop) {
|
|
214
|
+
return schedulePitStopAtNextEntryForSimulation(this, car, stop);
|
|
215
|
+
}
|
|
205
216
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const telemetry = car.lapTelemetry;
|
|
210
|
-
const currentRaceDistance = car.raceDistance ?? 0;
|
|
211
|
-
const previousDistance = Math.max(0, previousRaceDistance ?? currentRaceDistance);
|
|
212
|
-
const currentDistance = Math.max(0, currentRaceDistance);
|
|
213
|
-
const travelled = currentDistance - previousDistance;
|
|
214
|
-
const previousUpdateTime = Number.isFinite(telemetry.lastUpdatedAt) ? telemetry.lastUpdatedAt : currentTime;
|
|
215
|
-
const elapsedTime = Math.max(0, currentTime - previousUpdateTime);
|
|
216
|
-
|
|
217
|
-
if (!Number.isFinite(previousDistance) || !Number.isFinite(currentDistance) || travelled < -1e-3 || travelled > track.length / 2) {
|
|
218
|
-
resetLapTelemetry(car, currentTime, track, totalLaps);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const sectorLength = getSectorLength(track);
|
|
223
|
-
if (travelled > 1e-6) {
|
|
224
|
-
const firstBoundary = Math.floor(previousDistance / sectorLength) + 1;
|
|
225
|
-
const lastBoundary = Math.floor(currentDistance / sectorLength);
|
|
226
|
-
|
|
227
|
-
for (let boundary = firstBoundary; boundary <= lastBoundary; boundary += 1) {
|
|
228
|
-
const boundaryDistance = boundary * sectorLength;
|
|
229
|
-
if (boundaryDistance <= previousDistance + 1e-3 || boundaryDistance > currentDistance + 1e-3) continue;
|
|
230
|
-
|
|
231
|
-
const boundaryRatio = clamp((boundaryDistance - previousDistance) / travelled, 0, 1);
|
|
232
|
-
const crossingTime = previousUpdateTime + boundaryRatio * elapsedTime;
|
|
233
|
-
const sectorIndex = (boundary - 1) % TELEMETRY_SECTOR_COUNT;
|
|
234
|
-
const sectorTime = Math.max(0, crossingTime - telemetry.currentSectorStartedAt);
|
|
235
|
-
|
|
236
|
-
telemetry.currentSectors[sectorIndex] = sectorTime;
|
|
237
|
-
updateBestSector(telemetry, sectorIndex, sectorTime);
|
|
238
|
-
|
|
239
|
-
if (sectorIndex === TELEMETRY_SECTOR_COUNT - 1) {
|
|
240
|
-
const lapTime = Math.max(0, crossingTime - telemetry.currentLapStartedAt);
|
|
241
|
-
telemetry.lastLapTime = lapTime;
|
|
242
|
-
telemetry.bestLapTime = Number.isFinite(telemetry.bestLapTime)
|
|
243
|
-
? Math.min(telemetry.bestLapTime, lapTime)
|
|
244
|
-
: lapTime;
|
|
245
|
-
telemetry.lastSectors = serializeSectorTimes(telemetry.currentSectors);
|
|
246
|
-
telemetry.currentSectors = createEmptySectorTimes();
|
|
247
|
-
telemetry.currentLapStartedAt = crossingTime;
|
|
248
|
-
telemetry.completedLaps = Math.max(telemetry.completedLaps + 1, Math.min(Math.floor(boundaryDistance / track.length), totalLaps));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
telemetry.currentSectorStartedAt = crossingTime;
|
|
252
|
-
}
|
|
217
|
+
rearmCompletedPitStop(car, stop) {
|
|
218
|
+
return rearmCompletedPitStopForSimulation(this, car, stop);
|
|
253
219
|
}
|
|
254
220
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
221
|
+
canStartPitStop(car) {
|
|
222
|
+
return canStartPitStopForSimulation(this, car);
|
|
223
|
+
}
|
|
258
224
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const offset = index % 2 === 0 ? -GRID_LATERAL_OFFSET : GRID_LATERAL_OFFSET;
|
|
263
|
-
const position = offsetTrackPoint(start, offset);
|
|
264
|
-
const pace = clamp(driver.pace ?? seededRange(random, 0.95, 1.05), 0.88, 1.12);
|
|
265
|
-
const racecraft = clamp(driver.racecraft ?? seededRange(random, 0.65, 0.92), 0.45, 1);
|
|
266
|
-
const personality = buildDriverPersonality(driver, index, racecraft, random);
|
|
267
|
-
const launchSpeed = Math.max(70, 84 - index * 0.55 + pace * 4);
|
|
268
|
-
const vehicle = driver.vehicle ?? {};
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
id: driver.id ?? `car-${index + 1}`,
|
|
272
|
-
code: driver.code ?? `C${index + 1}`,
|
|
273
|
-
timingCode: driver.timingCode ?? driver.code ?? `C${index + 1}`,
|
|
274
|
-
driverNumber: driver.driverNumber ?? index + 1,
|
|
275
|
-
icon: driver.icon ?? String(index + 1).padStart(2, '0'),
|
|
276
|
-
raceName: driver.raceName ?? driver.code ?? `CAR${index + 1}`,
|
|
277
|
-
name: driver.name ?? `Car ${index + 1}`,
|
|
278
|
-
color: driver.color ?? '#e10600',
|
|
279
|
-
team: driver.team ? { ...driver.team } : null,
|
|
280
|
-
tire: driver.tire ?? ['M', 'H', 'S'][index % 3],
|
|
281
|
-
index,
|
|
282
|
-
x: position.x,
|
|
283
|
-
y: position.y,
|
|
284
|
-
previousX: position.x,
|
|
285
|
-
previousY: position.y,
|
|
286
|
-
heading: start.heading,
|
|
287
|
-
previousHeading: start.heading,
|
|
288
|
-
steeringAngle: 0,
|
|
289
|
-
yawRate: 0,
|
|
290
|
-
turnRadius: Infinity,
|
|
291
|
-
speed: standingStart ? 0 : launchSpeed,
|
|
292
|
-
throttle: 0,
|
|
293
|
-
brake: 0,
|
|
294
|
-
vehicleId: vehicle.id ?? null,
|
|
295
|
-
vehicleName: vehicle.name ?? null,
|
|
296
|
-
vehicleRatings: vehicle.ratings ? { ...vehicle.ratings } : null,
|
|
297
|
-
mass: vehicle.mass ?? 798 + seededRange(random, -5, 5),
|
|
298
|
-
powerNewtons: vehicle.powerNewtons ?? 43000 * pace,
|
|
299
|
-
brakeNewtons: vehicle.brakeNewtons ?? 59000,
|
|
300
|
-
dragCoefficient: vehicle.dragCoefficient ?? 0.33 + seededRange(random, -0.026, 0.026),
|
|
301
|
-
downforceCoefficient: vehicle.downforceCoefficient ?? 6.1 + seededRange(random, -0.18, 0.18),
|
|
302
|
-
tireGrip: vehicle.tireGrip ?? 2.22 + racecraft * 0.28 + seededRange(random, -0.03, 0.03),
|
|
303
|
-
tireCare: vehicle.tireCare ?? 1,
|
|
304
|
-
pace,
|
|
305
|
-
racecraft,
|
|
306
|
-
personality,
|
|
307
|
-
aggression: personality.baseAggression,
|
|
308
|
-
gridLocked: standingStart,
|
|
309
|
-
gridDistance,
|
|
310
|
-
gridOffset: offset,
|
|
311
|
-
desiredOffset: offset,
|
|
312
|
-
progress: start.distance,
|
|
313
|
-
raceDistance: gridDistance,
|
|
314
|
-
lap: 1,
|
|
315
|
-
rank: index + 1,
|
|
316
|
-
gapAhead: Infinity,
|
|
317
|
-
gapAheadSeconds: Infinity,
|
|
318
|
-
leaderGapSeconds: 0,
|
|
319
|
-
intervalAheadSeconds: Infinity,
|
|
320
|
-
drsEligible: false,
|
|
321
|
-
drsActive: false,
|
|
322
|
-
drsZoneId: null,
|
|
323
|
-
drsZoneEnabled: false,
|
|
324
|
-
timingHistory: [],
|
|
325
|
-
lapTelemetry: createLapTelemetry(track, 0, gridDistance),
|
|
326
|
-
drsDetection: {},
|
|
327
|
-
canAttack: true,
|
|
328
|
-
trackState: start,
|
|
329
|
-
contactCooldown: 0,
|
|
330
|
-
tireEnergy: 100,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
225
|
+
shouldStartPitStop(car) {
|
|
226
|
+
return shouldStartPitStopForSimulation(this, car);
|
|
227
|
+
}
|
|
333
228
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return Math.max(MIN_TOTAL_LAPS, Math.floor(numeric));
|
|
338
|
-
}
|
|
229
|
+
updateAutomaticPitIntent(car) {
|
|
230
|
+
return updateAutomaticPitIntentForSimulation(this, car);
|
|
231
|
+
}
|
|
339
232
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
points.forEach((point) => {
|
|
344
|
-
const projection = point.x * axis.x + point.y * axis.y;
|
|
345
|
-
min = Math.min(min, projection);
|
|
346
|
-
max = Math.max(max, projection);
|
|
347
|
-
});
|
|
348
|
-
return { min, max };
|
|
349
|
-
}
|
|
233
|
+
isPitLaneOpenForStops() {
|
|
234
|
+
return isPitLaneOpenForStopsForSimulation(this);
|
|
235
|
+
}
|
|
350
236
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return Math.min(first.max, second.max) - Math.max(first.min, second.min);
|
|
355
|
-
}
|
|
237
|
+
pitEntryLateThresholdDistance() {
|
|
238
|
+
return pitEntryLateThresholdDistanceForSimulation();
|
|
239
|
+
}
|
|
356
240
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const bCorners = getCarCorners(b);
|
|
360
|
-
const axes = [
|
|
361
|
-
normalizeVector({ x: aCorners[1].x - aCorners[0].x, y: aCorners[1].y - aCorners[0].y }),
|
|
362
|
-
normalizeVector({ x: aCorners[3].x - aCorners[0].x, y: aCorners[3].y - aCorners[0].y }),
|
|
363
|
-
normalizeVector({ x: bCorners[1].x - bCorners[0].x, y: bCorners[1].y - bCorners[0].y }),
|
|
364
|
-
normalizeVector({ x: bCorners[3].x - bCorners[0].x, y: bCorners[3].y - bCorners[0].y }),
|
|
365
|
-
];
|
|
366
|
-
|
|
367
|
-
let minimumOverlap = Infinity;
|
|
368
|
-
let minimumAxis = null;
|
|
369
|
-
|
|
370
|
-
for (const axis of axes) {
|
|
371
|
-
const overlap = overlapOnAxis(aCorners, bCorners, axis);
|
|
372
|
-
if (overlap <= 0) return null;
|
|
373
|
-
if (overlap < minimumOverlap) {
|
|
374
|
-
minimumOverlap = overlap;
|
|
375
|
-
minimumAxis = axis;
|
|
376
|
-
}
|
|
241
|
+
getPitStopBox(stop) {
|
|
242
|
+
return getPitStopBoxForSimulation(this, stop);
|
|
377
243
|
}
|
|
378
244
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
minimumAxis = { x: -minimumAxis.x, y: -minimumAxis.y };
|
|
245
|
+
isPitServiceBusy(car, box) {
|
|
246
|
+
return isPitServiceBusyForSimulation(this, car, box);
|
|
382
247
|
}
|
|
383
248
|
|
|
384
|
-
|
|
385
|
-
|
|
249
|
+
isPitServiceAreaOccupied(candidate, box) {
|
|
250
|
+
return isPitServiceAreaOccupiedForSimulation(this, candidate, box);
|
|
251
|
+
}
|
|
386
252
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const blendedHeading = a.heading + normalizeAngle(b.heading - a.heading) * 0.5;
|
|
391
|
-
let axis = normalizeVector({ x: Math.cos(blendedHeading), y: Math.sin(blendedHeading) });
|
|
392
|
-
const direction = { x: b.x - a.x, y: b.y - a.y };
|
|
393
|
-
const physicalLongitudinalGap = dot(direction, axis);
|
|
394
|
-
if (physicalLongitudinalGap < 0) axis = { x: -axis.x, y: -axis.y };
|
|
395
|
-
|
|
396
|
-
const physicalLongitudinalSeparation = Math.abs(physicalLongitudinalGap);
|
|
397
|
-
const lateralGap = Math.abs(direction.x * -axis.y + direction.y * axis.x);
|
|
398
|
-
const requiredGap = VEHICLE_LIMITS.carLength * 0.96;
|
|
399
|
-
|
|
400
|
-
if (Math.abs(longitudinalGap) > VEHICLE_LIMITS.carLength * 1.45) return null;
|
|
401
|
-
if (physicalLongitudinalSeparation >= requiredGap) return null;
|
|
402
|
-
if (lateralGap > VEHICLE_LIMITS.carWidth * 0.82) return null;
|
|
403
|
-
if (headingDelta > 0.58) return null;
|
|
404
|
-
|
|
405
|
-
return {
|
|
406
|
-
axis,
|
|
407
|
-
depth: requiredGap - physicalLongitudinalSeparation,
|
|
408
|
-
longitudinal: true,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
253
|
+
isPitServiceQueueOccupied(car, box) {
|
|
254
|
+
return isPitServiceQueueOccupiedForSimulation(this, car, box);
|
|
255
|
+
}
|
|
411
256
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
257
|
+
getPitBoxRaceDistance(stop, box) {
|
|
258
|
+
return getPitBoxRaceDistanceForSimulation(this, stop, box);
|
|
259
|
+
}
|
|
415
260
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
261
|
+
startPitStop(car) {
|
|
262
|
+
return startPitStopForSimulation(this, car);
|
|
263
|
+
}
|
|
419
264
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
265
|
+
beginPitQueue(car, box) {
|
|
266
|
+
return beginPitQueueForSimulation(this, car, box);
|
|
267
|
+
}
|
|
424
268
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (delta > trackLength / 2) delta -= trackLength;
|
|
429
|
-
return delta;
|
|
430
|
-
}
|
|
269
|
+
releasePitQueue(car, box, { fromCurrent = false } = {}) {
|
|
270
|
+
return releasePitQueueForSimulation(this, car, box, { fromCurrent });
|
|
271
|
+
}
|
|
431
272
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
while (history.length > 2 && (history[0].time < cutoff || history.length > TIMING_HISTORY_MAX_SAMPLES)) {
|
|
435
|
-
history.shift();
|
|
273
|
+
beginPitService(car, box) {
|
|
274
|
+
return beginPitServiceForSimulation(this, car, box);
|
|
436
275
|
}
|
|
437
|
-
}
|
|
438
276
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
277
|
+
beginPitPenaltyService(car) {
|
|
278
|
+
return beginPitPenaltyServiceForSimulation(this, car);
|
|
279
|
+
}
|
|
442
280
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (!Array.isArray(car.timingHistory)) {
|
|
446
|
-
resetTimingHistory(car, currentTime);
|
|
447
|
-
return;
|
|
281
|
+
calculatePitServiceProfile(car) {
|
|
282
|
+
return calculatePitServiceProfileForSimulation(this, car);
|
|
448
283
|
}
|
|
449
284
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
previous &&
|
|
453
|
-
Math.abs(previous.time - currentTime) <= 1e-6 &&
|
|
454
|
-
Math.abs(previous.raceDistance - car.raceDistance) <= 1e-3
|
|
455
|
-
) {
|
|
456
|
-
return;
|
|
285
|
+
beginTireService(car) {
|
|
286
|
+
return beginTireServiceForSimulation(this, car);
|
|
457
287
|
}
|
|
458
288
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
289
|
+
completePitPenaltyService(car) {
|
|
290
|
+
return completePitPenaltyServiceForSimulation(this, car);
|
|
291
|
+
}
|
|
462
292
|
|
|
463
|
-
|
|
464
|
-
|
|
293
|
+
getPitServicePenalties(driverId) {
|
|
294
|
+
return getPitServicePenaltiesForSimulation(this, driverId);
|
|
295
|
+
}
|
|
465
296
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (targetDistance < previous.raceDistance || targetDistance > current.raceDistance) continue;
|
|
297
|
+
completePitService(car) {
|
|
298
|
+
return completePitServiceForSimulation(this, car);
|
|
299
|
+
}
|
|
470
300
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const ratio = (targetDistance - previous.raceDistance) / coveredDistance;
|
|
474
|
-
return previous.time + (current.time - previous.time) * ratio;
|
|
301
|
+
finishPitExit(car) {
|
|
302
|
+
return finishPitExitForSimulation(this, car);
|
|
475
303
|
}
|
|
476
304
|
|
|
477
|
-
|
|
478
|
-
|
|
305
|
+
applyPitRoutePosition(car, delta) {
|
|
306
|
+
return applyPitRoutePositionForSimulation(this, car, delta);
|
|
307
|
+
}
|
|
479
308
|
|
|
480
|
-
|
|
481
|
-
|
|
309
|
+
advancePitStopCar(car, delta) {
|
|
310
|
+
return advancePitStopCarForSimulation(this, car, delta);
|
|
311
|
+
}
|
|
482
312
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return Math.max(0, currentTime - crossingTime);
|
|
313
|
+
step(dt) {
|
|
314
|
+
runRaceStep(this, dt);
|
|
486
315
|
}
|
|
487
316
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
317
|
+
recordPenalty(penalty) {
|
|
318
|
+
const car = this.cars.find((item) => item.id === penalty.driverId);
|
|
319
|
+
const entry = createPenaltyRecord({
|
|
320
|
+
sequence: this.nextPenaltyId,
|
|
321
|
+
time: this.time,
|
|
322
|
+
lap: this.computeLap(car?.raceDistance ?? 0),
|
|
323
|
+
penalty,
|
|
324
|
+
});
|
|
325
|
+
this.nextPenaltyId += 1;
|
|
326
|
+
this.penalties.push(entry);
|
|
327
|
+
if (entry.gridDrop > 0 && this.raceControl.mode === 'pre-start') {
|
|
328
|
+
this.applyGridDrop(entry.driverId, entry.gridDrop);
|
|
329
|
+
}
|
|
330
|
+
this.events.unshift(createPenaltyEvent(entry));
|
|
331
|
+
return entry;
|
|
332
|
+
}
|
|
491
333
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
334
|
+
servePenalty(penaltyId) {
|
|
335
|
+
const penalty = this.penalties.find((entry) => entry.id === penaltyId);
|
|
336
|
+
const result = servePenaltyRecord(penalty, this.time);
|
|
337
|
+
if (result) {
|
|
338
|
+
this.events.unshift(createPenaltyServedEvent(result, this.time));
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
501
342
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
name: car.name,
|
|
511
|
-
color: car.color,
|
|
512
|
-
team: car.team ? { ...car.team } : null,
|
|
513
|
-
tire: car.tire,
|
|
514
|
-
personality: { ...car.personality },
|
|
515
|
-
aggression: car.aggression,
|
|
516
|
-
aggressionPercent: Math.round((car.aggression ?? 0) * 100),
|
|
517
|
-
setup: {
|
|
518
|
-
vehicleId: car.vehicleId,
|
|
519
|
-
vehicleName: car.vehicleName,
|
|
520
|
-
vehicleRatings: car.vehicleRatings ? { ...car.vehicleRatings } : null,
|
|
521
|
-
maxSpeedKph: simSpeedToKph(VEHICLE_LIMITS.maxSpeed),
|
|
522
|
-
powerUnitKn: car.powerNewtons / 1000,
|
|
523
|
-
brakeSystemKn: car.brakeNewtons / 1000,
|
|
524
|
-
dragCoefficient: car.dragCoefficient,
|
|
525
|
-
downforceCoefficient: car.downforceCoefficient,
|
|
526
|
-
tireGrip: car.tireGrip,
|
|
527
|
-
tireCare: car.tireCare,
|
|
528
|
-
massKg: car.mass,
|
|
529
|
-
pace: car.pace,
|
|
530
|
-
racecraft: car.racecraft,
|
|
531
|
-
},
|
|
532
|
-
rank,
|
|
533
|
-
classifiedRank: car.classifiedRank ?? rank,
|
|
534
|
-
finished: Boolean(car.finished),
|
|
535
|
-
finishTime: car.finishTime ?? null,
|
|
536
|
-
previousX: car.previousX ?? car.x,
|
|
537
|
-
previousY: car.previousY ?? car.y,
|
|
538
|
-
x: car.x,
|
|
539
|
-
y: car.y,
|
|
540
|
-
previousHeading: car.previousHeading ?? car.heading,
|
|
541
|
-
heading: car.heading,
|
|
542
|
-
steeringAngle: car.steeringAngle,
|
|
543
|
-
yawRate: car.yawRate,
|
|
544
|
-
turnRadius: car.turnRadius,
|
|
545
|
-
speed: car.speed,
|
|
546
|
-
speedKph: simSpeedToKph(car.speed),
|
|
547
|
-
throttle: car.throttle,
|
|
548
|
-
brake: car.brake,
|
|
549
|
-
lateralAcceleration: car.lateralAcceleration,
|
|
550
|
-
progress: car.progress,
|
|
551
|
-
raceDistance: car.raceDistance,
|
|
552
|
-
distanceMeters: simUnitsToMeters(car.raceDistance),
|
|
553
|
-
lap: car.lap,
|
|
554
|
-
lapTelemetry: serializeLapTelemetry(car.lapTelemetry ?? createLapTelemetry({ length: 1 }, 0, 0)),
|
|
555
|
-
gapAhead: car.gapAhead,
|
|
556
|
-
gapAheadMeters: Number.isFinite(car.gapAhead) ? simUnitsToMeters(car.gapAhead) : Infinity,
|
|
557
|
-
gapAheadSeconds: car.gapAheadSeconds,
|
|
558
|
-
intervalAheadSeconds: car.intervalAheadSeconds,
|
|
559
|
-
leaderGapSeconds: car.leaderGapSeconds,
|
|
560
|
-
drsEligible: car.drsEligible,
|
|
561
|
-
drsActive: car.drsActive,
|
|
562
|
-
drsZoneId: car.drsZoneId,
|
|
563
|
-
drsZoneEnabled: car.drsZoneEnabled,
|
|
564
|
-
canAttack: car.canAttack,
|
|
565
|
-
signedOffset: car.trackState?.signedOffset ?? 0,
|
|
566
|
-
crossTrackError: car.trackState?.crossTrackError ?? 0,
|
|
567
|
-
surface: car.trackState?.surface ?? 'track',
|
|
568
|
-
contactCooldown: car.contactCooldown,
|
|
569
|
-
tireEnergy: car.tireEnergy,
|
|
570
|
-
positionSource: 'integrated-vehicle',
|
|
571
|
-
};
|
|
572
|
-
}
|
|
343
|
+
cancelPenalty(penaltyId) {
|
|
344
|
+
const penalty = this.penalties.find((entry) => entry.id === penaltyId);
|
|
345
|
+
const result = cancelPenaltyRecord(penalty, this.time);
|
|
346
|
+
if (result) {
|
|
347
|
+
this.events.unshift(createPenaltyCancelledEvent(result, this.time));
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
573
351
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
this.seed = seed;
|
|
577
|
-
this.random = createMulberry32(seed);
|
|
578
|
-
const trackDefinition = track ?? (trackSeed == null ? TRACK : createProceduralTrack(trackSeed));
|
|
579
|
-
this.track = buildTrackModel(trackDefinition);
|
|
580
|
-
this.trackSeed = this.track.seed ?? trackSeed;
|
|
581
|
-
this.rules = { ...DEFAULT_RULES, ...rules };
|
|
582
|
-
this.startLightsOutAt = this.rules.startLightCount * this.rules.startLightInterval + this.rules.startLightsOutHold;
|
|
583
|
-
this.totalLaps = normalizeTotalLaps(totalLaps);
|
|
584
|
-
this.time = 0;
|
|
585
|
-
this.events = [];
|
|
586
|
-
this.raceControl = {
|
|
587
|
-
mode: this.rules.standingStart === false ? 'green' : 'pre-start',
|
|
588
|
-
frozenOrder: null,
|
|
589
|
-
finished: false,
|
|
590
|
-
finishedAt: null,
|
|
591
|
-
winnerId: null,
|
|
592
|
-
classification: [],
|
|
593
|
-
finishOrder: [],
|
|
594
|
-
start: {
|
|
595
|
-
lightCount: this.rules.startLightCount,
|
|
596
|
-
lightsLit: 0,
|
|
597
|
-
lightsOutAt: this.startLightsOutAt,
|
|
598
|
-
released: this.rules.standingStart === false,
|
|
599
|
-
releasedAt: this.rules.standingStart === false ? 0 : null,
|
|
600
|
-
},
|
|
601
|
-
};
|
|
602
|
-
const safetyCarStart = pointAt(this.track, this.rules.safetyCarLeadDistance);
|
|
603
|
-
this.safetyCar = {
|
|
604
|
-
deployed: false,
|
|
605
|
-
progress: this.rules.safetyCarLeadDistance,
|
|
606
|
-
speed: this.rules.safetyCarSpeed,
|
|
607
|
-
previousX: safetyCarStart.x,
|
|
608
|
-
previousY: safetyCarStart.y,
|
|
609
|
-
previousHeading: safetyCarStart.heading,
|
|
610
|
-
x: safetyCarStart.x,
|
|
611
|
-
y: safetyCarStart.y,
|
|
612
|
-
heading: safetyCarStart.heading,
|
|
613
|
-
};
|
|
614
|
-
this.cars = drivers.map((driver, index) => createCar(driver, index, this.random, this.track, {
|
|
615
|
-
standingStart: this.raceControl.mode === 'pre-start',
|
|
616
|
-
}));
|
|
617
|
-
this.recalculateRaceState({ updateDrs: false });
|
|
618
|
-
this.cars.forEach((car) => resetTimingHistory(car, this.time));
|
|
619
|
-
this.cars.forEach((car) => resetLapTelemetry(car, this.time, this.track, this.totalLaps));
|
|
352
|
+
getDriverPenaltySeconds(driverId) {
|
|
353
|
+
return getPenaltyStats(this.getPenaltyStatsByDriver(), driverId).seconds;
|
|
620
354
|
}
|
|
621
355
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if (this.raceControl.finished) return;
|
|
625
|
-
if (next && this.raceControl.mode === 'pre-start') return;
|
|
626
|
-
if (next === this.safetyCar.deployed) return;
|
|
627
|
-
const ordered = this.orderedCars();
|
|
628
|
-
this.safetyCar.deployed = next;
|
|
629
|
-
this.raceControl.mode = next ? 'safety-car' : 'green';
|
|
630
|
-
this.raceControl.frozenOrder = next ? ordered.map((car) => car.id) : null;
|
|
631
|
-
if (next) {
|
|
632
|
-
const leader = ordered[0];
|
|
633
|
-
const safetyCarProgress = (leader?.raceDistance ?? 0) + this.rules.safetyCarLeadDistance;
|
|
634
|
-
if (this.safetyCar.progress < safetyCarProgress) {
|
|
635
|
-
this.moveSafetyCarTo(safetyCarProgress);
|
|
636
|
-
}
|
|
637
|
-
this.cars.forEach((car) => {
|
|
638
|
-
car.desiredOffset = 0;
|
|
639
|
-
car.drsActive = false;
|
|
640
|
-
car.drsEligible = false;
|
|
641
|
-
car.drsZoneId = null;
|
|
642
|
-
car.drsZoneEnabled = false;
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
this.events.unshift({ type: next ? 'safety-car' : 'green-flag', at: this.time });
|
|
356
|
+
getDriverPositionDrop(driverId) {
|
|
357
|
+
return getPenaltyStats(this.getPenaltyStatsByDriver(), driverId).positionDrop;
|
|
646
358
|
}
|
|
647
359
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (!car) return;
|
|
651
|
-
Object.assign(car, partial);
|
|
652
|
-
if (
|
|
653
|
-
partial.x != null ||
|
|
654
|
-
partial.y != null ||
|
|
655
|
-
partial.progress != null ||
|
|
656
|
-
partial.raceDistance != null
|
|
657
|
-
) {
|
|
658
|
-
car.gridLocked = false;
|
|
659
|
-
if (this.raceControl.mode === 'pre-start' && this.cars.every((item) => !item.gridLocked)) {
|
|
660
|
-
this.releaseRaceStart();
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
car.speed = clamp(car.speed, 0, VEHICLE_LIMITS.maxSpeed);
|
|
664
|
-
car.heading = normalizeAngle(car.heading);
|
|
665
|
-
car.trackState = nearestTrackState(this.track, car);
|
|
666
|
-
const delta = progressDelta(car.trackState.distance, car.progress ?? car.trackState.distance, this.track.length);
|
|
667
|
-
car.raceDistance = (car.raceDistance ?? car.trackState.distance) + delta;
|
|
668
|
-
car.progress = car.trackState.distance;
|
|
669
|
-
car.lap = this.computeLap(car.raceDistance);
|
|
670
|
-
car.drsZoneId = null;
|
|
671
|
-
car.drsZoneEnabled = false;
|
|
672
|
-
car.drsActive = false;
|
|
673
|
-
car.drsEligible = false;
|
|
674
|
-
car.drsDetection = {};
|
|
675
|
-
resetTimingHistory(car, this.time);
|
|
676
|
-
resetLapTelemetry(car, this.time, this.track, this.totalLaps);
|
|
677
|
-
this.recalculateRaceState({ updateDrs: false });
|
|
678
|
-
this.evaluateRaceFinish();
|
|
360
|
+
isDriverDisqualified(driverId) {
|
|
361
|
+
return getPenaltyStats(this.getPenaltyStatsByDriver(), driverId).disqualified;
|
|
679
362
|
}
|
|
680
363
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (!car) return;
|
|
684
|
-
car.manualControls = controls;
|
|
364
|
+
getPenaltyStatsByDriver() {
|
|
365
|
+
return buildPenaltyStatsByDriver(this.penalties);
|
|
685
366
|
}
|
|
686
367
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (car) car.manualControls = null;
|
|
368
|
+
applyGridDrop(driverId, positions) {
|
|
369
|
+
applyGridDropForSimulation(this, driverId, positions);
|
|
690
370
|
}
|
|
691
371
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
372
|
+
reviewCollision(first, second, collision) {
|
|
373
|
+
reviewCollisionForSimulation(this, first, second, collision);
|
|
374
|
+
}
|
|
695
375
|
|
|
696
|
-
|
|
697
|
-
this.
|
|
698
|
-
this.
|
|
699
|
-
this
|
|
376
|
+
reviewTireRequirement(car) {
|
|
377
|
+
if (this.stewardState.tireRequirement[car.id]) return;
|
|
378
|
+
this.stewardState.tireRequirement[car.id] = true;
|
|
379
|
+
reviewTireRequirementForSimulation(this, car);
|
|
380
|
+
}
|
|
700
381
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
382
|
+
reviewTrackLimits() {
|
|
383
|
+
reviewTrackLimitsForSimulation(this);
|
|
384
|
+
}
|
|
706
385
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const raceContext = this.driverRaceContext(orderedCars);
|
|
711
|
-
orderedCars.forEach((car, index) => {
|
|
712
|
-
car.previousX = car.x;
|
|
713
|
-
car.previousY = car.y;
|
|
714
|
-
car.previousHeading = car.heading;
|
|
715
|
-
car.previousProgress = car.progress;
|
|
716
|
-
const controls = decideDriverControls({
|
|
717
|
-
car,
|
|
718
|
-
orderIndex: index,
|
|
719
|
-
race: raceContext,
|
|
720
|
-
});
|
|
721
|
-
integrateVehiclePhysics(car, controls, delta);
|
|
722
|
-
this.applyRunoffResponse(car);
|
|
723
|
-
car.contactCooldown = Math.max(0, car.contactCooldown - delta);
|
|
724
|
-
});
|
|
386
|
+
reviewPitLaneSpeeding() {
|
|
387
|
+
reviewPitLaneSpeedingForSimulation(this);
|
|
388
|
+
}
|
|
725
389
|
|
|
726
|
-
|
|
727
|
-
this
|
|
728
|
-
this.evaluateRaceFinish();
|
|
390
|
+
vehicleSnapshotDependencies() {
|
|
391
|
+
return createVehicleSnapshotDependencies(this);
|
|
729
392
|
}
|
|
730
393
|
|
|
731
394
|
snapshot() {
|
|
732
|
-
|
|
733
|
-
return {
|
|
734
|
-
time: this.time,
|
|
735
|
-
world: WORLD,
|
|
736
|
-
track: this.track,
|
|
737
|
-
totalLaps: this.totalLaps,
|
|
738
|
-
raceControl: {
|
|
739
|
-
mode: this.raceControl.mode,
|
|
740
|
-
finished: this.raceControl.finished,
|
|
741
|
-
finishedAt: this.raceControl.finishedAt,
|
|
742
|
-
winner: this.getRaceWinnerSnapshot(),
|
|
743
|
-
classification: this.raceControl.classification.map((entry) => ({ ...entry })),
|
|
744
|
-
start: {
|
|
745
|
-
...this.raceControl.start,
|
|
746
|
-
visible: this.raceControl.mode === 'pre-start' ||
|
|
747
|
-
(this.raceControl.start.releasedAt != null && this.time - this.raceControl.start.releasedAt < 1.45),
|
|
748
|
-
},
|
|
749
|
-
},
|
|
750
|
-
safetyCar: { ...this.safetyCar },
|
|
751
|
-
rules: this.rules,
|
|
752
|
-
events: [...this.events],
|
|
753
|
-
cars: ordered.map((car, index) => serializeCar(car, index + 1)),
|
|
754
|
-
};
|
|
395
|
+
return snapshotRace(this);
|
|
755
396
|
}
|
|
756
397
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
const byId = new Map(this.cars.map((car) => [car.id, car]));
|
|
760
|
-
return this.raceControl.frozenOrder.map((id) => byId.get(id)).filter(Boolean);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
return [...this.cars].sort((a, b) => {
|
|
764
|
-
const delta = b.raceDistance - a.raceDistance;
|
|
765
|
-
return delta === 0 ? a.index - b.index : delta;
|
|
766
|
-
});
|
|
398
|
+
snapshotRender() {
|
|
399
|
+
return snapshotRaceRender(this);
|
|
767
400
|
}
|
|
768
401
|
|
|
769
|
-
|
|
770
|
-
return
|
|
771
|
-
track: this.track,
|
|
772
|
-
cars: this.cars,
|
|
773
|
-
orderedCars,
|
|
774
|
-
safetyCar: this.safetyCar,
|
|
775
|
-
rules: this.rules,
|
|
776
|
-
};
|
|
402
|
+
snapshotObservation() {
|
|
403
|
+
return snapshotRaceObservation(this);
|
|
777
404
|
}
|
|
778
405
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return clamp(personality.baseAggression * 0.62, 0.08, 0.62);
|
|
783
|
-
}
|
|
406
|
+
snapshotTraining() {
|
|
407
|
+
return snapshotRaceTraining(this);
|
|
408
|
+
}
|
|
784
409
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const tireConfidence = clamp(((car.tireEnergy ?? 100) - 42) / 58, 0, 1);
|
|
789
|
-
const patienceDamping = (1 - gapPressure) * personality.patience * 0.08;
|
|
410
|
+
consumeStepEvents() {
|
|
411
|
+
return [...this.events];
|
|
412
|
+
}
|
|
790
413
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
+ positionPressure * 0.26
|
|
794
|
-
+ gapPressure * (0.1 + personality.riskTolerance * 0.08)
|
|
795
|
-
- (1 - tireConfidence) * 0.1
|
|
796
|
-
- patienceDamping,
|
|
797
|
-
0.08,
|
|
798
|
-
1,
|
|
799
|
-
);
|
|
414
|
+
orderedCars() {
|
|
415
|
+
return orderedCarsForSimulation(this);
|
|
800
416
|
}
|
|
801
417
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
418
|
+
driverRaceContext(orderedCars = this.orderedCars()) {
|
|
419
|
+
return driverRaceContextForSimulation(this, orderedCars);
|
|
420
|
+
}
|
|
805
421
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
422
|
+
computeAggression(car, orderIndex = Math.max(0, (car.rank ?? 1) - 1)) {
|
|
423
|
+
return computeAggressionForSimulation(this, car, orderIndex);
|
|
424
|
+
}
|
|
811
425
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
0,
|
|
815
|
-
start.lightCount,
|
|
816
|
-
);
|
|
426
|
+
updateStartSequence() {
|
|
427
|
+
return updateStartSequenceForSimulation(this);
|
|
817
428
|
}
|
|
818
429
|
|
|
819
430
|
releaseRaceStart() {
|
|
820
|
-
|
|
821
|
-
this.raceControl.mode = 'green';
|
|
822
|
-
start.lightsLit = 0;
|
|
823
|
-
start.released = true;
|
|
824
|
-
start.releasedAt = this.time;
|
|
825
|
-
this.cars.forEach((car) => {
|
|
826
|
-
const wasGridLocked = car.gridLocked;
|
|
827
|
-
car.gridLocked = false;
|
|
828
|
-
const state = nearestTrackState(this.track, car, car.gridDistance);
|
|
829
|
-
car.trackState = state;
|
|
830
|
-
car.progress = state.distance;
|
|
831
|
-
if (wasGridLocked) car.raceDistance = car.gridDistance;
|
|
832
|
-
car.previousX = car.x;
|
|
833
|
-
car.previousY = car.y;
|
|
834
|
-
car.previousHeading = car.heading;
|
|
835
|
-
});
|
|
431
|
+
return releaseRaceStartForSimulation(this);
|
|
836
432
|
}
|
|
837
433
|
|
|
838
434
|
holdGridCars() {
|
|
839
|
-
this
|
|
840
|
-
if (!car.gridLocked) return;
|
|
841
|
-
const gridPoint = pointAt(this.track, car.gridDistance);
|
|
842
|
-
const position = offsetTrackPoint(gridPoint, car.gridOffset);
|
|
843
|
-
car.previousX = position.x;
|
|
844
|
-
car.previousY = position.y;
|
|
845
|
-
car.previousHeading = gridPoint.heading;
|
|
846
|
-
car.x = position.x;
|
|
847
|
-
car.y = position.y;
|
|
848
|
-
car.heading = gridPoint.heading;
|
|
849
|
-
car.speed = 0;
|
|
850
|
-
car.throttle = 0;
|
|
851
|
-
car.brake = 1;
|
|
852
|
-
car.steeringAngle = 0;
|
|
853
|
-
car.yawRate = 0;
|
|
854
|
-
car.turnRadius = Infinity;
|
|
855
|
-
car.progress = gridPoint.distance;
|
|
856
|
-
car.raceDistance = car.gridDistance;
|
|
857
|
-
car.trackState = nearestTrackState(this.track, car, car.gridDistance);
|
|
858
|
-
});
|
|
435
|
+
return holdGridCarsForSimulation(this);
|
|
859
436
|
}
|
|
860
437
|
|
|
861
438
|
updateSafetyCar(dt) {
|
|
862
|
-
|
|
863
|
-
const leader = this.orderedCars()[0];
|
|
864
|
-
const targetProgress = (leader?.raceDistance ?? 0) + this.rules.safetyCarLeadDistance;
|
|
865
|
-
const progress = Math.max(this.safetyCar.progress + this.safetyCar.speed * dt, targetProgress);
|
|
866
|
-
this.moveSafetyCarTo(progress);
|
|
439
|
+
updateSafetyCarState(this, dt);
|
|
867
440
|
}
|
|
868
441
|
|
|
869
442
|
moveSafetyCarTo(progress) {
|
|
870
|
-
|
|
871
|
-
this.safetyCar.previousX = this.safetyCar.x;
|
|
872
|
-
this.safetyCar.previousY = this.safetyCar.y;
|
|
873
|
-
this.safetyCar.previousHeading = this.safetyCar.heading;
|
|
874
|
-
this.safetyCar.progress = progress;
|
|
875
|
-
this.safetyCar.x = point.x;
|
|
876
|
-
this.safetyCar.y = point.y;
|
|
877
|
-
this.safetyCar.heading = point.heading;
|
|
443
|
+
moveSafetyCarToState(this, progress);
|
|
878
444
|
}
|
|
879
445
|
|
|
880
446
|
applyRunoffResponse(car) {
|
|
881
|
-
|
|
882
|
-
const signedLimit = this.track.width / 2 + this.track.gravelWidth + this.track.runoffWidth;
|
|
883
|
-
const overshoot = Math.abs(state.signedOffset) - signedLimit;
|
|
884
|
-
if (overshoot <= 0) {
|
|
885
|
-
car.trackState = state;
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const side = Math.sign(state.signedOffset) || 1;
|
|
890
|
-
car.x -= state.normalX * side * overshoot;
|
|
891
|
-
car.y -= state.normalY * side * overshoot;
|
|
892
|
-
car.speed = clamp(car.speed * clamp(1 - overshoot * 0.012, 0.22, 0.86), 0, VEHICLE_LIMITS.maxSpeed);
|
|
893
|
-
car.heading = normalizeAngle(car.heading - side * clamp(overshoot * 0.0028, 0.018, 0.08));
|
|
894
|
-
car.trackState = nearestTrackState(this.track, car, state.distance);
|
|
447
|
+
applyRunoffResponseForSimulation(this, car);
|
|
895
448
|
}
|
|
896
449
|
|
|
897
450
|
recalculateRaceState({ updateDrs = true } = {}) {
|
|
898
|
-
this
|
|
899
|
-
const previousRaceDistance = car.raceDistance;
|
|
900
|
-
if (car.gridLocked) {
|
|
901
|
-
const gridPoint = pointAt(this.track, car.gridDistance);
|
|
902
|
-
car.trackState = nearestTrackState(this.track, car, car.gridDistance);
|
|
903
|
-
car.progress = gridPoint.distance;
|
|
904
|
-
car.raceDistance = car.gridDistance;
|
|
905
|
-
car.lap = 1;
|
|
906
|
-
resetLapTelemetry(car, this.time, this.track, this.totalLaps);
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
car.trackState = nearestTrackState(this.track, car, car.progress);
|
|
911
|
-
const previousProgress = car.progress ?? car.trackState.distance;
|
|
912
|
-
const delta = progressDelta(car.trackState.distance, previousProgress, this.track.length);
|
|
913
|
-
car.raceDistance = (car.raceDistance ?? previousProgress) + delta;
|
|
914
|
-
car.progress = car.trackState.distance;
|
|
915
|
-
car.lap = this.computeLap(car.raceDistance);
|
|
916
|
-
updateLapTelemetry(car, previousRaceDistance, this.time, this.track, this.totalLaps);
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
updateSectorPerformance(this.cars);
|
|
920
|
-
this.cars.forEach((car) => recordTimingSample(car, this.time));
|
|
921
|
-
|
|
922
|
-
const ordered = this.orderedCars();
|
|
923
|
-
ordered.forEach((car, index) => {
|
|
924
|
-
const ahead = ordered[index - 1];
|
|
925
|
-
const gap = ahead ? ahead.raceDistance - car.raceDistance : Infinity;
|
|
926
|
-
car.rank = index + 1;
|
|
927
|
-
car.gapAhead = gap;
|
|
928
|
-
car.gapAheadSeconds = Number.isFinite(gap) ? estimateGapAheadSeconds(ahead, car, this.time) : Infinity;
|
|
929
|
-
car.intervalAheadSeconds = car.gapAheadSeconds;
|
|
930
|
-
car.leaderGapSeconds = ahead ? ahead.leaderGapSeconds + car.gapAheadSeconds : 0;
|
|
931
|
-
car.canAttack = !this.safetyCar.deployed && !car.finished;
|
|
932
|
-
car.aggression = this.computeAggression(car, index);
|
|
933
|
-
if (updateDrs) this.updateDrsLatch(car, ahead, index);
|
|
934
|
-
});
|
|
935
|
-
this.evaluateRaceFinish();
|
|
451
|
+
return recalculateRaceStateForSimulation(this, { updateDrs });
|
|
936
452
|
}
|
|
937
453
|
|
|
938
454
|
evaluateRaceFinish() {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const ordered = this.orderedCars();
|
|
943
|
-
const newlyFinished = ordered.filter((car) => !car.finished && car.raceDistance >= this.finishDistance);
|
|
944
|
-
if (newlyFinished.length === 0) return;
|
|
945
|
-
|
|
946
|
-
newlyFinished.forEach((car) => {
|
|
947
|
-
car.finished = true;
|
|
948
|
-
car.finishTime = this.time;
|
|
949
|
-
car.classifiedRank = this.raceControl.finishOrder.length + 1;
|
|
950
|
-
this.raceControl.finishOrder.push(car.id);
|
|
951
|
-
if (!this.raceControl.winnerId) this.raceControl.winnerId = car.id;
|
|
952
|
-
car.drsActive = false;
|
|
953
|
-
car.drsEligible = false;
|
|
954
|
-
car.drsZoneId = null;
|
|
955
|
-
car.drsZoneEnabled = false;
|
|
956
|
-
car.canAttack = false;
|
|
957
|
-
this.events.unshift({
|
|
958
|
-
type: 'car-finish',
|
|
959
|
-
at: this.time,
|
|
960
|
-
carId: car.id,
|
|
961
|
-
rank: car.classifiedRank,
|
|
962
|
-
winnerId: this.raceControl.winnerId,
|
|
963
|
-
});
|
|
964
|
-
});
|
|
455
|
+
evaluateRaceFinishForSimulation(this);
|
|
456
|
+
}
|
|
965
457
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const classification = this.buildClassificationFromFinishOrder();
|
|
969
|
-
this.raceControl.mode = 'safety-car';
|
|
970
|
-
this.raceControl.finished = true;
|
|
971
|
-
this.raceControl.finishedAt = this.time;
|
|
972
|
-
this.raceControl.classification = classification;
|
|
973
|
-
this.raceControl.frozenOrder = classification.map((entry) => entry.id);
|
|
974
|
-
this.safetyCar.deployed = true;
|
|
975
|
-
const leader = this.cars.find((car) => car.id === this.raceControl.winnerId) ?? ordered[0];
|
|
976
|
-
const safetyCarProgress = (leader?.raceDistance ?? 0) + this.rules.safetyCarLeadDistance;
|
|
977
|
-
if (this.safetyCar.progress < safetyCarProgress) {
|
|
978
|
-
this.moveSafetyCarTo(safetyCarProgress);
|
|
979
|
-
}
|
|
980
|
-
this.cars.forEach((car) => {
|
|
981
|
-
const classified = classification.find((entry) => entry.id === car.id);
|
|
982
|
-
car.classifiedRank = classified?.rank ?? car.rank;
|
|
983
|
-
car.desiredOffset = 0;
|
|
984
|
-
car.drsActive = false;
|
|
985
|
-
car.drsEligible = false;
|
|
986
|
-
car.drsZoneId = null;
|
|
987
|
-
car.drsZoneEnabled = false;
|
|
988
|
-
car.canAttack = false;
|
|
989
|
-
});
|
|
990
|
-
this.events.unshift({
|
|
991
|
-
type: 'race-finish',
|
|
992
|
-
at: this.time,
|
|
993
|
-
winnerId: this.raceControl.winnerId,
|
|
994
|
-
classification: classification.map((entry) => ({ id: entry.id, rank: entry.rank })),
|
|
995
|
-
});
|
|
458
|
+
applyOutstandingServicePenalties() {
|
|
459
|
+
applyOutstandingServicePenaltiesState(this, applyUnservedServicePenalty);
|
|
996
460
|
}
|
|
997
461
|
|
|
998
462
|
buildClassificationFromFinishOrder() {
|
|
999
|
-
|
|
1000
|
-
const ordered = this.raceControl.finishOrder
|
|
1001
|
-
.map((id) => byId.get(id))
|
|
1002
|
-
.filter(Boolean);
|
|
1003
|
-
return this.buildClassification(ordered);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
buildClassification(ordered = this.orderedCars()) {
|
|
1007
|
-
const leaderDistance = ordered[0]?.raceDistance ?? 0;
|
|
1008
|
-
return ordered.map((car, index) => ({
|
|
1009
|
-
id: car.id,
|
|
1010
|
-
code: car.code,
|
|
1011
|
-
timingCode: car.timingCode,
|
|
1012
|
-
name: car.name,
|
|
1013
|
-
rank: index + 1,
|
|
1014
|
-
raceDistance: car.raceDistance,
|
|
1015
|
-
distanceMeters: simUnitsToMeters(car.raceDistance),
|
|
1016
|
-
lap: this.computeLap(car.raceDistance),
|
|
1017
|
-
lapsCompleted: clamp(Math.floor(Math.max(0, car.raceDistance) / this.track.length), 0, this.totalLaps),
|
|
1018
|
-
gapMeters: simUnitsToMeters(Math.max(0, leaderDistance - car.raceDistance)),
|
|
1019
|
-
gapSeconds: index === 0 ? 0 : car.leaderGapSeconds,
|
|
1020
|
-
intervalSeconds: index === 0 ? 0 : car.intervalAheadSeconds,
|
|
1021
|
-
finished: car.raceDistance >= this.finishDistance,
|
|
1022
|
-
finishTime: car.finishTime ?? (car.raceDistance >= this.finishDistance ? this.time : null),
|
|
1023
|
-
}));
|
|
463
|
+
return buildClassificationFromFinishOrderState(this);
|
|
1024
464
|
}
|
|
1025
465
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (!car) return null;
|
|
1030
|
-
const rank = car.classifiedRank ?? car.rank ?? 1;
|
|
1031
|
-
return serializeCar(car, rank);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
updateDrsLatch(car, ahead, orderIndex) {
|
|
1035
|
-
if (this.safetyCar.deployed || car.finished) {
|
|
1036
|
-
car.drsEligible = false;
|
|
1037
|
-
car.drsActive = false;
|
|
1038
|
-
car.drsZoneId = null;
|
|
1039
|
-
car.drsZoneEnabled = false;
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
466
|
+
applyClassificationConsequences(ordered, penaltyStats = this.getPenaltyStatsByDriver()) {
|
|
467
|
+
return applyClassificationConsequencesState(this, ordered, penaltyStats);
|
|
468
|
+
}
|
|
1042
469
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
: null;
|
|
470
|
+
buildClassification(ordered = this.orderedCars(), penaltyStats = this.getPenaltyStatsByDriver()) {
|
|
471
|
+
return buildClassificationState(this, ordered, penaltyStats);
|
|
472
|
+
}
|
|
1047
473
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
474
|
+
getRaceWinnerSnapshot() {
|
|
475
|
+
return getRaceWinnerSnapshotForSimulation(this);
|
|
476
|
+
}
|
|
1052
477
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
));
|
|
1057
|
-
if (crossedZone) {
|
|
1058
|
-
car.drsZoneId = crossedZone.id;
|
|
1059
|
-
const crossing = recordDrsDetection(car, crossedZone.id, this.time);
|
|
1060
|
-
const aheadCrossing = ahead?.drsDetection?.[crossedZone.id];
|
|
1061
|
-
car.drsZoneEnabled = Boolean(
|
|
1062
|
-
orderIndex > 0 &&
|
|
1063
|
-
aheadCrossing &&
|
|
1064
|
-
aheadCrossing.passage === crossing.passage &&
|
|
1065
|
-
crossing.time >= aheadCrossing.time &&
|
|
1066
|
-
crossing.time - aheadCrossing.time <= this.rules.drsDetectionSeconds + 1e-6
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
478
|
+
getDrsReferenceCar(car) {
|
|
479
|
+
return getDrsReferenceCarForSimulation(this, car);
|
|
480
|
+
}
|
|
1070
481
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
482
|
+
updateDrsLatch(car, ahead, hasReference = Boolean(ahead)) {
|
|
483
|
+
updateDrsLatchState(car, ahead, {
|
|
484
|
+
hasReference,
|
|
485
|
+
safetyCarDeployed: this.safetyCar.deployed,
|
|
486
|
+
time: this.time,
|
|
487
|
+
track: this.track,
|
|
488
|
+
rules: this.rules,
|
|
489
|
+
});
|
|
1077
490
|
}
|
|
1078
491
|
|
|
1079
492
|
resolveCollisions() {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
for (let pass = 0; pass < 3; pass += 1) {
|
|
1083
|
-
for (let i = 0; i < this.cars.length; i += 1) {
|
|
1084
|
-
for (let j = i + 1; j < this.cars.length; j += 1) {
|
|
1085
|
-
const first = this.cars[i];
|
|
1086
|
-
const second = this.cars[j];
|
|
1087
|
-
const collision = detectObbCollision(first, second) ?? detectLongitudinalCollision(first, second);
|
|
1088
|
-
if (!collision) continue;
|
|
1089
|
-
|
|
1090
|
-
const correction = Math.min(
|
|
1091
|
-
collision.depth / 2 + (collision.longitudinal ? 0.35 : 0.65),
|
|
1092
|
-
MAX_COLLISION_CORRECTION,
|
|
1093
|
-
);
|
|
1094
|
-
first.x -= collision.axis.x * correction;
|
|
1095
|
-
first.y -= collision.axis.y * correction;
|
|
1096
|
-
second.x += collision.axis.x * correction;
|
|
1097
|
-
second.y += collision.axis.y * correction;
|
|
1098
|
-
|
|
1099
|
-
this.applyContactVelocityResponse(first, second, collision.axis);
|
|
1100
|
-
|
|
1101
|
-
const yawNudge = clamp(collision.depth * 0.0025, 0.008, 0.035);
|
|
1102
|
-
const freshContact = first.contactCooldown <= 0 && second.contactCooldown <= 0;
|
|
1103
|
-
first.heading = normalizeAngle(first.heading - collision.axis.y * yawNudge);
|
|
1104
|
-
second.heading = normalizeAngle(second.heading + collision.axis.y * yawNudge);
|
|
1105
|
-
first.contactCooldown = 1;
|
|
1106
|
-
second.contactCooldown = 1;
|
|
1107
|
-
|
|
1108
|
-
const contactKey = `${first.id}:${second.id}`;
|
|
1109
|
-
if (freshContact && pass === 0 && !reportedContacts.has(contactKey)) {
|
|
1110
|
-
reportedContacts.add(contactKey);
|
|
1111
|
-
this.events.unshift({ type: 'contact', at: this.time, carId: first.id, otherCarId: second.id });
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
493
|
+
resolveCollisionsForSimulation(this);
|
|
1116
494
|
}
|
|
1117
495
|
|
|
1118
496
|
applyContactVelocityResponse(first, second, axis) {
|
|
1119
|
-
|
|
1120
|
-
const secondForward = forwardVector(second);
|
|
1121
|
-
const firstNormal = dot(firstForward, axis);
|
|
1122
|
-
const secondNormal = dot(secondForward, axis);
|
|
1123
|
-
const relativeNormalVelocity = second.speed * secondNormal - first.speed * firstNormal;
|
|
1124
|
-
|
|
1125
|
-
if (relativeNormalVelocity < 0) {
|
|
1126
|
-
const impulse = clamp(-relativeNormalVelocity * (0.34 + this.rules.collisionRestitution), 0, 16);
|
|
1127
|
-
if (firstNormal > 0) first.speed = clamp(first.speed - impulse * firstNormal, 0, VEHICLE_LIMITS.maxSpeed);
|
|
1128
|
-
if (secondNormal < 0) second.speed = clamp(second.speed + impulse * secondNormal, 0, VEHICLE_LIMITS.maxSpeed);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
first.speed = clamp(first.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
1132
|
-
second.speed = clamp(second.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
497
|
+
applyContactVelocityResponseForSimulation(this, first, second, axis);
|
|
1133
498
|
}
|
|
1134
499
|
|
|
1135
500
|
computeLap(raceDistance) {
|
|
1136
|
-
return
|
|
501
|
+
return computeLapForDistance(raceDistance, this.track.length, this.totalLaps);
|
|
1137
502
|
}
|
|
1138
503
|
|
|
1139
504
|
get finishDistance() {
|
|
1140
|
-
return this.track.length
|
|
505
|
+
return finishDistanceForRace(this.track.length, this.totalLaps);
|
|
1141
506
|
}
|
|
1142
507
|
}
|
|
1143
508
|
|
|
1144
509
|
export function createRaceSimulation(options = {}) {
|
|
1145
|
-
|
|
510
|
+
const resolvedOptions = withWarmupSurfaceOptions(options, options?.warmup?.surface ?? 'simulation');
|
|
511
|
+
runWarmupWithGuard({
|
|
512
|
+
options: resolvedOptions,
|
|
513
|
+
surface: resolvedOptions.warmup.surface,
|
|
514
|
+
execute: ({ warmup }) => {
|
|
515
|
+
const warmupSimulation = new F1RaceSimulation(disableWarmupOptions(resolvedOptions, warmup.surface));
|
|
516
|
+
for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
return new F1RaceSimulation(resolvedOptions);
|
|
1146
520
|
}
|