@inventure71/paddockjs 1.0.0 → 2.0.1
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 +58 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +219 -12
- package/docs/architecture.md +126 -45
- package/docs/concepts.md +9 -5
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +317 -22
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +71 -14
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +75 -13
- package/docs/training.md +280 -32
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/package.json +12 -4
- package/src/api/PaddockSimulatorController.js +6 -33
- package/src/app/BrowserExpertAdapter.js +76 -9
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +291 -1630
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +4 -0
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +20 -0
- package/src/config/restartOptions.js +34 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +261 -0
- package/src/environment/episode.js +98 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +376 -10
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +199 -141
- package/src/environment/options.js +37 -5
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +272 -23
- package/src/environment/scenarios.js +57 -9
- 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 +75 -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 +66 -0
- package/src/environment/sensors/surfaceRays.js +360 -0
- package/src/environment/sensors/trackRays.js +424 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +159 -26
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +171 -6
- package/src/index.js +6 -32
- package/src/rendering/proceduralTrackAsset.js +20 -823
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +1 -1
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -619
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +239 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +60 -0
- package/src/simulation/pit/pitRouteMovement.js +167 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +43 -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 +94 -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 +67 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +129 -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 +92 -0
- package/src/simulation/race/stalledDnf.js +77 -0
- package/src/simulation/raceSimulation.js +231 -2817
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +144 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/trackLimitsSteward.js +1 -1
- package/src/simulation/rulesConfig.js +5 -323
- package/src/simulation/snapshots/raceSnapshots.js +193 -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/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +659 -0
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -1706
- package/src/simulation/vehicle/contactResolution.js +181 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +87 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +364 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
- package/src/simulation/vehicle/vehicleState.js +220 -0
- package/src/simulation/vehicle/wheelSurface.js +152 -0
- package/src/simulation/vehicleGeometry.js +1 -247
- package/src/simulation/vehiclePhysics.js +1 -136
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -338
- package/src/styles.css +42 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +14 -360
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +5 -4
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -1,1544 +1,176 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildTrackModel,
|
|
3
|
-
createProceduralTrack,
|
|
4
|
-
nearestTrackState,
|
|
5
|
-
offsetTrackPoint,
|
|
6
|
-
pointAt,
|
|
7
|
-
TRACK,
|
|
8
|
-
WORLD,
|
|
9
|
-
} from './trackModel.js';
|
|
10
|
-
import { buildDriverPersonality, decideDriverControls } from './driverController.js';
|
|
11
|
-
import { buildCollisionCandidatePairs, detectVehicleCollision } from './collisionGeometry.js';
|
|
12
|
-
import { calculateCollisionPenalties } from './rules/collisionSteward.js';
|
|
13
1
|
import {
|
|
14
2
|
applyUnservedServicePenalty,
|
|
15
3
|
cancelPenaltyRecord,
|
|
16
4
|
createPenaltyEvent,
|
|
17
5
|
createPenaltyRecord,
|
|
18
|
-
getPitServicePenaltySeconds,
|
|
19
|
-
isPenaltyActive,
|
|
20
|
-
isPenaltyPitServiceable,
|
|
21
|
-
serializePenalty,
|
|
22
|
-
servePitPenaltyRecord,
|
|
23
6
|
servePenaltyRecord,
|
|
24
7
|
} from './rules/penaltyLedger.js';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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';
|
|
108
|
+
|
|
35
109
|
export const FIXED_STEP = 1 / 60;
|
|
36
|
-
const MIN_TOTAL_LAPS = 1;
|
|
37
|
-
const MAX_COLLISION_CORRECTION = 4.5;
|
|
38
|
-
const GRID_SLOT_SPACING = metersToSimUnits(8);
|
|
39
|
-
const ROLLING_START_SLOT_SPACING = metersToSimUnits(35);
|
|
40
|
-
const GRID_FIRST_SLOT_DISTANCE = metersToSimUnits(-6);
|
|
41
|
-
const ROLLING_START_FIRST_DISTANCE = metersToSimUnits(-30);
|
|
42
|
-
const GRID_LATERAL_OFFSET = metersToSimUnits(3.2);
|
|
43
|
-
const TIMING_HISTORY_WINDOW_SECONDS = 18;
|
|
44
|
-
const TIMING_HISTORY_MAX_SAMPLES = 720;
|
|
45
|
-
const TIMING_LINE_TARGET_SPACING_METERS = 175;
|
|
46
|
-
const TIMING_LINE_HISTORY_LAPS = 3;
|
|
47
|
-
const TELEMETRY_SECTOR_COUNT = 3;
|
|
48
|
-
const PIT_ENTRY_APPROACH_DISTANCE = metersToSimUnits(250);
|
|
49
|
-
const PIT_ROUTE_LOOKAHEAD_MIN = metersToSimUnits(35);
|
|
50
|
-
const PIT_ROUTE_LOOKAHEAD_MAX = metersToSimUnits(88);
|
|
51
|
-
const PIT_ROUTE_FINISH_DISTANCE = metersToSimUnits(8.5);
|
|
52
|
-
const PIT_QUEUE_RELEASE_FINISH_DISTANCE = metersToSimUnits(4);
|
|
53
|
-
const PIT_QUEUE_CAPTURE_DISTANCE = metersToSimUnits(2.5);
|
|
54
|
-
const PIT_ENTRY_BOX_CAPTURE_DISTANCE = metersToSimUnits(20);
|
|
55
|
-
const PIT_BOX_STOP_SPEED = kphToSimSpeed(35);
|
|
56
|
-
const PIT_QUEUE_CAPTURE_SPEED = kphToSimSpeed(140);
|
|
57
|
-
const PIT_QUEUE_RELEASE_SPEED = kphToSimSpeed(30);
|
|
58
|
-
const PIT_SERVICE_CLEAR_DISTANCE = VEHICLE_LIMITS.carLength * 0.9;
|
|
59
|
-
const PIT_EXIT_RELEASE_SPEED_KPH = 95;
|
|
60
|
-
const PIT_LIMITER_BRAKE_DISTANCE = metersToSimUnits(295);
|
|
61
|
-
const PIT_LIMITER_APPROACH_SPEED_SLOPE = 0.045;
|
|
62
|
-
const PIT_ENTRY_CONNECTOR_OVERSPEED_KPH = 75;
|
|
63
|
-
const PIT_DRIVE_LANE_OFFSET_RATIO = 0.28;
|
|
64
|
-
const PIT_BOX_APPROACH_DISTANCE = metersToSimUnits(34);
|
|
65
|
-
const PIT_SERVICE_QUEUE_FALLBACK_GAP = metersToSimUnits(48);
|
|
66
|
-
const PIT_INTENT_NONE = 0;
|
|
67
|
-
const PIT_INTENT_IF_FREE = 1;
|
|
68
|
-
const PIT_INTENT_COMMITTED = 2;
|
|
69
110
|
|
|
70
111
|
export { DEFAULT_RULES };
|
|
71
112
|
|
|
72
|
-
function getStartGridSlot(index, { standingStart = false } = {}) {
|
|
73
|
-
const gridDistance = standingStart
|
|
74
|
-
? GRID_FIRST_SLOT_DISTANCE - index * GRID_SLOT_SPACING
|
|
75
|
-
: ROLLING_START_FIRST_DISTANCE - index * ROLLING_START_SLOT_SPACING;
|
|
76
|
-
const gridOffset = index % 2 === 0 ? -GRID_LATERAL_OFFSET : GRID_LATERAL_OFFSET;
|
|
77
|
-
return { gridDistance, gridOffset };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function isPitPositionControlledCar(car) {
|
|
81
|
-
const status = car?.pitStop?.status;
|
|
82
|
-
return status === 'entering' || status === 'queued' || status === 'servicing' || status === 'exiting';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function wrapProgress(value, length) {
|
|
86
|
-
return wrapDistance(value, length);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function distanceForward(from, to, length) {
|
|
90
|
-
return wrapProgress(to - from, length);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function crossesDistance(previous, current, target, length) {
|
|
94
|
-
const travelled = distanceForward(previous, current, length);
|
|
95
|
-
if (travelled <= 0 || travelled > length / 2) return false;
|
|
96
|
-
const targetOffset = distanceForward(previous, target, length);
|
|
97
|
-
return targetOffset > 0 && targetOffset <= travelled + 0.001;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function isProgressInZone(track, progress, zone) {
|
|
101
|
-
const wrapped = wrapProgress(progress, track.length);
|
|
102
|
-
const start = wrapProgress(zone.start, track.length);
|
|
103
|
-
const end = wrapProgress(zone.end, track.length);
|
|
104
|
-
if (zone.end - zone.start >= track.length) return true;
|
|
105
|
-
return end >= start
|
|
106
|
-
? wrapped >= start && wrapped <= end
|
|
107
|
-
: wrapped >= start || wrapped <= end;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function createEmptySectorTimes() {
|
|
111
|
-
return Array.from({ length: TELEMETRY_SECTOR_COUNT }, () => null);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function createEmptySectorPerformance() {
|
|
115
|
-
return {
|
|
116
|
-
current: createEmptySectorTimes(),
|
|
117
|
-
last: createEmptySectorTimes(),
|
|
118
|
-
best: createEmptySectorTimes(),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function getSectorLength(track) {
|
|
123
|
-
return track.length / TELEMETRY_SECTOR_COUNT;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getLapTelemetryPosition(track, raceDistance, totalLaps = Infinity) {
|
|
127
|
-
const positiveDistance = Math.max(0, raceDistance ?? 0);
|
|
128
|
-
const lapIndex = Math.min(
|
|
129
|
-
Math.floor(positiveDistance / track.length),
|
|
130
|
-
Math.max(0, totalLaps - 1),
|
|
131
|
-
);
|
|
132
|
-
const lapProgress = positiveDistance >= track.length * totalLaps
|
|
133
|
-
? track.length
|
|
134
|
-
: positiveDistance - lapIndex * track.length;
|
|
135
|
-
const sectorLength = getSectorLength(track);
|
|
136
|
-
const sectorIndex = Math.min(TELEMETRY_SECTOR_COUNT - 1, Math.floor(lapProgress / sectorLength));
|
|
137
|
-
const sectorStart = sectorIndex * sectorLength;
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
completedLaps: Math.floor(positiveDistance / track.length),
|
|
141
|
-
currentLap: Math.max(1, lapIndex + 1),
|
|
142
|
-
currentSector: sectorIndex + 1,
|
|
143
|
-
currentSectorProgress: clamp((lapProgress - sectorStart) / sectorLength, 0, 1),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function createSectorProgress(position, currentSectors = null) {
|
|
148
|
-
const activeIndex = clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
149
|
-
return createEmptySectorTimes().map((_, index) => {
|
|
150
|
-
if (index < activeIndex && Number.isFinite(currentSectors?.[index])) return 1;
|
|
151
|
-
if (index === activeIndex) return clamp(position.currentSectorProgress ?? 0, 0, 1);
|
|
152
|
-
return 0;
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function createLapTelemetry(track, currentTime = 0, raceDistance = 0, totalLaps = Infinity) {
|
|
157
|
-
const position = getLapTelemetryPosition(track, raceDistance, totalLaps);
|
|
158
|
-
return {
|
|
159
|
-
...position,
|
|
160
|
-
currentLapStartedAt: currentTime,
|
|
161
|
-
currentSectorStartedAt: currentTime,
|
|
162
|
-
lastUpdatedAt: currentTime,
|
|
163
|
-
currentLapTime: 0,
|
|
164
|
-
currentSectorElapsed: 0,
|
|
165
|
-
currentSectors: createEmptySectorTimes(),
|
|
166
|
-
sectorProgress: createSectorProgress(position),
|
|
167
|
-
liveSectors: createEmptySectorTimes(),
|
|
168
|
-
lastLapTime: null,
|
|
169
|
-
bestLapTime: null,
|
|
170
|
-
lastSectors: createEmptySectorTimes(),
|
|
171
|
-
bestSectors: createEmptySectorTimes(),
|
|
172
|
-
sectorPerformance: createEmptySectorPerformance(),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function resetLapTelemetry(car, currentTime, track, totalLaps) {
|
|
177
|
-
car.lapTelemetry = createLapTelemetry(track, currentTime, car.raceDistance, totalLaps);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function finiteOrNull(value) {
|
|
181
|
-
return Number.isFinite(value) ? value : null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function serializeSectorTimes(values) {
|
|
185
|
-
return createEmptySectorTimes().map((_, index) => finiteOrNull(values?.[index]));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function serializeLapTelemetry(telemetry) {
|
|
189
|
-
return {
|
|
190
|
-
currentLap: telemetry.currentLap,
|
|
191
|
-
currentSector: telemetry.currentSector,
|
|
192
|
-
currentLapTime: finiteOrNull(telemetry.currentLapTime),
|
|
193
|
-
currentSectorElapsed: finiteOrNull(telemetry.currentSectorElapsed),
|
|
194
|
-
currentSectorProgress: finiteOrNull(telemetry.currentSectorProgress),
|
|
195
|
-
currentSectors: serializeSectorTimes(telemetry.currentSectors),
|
|
196
|
-
sectorProgress: serializeSectorTimes(telemetry.sectorProgress),
|
|
197
|
-
liveSectors: serializeSectorTimes(telemetry.liveSectors),
|
|
198
|
-
lastLapTime: finiteOrNull(telemetry.lastLapTime),
|
|
199
|
-
bestLapTime: finiteOrNull(telemetry.bestLapTime),
|
|
200
|
-
lastSectors: serializeSectorTimes(telemetry.lastSectors),
|
|
201
|
-
bestSectors: serializeSectorTimes(telemetry.bestSectors),
|
|
202
|
-
sectorPerformance: {
|
|
203
|
-
current: serializeSectorPerformance(telemetry.sectorPerformance?.current),
|
|
204
|
-
last: serializeSectorPerformance(telemetry.sectorPerformance?.last),
|
|
205
|
-
best: serializeSectorPerformance(telemetry.sectorPerformance?.best),
|
|
206
|
-
},
|
|
207
|
-
completedLaps: telemetry.completedLaps,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function serializeSectorPerformance(values) {
|
|
212
|
-
return createEmptySectorTimes().map((_, index) => values?.[index] ?? null);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function updateBestSector(telemetry, sectorIndex, sectorTime) {
|
|
216
|
-
const previousBest = telemetry.bestSectors[sectorIndex];
|
|
217
|
-
if (!Number.isFinite(previousBest) || sectorTime < previousBest) {
|
|
218
|
-
telemetry.bestSectors[sectorIndex] = sectorTime;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function clearFutureSectorTelemetry(telemetry) {
|
|
223
|
-
const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
224
|
-
for (let index = activeIndex; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
225
|
-
telemetry.currentSectors[index] = null;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function syncLiveSectorTelemetry(telemetry, track) {
|
|
230
|
-
const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
231
|
-
|
|
232
|
-
telemetry.liveSectors = createEmptySectorTimes();
|
|
233
|
-
telemetry.currentSectors.forEach((time, index) => {
|
|
234
|
-
if (index >= activeIndex) return;
|
|
235
|
-
if (!Number.isFinite(time)) return;
|
|
236
|
-
telemetry.liveSectors[index] = time;
|
|
237
|
-
});
|
|
238
|
-
telemetry.liveSectors[activeIndex] = telemetry.currentSectorElapsed;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps) {
|
|
242
|
-
const position = getLapTelemetryPosition(track, currentRaceDistance, totalLaps);
|
|
243
|
-
telemetry.currentLap = position.currentLap;
|
|
244
|
-
telemetry.currentSector = position.currentSector;
|
|
245
|
-
telemetry.currentSectorProgress = position.currentSectorProgress;
|
|
246
|
-
clearFutureSectorTelemetry(telemetry);
|
|
247
|
-
telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors);
|
|
248
|
-
telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(position.completedLaps, totalLaps));
|
|
249
|
-
telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
|
|
250
|
-
telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
|
|
251
|
-
syncLiveSectorTelemetry(telemetry, track);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function classifySectorPerformance(value, personalBest, overallBest) {
|
|
255
|
-
if (!Number.isFinite(value)) return null;
|
|
256
|
-
if (Number.isFinite(overallBest) && Math.abs(value - overallBest) <= 1e-6) return 'overall-best';
|
|
257
|
-
if (Number.isFinite(personalBest) && Math.abs(value - personalBest) <= 1e-6) return 'personal-best';
|
|
258
|
-
return 'slower';
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function updateSectorPerformance(cars) {
|
|
262
|
-
const overallBestSectors = createEmptySectorTimes();
|
|
263
|
-
cars.forEach((car) => {
|
|
264
|
-
car.lapTelemetry?.bestSectors?.forEach((time, index) => {
|
|
265
|
-
if (!Number.isFinite(time)) return;
|
|
266
|
-
const previousBest = overallBestSectors[index];
|
|
267
|
-
if (!Number.isFinite(previousBest) || time < previousBest) overallBestSectors[index] = time;
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
cars.forEach((car) => {
|
|
272
|
-
if (!car.lapTelemetry) return;
|
|
273
|
-
const telemetry = car.lapTelemetry;
|
|
274
|
-
telemetry.sectorPerformance = {
|
|
275
|
-
current: telemetry.currentSectors.map((time, index) => (
|
|
276
|
-
classifySectorPerformance(time, telemetry.bestSectors[index], overallBestSectors[index])
|
|
277
|
-
)),
|
|
278
|
-
last: telemetry.lastSectors.map((time, index) => (
|
|
279
|
-
classifySectorPerformance(time, telemetry.bestSectors[index], overallBestSectors[index])
|
|
280
|
-
)),
|
|
281
|
-
best: telemetry.bestSectors.map((time, index) => (
|
|
282
|
-
classifySectorPerformance(time, telemetry.bestSectors[index], overallBestSectors[index])
|
|
283
|
-
)),
|
|
284
|
-
};
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function updateLapTelemetry(car, previousRaceDistance, currentTime, track, totalLaps) {
|
|
289
|
-
if (!car.lapTelemetry) resetLapTelemetry(car, currentTime, track, totalLaps);
|
|
290
|
-
|
|
291
|
-
const telemetry = car.lapTelemetry;
|
|
292
|
-
const currentRaceDistance = car.raceDistance ?? 0;
|
|
293
|
-
const previousDistance = Math.max(0, previousRaceDistance ?? currentRaceDistance);
|
|
294
|
-
const currentDistance = Math.max(0, currentRaceDistance);
|
|
295
|
-
const travelled = currentDistance - previousDistance;
|
|
296
|
-
const previousUpdateTime = Number.isFinite(telemetry.lastUpdatedAt) ? telemetry.lastUpdatedAt : currentTime;
|
|
297
|
-
const elapsedTime = Math.max(0, currentTime - previousUpdateTime);
|
|
298
|
-
|
|
299
|
-
if (!Number.isFinite(previousDistance) || !Number.isFinite(currentDistance) || travelled < -1e-3 || travelled > track.length / 2) {
|
|
300
|
-
resetLapTelemetry(car, currentTime, track, totalLaps);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const sectorLength = getSectorLength(track);
|
|
305
|
-
if (travelled > 1e-6) {
|
|
306
|
-
const firstBoundary = Math.floor(previousDistance / sectorLength) + 1;
|
|
307
|
-
const lastBoundary = Math.floor(currentDistance / sectorLength);
|
|
308
|
-
|
|
309
|
-
for (let boundary = firstBoundary; boundary <= lastBoundary; boundary += 1) {
|
|
310
|
-
const boundaryDistance = boundary * sectorLength;
|
|
311
|
-
if (boundaryDistance <= previousDistance + 1e-3 || boundaryDistance > currentDistance + 1e-3) continue;
|
|
312
|
-
|
|
313
|
-
const boundaryRatio = clamp((boundaryDistance - previousDistance) / travelled, 0, 1);
|
|
314
|
-
const crossingTime = previousUpdateTime + boundaryRatio * elapsedTime;
|
|
315
|
-
const sectorIndex = (boundary - 1) % TELEMETRY_SECTOR_COUNT;
|
|
316
|
-
const sectorTime = Math.max(0, crossingTime - telemetry.currentSectorStartedAt);
|
|
317
|
-
|
|
318
|
-
telemetry.currentSectors[sectorIndex] = sectorTime;
|
|
319
|
-
updateBestSector(telemetry, sectorIndex, sectorTime);
|
|
320
|
-
|
|
321
|
-
if (sectorIndex === TELEMETRY_SECTOR_COUNT - 1) {
|
|
322
|
-
const lapTime = Math.max(0, crossingTime - telemetry.currentLapStartedAt);
|
|
323
|
-
telemetry.lastLapTime = lapTime;
|
|
324
|
-
telemetry.bestLapTime = Number.isFinite(telemetry.bestLapTime)
|
|
325
|
-
? Math.min(telemetry.bestLapTime, lapTime)
|
|
326
|
-
: lapTime;
|
|
327
|
-
telemetry.lastSectors = serializeSectorTimes(telemetry.currentSectors);
|
|
328
|
-
telemetry.currentSectors = createEmptySectorTimes();
|
|
329
|
-
telemetry.currentLapStartedAt = crossingTime;
|
|
330
|
-
telemetry.completedLaps = Math.max(telemetry.completedLaps + 1, Math.min(Math.floor(boundaryDistance / track.length), totalLaps));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
telemetry.currentSectorStartedAt = crossingTime;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps);
|
|
338
|
-
telemetry.lastUpdatedAt = currentTime;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function createCar(driver, index, random, track, { standingStart = false } = {}) {
|
|
342
|
-
const { gridDistance, gridOffset } = getStartGridSlot(index, { standingStart });
|
|
343
|
-
const start = pointAt(track, gridDistance);
|
|
344
|
-
const position = offsetTrackPoint(start, gridOffset);
|
|
345
|
-
const pace = clamp(driver.pace ?? seededRange(random, 0.95, 1.05), 0.88, 1.12);
|
|
346
|
-
const racecraft = clamp(driver.racecraft ?? seededRange(random, 0.65, 0.92), 0.45, 1);
|
|
347
|
-
const personality = buildDriverPersonality(driver, index, racecraft, random);
|
|
348
|
-
const launchSpeed = kphToSimSpeed(Math.max(70, 84 - index * 0.55 + pace * 4));
|
|
349
|
-
const vehicle = driver.vehicle ?? {};
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
id: driver.id ?? `car-${index + 1}`,
|
|
353
|
-
code: driver.code ?? `C${index + 1}`,
|
|
354
|
-
timingCode: driver.timingCode ?? driver.code ?? `C${index + 1}`,
|
|
355
|
-
driverNumber: driver.driverNumber ?? index + 1,
|
|
356
|
-
icon: driver.icon ?? String(index + 1).padStart(2, '0'),
|
|
357
|
-
raceName: driver.raceName ?? driver.code ?? `CAR${index + 1}`,
|
|
358
|
-
name: driver.name ?? `Car ${index + 1}`,
|
|
359
|
-
color: driver.color ?? '#e10600',
|
|
360
|
-
team: driver.team ? { ...driver.team } : null,
|
|
361
|
-
tire: driver.tire ?? ['M', 'H', 'S'][index % 3],
|
|
362
|
-
index,
|
|
363
|
-
x: position.x,
|
|
364
|
-
y: position.y,
|
|
365
|
-
previousX: position.x,
|
|
366
|
-
previousY: position.y,
|
|
367
|
-
heading: start.heading,
|
|
368
|
-
previousHeading: start.heading,
|
|
369
|
-
steeringAngle: 0,
|
|
370
|
-
yawRate: 0,
|
|
371
|
-
turnRadius: Infinity,
|
|
372
|
-
speed: standingStart ? 0 : launchSpeed,
|
|
373
|
-
throttle: 0,
|
|
374
|
-
brake: 0,
|
|
375
|
-
vehicleId: vehicle.id ?? null,
|
|
376
|
-
vehicleName: vehicle.name ?? null,
|
|
377
|
-
vehicleRatings: vehicle.ratings ? { ...vehicle.ratings } : null,
|
|
378
|
-
mass: vehicle.mass ?? 798 + seededRange(random, -5, 5),
|
|
379
|
-
powerNewtons: vehicle.powerNewtons ?? 43000 * pace,
|
|
380
|
-
brakeNewtons: vehicle.brakeNewtons ?? 59000,
|
|
381
|
-
dragCoefficient: vehicle.dragCoefficient ?? 0.33 + seededRange(random, -0.026, 0.026),
|
|
382
|
-
downforceCoefficient: vehicle.downforceCoefficient ?? 6.1 + seededRange(random, -0.18, 0.18),
|
|
383
|
-
tireGrip: vehicle.tireGrip ?? 2.22 + racecraft * 0.28 + seededRange(random, -0.03, 0.03),
|
|
384
|
-
tireCare: vehicle.tireCare ?? 1,
|
|
385
|
-
pace,
|
|
386
|
-
racecraft,
|
|
387
|
-
personality,
|
|
388
|
-
aggression: personality.baseAggression,
|
|
389
|
-
gridLocked: standingStart,
|
|
390
|
-
gridDistance,
|
|
391
|
-
gridOffset,
|
|
392
|
-
desiredOffset: gridOffset,
|
|
393
|
-
progress: start.distance,
|
|
394
|
-
raceDistance: gridDistance,
|
|
395
|
-
lap: 1,
|
|
396
|
-
rank: index + 1,
|
|
397
|
-
gapAhead: Infinity,
|
|
398
|
-
gapAheadSeconds: Infinity,
|
|
399
|
-
leaderGapSeconds: 0,
|
|
400
|
-
intervalAheadSeconds: Infinity,
|
|
401
|
-
gapAheadLaps: 0,
|
|
402
|
-
intervalAheadLaps: 0,
|
|
403
|
-
leaderGapLaps: 0,
|
|
404
|
-
drsEligible: false,
|
|
405
|
-
drsActive: false,
|
|
406
|
-
drsZoneId: null,
|
|
407
|
-
drsZoneEnabled: false,
|
|
408
|
-
timingHistory: [],
|
|
409
|
-
lapTelemetry: createLapTelemetry(track, 0, gridDistance),
|
|
410
|
-
drsDetection: {},
|
|
411
|
-
canAttack: true,
|
|
412
|
-
trackState: start,
|
|
413
|
-
contactCooldown: 0,
|
|
414
|
-
tireEnergy: 100,
|
|
415
|
-
usedTireCompounds: [driver.tire ?? ['M', 'H', 'S'][index % 3]],
|
|
416
|
-
automaticPitIntentEnabled: true,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function normalizeTotalLaps(value) {
|
|
421
|
-
const numeric = Number(value);
|
|
422
|
-
if (!Number.isFinite(numeric)) return MIN_TOTAL_LAPS;
|
|
423
|
-
return Math.max(MIN_TOTAL_LAPS, Math.floor(numeric));
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function forwardVector(car) {
|
|
427
|
-
return { x: Math.cos(car.heading), y: Math.sin(car.heading) };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function velocityVector(car) {
|
|
431
|
-
const forward = forwardVector(car);
|
|
432
|
-
return { x: forward.x * car.speed, y: forward.y * car.speed };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function createCollisionStewardContext(first, second, collision) {
|
|
436
|
-
const distanceDelta = collision.trackLength
|
|
437
|
-
? progressDelta(second.progress ?? second.raceDistance ?? 0, first.progress ?? first.raceDistance ?? 0, collision.trackLength)
|
|
438
|
-
: (second.raceDistance ?? 0) - (first.raceDistance ?? 0);
|
|
439
|
-
const sideBySideTolerance = VEHICLE_LIMITS.carLength * 0.18;
|
|
440
|
-
if (Math.abs(distanceDelta) <= sideBySideTolerance) {
|
|
441
|
-
const firstVelocity = velocityVector(first);
|
|
442
|
-
const secondVelocity = velocityVector(second);
|
|
443
|
-
const relativeVelocity = {
|
|
444
|
-
x: firstVelocity.x - secondVelocity.x,
|
|
445
|
-
y: firstVelocity.y - secondVelocity.y,
|
|
446
|
-
};
|
|
447
|
-
return {
|
|
448
|
-
...collision,
|
|
449
|
-
impactSpeed: Math.hypot(relativeVelocity.x, relativeVelocity.y),
|
|
450
|
-
aheadDriverId: null,
|
|
451
|
-
atFaultDriverId: null,
|
|
452
|
-
sharedFault: true,
|
|
453
|
-
sharedFaultDriverIds: [first.id, second.id],
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const firstBehind = distanceDelta > 0;
|
|
458
|
-
const behind = firstBehind ? first : second;
|
|
459
|
-
const ahead = firstBehind ? second : first;
|
|
460
|
-
const directionBehindToAhead = normalizeVector({
|
|
461
|
-
x: ahead.x - behind.x,
|
|
462
|
-
y: ahead.y - behind.y,
|
|
463
|
-
});
|
|
464
|
-
const behindVelocity = velocityVector(behind);
|
|
465
|
-
const aheadVelocity = velocityVector(ahead);
|
|
466
|
-
const relativeVelocity = {
|
|
467
|
-
x: behindVelocity.x - aheadVelocity.x,
|
|
468
|
-
y: behindVelocity.y - aheadVelocity.y,
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
return {
|
|
472
|
-
...collision,
|
|
473
|
-
impactSpeed: Math.max(0, dot(relativeVelocity, directionBehindToAhead)),
|
|
474
|
-
aheadDriverId: ahead.id,
|
|
475
|
-
atFaultDriverId: behind.id,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function dot(a, b) {
|
|
480
|
-
return a.x * b.x + a.y * b.y;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function normalizeVector(vector) {
|
|
484
|
-
const length = Math.hypot(vector.x, vector.y) || 1;
|
|
485
|
-
return { x: vector.x / length, y: vector.y / length };
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function progressDelta(a, b, trackLength) {
|
|
489
|
-
let delta = a - b;
|
|
490
|
-
if (delta < -trackLength / 2) delta += trackLength;
|
|
491
|
-
if (delta > trackLength / 2) delta -= trackLength;
|
|
492
|
-
return delta;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function trimTimingHistory(history, currentTime) {
|
|
496
|
-
const cutoff = currentTime - TIMING_HISTORY_WINDOW_SECONDS;
|
|
497
|
-
while (history.length > 2 && (history[0].time < cutoff || history.length > TIMING_HISTORY_MAX_SAMPLES)) {
|
|
498
|
-
history.shift();
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function resetTimingHistory(car, currentTime) {
|
|
503
|
-
car.timingHistory = [{ time: currentTime, raceDistance: car.raceDistance }];
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function recordTimingSample(car, currentTime) {
|
|
507
|
-
if (!Number.isFinite(car.raceDistance)) return;
|
|
508
|
-
if (!Array.isArray(car.timingHistory)) {
|
|
509
|
-
resetTimingHistory(car, currentTime);
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const previous = car.timingHistory[car.timingHistory.length - 1];
|
|
514
|
-
if (
|
|
515
|
-
previous &&
|
|
516
|
-
Math.abs(previous.time - currentTime) <= 1e-6 &&
|
|
517
|
-
Math.abs(previous.raceDistance - car.raceDistance) <= 1e-3
|
|
518
|
-
) {
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
car.timingHistory.push({ time: currentTime, raceDistance: car.raceDistance });
|
|
523
|
-
trimTimingHistory(car.timingHistory, currentTime);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function interpolateTimeAtDistance(history, targetDistance) {
|
|
527
|
-
if (!Array.isArray(history) || history.length < 2) return null;
|
|
528
|
-
|
|
529
|
-
for (let index = history.length - 1; index > 0; index -= 1) {
|
|
530
|
-
const current = history[index];
|
|
531
|
-
const previous = history[index - 1];
|
|
532
|
-
if (targetDistance < previous.raceDistance || targetDistance > current.raceDistance) continue;
|
|
533
|
-
|
|
534
|
-
const coveredDistance = current.raceDistance - previous.raceDistance;
|
|
535
|
-
if (coveredDistance <= 1e-6) return current.time;
|
|
536
|
-
const ratio = (targetDistance - previous.raceDistance) / coveredDistance;
|
|
537
|
-
return previous.time + (current.time - previous.time) * ratio;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function estimateGapAheadSeconds(ahead, car, currentTime, track) {
|
|
544
|
-
if (!ahead) return Infinity;
|
|
545
|
-
|
|
546
|
-
const timingLineGap = estimateTimingLineGapSeconds(ahead, car, currentTime, track);
|
|
547
|
-
if (Number.isFinite(timingLineGap)) return timingLineGap;
|
|
548
|
-
|
|
549
|
-
const crossingTime = interpolateTimeAtDistance(ahead.timingHistory, car.raceDistance);
|
|
550
|
-
if (Number.isFinite(crossingTime)) {
|
|
551
|
-
return Math.max(0, currentTime - crossingTime);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const speedReference = Math.max((ahead.speed + car.speed) * 0.5, 1);
|
|
555
|
-
return Math.max(0, (ahead.raceDistance - car.raceDistance) / speedReference);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function wholeLapGap(aheadDistance, carDistance, trackLength) {
|
|
559
|
-
if (!Number.isFinite(aheadDistance) || !Number.isFinite(carDistance)) return 0;
|
|
560
|
-
if (!Number.isFinite(trackLength) || trackLength <= 0) return 0;
|
|
561
|
-
return Math.max(0, Math.floor((aheadDistance - carDistance + 1e-6) / trackLength));
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function createTimingLines(track) {
|
|
565
|
-
const targetSpacing = metersToSimUnits(TIMING_LINE_TARGET_SPACING_METERS);
|
|
566
|
-
const count = Math.max(1, Math.round(track.length / targetSpacing));
|
|
567
|
-
const spacing = track.length / count;
|
|
568
|
-
|
|
569
|
-
return {
|
|
570
|
-
spacing,
|
|
571
|
-
spacingMeters: simUnitsToMeters(spacing),
|
|
572
|
-
count,
|
|
573
|
-
lines: Array.from({ length: count }, (_, index) => {
|
|
574
|
-
const distance = index * spacing;
|
|
575
|
-
const point = pointAt(track, distance);
|
|
576
|
-
return {
|
|
577
|
-
index,
|
|
578
|
-
distance,
|
|
579
|
-
distanceMeters: simUnitsToMeters(distance),
|
|
580
|
-
x: point.x,
|
|
581
|
-
y: point.y,
|
|
582
|
-
heading: point.heading,
|
|
583
|
-
};
|
|
584
|
-
}),
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function getTimingLineNumber(track, raceDistance) {
|
|
589
|
-
const spacing = track.timingLines?.spacing;
|
|
590
|
-
if (!Number.isFinite(spacing) || spacing <= 0 || !Number.isFinite(raceDistance)) return null;
|
|
591
|
-
return Math.floor(Math.max(0, raceDistance) / spacing);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function resetTimingLineCrossings(car, currentTime) {
|
|
595
|
-
car.timingLineCrossings = Object.create(null);
|
|
596
|
-
car.timingLineLastUpdatedAt = currentTime;
|
|
597
|
-
car.previousRaceDistanceForTiming = car.raceDistance;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function recordTimingLineCrossings(car, previousRaceDistance, currentTime, track) {
|
|
601
|
-
const spacing = track.timingLines?.spacing;
|
|
602
|
-
if (!Number.isFinite(spacing) || spacing <= 0 || !Number.isFinite(car.raceDistance)) return;
|
|
603
|
-
if (!car.timingLineCrossings || typeof car.timingLineCrossings !== 'object') {
|
|
604
|
-
resetTimingLineCrossings(car, currentTime);
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const previousDistance = Number.isFinite(previousRaceDistance) ? previousRaceDistance : car.raceDistance;
|
|
609
|
-
const currentDistance = car.raceDistance;
|
|
610
|
-
const travelled = currentDistance - previousDistance;
|
|
611
|
-
const previousTime = Number.isFinite(car.timingLineLastUpdatedAt) ? car.timingLineLastUpdatedAt : currentTime;
|
|
612
|
-
const elapsed = Math.max(0, currentTime - previousTime);
|
|
613
|
-
|
|
614
|
-
if (travelled > 1e-6 && travelled < track.length / 2) {
|
|
615
|
-
const firstLine = Math.floor(Math.max(0, previousDistance) / spacing) + 1;
|
|
616
|
-
const lastLine = Math.floor(Math.max(0, currentDistance) / spacing);
|
|
617
|
-
|
|
618
|
-
for (let lineNumber = firstLine; lineNumber <= lastLine; lineNumber += 1) {
|
|
619
|
-
const lineDistance = lineNumber * spacing;
|
|
620
|
-
if (lineDistance <= previousDistance + 1e-6 || lineDistance > currentDistance + 1e-6) continue;
|
|
621
|
-
const ratio = clamp((lineDistance - previousDistance) / travelled, 0, 1);
|
|
622
|
-
car.timingLineCrossings[lineNumber] = previousTime + ratio * elapsed;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
car.timingLineLastUpdatedAt = currentTime;
|
|
627
|
-
const latestLine = getTimingLineNumber(track, currentDistance);
|
|
628
|
-
if (latestLine != null) {
|
|
629
|
-
const cutoff = latestLine - (track.timingLines.count * TIMING_LINE_HISTORY_LAPS);
|
|
630
|
-
Object.keys(car.timingLineCrossings).forEach((key) => {
|
|
631
|
-
if (Number(key) < cutoff) delete car.timingLineCrossings[key];
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function getTimingLineCrossingTime(car, lineNumber) {
|
|
637
|
-
const value = car?.timingLineCrossings?.[lineNumber];
|
|
638
|
-
return Number.isFinite(value) ? value : null;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function estimateTimingLineGapSeconds(ahead, car, currentTime, track) {
|
|
642
|
-
if (!ahead || !car) return Infinity;
|
|
643
|
-
const currentLine = getTimingLineNumber(track, Math.min(ahead.raceDistance ?? 0, car.raceDistance ?? 0));
|
|
644
|
-
if (currentLine == null) return null;
|
|
645
|
-
const oldestLine = Math.max(0, currentLine - (track.timingLines?.count ?? 0));
|
|
646
|
-
|
|
647
|
-
for (let lineNumber = currentLine; lineNumber >= oldestLine; lineNumber -= 1) {
|
|
648
|
-
const aheadTime = getTimingLineCrossingTime(ahead, lineNumber);
|
|
649
|
-
const carTime = getTimingLineCrossingTime(car, lineNumber);
|
|
650
|
-
if (Number.isFinite(aheadTime) && Number.isFinite(carTime)) {
|
|
651
|
-
return Math.max(0, carTime - aheadTime);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function recordDrsDetection(car, zoneId, currentTime) {
|
|
659
|
-
const previous = car.drsDetection?.[zoneId] ?? { passage: 0, time: -Infinity };
|
|
660
|
-
const next = { passage: previous.passage + 1, time: currentTime };
|
|
661
|
-
car.drsDetection = {
|
|
662
|
-
...(car.drsDetection ?? {}),
|
|
663
|
-
[zoneId]: next,
|
|
664
|
-
};
|
|
665
|
-
return next;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function clonePointLike(point) {
|
|
669
|
-
if (!point) return point;
|
|
670
|
-
return { ...point };
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function clonePointArray(points) {
|
|
674
|
-
return Array.isArray(points) ? points.map(clonePointLike) : points;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function clonePitLaneModel(pitLane) {
|
|
678
|
-
if (!pitLane) return pitLane;
|
|
679
|
-
return {
|
|
680
|
-
...pitLane,
|
|
681
|
-
entry: pitLane.entry ? {
|
|
682
|
-
...pitLane.entry,
|
|
683
|
-
trackPoint: clonePointLike(pitLane.entry.trackPoint),
|
|
684
|
-
edgePoint: clonePointLike(pitLane.entry.edgePoint),
|
|
685
|
-
trackConnectPoint: clonePointLike(pitLane.entry.trackConnectPoint),
|
|
686
|
-
lanePoint: clonePointLike(pitLane.entry.lanePoint),
|
|
687
|
-
roadCenterline: clonePointArray(pitLane.entry.roadCenterline),
|
|
688
|
-
connector: clonePointArray(pitLane.entry.connector),
|
|
689
|
-
} : pitLane.entry,
|
|
690
|
-
exit: pitLane.exit ? {
|
|
691
|
-
...pitLane.exit,
|
|
692
|
-
trackPoint: clonePointLike(pitLane.exit.trackPoint),
|
|
693
|
-
edgePoint: clonePointLike(pitLane.exit.edgePoint),
|
|
694
|
-
trackConnectPoint: clonePointLike(pitLane.exit.trackConnectPoint),
|
|
695
|
-
lanePoint: clonePointLike(pitLane.exit.lanePoint),
|
|
696
|
-
roadCenterline: clonePointArray(pitLane.exit.roadCenterline),
|
|
697
|
-
connector: clonePointArray(pitLane.exit.connector),
|
|
698
|
-
} : pitLane.exit,
|
|
699
|
-
mainLane: pitLane.mainLane ? {
|
|
700
|
-
...pitLane.mainLane,
|
|
701
|
-
start: clonePointLike(pitLane.mainLane.start),
|
|
702
|
-
end: clonePointLike(pitLane.mainLane.end),
|
|
703
|
-
points: clonePointArray(pitLane.mainLane.points),
|
|
704
|
-
} : pitLane.mainLane,
|
|
705
|
-
workingLane: pitLane.workingLane ? {
|
|
706
|
-
...pitLane.workingLane,
|
|
707
|
-
start: clonePointLike(pitLane.workingLane.start),
|
|
708
|
-
end: clonePointLike(pitLane.workingLane.end),
|
|
709
|
-
points: clonePointArray(pitLane.workingLane.points),
|
|
710
|
-
} : pitLane.workingLane,
|
|
711
|
-
fastLane: pitLane.fastLane ? { ...pitLane.fastLane } : pitLane.fastLane,
|
|
712
|
-
serviceNormal: clonePointLike(pitLane.serviceNormal),
|
|
713
|
-
boxes: Array.isArray(pitLane.boxes)
|
|
714
|
-
? pitLane.boxes.map((box) => ({
|
|
715
|
-
...box,
|
|
716
|
-
laneTarget: clonePointLike(box.laneTarget),
|
|
717
|
-
center: clonePointLike(box.center),
|
|
718
|
-
corners: clonePointArray(box.corners),
|
|
719
|
-
}))
|
|
720
|
-
: pitLane.boxes,
|
|
721
|
-
serviceAreas: Array.isArray(pitLane.serviceAreas)
|
|
722
|
-
? pitLane.serviceAreas.map((area) => ({
|
|
723
|
-
...area,
|
|
724
|
-
laneTarget: clonePointLike(area.laneTarget),
|
|
725
|
-
center: clonePointLike(area.center),
|
|
726
|
-
queuePoint: clonePointLike(area.queuePoint),
|
|
727
|
-
corners: clonePointArray(area.corners),
|
|
728
|
-
queueCorners: clonePointArray(area.queueCorners),
|
|
729
|
-
garageBoxIds: [...(area.garageBoxIds ?? [])],
|
|
730
|
-
pitCrew: area.pitCrew ? { ...area.pitCrew } : area.pitCrew,
|
|
731
|
-
}))
|
|
732
|
-
: pitLane.serviceAreas,
|
|
733
|
-
teams: Array.isArray(pitLane.teams)
|
|
734
|
-
? pitLane.teams.map((team) => ({ ...team, boxIds: [...(team.boxIds ?? [])], pitCrew: team.pitCrew ? { ...team.pitCrew } : team.pitCrew }))
|
|
735
|
-
: pitLane.teams,
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function routePoint(point, heading = point?.heading, options = {}) {
|
|
740
|
-
if (!point) return null;
|
|
741
|
-
return {
|
|
742
|
-
x: point.x,
|
|
743
|
-
y: point.y,
|
|
744
|
-
heading,
|
|
745
|
-
limiterActive: Boolean(options.limiterActive ?? point.limiterActive),
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function offsetPitLanePoint(pitLane, point, lateralOffset) {
|
|
750
|
-
return {
|
|
751
|
-
...point,
|
|
752
|
-
x: point.x + pitLane.serviceNormal.x * lateralOffset,
|
|
753
|
-
y: point.y + pitLane.serviceNormal.y * lateralOffset,
|
|
754
|
-
heading: pitLane.mainLane.heading,
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function pitMainLanePointAt(pitLane, distanceAlongLane, lateralOffset = 0) {
|
|
759
|
-
const amount = pitLane.mainLane.length > 0
|
|
760
|
-
? clamp(distanceAlongLane / pitLane.mainLane.length, 0, 1)
|
|
761
|
-
: 0;
|
|
762
|
-
const point = {
|
|
763
|
-
x: pitLane.mainLane.start.x + (pitLane.mainLane.end.x - pitLane.mainLane.start.x) * amount,
|
|
764
|
-
y: pitLane.mainLane.start.y + (pitLane.mainLane.end.y - pitLane.mainLane.start.y) * amount,
|
|
765
|
-
heading: pitLane.mainLane.heading,
|
|
766
|
-
};
|
|
767
|
-
return offsetPitLanePoint(pitLane, point, lateralOffset);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function pitDriveLaneOffset(pitLane) {
|
|
771
|
-
return pitLane.fastLane?.offset ?? 0;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function sameRoutePoint(first, second) {
|
|
775
|
-
return first && second && Math.hypot(first.x - second.x, first.y - second.y) < 0.001;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function shiftPreviousRenderPose(car, dx, dy, dheading = 0) {
|
|
779
|
-
car.previousX = Number.isFinite(car.previousX) ? car.previousX + dx : car.x + dx;
|
|
780
|
-
car.previousY = Number.isFinite(car.previousY) ? car.previousY + dy : car.y + dy;
|
|
781
|
-
car.previousHeading = normalizeAngle(
|
|
782
|
-
(Number.isFinite(car.previousHeading) ? car.previousHeading : car.heading) + dheading,
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function pointDistance(first, second) {
|
|
787
|
-
if (!first || !second) return Infinity;
|
|
788
|
-
return Math.hypot(first.x - second.x, first.y - second.y);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function createRoute(points) {
|
|
792
|
-
const routePoints = points
|
|
793
|
-
.filter(Boolean)
|
|
794
|
-
.map((point) => routePoint(point))
|
|
795
|
-
.filter(Boolean)
|
|
796
|
-
.reduce((deduped, point) => {
|
|
797
|
-
if (!sameRoutePoint(deduped.at(-1), point)) {
|
|
798
|
-
deduped.push(point);
|
|
799
|
-
} else if (point.limiterActive) {
|
|
800
|
-
deduped.at(-1).limiterActive = true;
|
|
801
|
-
}
|
|
802
|
-
return deduped;
|
|
803
|
-
}, []);
|
|
804
|
-
const segments = [];
|
|
805
|
-
let totalLength = 0;
|
|
806
|
-
|
|
807
|
-
for (let index = 0; index < routePoints.length - 1; index += 1) {
|
|
808
|
-
const start = routePoints[index];
|
|
809
|
-
const end = routePoints[index + 1];
|
|
810
|
-
const length = Math.hypot(end.x - start.x, end.y - start.y);
|
|
811
|
-
if (length <= 0.001) continue;
|
|
812
|
-
const heading = Math.atan2(end.y - start.y, end.x - start.x);
|
|
813
|
-
segments.push({
|
|
814
|
-
start,
|
|
815
|
-
end,
|
|
816
|
-
length,
|
|
817
|
-
heading,
|
|
818
|
-
startDistance: totalLength,
|
|
819
|
-
endDistance: totalLength + length,
|
|
820
|
-
limiterActive: Boolean(start.limiterActive && end.limiterActive),
|
|
821
|
-
});
|
|
822
|
-
totalLength += length;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
let nextLimiterStartDistance = Infinity;
|
|
826
|
-
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
827
|
-
const segment = segments[index];
|
|
828
|
-
if (segment.limiterActive) nextLimiterStartDistance = segment.startDistance;
|
|
829
|
-
segment.nextLimiterStartDistance = nextLimiterStartDistance;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
return {
|
|
833
|
-
points: routePoints,
|
|
834
|
-
segments,
|
|
835
|
-
length: totalLength,
|
|
836
|
-
segmentCursor: 0,
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function findRouteSegment(route, distanceAlong, endPadding = 0) {
|
|
841
|
-
if (!route?.segments?.length) return null;
|
|
842
|
-
const clampedDistance = clamp(distanceAlong, 0, route.length);
|
|
843
|
-
const segments = route.segments;
|
|
844
|
-
let index = clamp(Math.floor(route.segmentCursor ?? 0), 0, segments.length - 1);
|
|
845
|
-
let segment = segments[index];
|
|
846
|
-
|
|
847
|
-
if (
|
|
848
|
-
clampedDistance >= segment.startDistance - 0.001 &&
|
|
849
|
-
clampedDistance <= segment.endDistance - endPadding
|
|
850
|
-
) {
|
|
851
|
-
return segment;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
if (clampedDistance > segment.endDistance - endPadding) {
|
|
855
|
-
while (index < segments.length - 1 && clampedDistance > segments[index].endDistance - endPadding) {
|
|
856
|
-
index += 1;
|
|
857
|
-
}
|
|
858
|
-
} else {
|
|
859
|
-
while (index > 0 && clampedDistance < segments[index].startDistance - 0.001) {
|
|
860
|
-
index -= 1;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
segment = segments[index];
|
|
865
|
-
if (
|
|
866
|
-
clampedDistance < segment.startDistance - 0.001 ||
|
|
867
|
-
clampedDistance > segment.endDistance - endPadding
|
|
868
|
-
) {
|
|
869
|
-
segment = segments.find((candidate) => (
|
|
870
|
-
clampedDistance >= candidate.startDistance - 0.001 &&
|
|
871
|
-
clampedDistance <= candidate.endDistance - endPadding
|
|
872
|
-
)) ?? segments.at(-1);
|
|
873
|
-
index = segments.indexOf(segment);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
route.segmentCursor = Math.max(0, index);
|
|
877
|
-
return segment;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function sampleRoute(route, distanceAlong) {
|
|
881
|
-
if (!route?.segments?.length) return route?.points?.[0] ?? null;
|
|
882
|
-
const clampedDistance = clamp(distanceAlong, 0, route.length);
|
|
883
|
-
const segment = findRouteSegment(route, clampedDistance) ?? route.segments.at(-1);
|
|
884
|
-
const amount = clamp((clampedDistance - segment.startDistance) / segment.length, 0, 1);
|
|
885
|
-
|
|
886
|
-
return {
|
|
887
|
-
x: segment.start.x + (segment.end.x - segment.start.x) * amount,
|
|
888
|
-
y: segment.start.y + (segment.end.y - segment.start.y) * amount,
|
|
889
|
-
heading: segment.heading,
|
|
890
|
-
limiterActive: segment.limiterActive,
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function routeLimiterActiveAt(route, distanceAlong) {
|
|
895
|
-
if (!route?.segments?.length) return false;
|
|
896
|
-
const clampedDistance = clamp(distanceAlong, 0, route.length);
|
|
897
|
-
const segment = findRouteSegment(route, clampedDistance, 0.001) ?? route.segments.at(-1);
|
|
898
|
-
return Boolean(segment?.limiterActive);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function distanceToNextLimiterSegment(route, distanceAlong) {
|
|
902
|
-
if (!route?.segments?.length) return Infinity;
|
|
903
|
-
const clampedDistance = clamp(distanceAlong, 0, route.length);
|
|
904
|
-
const segment = findRouteSegment(route, clampedDistance, 0.001);
|
|
905
|
-
if (!segment) return Infinity;
|
|
906
|
-
if (segment.limiterActive && segment.endDistance > clampedDistance + 0.001) return 0;
|
|
907
|
-
return Math.max(0, segment.nextLimiterStartDistance - clampedDistance);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
function easeInOut(value) {
|
|
911
|
-
const amount = clamp(value, 0, 1);
|
|
912
|
-
return amount * amount * (3 - 2 * amount);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function projectPositionToSegment(position, segment) {
|
|
916
|
-
const dx = segment.end.x - segment.start.x;
|
|
917
|
-
const dy = segment.end.y - segment.start.y;
|
|
918
|
-
const lengthSquared = dx * dx + dy * dy;
|
|
919
|
-
const amount = lengthSquared > 0
|
|
920
|
-
? clamp(((position.x - segment.start.x) * dx + (position.y - segment.start.y) * dy) / lengthSquared, 0, 1)
|
|
921
|
-
: 0;
|
|
922
|
-
const x = segment.start.x + dx * amount;
|
|
923
|
-
const y = segment.start.y + dy * amount;
|
|
924
|
-
const distanceSquared = (position.x - x) ** 2 + (position.y - y) ** 2;
|
|
925
|
-
|
|
926
|
-
return {
|
|
927
|
-
distanceAlong: segment.startDistance + segment.length * amount,
|
|
928
|
-
distanceSquared,
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function nearestDistanceOnRoute(route, position, previousDistance = 0) {
|
|
933
|
-
if (!route?.segments?.length) return 0;
|
|
934
|
-
let best = null;
|
|
935
|
-
const backtrackAllowance = metersToSimUnits(90);
|
|
936
|
-
|
|
937
|
-
route.segments.forEach((segment) => {
|
|
938
|
-
if (segment.endDistance < previousDistance - backtrackAllowance) return;
|
|
939
|
-
const projected = projectPositionToSegment(position, segment);
|
|
940
|
-
if (!best || projected.distanceSquared < best.distanceSquared) best = projected;
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
return clamp(best?.distanceAlong ?? previousDistance, 0, route.length);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function createPitApproachPoints(track, car, pitLane, entryRaceDistance) {
|
|
947
|
-
const currentDistance = car.raceDistance ?? entryRaceDistance - PIT_ENTRY_APPROACH_DISTANCE;
|
|
948
|
-
const remaining = Math.max(0, entryRaceDistance - currentDistance);
|
|
949
|
-
const currentState = nearestTrackState(track, car, car.progress);
|
|
950
|
-
const startOffset = currentState.inPitLane ? 0 : currentState.signedOffset ?? 0;
|
|
951
|
-
const entryConnectState = nearestTrackState(track, pitLane.entry.trackConnectPoint, pitLane.entry.trackDistance);
|
|
952
|
-
const targetOffset = entryConnectState.signedOffset ?? 0;
|
|
953
|
-
const points = [routePoint(car, car.heading)];
|
|
954
|
-
const straightDistance = remaining > metersToSimUnits(14)
|
|
955
|
-
? Math.min(remaining * 0.22, metersToSimUnits(42))
|
|
956
|
-
: 0;
|
|
957
|
-
if (straightDistance > metersToSimUnits(1)) {
|
|
958
|
-
points.push(routePoint({
|
|
959
|
-
x: car.x + Math.cos(car.heading) * straightDistance,
|
|
960
|
-
y: car.y + Math.sin(car.heading) * straightDistance,
|
|
961
|
-
}, car.heading));
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
const transitionDistance = Math.max(0, remaining - straightDistance);
|
|
965
|
-
const steps = Math.max(4, Math.ceil(Math.max(transitionDistance, PIT_ENTRY_APPROACH_DISTANCE * 0.45) / metersToSimUnits(74)));
|
|
966
|
-
for (let index = 1; index <= steps; index += 1) {
|
|
967
|
-
const amount = index / steps;
|
|
968
|
-
const distance = currentDistance + straightDistance + transitionDistance * amount;
|
|
969
|
-
const trackPoint = pointAt(track, distance);
|
|
970
|
-
const lateralAmount = easeInOut(amount);
|
|
971
|
-
const offset = startOffset + (targetOffset - startOffset) * lateralAmount;
|
|
972
|
-
const point = offsetTrackPoint(trackPoint, offset);
|
|
973
|
-
points.push(routePoint(point, trackPoint.heading));
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
points.push(routePoint(pitLane.entry.trackConnectPoint, pitLane.entry.trackPoint?.heading));
|
|
977
|
-
return points;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function firstDifferentCompound(currentTire, compounds) {
|
|
981
|
-
const available = Array.isArray(compounds) && compounds.length ? compounds : ['S', 'M', 'H'];
|
|
982
|
-
return available.find((compound) => compound !== currentTire) ?? currentTire;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function normalizePitCompound(value, compounds) {
|
|
986
|
-
if (value == null || value === '') return null;
|
|
987
|
-
const available = Array.isArray(compounds) && compounds.length ? compounds : ['S', 'M', 'H'];
|
|
988
|
-
return available.includes(value) ? value : null;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function normalizePitIntent(value) {
|
|
992
|
-
const number = Number(value);
|
|
993
|
-
if (!Number.isInteger(number) || number < PIT_INTENT_NONE || number > PIT_INTENT_COMMITTED) return null;
|
|
994
|
-
return number;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function normalizePitCrewStats(value) {
|
|
998
|
-
const source = value && typeof value === 'object' ? value : {};
|
|
999
|
-
const rating = (entry) => {
|
|
1000
|
-
const numeric = Number(entry);
|
|
1001
|
-
return Number.isFinite(numeric) ? clamp(numeric, 0, 1) : 0.5;
|
|
1002
|
-
};
|
|
1003
|
-
return {
|
|
1004
|
-
speed: rating(source.speed ?? source.pace),
|
|
1005
|
-
consistency: rating(source.consistency),
|
|
1006
|
-
reliability: rating(source.reliability),
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function pitLaneStatusSnapshot(raceControl, pitLane, pitStops) {
|
|
1011
|
-
const enabled = Boolean(pitLane?.enabled && pitStops?.enabled);
|
|
1012
|
-
const redFlag = Boolean(raceControl.redFlag);
|
|
1013
|
-
const open = enabled && Boolean(raceControl.pitLaneOpen) && !redFlag;
|
|
1014
|
-
return {
|
|
1015
|
-
enabled,
|
|
1016
|
-
open,
|
|
1017
|
-
reason: !enabled ? 'unavailable' : redFlag ? 'red-flag' : open ? 'open' : 'closed',
|
|
1018
|
-
color: open ? 'green' : redFlag ? 'yellow' : 'red',
|
|
1019
|
-
light: open ? '#22c55e' : redFlag ? '#facc15' : '#ef4444',
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function serializePitStop(pitStop) {
|
|
1024
|
-
if (!pitStop) return null;
|
|
1025
|
-
return {
|
|
1026
|
-
status: pitStop.status,
|
|
1027
|
-
intent: normalizePitIntent(pitStop.intent) ?? PIT_INTENT_NONE,
|
|
1028
|
-
phase: pitStop.phase ?? null,
|
|
1029
|
-
boxIndex: pitStop.boxIndex,
|
|
1030
|
-
boxId: pitStop.boxId,
|
|
1031
|
-
garageBoxIndex: pitStop.garageBoxIndex ?? null,
|
|
1032
|
-
garageBoxId: pitStop.garageBoxId ?? null,
|
|
1033
|
-
teamId: pitStop.teamId ?? null,
|
|
1034
|
-
teamColor: pitStop.teamColor ?? null,
|
|
1035
|
-
stopsCompleted: pitStop.stopsCompleted ?? 0,
|
|
1036
|
-
queueingForService: Boolean(pitStop.queueingForService),
|
|
1037
|
-
plannedRaceDistance: finiteOrNull(pitStop.plannedRaceDistance),
|
|
1038
|
-
entryRaceDistance: finiteOrNull(pitStop.entryRaceDistance),
|
|
1039
|
-
serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
|
|
1040
|
-
penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
|
|
1041
|
-
penaltyServiceTotalSeconds: finiteOrNull(pitStop.penaltyServiceTotal),
|
|
1042
|
-
servingPenaltyIds: [...(pitStop.servingPenaltyIds ?? [])],
|
|
1043
|
-
targetTire: pitStop.targetTire ?? null,
|
|
1044
|
-
serviceProfile: pitStop.serviceProfile ? { ...pitStop.serviceProfile } : null,
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function serializeWheels(wheels = []) {
|
|
1049
|
-
return wheels.map((wheel) => ({
|
|
1050
|
-
id: wheel.id,
|
|
1051
|
-
x: wheel.x,
|
|
1052
|
-
y: wheel.y,
|
|
1053
|
-
signedOffset: wheel.signedOffset,
|
|
1054
|
-
crossTrackError: wheel.crossTrackError,
|
|
1055
|
-
surface: wheel.surface,
|
|
1056
|
-
onTrack: Boolean(wheel.onTrack),
|
|
1057
|
-
inPitLane: Boolean(wheel.inPitLane),
|
|
1058
|
-
fullyOutsideWhiteLine: Boolean(wheel.fullyOutsideWhiteLine),
|
|
1059
|
-
}));
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function serializeCar(car, rank, penaltySeconds = 0) {
|
|
1063
|
-
const finishTime = car.finishTime ?? null;
|
|
1064
|
-
const raceStatus = car.finished ? 'waved-flag' : 'racing';
|
|
1065
|
-
return {
|
|
1066
|
-
id: car.id,
|
|
1067
|
-
code: car.code,
|
|
1068
|
-
timingCode: car.timingCode,
|
|
1069
|
-
driverNumber: car.driverNumber,
|
|
1070
|
-
icon: car.icon,
|
|
1071
|
-
raceName: car.raceName,
|
|
1072
|
-
name: car.name,
|
|
1073
|
-
color: car.color,
|
|
1074
|
-
team: car.team ? { ...car.team } : null,
|
|
1075
|
-
tire: car.tire,
|
|
1076
|
-
personality: { ...car.personality },
|
|
1077
|
-
aggression: car.aggression,
|
|
1078
|
-
aggressionPercent: Math.round((car.aggression ?? 0) * 100),
|
|
1079
|
-
setup: {
|
|
1080
|
-
vehicleId: car.vehicleId,
|
|
1081
|
-
vehicleName: car.vehicleName,
|
|
1082
|
-
vehicleRatings: car.vehicleRatings ? { ...car.vehicleRatings } : null,
|
|
1083
|
-
maxSpeedKph: simSpeedToKph(VEHICLE_LIMITS.maxSpeed),
|
|
1084
|
-
powerUnitKn: car.powerNewtons / 1000,
|
|
1085
|
-
brakeSystemKn: car.brakeNewtons / 1000,
|
|
1086
|
-
dragCoefficient: car.dragCoefficient,
|
|
1087
|
-
downforceCoefficient: car.downforceCoefficient,
|
|
1088
|
-
tireGrip: car.tireGrip,
|
|
1089
|
-
tireCare: car.tireCare,
|
|
1090
|
-
massKg: car.mass,
|
|
1091
|
-
pace: car.pace,
|
|
1092
|
-
racecraft: car.racecraft,
|
|
1093
|
-
},
|
|
1094
|
-
rank,
|
|
1095
|
-
classifiedRank: car.classifiedRank ?? rank,
|
|
1096
|
-
finishRank: car.finishRank ?? null,
|
|
1097
|
-
status: raceStatus,
|
|
1098
|
-
raceStatus,
|
|
1099
|
-
wavedFlag: Boolean(car.finished),
|
|
1100
|
-
finished: Boolean(car.finished),
|
|
1101
|
-
finishTime,
|
|
1102
|
-
penaltySeconds,
|
|
1103
|
-
adjustedFinishTime: finishTime == null ? null : finishTime + penaltySeconds,
|
|
1104
|
-
previousX: car.previousX ?? car.x,
|
|
1105
|
-
previousY: car.previousY ?? car.y,
|
|
1106
|
-
x: car.x,
|
|
1107
|
-
y: car.y,
|
|
1108
|
-
previousHeading: car.previousHeading ?? car.heading,
|
|
1109
|
-
heading: car.heading,
|
|
1110
|
-
steeringAngle: car.steeringAngle,
|
|
1111
|
-
yawRate: car.yawRate,
|
|
1112
|
-
turnRadius: car.turnRadius,
|
|
1113
|
-
speed: car.speed,
|
|
1114
|
-
speedKph: simSpeedToKph(car.speed),
|
|
1115
|
-
throttle: car.throttle,
|
|
1116
|
-
brake: car.brake,
|
|
1117
|
-
lateralAcceleration: car.lateralAcceleration,
|
|
1118
|
-
progress: car.progress,
|
|
1119
|
-
raceDistance: car.raceDistance,
|
|
1120
|
-
distanceMeters: simUnitsToMeters(car.raceDistance),
|
|
1121
|
-
lap: car.lap,
|
|
1122
|
-
lapTelemetry: serializeLapTelemetry(car.lapTelemetry ?? createLapTelemetry({ length: 1 }, 0, 0)),
|
|
1123
|
-
gapAhead: car.gapAhead,
|
|
1124
|
-
gapAheadMeters: Number.isFinite(car.gapAhead) ? simUnitsToMeters(car.gapAhead) : Infinity,
|
|
1125
|
-
gapAheadSeconds: car.gapAheadSeconds,
|
|
1126
|
-
intervalAheadSeconds: car.intervalAheadSeconds,
|
|
1127
|
-
leaderGapSeconds: car.leaderGapSeconds,
|
|
1128
|
-
gapAheadLaps: car.gapAheadLaps ?? 0,
|
|
1129
|
-
intervalAheadLaps: car.intervalAheadLaps ?? car.gapAheadLaps ?? 0,
|
|
1130
|
-
leaderGapLaps: car.leaderGapLaps ?? 0,
|
|
1131
|
-
drsEligible: car.drsEligible,
|
|
1132
|
-
drsActive: car.drsActive,
|
|
1133
|
-
drsZoneId: car.drsZoneId,
|
|
1134
|
-
drsZoneEnabled: car.drsZoneEnabled,
|
|
1135
|
-
canAttack: car.canAttack,
|
|
1136
|
-
signedOffset: car.trackState?.signedOffset ?? 0,
|
|
1137
|
-
crossTrackError: car.trackState?.crossTrackError ?? 0,
|
|
1138
|
-
surface: car.trackState?.surface ?? 'track',
|
|
1139
|
-
wheels: serializeWheels(car.wheelStates),
|
|
1140
|
-
inPitLane: Boolean(car.trackState?.inPitLane),
|
|
1141
|
-
pitLanePart: car.trackState?.pitLanePart ?? null,
|
|
1142
|
-
pitBoxId: car.trackState?.pitBoxId ?? null,
|
|
1143
|
-
pitLaneCrossTrackError: car.trackState?.pitLaneCrossTrackError ?? null,
|
|
1144
|
-
contactCooldown: car.contactCooldown,
|
|
1145
|
-
tireEnergy: car.tireEnergy,
|
|
1146
|
-
usedTireCompounds: [...(car.usedTireCompounds ?? [])],
|
|
1147
|
-
pitIntent: normalizePitIntent(car.pitStop?.intent) ?? PIT_INTENT_NONE,
|
|
1148
|
-
pitStop: serializePitStop(car.pitStop),
|
|
1149
|
-
positionSource: 'integrated-vehicle',
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
function serializeRenderPitStop(pitStop) {
|
|
1154
|
-
if (!pitStop) return null;
|
|
1155
|
-
return {
|
|
1156
|
-
phase: pitStop.phase ?? null,
|
|
1157
|
-
serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
|
|
1158
|
-
penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function serializeRenderCar(car) {
|
|
1163
|
-
return {
|
|
1164
|
-
id: car.id,
|
|
1165
|
-
color: car.color,
|
|
1166
|
-
previousX: car.previousX ?? car.x,
|
|
1167
|
-
previousY: car.previousY ?? car.y,
|
|
1168
|
-
x: car.x,
|
|
1169
|
-
y: car.y,
|
|
1170
|
-
previousHeading: car.previousHeading ?? car.heading,
|
|
1171
|
-
heading: car.heading,
|
|
1172
|
-
drsActive: Boolean(car.drsActive),
|
|
1173
|
-
pitStop: serializeRenderPitStop(car.pitStop),
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
function serializeObservationPitStop(pitStop) {
|
|
1178
|
-
if (!pitStop) return null;
|
|
1179
|
-
return {
|
|
1180
|
-
status: pitStop.status,
|
|
1181
|
-
intent: normalizePitIntent(pitStop.intent) ?? PIT_INTENT_NONE,
|
|
1182
|
-
phase: pitStop.phase ?? null,
|
|
1183
|
-
targetTire: pitStop.targetTire ?? null,
|
|
1184
|
-
serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
|
|
1185
|
-
penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
|
|
1186
|
-
stopsCompleted: pitStop.stopsCompleted ?? 0,
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function serializeObservationCar(car, rank) {
|
|
1191
|
-
return {
|
|
1192
|
-
id: car.id,
|
|
1193
|
-
rank,
|
|
1194
|
-
previousX: car.previousX ?? car.x,
|
|
1195
|
-
previousY: car.previousY ?? car.y,
|
|
1196
|
-
x: car.x,
|
|
1197
|
-
y: car.y,
|
|
1198
|
-
previousHeading: car.previousHeading ?? car.heading,
|
|
1199
|
-
heading: car.heading,
|
|
1200
|
-
steeringAngle: car.steeringAngle,
|
|
1201
|
-
yawRate: car.yawRate,
|
|
1202
|
-
speed: car.speed,
|
|
1203
|
-
speedKph: simSpeedToKph(car.speed),
|
|
1204
|
-
throttle: car.throttle,
|
|
1205
|
-
brake: car.brake,
|
|
1206
|
-
progress: car.progress,
|
|
1207
|
-
raceDistance: car.raceDistance,
|
|
1208
|
-
lap: car.lap,
|
|
1209
|
-
lapTelemetry: serializeLapTelemetry(car.lapTelemetry ?? createLapTelemetry({ length: 1 }, 0, 0)),
|
|
1210
|
-
signedOffset: car.trackState?.signedOffset ?? 0,
|
|
1211
|
-
crossTrackError: car.trackState?.crossTrackError ?? 0,
|
|
1212
|
-
surface: car.trackState?.surface ?? 'track',
|
|
1213
|
-
trackHeadingError: car.trackHeadingError,
|
|
1214
|
-
trackState: car.trackState ? {
|
|
1215
|
-
distance: car.trackState.distance,
|
|
1216
|
-
signedOffset: car.trackState.signedOffset,
|
|
1217
|
-
crossTrackError: car.trackState.crossTrackError,
|
|
1218
|
-
surface: car.trackState.surface,
|
|
1219
|
-
inPitLane: Boolean(car.trackState.inPitLane),
|
|
1220
|
-
pitLanePart: car.trackState.pitLanePart ?? null,
|
|
1221
|
-
pitBoxId: car.trackState.pitBoxId ?? null,
|
|
1222
|
-
curvature: car.trackState.curvature ?? 0,
|
|
1223
|
-
} : null,
|
|
1224
|
-
wheels: serializeWheels(car.wheelStates),
|
|
1225
|
-
inPitLane: Boolean(car.trackState?.inPitLane),
|
|
1226
|
-
pitLanePart: car.trackState?.pitLanePart ?? null,
|
|
1227
|
-
pitBoxId: car.trackState?.pitBoxId ?? null,
|
|
1228
|
-
tireEnergy: car.tireEnergy ?? null,
|
|
1229
|
-
pitIntent: normalizePitIntent(car.pitStop?.intent) ?? PIT_INTENT_NONE,
|
|
1230
|
-
pitStop: serializeObservationPitStop(car.pitStop),
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
function createEmptyPenaltyStats() {
|
|
1235
|
-
return {
|
|
1236
|
-
seconds: 0,
|
|
1237
|
-
positionDrop: 0,
|
|
1238
|
-
disqualified: false,
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
function getPenaltyStats(statsByDriver, driverId) {
|
|
1243
|
-
return statsByDriver.get(driverId) ?? createEmptyPenaltyStats();
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
function buildPenaltyStatsByDriver(penalties = []) {
|
|
1247
|
-
const byDriver = new Map();
|
|
1248
|
-
penalties.forEach((penalty) => {
|
|
1249
|
-
if (!penalty?.driverId || !isPenaltyActive(penalty)) return;
|
|
1250
|
-
const stats = byDriver.get(penalty.driverId) ?? createEmptyPenaltyStats();
|
|
1251
|
-
stats.seconds += Number(penalty.penaltySeconds) || 0;
|
|
1252
|
-
stats.positionDrop += Number(penalty.positionDrop) || 0;
|
|
1253
|
-
stats.disqualified = stats.disqualified || Boolean(penalty.disqualified);
|
|
1254
|
-
byDriver.set(penalty.driverId, stats);
|
|
1255
|
-
});
|
|
1256
|
-
return byDriver;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
function isLegallyInsidePitLaneForTrackLimits(car) {
|
|
1260
|
-
if (!car.trackState?.inPitLane) return false;
|
|
1261
|
-
const wheels = car.wheelStates ?? [];
|
|
1262
|
-
return wheels.length > 0 && wheels.some((wheel) => wheel.inPitLane);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
113
|
export class F1RaceSimulation {
|
|
1266
|
-
constructor(
|
|
1267
|
-
this
|
|
1268
|
-
this.random = createMulberry32(seed);
|
|
1269
|
-
const trackDefinition = track ?? (trackSeed == null ? TRACK : createProceduralTrack(trackSeed));
|
|
1270
|
-
const builtTrack = buildTrackModel(trackDefinition);
|
|
1271
|
-
this.track = {
|
|
1272
|
-
...builtTrack,
|
|
1273
|
-
pitLane: clonePitLaneModel(builtTrack.pitLane),
|
|
1274
|
-
};
|
|
1275
|
-
this.track.timingLines = createTimingLines(this.track);
|
|
1276
|
-
this.trackSeed = this.track.seed ?? trackSeed;
|
|
1277
|
-
this.rules = normalizeRaceRules(rules);
|
|
1278
|
-
this.startLightsOutAt = this.rules.startLightCount * this.rules.startLightInterval + this.rules.startLightsOutHold;
|
|
1279
|
-
this.totalLaps = normalizeTotalLaps(totalLaps);
|
|
1280
|
-
this.time = 0;
|
|
1281
|
-
this.events = [];
|
|
1282
|
-
this.penalties = [];
|
|
1283
|
-
this.nextPenaltyId = 1;
|
|
1284
|
-
this.stewardState = {
|
|
1285
|
-
trackLimits: Object.create(null),
|
|
1286
|
-
pitLaneSpeeding: Object.create(null),
|
|
1287
|
-
tireRequirement: Object.create(null),
|
|
1288
|
-
};
|
|
1289
|
-
this.raceControl = {
|
|
1290
|
-
mode: this.rules.standingStart === false ? 'green' : 'pre-start',
|
|
1291
|
-
frozenOrder: null,
|
|
1292
|
-
redFlag: false,
|
|
1293
|
-
pitLaneOpen: true,
|
|
1294
|
-
finished: false,
|
|
1295
|
-
finishedAt: null,
|
|
1296
|
-
winnerId: null,
|
|
1297
|
-
classification: [],
|
|
1298
|
-
finishOrder: [],
|
|
1299
|
-
start: {
|
|
1300
|
-
lightCount: this.rules.startLightCount,
|
|
1301
|
-
lightsLit: 0,
|
|
1302
|
-
lightsOutAt: this.startLightsOutAt,
|
|
1303
|
-
released: this.rules.standingStart === false,
|
|
1304
|
-
releasedAt: this.rules.standingStart === false ? 0 : null,
|
|
1305
|
-
},
|
|
1306
|
-
};
|
|
1307
|
-
const safetyCarStart = pointAt(this.track, this.rules.safetyCarLeadDistance);
|
|
1308
|
-
this.safetyCar = {
|
|
1309
|
-
deployed: false,
|
|
1310
|
-
progress: this.rules.safetyCarLeadDistance,
|
|
1311
|
-
speed: this.rules.safetyCarSpeed,
|
|
1312
|
-
previousX: safetyCarStart.x,
|
|
1313
|
-
previousY: safetyCarStart.y,
|
|
1314
|
-
previousHeading: safetyCarStart.heading,
|
|
1315
|
-
x: safetyCarStart.x,
|
|
1316
|
-
y: safetyCarStart.y,
|
|
1317
|
-
heading: safetyCarStart.heading,
|
|
1318
|
-
};
|
|
1319
|
-
this.cars = drivers.map((driver, index) => createCar(driver, index, this.random, this.track, {
|
|
1320
|
-
standingStart: this.raceControl.mode === 'pre-start',
|
|
1321
|
-
}));
|
|
1322
|
-
this.assignPitLaneTeams();
|
|
1323
|
-
this.initializePitStops();
|
|
1324
|
-
this.recalculateRaceState({ updateDrs: false });
|
|
1325
|
-
this.cars.forEach((car) => resetTimingHistory(car, this.time));
|
|
1326
|
-
this.cars.forEach((car) => resetTimingLineCrossings(car, this.time));
|
|
1327
|
-
this.cars.forEach((car) => resetLapTelemetry(car, this.time, this.track, this.totalLaps));
|
|
114
|
+
constructor(options = {}) {
|
|
115
|
+
initializeRaceSimulation(this, options);
|
|
1328
116
|
}
|
|
1329
117
|
|
|
1330
118
|
assignPitLaneTeams() {
|
|
1331
|
-
|
|
1332
|
-
if (!pitLane?.enabled || !Array.isArray(pitLane.boxes)) return;
|
|
1333
|
-
const boxesPerTeam = pitLane.boxesPerTeam ?? 2;
|
|
1334
|
-
const teamCount = pitLane.teamCount ?? Math.ceil(pitLane.boxes.length / boxesPerTeam);
|
|
1335
|
-
|
|
1336
|
-
pitLane.teams = Array.from({ length: teamCount }, (_, teamIndex) => {
|
|
1337
|
-
const primary = this.cars[teamIndex * boxesPerTeam] ?? this.cars[teamIndex] ?? null;
|
|
1338
|
-
const secondary = this.cars[teamIndex * boxesPerTeam + 1] ?? null;
|
|
1339
|
-
const team = primary?.team ?? secondary?.team ?? null;
|
|
1340
|
-
const id = team?.id ?? `team-${teamIndex + 1}`;
|
|
1341
|
-
const name = team?.name ?? (primary ? `${primary.name} Team` : `Team ${teamIndex + 1}`);
|
|
1342
|
-
const color = team?.color ?? primary?.color ?? secondary?.color ?? '#f8fafc';
|
|
1343
|
-
const pitCrew = normalizePitCrewStats(
|
|
1344
|
-
team?.pitCrew ?? team?.pitCrewStats ?? primary?.team?.pitCrew ?? secondary?.team?.pitCrew,
|
|
1345
|
-
);
|
|
1346
|
-
const boxIds = pitLane.boxes
|
|
1347
|
-
.filter((box) => box.teamIndex === teamIndex)
|
|
1348
|
-
.map((box) => box.id);
|
|
1349
|
-
|
|
1350
|
-
pitLane.boxes
|
|
1351
|
-
.filter((box) => box.teamIndex === teamIndex)
|
|
1352
|
-
.forEach((box) => {
|
|
1353
|
-
box.teamId = id;
|
|
1354
|
-
box.teamName = name;
|
|
1355
|
-
box.teamColor = color;
|
|
1356
|
-
});
|
|
1357
|
-
const serviceArea = pitLane.serviceAreas?.[teamIndex] ?? null;
|
|
1358
|
-
if (serviceArea) {
|
|
1359
|
-
serviceArea.teamId = id;
|
|
1360
|
-
serviceArea.teamName = name;
|
|
1361
|
-
serviceArea.teamColor = color;
|
|
1362
|
-
serviceArea.pitCrew = pitCrew;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
return {
|
|
1366
|
-
id,
|
|
1367
|
-
name,
|
|
1368
|
-
color,
|
|
1369
|
-
index: teamIndex,
|
|
1370
|
-
boxIds,
|
|
1371
|
-
serviceAreaId: serviceArea?.id ?? null,
|
|
1372
|
-
pitCrew,
|
|
1373
|
-
};
|
|
1374
|
-
});
|
|
119
|
+
assignPitLaneTeamsState({ cars: this.cars, pitLane: this.track.pitLane });
|
|
1375
120
|
}
|
|
1376
121
|
|
|
1377
122
|
initializePitStops() {
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
this.
|
|
1385
|
-
|
|
1386
|
-
));
|
|
1387
|
-
|
|
1388
|
-
this.cars.forEach((car, index) => {
|
|
1389
|
-
const stopLapBase = this.track.length * (1 + (index % pitWindowLapCount));
|
|
1390
|
-
const trainPosition = Math.floor(index / pitWindowLapCount);
|
|
1391
|
-
const teamIndex = Math.floor(index / boxesPerTeam);
|
|
1392
|
-
const teamBoxIndex = index % boxesPerTeam;
|
|
1393
|
-
const garageBoxIndex = Math.min(pitLane.boxes.length - 1, teamIndex * boxesPerTeam + teamBoxIndex);
|
|
1394
|
-
const garageBox = pitLane.boxes[garageBoxIndex] ?? pitLane.boxes[index % pitLane.boxes.length];
|
|
1395
|
-
const serviceArea = pitLane.serviceAreas?.[teamIndex] ??
|
|
1396
|
-
pitLane.serviceAreas?.[index % pitLane.serviceAreas.length] ??
|
|
1397
|
-
garageBox;
|
|
1398
|
-
car.pitStop = {
|
|
1399
|
-
status: 'pending',
|
|
1400
|
-
phase: null,
|
|
1401
|
-
boxIndex: serviceArea.index,
|
|
1402
|
-
boxId: serviceArea.id,
|
|
1403
|
-
garageBoxIndex,
|
|
1404
|
-
garageBoxId: garageBox.id,
|
|
1405
|
-
teamId: serviceArea.teamId ?? garageBox.teamId ?? null,
|
|
1406
|
-
teamColor: serviceArea.teamColor ?? garageBox.teamColor ?? null,
|
|
1407
|
-
stopsCompleted: 0,
|
|
1408
|
-
entryRaceDistance: stopLapBase + pitLane.entry.distanceFromStart,
|
|
1409
|
-
plannedRaceDistance: stopLapBase + pitLane.entry.distanceFromStart -
|
|
1410
|
-
PIT_ENTRY_APPROACH_DISTANCE,
|
|
1411
|
-
trainPosition,
|
|
1412
|
-
lapBase: stopLapBase,
|
|
1413
|
-
serviceRemaining: 0,
|
|
1414
|
-
penaltyServiceRemaining: 0,
|
|
1415
|
-
penaltyServiceTotal: 0,
|
|
1416
|
-
servingPenaltyIds: [],
|
|
1417
|
-
serviceProfile: null,
|
|
1418
|
-
queueingForService: false,
|
|
1419
|
-
route: null,
|
|
1420
|
-
routeProgress: 0,
|
|
1421
|
-
routeStartRaceDistance: null,
|
|
1422
|
-
routeEndRaceDistance: null,
|
|
1423
|
-
targetTire: firstDifferentCompound(car.tire, this.rules.modules?.tireStrategy?.compounds),
|
|
1424
|
-
intent: PIT_INTENT_NONE,
|
|
1425
|
-
};
|
|
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,
|
|
1426
131
|
});
|
|
1427
132
|
}
|
|
1428
133
|
|
|
1429
134
|
setSafetyCar(deployed) {
|
|
1430
|
-
|
|
1431
|
-
if (this.raceControl.finished) return;
|
|
1432
|
-
if (next && this.raceControl.mode === 'pre-start') return;
|
|
1433
|
-
if (this.raceControl.redFlag) return;
|
|
1434
|
-
if (next === this.safetyCar.deployed) return;
|
|
1435
|
-
const ordered = this.orderedCars();
|
|
1436
|
-
this.safetyCar.deployed = next;
|
|
1437
|
-
this.raceControl.mode = next ? 'safety-car' : 'green';
|
|
1438
|
-
this.raceControl.frozenOrder = next ? ordered.map((car) => car.id) : null;
|
|
1439
|
-
if (next) {
|
|
1440
|
-
const leader = ordered[0];
|
|
1441
|
-
const safetyCarProgress = (leader?.raceDistance ?? 0) + this.rules.safetyCarLeadDistance;
|
|
1442
|
-
if (this.safetyCar.progress < safetyCarProgress) {
|
|
1443
|
-
this.moveSafetyCarTo(safetyCarProgress);
|
|
1444
|
-
}
|
|
1445
|
-
this.cars.forEach((car) => {
|
|
1446
|
-
car.desiredOffset = 0;
|
|
1447
|
-
car.drsActive = false;
|
|
1448
|
-
car.drsEligible = false;
|
|
1449
|
-
car.drsZoneId = null;
|
|
1450
|
-
car.drsZoneEnabled = false;
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
this.events.unshift({ type: next ? 'safety-car' : 'green-flag', at: this.time });
|
|
135
|
+
setSafetyCarState(this, deployed);
|
|
1454
136
|
}
|
|
1455
137
|
|
|
1456
138
|
setRedFlag(deployed) {
|
|
1457
|
-
|
|
1458
|
-
if (this.raceControl.finished) return;
|
|
1459
|
-
if (next && this.raceControl.mode === 'pre-start') return;
|
|
1460
|
-
if (next === Boolean(this.raceControl.redFlag)) return;
|
|
1461
|
-
this.raceControl.redFlag = next;
|
|
1462
|
-
if (next) {
|
|
1463
|
-
this.safetyCar.deployed = false;
|
|
1464
|
-
this.raceControl.mode = 'red-flag';
|
|
1465
|
-
this.raceControl.frozenOrder = this.orderedCars().map((car) => car.id);
|
|
1466
|
-
this.cars.forEach((car) => {
|
|
1467
|
-
car.speed = 0;
|
|
1468
|
-
car.throttle = 0;
|
|
1469
|
-
car.brake = 1;
|
|
1470
|
-
car.drsActive = false;
|
|
1471
|
-
car.drsEligible = false;
|
|
1472
|
-
car.drsZoneId = null;
|
|
1473
|
-
car.drsZoneEnabled = false;
|
|
1474
|
-
car.canAttack = false;
|
|
1475
|
-
});
|
|
1476
|
-
} else {
|
|
1477
|
-
this.raceControl.mode = this.safetyCar.deployed ? 'safety-car' : 'green';
|
|
1478
|
-
this.raceControl.frozenOrder = this.safetyCar.deployed
|
|
1479
|
-
? this.orderedCars().map((car) => car.id)
|
|
1480
|
-
: null;
|
|
1481
|
-
this.cars.forEach((car) => {
|
|
1482
|
-
if (!car.finished) {
|
|
1483
|
-
car.speed = Math.max(car.speed, kphToSimSpeed(60));
|
|
1484
|
-
car.brake = 0;
|
|
1485
|
-
car.throttle = Math.max(car.throttle ?? 0, 0.35);
|
|
1486
|
-
}
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
this.events.unshift({ type: next ? 'red-flag' : 'green-flag', at: this.time });
|
|
139
|
+
setRedFlagState(this, deployed);
|
|
1490
140
|
}
|
|
1491
141
|
|
|
1492
142
|
setPitLaneOpen(open) {
|
|
1493
|
-
|
|
1494
|
-
if (next === Boolean(this.raceControl.pitLaneOpen)) return;
|
|
1495
|
-
this.raceControl.pitLaneOpen = next;
|
|
1496
|
-
this.events.unshift({ type: next ? 'pit-lane-open' : 'pit-lane-closed', at: this.time });
|
|
143
|
+
setPitLaneOpenState(this, open);
|
|
1497
144
|
}
|
|
1498
145
|
|
|
1499
146
|
setCarState(id, partial) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
car
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1528
|
-
if (!hasExplicitRaceDistance) {
|
|
1529
|
-
const delta = progressDelta(car.trackState.distance, car.progress ?? car.trackState.distance, this.track.length);
|
|
1530
|
-
car.raceDistance = (car.raceDistance ?? car.trackState.distance) + delta;
|
|
1531
|
-
}
|
|
1532
|
-
car.progress = car.trackState.distance;
|
|
1533
|
-
car.lap = this.computeLap(car.raceDistance);
|
|
1534
|
-
car.drsZoneId = null;
|
|
1535
|
-
car.drsZoneEnabled = false;
|
|
1536
|
-
car.drsActive = false;
|
|
1537
|
-
car.drsEligible = false;
|
|
1538
|
-
car.drsDetection = {};
|
|
1539
|
-
resetTimingHistory(car, this.time);
|
|
1540
|
-
resetTimingLineCrossings(car, this.time);
|
|
1541
|
-
resetLapTelemetry(car, this.time, this.track, this.totalLaps);
|
|
147
|
+
this.setCarStates({ [id]: partial });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
setCarStates(partialsById = {}) {
|
|
151
|
+
const context = {
|
|
152
|
+
cars: this.cars,
|
|
153
|
+
computeLap: (raceDistance) => this.computeLap(raceDistance),
|
|
154
|
+
nearestDistanceOnRoute,
|
|
155
|
+
physicsMode: this.physicsMode,
|
|
156
|
+
progressDelta,
|
|
157
|
+
raceControl: this.raceControl,
|
|
158
|
+
releaseRaceStart: () => this.releaseRaceStart(),
|
|
159
|
+
resetLapTelemetry,
|
|
160
|
+
resetTimingHistory,
|
|
161
|
+
resetTimingLineCrossings,
|
|
162
|
+
time: this.time,
|
|
163
|
+
totalLaps: this.totalLaps,
|
|
164
|
+
track: this.track,
|
|
165
|
+
};
|
|
166
|
+
let applied = false;
|
|
167
|
+
Object.entries(partialsById).forEach(([id, partial]) => {
|
|
168
|
+
const car = this.cars.find((item) => item.id === id);
|
|
169
|
+
if (!car) return;
|
|
170
|
+
applyExternalCarState(car, partial, context);
|
|
171
|
+
applied = true;
|
|
172
|
+
});
|
|
173
|
+
if (!applied) return;
|
|
1542
174
|
this.recalculateRaceState({ updateDrs: false });
|
|
1543
175
|
this.evaluateRaceFinish();
|
|
1544
176
|
}
|
|
@@ -1563,7 +195,7 @@ export class F1RaceSimulation {
|
|
|
1563
195
|
|
|
1564
196
|
getPitIntent(id) {
|
|
1565
197
|
const car = this.cars.find((item) => item.id === id);
|
|
1566
|
-
return
|
|
198
|
+
return normalizePitIntentForRace(car);
|
|
1567
199
|
}
|
|
1568
200
|
|
|
1569
201
|
getPitTargetCompound(id) {
|
|
@@ -1572,749 +204,115 @@ export class F1RaceSimulation {
|
|
|
1572
204
|
}
|
|
1573
205
|
|
|
1574
206
|
setPitIntent(id, intent, targetCompound = undefined) {
|
|
1575
|
-
|
|
1576
|
-
? intent
|
|
1577
|
-
: { intent, targetCompound };
|
|
1578
|
-
const nextIntent = normalizePitIntent(request.intent ?? request.pitIntent);
|
|
1579
|
-
if (nextIntent == null) return false;
|
|
1580
|
-
const car = this.cars.find((item) => item.id === id);
|
|
1581
|
-
const stop = car?.pitStop;
|
|
1582
|
-
const pitStops = this.rules.modules?.pitStops;
|
|
1583
|
-
if (!car || !stop || !pitStops?.enabled || !this.track.pitLane?.enabled) return false;
|
|
1584
|
-
if (this.isCarInActivePitStop(car)) return false;
|
|
1585
|
-
|
|
1586
|
-
const compoundRequest = request.targetCompound ??
|
|
1587
|
-
request.compound ??
|
|
1588
|
-
request.pitCompound ??
|
|
1589
|
-
request.pitTargetCompound ??
|
|
1590
|
-
request.targetTire;
|
|
1591
|
-
const hasCompoundRequest = compoundRequest != null && compoundRequest !== '';
|
|
1592
|
-
if (hasCompoundRequest) {
|
|
1593
|
-
const targetTire = normalizePitCompound(compoundRequest, this.rules.modules?.tireStrategy?.compounds);
|
|
1594
|
-
if (!targetTire) return false;
|
|
1595
|
-
stop.targetTire = targetTire;
|
|
1596
|
-
} else if (nextIntent !== PIT_INTENT_NONE && (!stop.targetTire || stop.status === 'completed')) {
|
|
1597
|
-
stop.targetTire = firstDifferentCompound(car.tire, this.rules.modules?.tireStrategy?.compounds);
|
|
1598
|
-
} else if (nextIntent === PIT_INTENT_NONE) {
|
|
1599
|
-
stop.targetTire = firstDifferentCompound(car.tire, this.rules.modules?.tireStrategy?.compounds);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
stop.intent = nextIntent;
|
|
1603
|
-
if (stop.status === 'pending' && nextIntent !== PIT_INTENT_NONE) {
|
|
1604
|
-
this.schedulePitStopAtNextEntry(car, stop);
|
|
1605
|
-
} else if (stop.status === 'completed' && nextIntent !== PIT_INTENT_NONE) {
|
|
1606
|
-
this.rearmCompletedPitStop(car, stop);
|
|
1607
|
-
}
|
|
1608
|
-
return true;
|
|
207
|
+
return setPitIntentForSimulation(this, id, intent, targetCompound);
|
|
1609
208
|
}
|
|
1610
209
|
|
|
1611
210
|
isCarInActivePitStop(car) {
|
|
1612
|
-
return
|
|
211
|
+
return isCarInActivePitStopForSimulation(this, car);
|
|
1613
212
|
}
|
|
1614
213
|
|
|
1615
214
|
schedulePitStopAtNextEntry(car, stop) {
|
|
1616
|
-
|
|
1617
|
-
if (!stop || !pitLane?.entry || !Number.isFinite(this.track.length) || this.track.length <= 0) return false;
|
|
1618
|
-
const raceDistance = car.raceDistance ?? 0;
|
|
1619
|
-
const entryOffset = pitLane.entry.distanceFromStart ?? 0;
|
|
1620
|
-
let lapBase = Math.floor((raceDistance - entryOffset) / this.track.length) * this.track.length;
|
|
1621
|
-
let entryRaceDistance = lapBase + entryOffset;
|
|
1622
|
-
|
|
1623
|
-
while (raceDistance > entryRaceDistance + metersToSimUnits(90)) {
|
|
1624
|
-
lapBase += this.track.length;
|
|
1625
|
-
entryRaceDistance = lapBase + entryOffset;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
stop.lapBase = lapBase;
|
|
1629
|
-
stop.entryRaceDistance = entryRaceDistance;
|
|
1630
|
-
stop.plannedRaceDistance = entryRaceDistance - PIT_ENTRY_APPROACH_DISTANCE;
|
|
1631
|
-
return true;
|
|
215
|
+
return schedulePitStopAtNextEntryForSimulation(this, car, stop);
|
|
1632
216
|
}
|
|
1633
217
|
|
|
1634
218
|
rearmCompletedPitStop(car, stop) {
|
|
1635
|
-
|
|
1636
|
-
stop.status = 'pending';
|
|
1637
|
-
stop.phase = null;
|
|
1638
|
-
stop.route = null;
|
|
1639
|
-
stop.routeProgress = 0;
|
|
1640
|
-
stop.routeStartRaceDistance = null;
|
|
1641
|
-
stop.routeEndRaceDistance = null;
|
|
1642
|
-
stop.serviceRemaining = 0;
|
|
1643
|
-
stop.penaltyServiceRemaining = 0;
|
|
1644
|
-
stop.penaltyServiceTotal = 0;
|
|
1645
|
-
stop.servingPenaltyIds = [];
|
|
1646
|
-
stop.queueingForService = false;
|
|
1647
|
-
stop.serviceProfile = null;
|
|
1648
|
-
stop.targetTire = stop.targetTire ?? firstDifferentCompound(car.tire, this.rules.modules?.tireStrategy?.compounds);
|
|
1649
|
-
return this.schedulePitStopAtNextEntry(car, stop);
|
|
219
|
+
return rearmCompletedPitStopForSimulation(this, car, stop);
|
|
1650
220
|
}
|
|
1651
221
|
|
|
1652
222
|
canStartPitStop(car) {
|
|
1653
|
-
|
|
1654
|
-
const pitStops = this.rules.modules?.pitStops;
|
|
1655
|
-
if (!stop || !pitStops?.enabled) return false;
|
|
1656
|
-
const active = this.cars.filter((candidate) => candidate !== car && this.isCarInActivePitStop(candidate));
|
|
1657
|
-
const maxConcurrentPitLaneCars = Math.max(1, Math.floor(pitStops.maxConcurrentPitLaneCars ?? 3));
|
|
1658
|
-
if (active.length >= maxConcurrentPitLaneCars) return false;
|
|
1659
|
-
|
|
1660
|
-
if (!pitStops.doubleStacking && stop.teamId) {
|
|
1661
|
-
const box = this.getPitStopBox(stop);
|
|
1662
|
-
if (this.isPitServiceQueueOccupied(car, box)) return false;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
const minimumGap = Math.max(0, pitStops.minimumPitLaneGap ?? 0);
|
|
1666
|
-
const candidateDistance = car.raceDistance ?? 0;
|
|
1667
|
-
return active.every((candidate) => {
|
|
1668
|
-
const gap = (candidate.raceDistance ?? 0) - candidateDistance;
|
|
1669
|
-
return gap < 0 || gap >= minimumGap;
|
|
1670
|
-
});
|
|
223
|
+
return canStartPitStopForSimulation(this, car);
|
|
1671
224
|
}
|
|
1672
225
|
|
|
1673
226
|
shouldStartPitStop(car) {
|
|
1674
|
-
|
|
1675
|
-
if (!stop || stop.status !== 'pending' || car.finished) return false;
|
|
1676
|
-
if (!pitLaneStatusSnapshot(this.raceControl, this.track.pitLane, this.rules.modules?.pitStops).open) return false;
|
|
1677
|
-
const intent = normalizePitIntent(stop.intent) ?? PIT_INTENT_NONE;
|
|
1678
|
-
if (intent === PIT_INTENT_NONE) return false;
|
|
1679
|
-
const raceDistance = car.raceDistance ?? 0;
|
|
1680
|
-
if (raceDistance > stop.entryRaceDistance + metersToSimUnits(90)) {
|
|
1681
|
-
this.schedulePitStopAtNextEntry(car, stop);
|
|
1682
|
-
return false;
|
|
1683
|
-
}
|
|
1684
|
-
return raceDistance >= stop.plannedRaceDistance &&
|
|
1685
|
-
(intent === PIT_INTENT_COMMITTED || this.canStartPitStop(car));
|
|
227
|
+
return shouldStartPitStopForSimulation(this, car);
|
|
1686
228
|
}
|
|
1687
229
|
|
|
1688
230
|
updateAutomaticPitIntent(car) {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
if (!stop || !['pending', 'completed'].includes(stop.status) || !pitStops?.enabled || car.finished) return;
|
|
1692
|
-
if (car.automaticPitIntentEnabled === false) return;
|
|
1693
|
-
const tireEnergy = Number(car.tireEnergy ?? 100);
|
|
1694
|
-
const requestThreshold = Number(pitStops.tirePitRequestThresholdPercent ?? 50);
|
|
1695
|
-
const commitThreshold = Number(pitStops.tirePitCommitThresholdPercent ?? 30);
|
|
1696
|
-
if (!Number.isFinite(tireEnergy)) return;
|
|
231
|
+
return updateAutomaticPitIntentForSimulation(this, car);
|
|
232
|
+
}
|
|
1697
233
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
if (nextIntent === PIT_INTENT_NONE || nextIntent <= (normalizePitIntent(stop.intent) ?? PIT_INTENT_NONE)) return;
|
|
234
|
+
isPitLaneOpenForStops() {
|
|
235
|
+
return isPitLaneOpenForStopsForSimulation(this);
|
|
236
|
+
}
|
|
1702
237
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
else this.schedulePitStopAtNextEntry(car, stop);
|
|
238
|
+
pitEntryLateThresholdDistance() {
|
|
239
|
+
return pitEntryLateThresholdDistanceForSimulation();
|
|
1706
240
|
}
|
|
1707
241
|
|
|
1708
242
|
getPitStopBox(stop) {
|
|
1709
|
-
return this
|
|
1710
|
-
this.track.pitLane?.boxes?.find((box) => box.id === stop?.boxId) ??
|
|
1711
|
-
null;
|
|
243
|
+
return getPitStopBoxForSimulation(this, stop);
|
|
1712
244
|
}
|
|
1713
245
|
|
|
1714
246
|
isPitServiceBusy(car, box) {
|
|
1715
|
-
return this
|
|
1716
|
-
candidate !== car &&
|
|
1717
|
-
candidate.pitStop?.boxId === box?.id &&
|
|
1718
|
-
this.isPitServiceAreaOccupied(candidate, box)
|
|
1719
|
-
));
|
|
247
|
+
return isPitServiceBusyForSimulation(this, car, box);
|
|
1720
248
|
}
|
|
1721
249
|
|
|
1722
250
|
isPitServiceAreaOccupied(candidate, box) {
|
|
1723
|
-
|
|
1724
|
-
const phase = candidate?.pitStop?.phase;
|
|
1725
|
-
if (!status || !box) return false;
|
|
1726
|
-
if (status === 'servicing') return true;
|
|
1727
|
-
if (status === 'entering' && phase === 'queue-release') return true;
|
|
1728
|
-
if (status === 'exiting') return pointDistance(candidate, box.center) < PIT_SERVICE_CLEAR_DISTANCE;
|
|
1729
|
-
return false;
|
|
251
|
+
return isPitServiceAreaOccupiedForSimulation(this, candidate, box);
|
|
1730
252
|
}
|
|
1731
253
|
|
|
1732
254
|
isPitServiceQueueOccupied(car, box) {
|
|
1733
|
-
return this
|
|
1734
|
-
candidate !== car &&
|
|
1735
|
-
candidate.pitStop?.boxId === box?.id &&
|
|
1736
|
-
(
|
|
1737
|
-
candidate.pitStop?.status === 'queued' ||
|
|
1738
|
-
Boolean(candidate.pitStop?.queueingForService)
|
|
1739
|
-
)
|
|
1740
|
-
));
|
|
255
|
+
return isPitServiceQueueOccupiedForSimulation(this, car, box);
|
|
1741
256
|
}
|
|
1742
257
|
|
|
1743
258
|
getPitBoxRaceDistance(stop, box) {
|
|
1744
|
-
|
|
1745
|
-
const amount = pitLane?.mainLane?.length > 0 ? box.distanceAlongLane / pitLane.mainLane.length : 0;
|
|
1746
|
-
return stop.lapBase + pitLane.entry.distanceFromStart +
|
|
1747
|
-
(pitLane.exit.distanceFromStart - pitLane.entry.distanceFromStart) * amount;
|
|
259
|
+
return getPitBoxRaceDistanceForSimulation(this, stop, box);
|
|
1748
260
|
}
|
|
1749
261
|
|
|
1750
262
|
startPitStop(car) {
|
|
1751
|
-
|
|
1752
|
-
const box = this.getPitStopBox(stop);
|
|
1753
|
-
const pitLane = this.track.pitLane;
|
|
1754
|
-
if (!stop || !box || !pitLane?.enabled) return false;
|
|
1755
|
-
|
|
1756
|
-
car.gridLocked = false;
|
|
1757
|
-
car.drsActive = false;
|
|
1758
|
-
car.drsEligible = false;
|
|
1759
|
-
car.drsZoneId = null;
|
|
1760
|
-
car.drsZoneEnabled = false;
|
|
1761
|
-
car.canAttack = false;
|
|
1762
|
-
stop.targetTire = stop.targetTire ?? firstDifferentCompound(car.tire, this.rules.modules?.tireStrategy?.compounds);
|
|
1763
|
-
|
|
1764
|
-
const currentState = applyWheelSurfaceState(car, this.track).representativeState;
|
|
1765
|
-
this.events.unshift({
|
|
1766
|
-
type: 'pit-entry',
|
|
1767
|
-
at: this.time,
|
|
1768
|
-
carId: car.id,
|
|
1769
|
-
boxId: box.id,
|
|
1770
|
-
teamId: box.teamId ?? null,
|
|
1771
|
-
});
|
|
1772
|
-
|
|
1773
|
-
if (currentState.surface === 'pit-box' && currentState.pitBoxId === box.id) {
|
|
1774
|
-
this.beginPitService(car, box);
|
|
1775
|
-
return true;
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
const hasServiceQueue = Boolean(box.queuePoint);
|
|
1779
|
-
const shouldStageForService = hasServiceQueue;
|
|
1780
|
-
const queueDistanceAlongLane = box.queueDistanceAlongLane ??
|
|
1781
|
-
Math.max(0, box.distanceAlongLane - PIT_SERVICE_QUEUE_FALLBACK_GAP);
|
|
1782
|
-
const stopDistanceAlongLane = shouldStageForService
|
|
1783
|
-
? queueDistanceAlongLane
|
|
1784
|
-
: box.distanceAlongLane;
|
|
1785
|
-
const serviceTarget = shouldStageForService ? box.queuePoint : box.center;
|
|
1786
|
-
const driveLaneOffset = pitDriveLaneOffset(pitLane);
|
|
1787
|
-
const boxApproachDistance = Math.max(
|
|
1788
|
-
0,
|
|
1789
|
-
(hasServiceQueue ? queueDistanceAlongLane : stopDistanceAlongLane) - PIT_BOX_APPROACH_DISTANCE,
|
|
1790
|
-
);
|
|
1791
|
-
const route = createRoute([
|
|
1792
|
-
...createPitApproachPoints(this.track, car, pitLane, stop.entryRaceDistance),
|
|
1793
|
-
...(pitLane.entry.roadCenterline ?? []).map((point) => routePoint(point)),
|
|
1794
|
-
routePoint(pitMainLanePointAt(pitLane, 0, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
1795
|
-
routePoint(pitMainLanePointAt(pitLane, boxApproachDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
1796
|
-
routePoint(serviceTarget, pitLane.mainLane.heading, { limiterActive: true }),
|
|
1797
|
-
]);
|
|
1798
|
-
stop.status = 'entering';
|
|
1799
|
-
stop.phase = 'entry';
|
|
1800
|
-
stop.queueingForService = shouldStageForService;
|
|
1801
|
-
stop.route = route;
|
|
1802
|
-
stop.routeProgress = 0;
|
|
1803
|
-
stop.routeStartRaceDistance = car.raceDistance ?? stop.plannedRaceDistance;
|
|
1804
|
-
stop.routeEndRaceDistance = this.getPitBoxRaceDistance(stop, {
|
|
1805
|
-
...box,
|
|
1806
|
-
distanceAlongLane: stopDistanceAlongLane,
|
|
1807
|
-
});
|
|
1808
|
-
return true;
|
|
263
|
+
return startPitStopForSimulation(this, car);
|
|
1809
264
|
}
|
|
1810
265
|
|
|
1811
266
|
beginPitQueue(car, box) {
|
|
1812
|
-
|
|
1813
|
-
if (!stop || !box?.queuePoint) return false;
|
|
1814
|
-
stop.status = 'queued';
|
|
1815
|
-
stop.phase = 'queue';
|
|
1816
|
-
stop.route = null;
|
|
1817
|
-
stop.routeProgress = 0;
|
|
1818
|
-
stop.queueingForService = false;
|
|
1819
|
-
car.x = box.queuePoint.x;
|
|
1820
|
-
car.y = box.queuePoint.y;
|
|
1821
|
-
car.heading = this.track.pitLane.mainLane.heading;
|
|
1822
|
-
car.speed = 0;
|
|
1823
|
-
car.throttle = 0;
|
|
1824
|
-
car.brake = 1;
|
|
1825
|
-
applyWheelSurfaceState(car, this.track);
|
|
1826
|
-
car.progress = car.trackState.distance;
|
|
1827
|
-
car.raceDistance = this.getPitBoxRaceDistance(stop, {
|
|
1828
|
-
...box,
|
|
1829
|
-
distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
|
|
1830
|
-
});
|
|
1831
|
-
return true;
|
|
267
|
+
return beginPitQueueForSimulation(this, car, box);
|
|
1832
268
|
}
|
|
1833
269
|
|
|
1834
270
|
releasePitQueue(car, box, { fromCurrent = false } = {}) {
|
|
1835
|
-
|
|
1836
|
-
if (!stop || !box?.center) return false;
|
|
1837
|
-
const route = createRoute([
|
|
1838
|
-
...(fromCurrent ? [routePoint(car, car.heading, { limiterActive: true })] : []),
|
|
1839
|
-
routePoint(box.queuePoint, this.track.pitLane.mainLane.heading, { limiterActive: true }),
|
|
1840
|
-
routePoint(box.center, this.track.pitLane.mainLane.heading, { limiterActive: true }),
|
|
1841
|
-
]);
|
|
1842
|
-
stop.status = 'entering';
|
|
1843
|
-
stop.phase = 'queue-release';
|
|
1844
|
-
stop.queueingForService = false;
|
|
1845
|
-
stop.route = route;
|
|
1846
|
-
stop.routeProgress = 0;
|
|
1847
|
-
stop.routeStartRaceDistance = fromCurrent
|
|
1848
|
-
? car.raceDistance ?? this.getPitBoxRaceDistance(stop, {
|
|
1849
|
-
...box,
|
|
1850
|
-
distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
|
|
1851
|
-
})
|
|
1852
|
-
: this.getPitBoxRaceDistance(stop, {
|
|
1853
|
-
...box,
|
|
1854
|
-
distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
|
|
1855
|
-
});
|
|
1856
|
-
stop.routeEndRaceDistance = this.getPitBoxRaceDistance(stop, box);
|
|
1857
|
-
return true;
|
|
271
|
+
return releasePitQueueForSimulation(this, car, box, { fromCurrent });
|
|
1858
272
|
}
|
|
1859
273
|
|
|
1860
274
|
beginPitService(car, box) {
|
|
1861
|
-
|
|
1862
|
-
stop.status = 'servicing';
|
|
1863
|
-
stop.route = null;
|
|
1864
|
-
stop.routeProgress = 0;
|
|
1865
|
-
car.x = box.center.x;
|
|
1866
|
-
car.y = box.center.y;
|
|
1867
|
-
car.heading = this.track.pitLane.mainLane.heading;
|
|
1868
|
-
car.speed = 0;
|
|
1869
|
-
car.throttle = 0;
|
|
1870
|
-
car.brake = 1;
|
|
1871
|
-
car.steeringAngle = 0;
|
|
1872
|
-
car.yawRate = 0;
|
|
1873
|
-
car.turnRadius = Infinity;
|
|
1874
|
-
applyWheelSurfaceState(car, this.track);
|
|
1875
|
-
car.progress = car.trackState.distance;
|
|
1876
|
-
car.raceDistance = this.getPitBoxRaceDistance(stop, box);
|
|
1877
|
-
|
|
1878
|
-
if (this.beginPitPenaltyService(car)) return;
|
|
1879
|
-
this.beginTireService(car);
|
|
275
|
+
return beginPitServiceForSimulation(this, car, box);
|
|
1880
276
|
}
|
|
1881
277
|
|
|
1882
278
|
beginPitPenaltyService(car) {
|
|
1883
|
-
|
|
1884
|
-
const penalties = this.getPitServicePenalties(car.id);
|
|
1885
|
-
if (!stop || !penalties.length) return false;
|
|
1886
|
-
|
|
1887
|
-
const totalSeconds = penalties.reduce((total, penalty) => (
|
|
1888
|
-
total + getPitServicePenaltySeconds(penalty)
|
|
1889
|
-
), 0);
|
|
1890
|
-
stop.servingPenaltyIds = penalties.map((penalty) => penalty.id);
|
|
1891
|
-
stop.penaltyServiceTotal = totalSeconds;
|
|
1892
|
-
stop.penaltyServiceRemaining = totalSeconds;
|
|
1893
|
-
|
|
1894
|
-
if (totalSeconds <= 0) {
|
|
1895
|
-
this.completePitPenaltyService(car);
|
|
1896
|
-
return true;
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
stop.phase = 'penalty';
|
|
1900
|
-
stop.serviceRemaining = totalSeconds;
|
|
1901
|
-
this.events.unshift({
|
|
1902
|
-
type: 'pit-penalty-service-start',
|
|
1903
|
-
at: this.time,
|
|
1904
|
-
carId: car.id,
|
|
1905
|
-
penaltyIds: [...stop.servingPenaltyIds],
|
|
1906
|
-
penaltyServiceSeconds: totalSeconds,
|
|
1907
|
-
});
|
|
1908
|
-
return true;
|
|
279
|
+
return beginPitPenaltyServiceForSimulation(this, car);
|
|
1909
280
|
}
|
|
1910
281
|
|
|
1911
282
|
calculatePitServiceProfile(car) {
|
|
1912
|
-
|
|
1913
|
-
const variability = pitStops.variability ?? {};
|
|
1914
|
-
const baseSeconds = Math.max(0, Number(pitStops.defaultStopSeconds) || 2.8);
|
|
1915
|
-
const box = this.getPitStopBox(car?.pitStop);
|
|
1916
|
-
const team = this.track.pitLane?.teams?.find((entry) => entry.id === car?.pitStop?.teamId);
|
|
1917
|
-
const pitCrew = normalizePitCrewStats(
|
|
1918
|
-
box?.pitCrew ?? team?.pitCrew ?? car?.team?.pitCrew ?? car?.team?.pitCrewStats,
|
|
1919
|
-
);
|
|
1920
|
-
const profile = {
|
|
1921
|
-
baseSeconds,
|
|
1922
|
-
seconds: baseSeconds,
|
|
1923
|
-
perfect: Boolean(variability.perfect),
|
|
1924
|
-
variabilityEnabled: Boolean(variability.enabled),
|
|
1925
|
-
teamId: car?.pitStop?.teamId ?? box?.teamId ?? null,
|
|
1926
|
-
pitCrew,
|
|
1927
|
-
speedDeltaSeconds: 0,
|
|
1928
|
-
consistencyDeltaSeconds: 0,
|
|
1929
|
-
issueDelaySeconds: 0,
|
|
1930
|
-
issue: null,
|
|
1931
|
-
};
|
|
1932
|
-
|
|
1933
|
-
if (!variability.enabled || variability.perfect) return profile;
|
|
1934
|
-
|
|
1935
|
-
const speedImpact = Math.max(0, Number(variability.speedImpactSeconds) || 0);
|
|
1936
|
-
const jitterImpact = Math.max(0, Number(variability.consistencyJitterSeconds) || 0);
|
|
1937
|
-
const issueChance = clamp(Number(variability.issueChance) || 0, 0, 1);
|
|
1938
|
-
const issueMaxDelay = Math.max(0, Number(variability.issueMaxDelaySeconds) || 0);
|
|
1939
|
-
profile.speedDeltaSeconds = (0.5 - pitCrew.speed) * speedImpact;
|
|
1940
|
-
profile.consistencyDeltaSeconds = (this.random() - 0.5) * (1 - pitCrew.consistency) * jitterImpact;
|
|
1941
|
-
const effectiveIssueChance = issueChance * (1 - pitCrew.reliability);
|
|
1942
|
-
if (this.random() < effectiveIssueChance) {
|
|
1943
|
-
profile.issueDelaySeconds = 0.35 + this.random() * issueMaxDelay * (1 - pitCrew.reliability);
|
|
1944
|
-
profile.issue = 'slow-stop';
|
|
1945
|
-
}
|
|
1946
|
-
profile.seconds = Math.max(
|
|
1947
|
-
Math.min(1.6, baseSeconds),
|
|
1948
|
-
baseSeconds + profile.speedDeltaSeconds + profile.consistencyDeltaSeconds + profile.issueDelaySeconds,
|
|
1949
|
-
);
|
|
1950
|
-
return profile;
|
|
283
|
+
return calculatePitServiceProfileForSimulation(this, car);
|
|
1951
284
|
}
|
|
1952
285
|
|
|
1953
286
|
beginTireService(car) {
|
|
1954
|
-
|
|
1955
|
-
if (!stop) return;
|
|
1956
|
-
const serviceProfile = this.calculatePitServiceProfile(car);
|
|
1957
|
-
stop.phase = 'service';
|
|
1958
|
-
stop.penaltyServiceRemaining = 0;
|
|
1959
|
-
stop.serviceProfile = serviceProfile;
|
|
1960
|
-
stop.serviceRemaining = serviceProfile.seconds;
|
|
1961
|
-
this.events.unshift({
|
|
1962
|
-
type: 'pit-stop-start',
|
|
1963
|
-
at: this.time,
|
|
1964
|
-
carId: car.id,
|
|
1965
|
-
boxId: stop.boxId,
|
|
1966
|
-
teamId: stop.teamId ?? null,
|
|
1967
|
-
targetTire: stop.targetTire,
|
|
1968
|
-
serviceSeconds: serviceProfile.seconds,
|
|
1969
|
-
serviceIssue: serviceProfile.issue,
|
|
1970
|
-
});
|
|
287
|
+
return beginTireServiceForSimulation(this, car);
|
|
1971
288
|
}
|
|
1972
289
|
|
|
1973
290
|
completePitPenaltyService(car) {
|
|
1974
|
-
|
|
1975
|
-
if (!stop) return;
|
|
1976
|
-
const servedIds = [...(stop.servingPenaltyIds ?? [])];
|
|
1977
|
-
servedIds.forEach((penaltyId) => {
|
|
1978
|
-
const penalty = this.penalties.find((entry) => entry.id === penaltyId);
|
|
1979
|
-
const beforeStatus = penalty?.status;
|
|
1980
|
-
const result = servePitPenaltyRecord(penalty, this.time);
|
|
1981
|
-
if (result && beforeStatus !== result.status) {
|
|
1982
|
-
this.events.unshift({
|
|
1983
|
-
type: 'penalty-served',
|
|
1984
|
-
at: this.time,
|
|
1985
|
-
penaltyId: result.id,
|
|
1986
|
-
driverId: result.driverId,
|
|
1987
|
-
serviceType: result.serviceType,
|
|
1988
|
-
serviceContext: 'pit-stop',
|
|
1989
|
-
});
|
|
1990
|
-
}
|
|
1991
|
-
});
|
|
1992
|
-
stop.penaltyServiceRemaining = 0;
|
|
1993
|
-
stop.serviceRemaining = 0;
|
|
1994
|
-
this.events.unshift({
|
|
1995
|
-
type: 'pit-penalty-service-complete',
|
|
1996
|
-
at: this.time,
|
|
1997
|
-
carId: car.id,
|
|
1998
|
-
penaltyIds: servedIds,
|
|
1999
|
-
});
|
|
2000
|
-
this.beginTireService(car);
|
|
291
|
+
return completePitPenaltyServiceForSimulation(this, car);
|
|
2001
292
|
}
|
|
2002
293
|
|
|
2003
294
|
getPitServicePenalties(driverId) {
|
|
2004
|
-
return this
|
|
2005
|
-
penalty.driverId === driverId && isPenaltyPitServiceable(penalty)
|
|
2006
|
-
));
|
|
295
|
+
return getPitServicePenaltiesForSimulation(this, driverId);
|
|
2007
296
|
}
|
|
2008
297
|
|
|
2009
298
|
completePitService(car) {
|
|
2010
|
-
|
|
2011
|
-
const box = this.getPitStopBox(stop);
|
|
2012
|
-
const pitLane = this.track.pitLane;
|
|
2013
|
-
if (!stop || !box || !pitLane?.enabled) return;
|
|
2014
|
-
|
|
2015
|
-
car.tire = stop.targetTire ?? car.tire;
|
|
2016
|
-
car.tireEnergy = 100;
|
|
2017
|
-
if (car.tire && !car.usedTireCompounds.includes(car.tire)) car.usedTireCompounds.push(car.tire);
|
|
2018
|
-
this.events.unshift({
|
|
2019
|
-
type: 'pit-stop-complete',
|
|
2020
|
-
at: this.time,
|
|
2021
|
-
carId: car.id,
|
|
2022
|
-
boxId: box.id,
|
|
2023
|
-
teamId: box.teamId ?? null,
|
|
2024
|
-
tire: car.tire,
|
|
2025
|
-
});
|
|
2026
|
-
|
|
2027
|
-
const driveLaneOffset = pitDriveLaneOffset(pitLane);
|
|
2028
|
-
const boxReleaseDistance = Math.min(pitLane.mainLane.length, box.distanceAlongLane + PIT_BOX_APPROACH_DISTANCE);
|
|
2029
|
-
const route = createRoute([
|
|
2030
|
-
routePoint(box.center, pitLane.mainLane.heading, { limiterActive: true }),
|
|
2031
|
-
routePoint(box.laneTarget, pitLane.mainLane.heading, { limiterActive: true }),
|
|
2032
|
-
routePoint(pitMainLanePointAt(pitLane, boxReleaseDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
2033
|
-
routePoint(pitMainLanePointAt(pitLane, pitLane.mainLane.length, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
2034
|
-
...(pitLane.exit.roadCenterline ?? []).map((point) => routePoint(point)),
|
|
2035
|
-
]);
|
|
2036
|
-
stop.status = 'exiting';
|
|
2037
|
-
stop.phase = 'exit';
|
|
2038
|
-
stop.route = route;
|
|
2039
|
-
stop.routeProgress = 0;
|
|
2040
|
-
stop.routeStartRaceDistance = this.getPitBoxRaceDistance(stop, box);
|
|
2041
|
-
stop.routeEndRaceDistance = stop.lapBase + pitLane.exit.distanceFromStart;
|
|
299
|
+
return completePitServiceForSimulation(this, car);
|
|
2042
300
|
}
|
|
2043
301
|
|
|
2044
302
|
finishPitExit(car) {
|
|
2045
|
-
|
|
2046
|
-
if (!stop) return;
|
|
2047
|
-
stop.status = 'completed';
|
|
2048
|
-
stop.phase = null;
|
|
2049
|
-
stop.route = null;
|
|
2050
|
-
stop.routeProgress = 0;
|
|
2051
|
-
stop.serviceRemaining = 0;
|
|
2052
|
-
stop.queueingForService = false;
|
|
2053
|
-
stop.stopsCompleted = (stop.stopsCompleted ?? 0) + 1;
|
|
2054
|
-
stop.intent = PIT_INTENT_NONE;
|
|
2055
|
-
car.speed = Math.max(car.speed, kphToSimSpeed(PIT_EXIT_RELEASE_SPEED_KPH));
|
|
2056
|
-
car.throttle = 0.55;
|
|
2057
|
-
car.brake = 0;
|
|
2058
|
-
applyWheelSurfaceState(car, this.track);
|
|
2059
|
-
car.progress = car.trackState.distance;
|
|
2060
|
-
car.raceDistance = Math.max(car.raceDistance ?? 0, stop.routeEndRaceDistance ?? car.raceDistance ?? 0);
|
|
2061
|
-
this.events.unshift({
|
|
2062
|
-
type: 'pit-exit',
|
|
2063
|
-
at: this.time,
|
|
2064
|
-
carId: car.id,
|
|
2065
|
-
boxId: stop.boxId,
|
|
2066
|
-
teamId: stop.teamId ?? null,
|
|
2067
|
-
});
|
|
303
|
+
return finishPitExitForSimulation(this, car);
|
|
2068
304
|
}
|
|
2069
305
|
|
|
2070
306
|
applyPitRoutePosition(car, delta) {
|
|
2071
|
-
|
|
2072
|
-
if (!stop?.route) return false;
|
|
2073
|
-
const speedLimit = kphToSimSpeed(this.rules.modules?.pitStops?.pitLaneSpeedLimitKph ?? 80);
|
|
2074
|
-
const route = stop.route;
|
|
2075
|
-
if (stop.phase === 'queue-release') {
|
|
2076
|
-
const nextProgress = Math.min(route.length, (stop.routeProgress ?? 0) + PIT_QUEUE_RELEASE_SPEED * delta);
|
|
2077
|
-
const point = sampleRoute(route, nextProgress);
|
|
2078
|
-
if (!point) return false;
|
|
2079
|
-
const previousHeading = car.heading;
|
|
2080
|
-
car.previousX = car.x;
|
|
2081
|
-
car.previousY = car.y;
|
|
2082
|
-
car.previousHeading = car.heading;
|
|
2083
|
-
car.x = point.x;
|
|
2084
|
-
car.y = point.y;
|
|
2085
|
-
car.heading = point.heading ?? this.track.pitLane.mainLane.heading;
|
|
2086
|
-
car.speed = PIT_QUEUE_RELEASE_SPEED;
|
|
2087
|
-
car.throttle = 0.32;
|
|
2088
|
-
car.brake = 0;
|
|
2089
|
-
car.yawRate = normalizeAngle(car.heading - previousHeading) / Math.max(delta, 1e-6);
|
|
2090
|
-
car.steeringAngle = clamp(
|
|
2091
|
-
Math.atan((car.yawRate * VEHICLE_LIMITS.wheelbase) / Math.max(car.speed, 1)),
|
|
2092
|
-
-VEHICLE_LIMITS.maxSteer,
|
|
2093
|
-
VEHICLE_LIMITS.maxSteer,
|
|
2094
|
-
);
|
|
2095
|
-
car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
|
|
2096
|
-
stop.routeProgress = nextProgress;
|
|
2097
|
-
const routeAmount = route.length > 0 ? stop.routeProgress / route.length : 1;
|
|
2098
|
-
car.raceDistance = stop.routeStartRaceDistance +
|
|
2099
|
-
(stop.routeEndRaceDistance - stop.routeStartRaceDistance) * routeAmount;
|
|
2100
|
-
applyWheelSurfaceState(car, this.track);
|
|
2101
|
-
car.progress = car.trackState.distance;
|
|
2102
|
-
car.lap = this.computeLap(car.raceDistance);
|
|
2103
|
-
return route.length - stop.routeProgress <= PIT_QUEUE_RELEASE_FINISH_DISTANCE;
|
|
2104
|
-
}
|
|
2105
|
-
stop.routeProgress = clamp(stop.routeProgress ?? nearestDistanceOnRoute(route, car, 0), 0, route.length);
|
|
2106
|
-
const routeEnd = route.points.at(-1);
|
|
2107
|
-
const limiterActive = routeLimiterActiveAt(route, stop.routeProgress);
|
|
2108
|
-
let targetSpeed = limiterActive
|
|
2109
|
-
? Math.min(speedLimit, VEHICLE_LIMITS.maxSpeed)
|
|
2110
|
-
: Math.min(kphToSimSpeed(150), VEHICLE_LIMITS.maxSpeed);
|
|
2111
|
-
if (!limiterActive && stop.status === 'entering') {
|
|
2112
|
-
targetSpeed = Math.min(targetSpeed, speedLimit + kphToSimSpeed(PIT_ENTRY_CONNECTOR_OVERSPEED_KPH));
|
|
2113
|
-
const distanceToLimiter = distanceToNextLimiterSegment(route, stop.routeProgress);
|
|
2114
|
-
if (distanceToLimiter < PIT_LIMITER_BRAKE_DISTANCE) {
|
|
2115
|
-
targetSpeed = Math.min(
|
|
2116
|
-
targetSpeed,
|
|
2117
|
-
speedLimit + Math.max(0, distanceToLimiter * PIT_LIMITER_APPROACH_SPEED_SLOPE),
|
|
2118
|
-
);
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
if (stop.status === 'entering') {
|
|
2122
|
-
const remainingBefore = Math.max(0, route.length - stop.routeProgress);
|
|
2123
|
-
const captureDistance = stop.queueingForService ? PIT_QUEUE_CAPTURE_DISTANCE : PIT_ENTRY_BOX_CAPTURE_DISTANCE;
|
|
2124
|
-
const brakeZone = stop.queueingForService ? metersToSimUnits(25) : metersToSimUnits(80);
|
|
2125
|
-
if (remainingBefore < brakeZone) {
|
|
2126
|
-
const brakingSlope = stop.queueingForService ? kphToSimSpeed(62) / metersToSimUnits(100) : kphToSimSpeed(24) / metersToSimUnits(100);
|
|
2127
|
-
const captureSpeedFloor = stop.queueingForService ? kphToSimSpeed(42) : kphToSimSpeed(28);
|
|
2128
|
-
const approachSpeed = remainingBefore > captureDistance
|
|
2129
|
-
? Math.max(captureSpeedFloor, remainingBefore * brakingSlope)
|
|
2130
|
-
: remainingBefore * brakingSlope;
|
|
2131
|
-
targetSpeed = Math.min(targetSpeed, clamp(approachSpeed, 0, speedLimit * 0.78));
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
const nextProgress = Math.min(route.length, stop.routeProgress + Math.max(0, targetSpeed) * delta);
|
|
2136
|
-
const point = sampleRoute(route, nextProgress);
|
|
2137
|
-
if (!point) return false;
|
|
2138
|
-
const previousSpeed = car.speed;
|
|
2139
|
-
const previousHeading = car.heading;
|
|
2140
|
-
car.previousX = car.x;
|
|
2141
|
-
car.previousY = car.y;
|
|
2142
|
-
car.previousHeading = car.heading;
|
|
2143
|
-
car.x = point.x;
|
|
2144
|
-
car.y = point.y;
|
|
2145
|
-
car.heading = point.heading ?? car.heading;
|
|
2146
|
-
car.speed = targetSpeed;
|
|
2147
|
-
car.throttle = limiterActive ? 0.28 : 0.46;
|
|
2148
|
-
car.brake = targetSpeed < previousSpeed ? 0.4 : 0;
|
|
2149
|
-
car.yawRate = normalizeAngle(car.heading - previousHeading) / Math.max(delta, 1e-6);
|
|
2150
|
-
car.steeringAngle = clamp(
|
|
2151
|
-
Math.atan((car.yawRate * VEHICLE_LIMITS.wheelbase) / Math.max(car.speed, 1)),
|
|
2152
|
-
-VEHICLE_LIMITS.maxSteer,
|
|
2153
|
-
VEHICLE_LIMITS.maxSteer,
|
|
2154
|
-
);
|
|
2155
|
-
car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
|
|
2156
|
-
applyWheelSurfaceState(car, this.track);
|
|
2157
|
-
stop.routeProgress = nextProgress;
|
|
2158
|
-
const routeAmount = route.length > 0 ? stop.routeProgress / route.length : 1;
|
|
2159
|
-
car.raceDistance = stop.routeStartRaceDistance +
|
|
2160
|
-
(stop.routeEndRaceDistance - stop.routeStartRaceDistance) * routeAmount;
|
|
2161
|
-
car.progress = car.trackState.distance;
|
|
2162
|
-
car.lap = this.computeLap(car.raceDistance);
|
|
2163
|
-
const remainingAfter = Math.max(0, route.length - stop.routeProgress);
|
|
2164
|
-
if (stop.status === 'entering') {
|
|
2165
|
-
const captureDistance = stop.queueingForService ? PIT_QUEUE_CAPTURE_DISTANCE : PIT_ENTRY_BOX_CAPTURE_DISTANCE;
|
|
2166
|
-
const captureSpeed = stop.queueingForService ? PIT_QUEUE_CAPTURE_SPEED : PIT_BOX_STOP_SPEED;
|
|
2167
|
-
return remainingAfter <= captureDistance &&
|
|
2168
|
-
pointDistance(car, routeEnd) <= captureDistance &&
|
|
2169
|
-
car.speed <= captureSpeed;
|
|
2170
|
-
}
|
|
2171
|
-
return remainingAfter <= PIT_ROUTE_FINISH_DISTANCE;
|
|
307
|
+
return applyPitRoutePositionForSimulation(this, car, delta);
|
|
2172
308
|
}
|
|
2173
309
|
|
|
2174
310
|
advancePitStopCar(car, delta) {
|
|
2175
|
-
|
|
2176
|
-
const stop = car.pitStop;
|
|
2177
|
-
if (!pitStops?.enabled || !stop) return false;
|
|
2178
|
-
if (stop.status === 'pending' || stop.status === 'completed') this.updateAutomaticPitIntent(car);
|
|
2179
|
-
if (stop.status === 'completed') return false;
|
|
2180
|
-
if (stop.status === 'pending' && !this.shouldStartPitStop(car)) return false;
|
|
2181
|
-
if (stop.status === 'pending') this.startPitStop(car);
|
|
2182
|
-
|
|
2183
|
-
if (stop.status === 'entering') {
|
|
2184
|
-
const finishedRoute = this.applyPitRoutePosition(car, delta);
|
|
2185
|
-
const box = this.getPitStopBox(stop);
|
|
2186
|
-
if (finishedRoute && box) {
|
|
2187
|
-
if (stop.queueingForService) {
|
|
2188
|
-
if (!this.isPitServiceBusy(car, box) && !this.isPitServiceQueueOccupied(car, box)) {
|
|
2189
|
-
this.releasePitQueue(car, box, { fromCurrent: true });
|
|
2190
|
-
} else {
|
|
2191
|
-
this.beginPitQueue(car, box);
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
else this.beginPitService(car, box);
|
|
2195
|
-
}
|
|
2196
|
-
car.contactCooldown = Math.max(0, car.contactCooldown - delta);
|
|
2197
|
-
return true;
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
if (stop.status === 'queued') {
|
|
2201
|
-
const box = this.getPitStopBox(stop);
|
|
2202
|
-
if (!box) return true;
|
|
2203
|
-
car.previousX = car.x;
|
|
2204
|
-
car.previousY = car.y;
|
|
2205
|
-
car.previousHeading = car.heading;
|
|
2206
|
-
car.x = box.queuePoint.x;
|
|
2207
|
-
car.y = box.queuePoint.y;
|
|
2208
|
-
car.heading = this.track.pitLane.mainLane.heading;
|
|
2209
|
-
car.speed = 0;
|
|
2210
|
-
car.throttle = 0;
|
|
2211
|
-
car.brake = 1;
|
|
2212
|
-
applyWheelSurfaceState(car, this.track);
|
|
2213
|
-
car.progress = car.trackState.distance;
|
|
2214
|
-
car.raceDistance = this.getPitBoxRaceDistance(stop, {
|
|
2215
|
-
...box,
|
|
2216
|
-
distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
|
|
2217
|
-
});
|
|
2218
|
-
if (!this.isPitServiceBusy(car, box)) this.releasePitQueue(car, box);
|
|
2219
|
-
return true;
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
if (stop.status === 'servicing') {
|
|
2223
|
-
const box = this.getPitStopBox(stop);
|
|
2224
|
-
if (!box) return true;
|
|
2225
|
-
if (stop.phase === 'penalty') {
|
|
2226
|
-
stop.penaltyServiceRemaining = Math.max(0, (stop.penaltyServiceRemaining ?? 0) - delta);
|
|
2227
|
-
stop.serviceRemaining = stop.penaltyServiceRemaining;
|
|
2228
|
-
} else {
|
|
2229
|
-
stop.serviceRemaining = Math.max(0, (stop.serviceRemaining ?? 0) - delta);
|
|
2230
|
-
}
|
|
2231
|
-
car.previousX = car.x;
|
|
2232
|
-
car.previousY = car.y;
|
|
2233
|
-
car.previousHeading = car.heading;
|
|
2234
|
-
car.x = box.center.x;
|
|
2235
|
-
car.y = box.center.y;
|
|
2236
|
-
car.heading = this.track.pitLane.mainLane.heading;
|
|
2237
|
-
car.speed = 0;
|
|
2238
|
-
car.throttle = 0;
|
|
2239
|
-
car.brake = 1;
|
|
2240
|
-
applyWheelSurfaceState(car, this.track);
|
|
2241
|
-
car.progress = car.trackState.distance;
|
|
2242
|
-
car.raceDistance = this.getPitBoxRaceDistance(stop, box);
|
|
2243
|
-
if (stop.phase === 'penalty' && stop.penaltyServiceRemaining <= 0) {
|
|
2244
|
-
this.completePitPenaltyService(car);
|
|
2245
|
-
} else if (stop.serviceRemaining <= 0) {
|
|
2246
|
-
this.completePitService(car);
|
|
2247
|
-
}
|
|
2248
|
-
return true;
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
if (stop.status === 'exiting') {
|
|
2252
|
-
const finishedRoute = this.applyPitRoutePosition(car, delta);
|
|
2253
|
-
if (finishedRoute) this.finishPitExit(car);
|
|
2254
|
-
car.contactCooldown = Math.max(0, car.contactCooldown - delta);
|
|
2255
|
-
return true;
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
return false;
|
|
311
|
+
return advancePitStopCarForSimulation(this, car, delta);
|
|
2259
312
|
}
|
|
2260
313
|
|
|
2261
314
|
step(dt) {
|
|
2262
|
-
|
|
2263
|
-
if (!Number.isFinite(delta) || delta <= 0) return;
|
|
2264
|
-
|
|
2265
|
-
this.time += delta;
|
|
2266
|
-
this.events = [];
|
|
2267
|
-
this.updateStartSequence();
|
|
2268
|
-
this.recalculateRaceState({ updateDrs: false });
|
|
2269
|
-
|
|
2270
|
-
if (this.raceControl.mode === 'pre-start' && this.cars.every((car) => car.gridLocked)) {
|
|
2271
|
-
this.holdGridCars();
|
|
2272
|
-
this.recalculateRaceState({ updateDrs: false });
|
|
2273
|
-
return;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
if (this.raceControl.redFlag) {
|
|
2277
|
-
this.cars.forEach((car) => {
|
|
2278
|
-
car.previousX = car.x;
|
|
2279
|
-
car.previousY = car.y;
|
|
2280
|
-
car.previousHeading = car.heading;
|
|
2281
|
-
car.speed = 0;
|
|
2282
|
-
car.throttle = 0;
|
|
2283
|
-
car.brake = 1;
|
|
2284
|
-
car.drsActive = false;
|
|
2285
|
-
car.drsEligible = false;
|
|
2286
|
-
car.drsZoneId = null;
|
|
2287
|
-
car.drsZoneEnabled = false;
|
|
2288
|
-
car.canAttack = false;
|
|
2289
|
-
});
|
|
2290
|
-
this.recalculateRaceState({ updateDrs: false });
|
|
2291
|
-
return;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
this.updateSafetyCar(delta);
|
|
2295
|
-
|
|
2296
|
-
const orderedCars = this.orderedCars();
|
|
2297
|
-
const raceContext = this.driverRaceContext(orderedCars);
|
|
2298
|
-
orderedCars.forEach((car, index) => {
|
|
2299
|
-
car.previousX = car.x;
|
|
2300
|
-
car.previousY = car.y;
|
|
2301
|
-
car.previousHeading = car.heading;
|
|
2302
|
-
car.previousProgress = car.progress;
|
|
2303
|
-
if (this.advancePitStopCar(car, delta)) return;
|
|
2304
|
-
const controls = decideDriverControls({
|
|
2305
|
-
car,
|
|
2306
|
-
orderIndex: index,
|
|
2307
|
-
race: raceContext,
|
|
2308
|
-
});
|
|
2309
|
-
integrateVehiclePhysics(car, controls, delta);
|
|
2310
|
-
this.applyRunoffResponse(car);
|
|
2311
|
-
car.contactCooldown = Math.max(0, car.contactCooldown - delta);
|
|
2312
|
-
});
|
|
2313
|
-
|
|
2314
|
-
this.resolveCollisions();
|
|
2315
|
-
this.recalculateRaceState();
|
|
2316
|
-
this.reviewTrackLimits();
|
|
2317
|
-
this.reviewPitLaneSpeeding();
|
|
315
|
+
runRaceStep(this, dt);
|
|
2318
316
|
}
|
|
2319
317
|
|
|
2320
318
|
recordPenalty(penalty) {
|
|
@@ -2338,13 +336,7 @@ export class F1RaceSimulation {
|
|
|
2338
336
|
const penalty = this.penalties.find((entry) => entry.id === penaltyId);
|
|
2339
337
|
const result = servePenaltyRecord(penalty, this.time);
|
|
2340
338
|
if (result) {
|
|
2341
|
-
this.events.unshift(
|
|
2342
|
-
type: 'penalty-served',
|
|
2343
|
-
at: this.time,
|
|
2344
|
-
penaltyId: result.id,
|
|
2345
|
-
driverId: result.driverId,
|
|
2346
|
-
serviceType: result.serviceType,
|
|
2347
|
-
});
|
|
339
|
+
this.events.unshift(createPenaltyServedEvent(result, this.time));
|
|
2348
340
|
}
|
|
2349
341
|
return result;
|
|
2350
342
|
}
|
|
@@ -2353,12 +345,7 @@ export class F1RaceSimulation {
|
|
|
2353
345
|
const penalty = this.penalties.find((entry) => entry.id === penaltyId);
|
|
2354
346
|
const result = cancelPenaltyRecord(penalty, this.time);
|
|
2355
347
|
if (result) {
|
|
2356
|
-
this.events.unshift(
|
|
2357
|
-
type: 'penalty-cancelled',
|
|
2358
|
-
at: this.time,
|
|
2359
|
-
penaltyId: result.id,
|
|
2360
|
-
driverId: result.driverId,
|
|
2361
|
-
});
|
|
348
|
+
this.events.unshift(createPenaltyCancelledEvent(result, this.time));
|
|
2362
349
|
}
|
|
2363
350
|
return result;
|
|
2364
351
|
}
|
|
@@ -2380,175 +367,45 @@ export class F1RaceSimulation {
|
|
|
2380
367
|
}
|
|
2381
368
|
|
|
2382
369
|
applyGridDrop(driverId, positions) {
|
|
2383
|
-
|
|
2384
|
-
if (drop <= 0) return;
|
|
2385
|
-
|
|
2386
|
-
const ordered = [...this.cars].sort((left, right) => {
|
|
2387
|
-
const delta = right.gridDistance - left.gridDistance;
|
|
2388
|
-
return delta === 0 ? left.index - right.index : delta;
|
|
2389
|
-
});
|
|
2390
|
-
const currentIndex = ordered.findIndex((car) => car.id === driverId);
|
|
2391
|
-
if (currentIndex < 0) return;
|
|
2392
|
-
const [car] = ordered.splice(currentIndex, 1);
|
|
2393
|
-
ordered.splice(Math.min(ordered.length, currentIndex + drop), 0, car);
|
|
2394
|
-
|
|
2395
|
-
ordered.forEach((entry, index) => {
|
|
2396
|
-
const { gridDistance, gridOffset } = getStartGridSlot(index, { standingStart: true });
|
|
2397
|
-
const gridPoint = pointAt(this.track, gridDistance);
|
|
2398
|
-
const position = offsetTrackPoint(gridPoint, gridOffset);
|
|
2399
|
-
entry.gridDistance = gridDistance;
|
|
2400
|
-
entry.gridOffset = gridOffset;
|
|
2401
|
-
entry.rank = index + 1;
|
|
2402
|
-
if (entry.gridLocked) {
|
|
2403
|
-
entry.x = position.x;
|
|
2404
|
-
entry.y = position.y;
|
|
2405
|
-
entry.previousX = position.x;
|
|
2406
|
-
entry.previousY = position.y;
|
|
2407
|
-
entry.heading = gridPoint.heading;
|
|
2408
|
-
entry.previousHeading = gridPoint.heading;
|
|
2409
|
-
entry.progress = gridPoint.distance;
|
|
2410
|
-
entry.raceDistance = gridDistance;
|
|
2411
|
-
applyWheelSurfaceState(entry, this.track);
|
|
2412
|
-
}
|
|
2413
|
-
});
|
|
370
|
+
applyGridDropForSimulation(this, driverId, positions);
|
|
2414
371
|
}
|
|
2415
372
|
|
|
2416
373
|
reviewCollision(first, second, collision) {
|
|
2417
|
-
|
|
2418
|
-
calculateCollisionPenalties({ first, second, collision, rule })
|
|
2419
|
-
.forEach((penalty) => this.recordPenalty(penalty));
|
|
374
|
+
reviewCollisionForSimulation(this, first, second, collision);
|
|
2420
375
|
}
|
|
2421
376
|
|
|
2422
377
|
reviewTireRequirement(car) {
|
|
2423
378
|
if (this.stewardState.tireRequirement[car.id]) return;
|
|
2424
|
-
const rule = getPenaltyRule(this.rules, 'tireRequirement');
|
|
2425
|
-
const penalty = calculateTireRequirementPenalty({
|
|
2426
|
-
car,
|
|
2427
|
-
tireStrategy: this.rules.modules?.tireStrategy,
|
|
2428
|
-
rule,
|
|
2429
|
-
});
|
|
2430
379
|
this.stewardState.tireRequirement[car.id] = true;
|
|
2431
|
-
|
|
380
|
+
reviewTireRequirementForSimulation(this, car);
|
|
2432
381
|
}
|
|
2433
382
|
|
|
2434
383
|
reviewTrackLimits() {
|
|
2435
|
-
|
|
2436
|
-
if (!rule) return;
|
|
2437
|
-
|
|
2438
|
-
this.cars.forEach((car) => {
|
|
2439
|
-
const currentState = this.stewardState.trackLimits[car.id];
|
|
2440
|
-
if (isLegallyInsidePitLaneForTrackLimits(car) && !currentState?.active) {
|
|
2441
|
-
return;
|
|
2442
|
-
}
|
|
2443
|
-
const review = calculateTrackLimitReview({
|
|
2444
|
-
car,
|
|
2445
|
-
rule,
|
|
2446
|
-
track: this.track,
|
|
2447
|
-
stewardState: currentState,
|
|
2448
|
-
});
|
|
2449
|
-
this.stewardState.trackLimits[car.id] = review.nextState;
|
|
2450
|
-
if (review.event) this.events.unshift({ ...review.event, at: this.time });
|
|
2451
|
-
if (review.penalty) this.recordPenalty(review.penalty);
|
|
2452
|
-
});
|
|
384
|
+
reviewTrackLimitsForSimulation(this);
|
|
2453
385
|
}
|
|
2454
386
|
|
|
2455
387
|
reviewPitLaneSpeeding() {
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
const review = calculatePitLaneSpeedingReview({
|
|
2462
|
-
car: {
|
|
2463
|
-
...car,
|
|
2464
|
-
speedKph: simSpeedToKph(car.speed),
|
|
2465
|
-
},
|
|
2466
|
-
rule,
|
|
2467
|
-
stewardState: currentState,
|
|
2468
|
-
});
|
|
2469
|
-
this.stewardState.pitLaneSpeeding[car.id] = review.nextState;
|
|
2470
|
-
if (review.event) this.events.unshift({ ...review.event, at: this.time });
|
|
2471
|
-
if (review.penalty) this.recordPenalty(review.penalty);
|
|
2472
|
-
});
|
|
388
|
+
reviewPitLaneSpeedingForSimulation(this);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
vehicleSnapshotDependencies() {
|
|
392
|
+
return createVehicleSnapshotDependencies(this);
|
|
2473
393
|
}
|
|
2474
394
|
|
|
2475
395
|
snapshot() {
|
|
2476
|
-
|
|
2477
|
-
const pitLaneStatus = pitLaneStatusSnapshot(this.raceControl, this.track.pitLane, this.rules.modules?.pitStops);
|
|
2478
|
-
const penaltyStats = this.getPenaltyStatsByDriver();
|
|
2479
|
-
return {
|
|
2480
|
-
time: this.time,
|
|
2481
|
-
world: WORLD,
|
|
2482
|
-
track: this.track,
|
|
2483
|
-
totalLaps: this.totalLaps,
|
|
2484
|
-
raceControl: {
|
|
2485
|
-
mode: this.raceControl.mode,
|
|
2486
|
-
redFlag: Boolean(this.raceControl.redFlag),
|
|
2487
|
-
pitLaneOpen: pitLaneStatus.open,
|
|
2488
|
-
pitLaneStatus,
|
|
2489
|
-
finished: this.raceControl.finished,
|
|
2490
|
-
finishedAt: this.raceControl.finishedAt,
|
|
2491
|
-
winner: this.getRaceWinnerSnapshot(),
|
|
2492
|
-
classification: this.raceControl.classification.map((entry) => ({ ...entry })),
|
|
2493
|
-
start: {
|
|
2494
|
-
...this.raceControl.start,
|
|
2495
|
-
visible: this.raceControl.mode === 'pre-start' ||
|
|
2496
|
-
(this.raceControl.start.releasedAt != null && this.time - this.raceControl.start.releasedAt < 1.45),
|
|
2497
|
-
},
|
|
2498
|
-
},
|
|
2499
|
-
pitLaneStatus,
|
|
2500
|
-
safetyCar: { ...this.safetyCar },
|
|
2501
|
-
rules: this.rules,
|
|
2502
|
-
events: [...this.events],
|
|
2503
|
-
penalties: this.penalties.map(serializePenalty),
|
|
2504
|
-
cars: ordered.map((car, index) => serializeCar(car, index + 1, getPenaltyStats(penaltyStats, car.id).seconds)),
|
|
2505
|
-
};
|
|
396
|
+
return snapshotRace(this);
|
|
2506
397
|
}
|
|
2507
398
|
|
|
2508
399
|
snapshotRender() {
|
|
2509
|
-
|
|
2510
|
-
return {
|
|
2511
|
-
time: this.time,
|
|
2512
|
-
world: WORLD,
|
|
2513
|
-
track: this.track,
|
|
2514
|
-
totalLaps: this.totalLaps,
|
|
2515
|
-
raceControl: {
|
|
2516
|
-
mode: this.raceControl.mode,
|
|
2517
|
-
redFlag: Boolean(this.raceControl.redFlag),
|
|
2518
|
-
pitLaneOpen: pitLaneStatus.open,
|
|
2519
|
-
pitLaneStatus,
|
|
2520
|
-
finished: this.raceControl.finished,
|
|
2521
|
-
start: {
|
|
2522
|
-
...this.raceControl.start,
|
|
2523
|
-
visible: this.raceControl.mode === 'pre-start' ||
|
|
2524
|
-
(this.raceControl.start.releasedAt != null && this.time - this.raceControl.start.releasedAt < 1.45),
|
|
2525
|
-
},
|
|
2526
|
-
},
|
|
2527
|
-
pitLaneStatus,
|
|
2528
|
-
safetyCar: { ...this.safetyCar },
|
|
2529
|
-
cars: this.orderedCars().map(serializeRenderCar),
|
|
2530
|
-
};
|
|
400
|
+
return snapshotRaceRender(this);
|
|
2531
401
|
}
|
|
2532
402
|
|
|
2533
403
|
snapshotObservation() {
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
totalLaps: this.totalLaps,
|
|
2540
|
-
raceControl: {
|
|
2541
|
-
mode: this.raceControl.mode,
|
|
2542
|
-
redFlag: Boolean(this.raceControl.redFlag),
|
|
2543
|
-
pitLaneOpen: pitLaneStatus.open,
|
|
2544
|
-
pitLaneStatus,
|
|
2545
|
-
finished: this.raceControl.finished,
|
|
2546
|
-
},
|
|
2547
|
-
pitLaneStatus,
|
|
2548
|
-
safetyCar: { ...this.safetyCar },
|
|
2549
|
-
events: [...this.events],
|
|
2550
|
-
cars: this.orderedCars().map((car, index) => serializeObservationCar(car, index + 1)),
|
|
2551
|
-
};
|
|
404
|
+
return snapshotRaceObservation(this);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
snapshotTraining() {
|
|
408
|
+
return snapshotRaceTraining(this);
|
|
2552
409
|
}
|
|
2553
410
|
|
|
2554
411
|
consumeStepEvents() {
|
|
@@ -2556,552 +413,109 @@ export class F1RaceSimulation {
|
|
|
2556
413
|
}
|
|
2557
414
|
|
|
2558
415
|
orderedCars() {
|
|
2559
|
-
|
|
2560
|
-
const delta = b.raceDistance - a.raceDistance;
|
|
2561
|
-
return delta === 0 ? a.index - b.index : delta;
|
|
2562
|
-
});
|
|
2563
|
-
const byId = new Map(this.cars.map((car) => [car.id, car]));
|
|
2564
|
-
|
|
2565
|
-
if (this.raceControl.finished && this.raceControl.classification?.length) {
|
|
2566
|
-
const classified = this.raceControl.classification.map((entry) => byId.get(entry.id)).filter(Boolean);
|
|
2567
|
-
const classifiedIds = new Set(classified.map((car) => car.id));
|
|
2568
|
-
return [
|
|
2569
|
-
...classified,
|
|
2570
|
-
...sortLive(this.cars.filter((car) => !classifiedIds.has(car.id))),
|
|
2571
|
-
];
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
if (this.raceControl.finishOrder?.length) {
|
|
2575
|
-
const finished = this.raceControl.finishOrder.map((id) => byId.get(id)).filter(Boolean);
|
|
2576
|
-
const finishedIds = new Set(finished.map((car) => car.id));
|
|
2577
|
-
const missedFinished = this.cars
|
|
2578
|
-
.filter((car) => car.finished && !finishedIds.has(car.id))
|
|
2579
|
-
.sort((a, b) => {
|
|
2580
|
-
const delta = (a.finishRank ?? Infinity) - (b.finishRank ?? Infinity);
|
|
2581
|
-
return delta === 0 ? a.index - b.index : delta;
|
|
2582
|
-
});
|
|
2583
|
-
const running = sortLive(this.cars.filter((car) => !finishedIds.has(car.id) && !car.finished));
|
|
2584
|
-
return [...finished, ...missedFinished, ...running];
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
if (this.safetyCar.deployed && this.raceControl.frozenOrder?.length) {
|
|
2588
|
-
return this.raceControl.frozenOrder.map((id) => byId.get(id)).filter(Boolean);
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
return sortLive(this.cars);
|
|
416
|
+
return orderedCarsForSimulation(this);
|
|
2592
417
|
}
|
|
2593
418
|
|
|
2594
419
|
driverRaceContext(orderedCars = this.orderedCars()) {
|
|
2595
|
-
return
|
|
2596
|
-
track: this.track,
|
|
2597
|
-
cars: this.cars,
|
|
2598
|
-
orderedCars,
|
|
2599
|
-
safetyCar: this.safetyCar,
|
|
2600
|
-
rules: this.rules,
|
|
2601
|
-
};
|
|
420
|
+
return driverRaceContextForSimulation(this, orderedCars);
|
|
2602
421
|
}
|
|
2603
422
|
|
|
2604
423
|
computeAggression(car, orderIndex = Math.max(0, (car.rank ?? 1) - 1)) {
|
|
2605
|
-
|
|
2606
|
-
if (this.safetyCar.deployed || car.canAttack === false) {
|
|
2607
|
-
return clamp(personality.baseAggression * 0.62, 0.08, 0.62);
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
const fieldDepth = Math.max(1, this.cars.length - 1);
|
|
2611
|
-
const positionPressure = clamp(orderIndex / fieldDepth, 0, 1);
|
|
2612
|
-
const gapPressure = Number.isFinite(car.gapAhead) ? clamp((230 - car.gapAhead) / 230, 0, 1) : 0;
|
|
2613
|
-
const tireConfidence = clamp(((car.tireEnergy ?? 100) - 42) / 58, 0, 1);
|
|
2614
|
-
const patienceDamping = (1 - gapPressure) * personality.patience * 0.08;
|
|
2615
|
-
|
|
2616
|
-
return clamp(
|
|
2617
|
-
personality.baseAggression
|
|
2618
|
-
+ positionPressure * 0.26
|
|
2619
|
-
+ gapPressure * (0.1 + personality.riskTolerance * 0.08)
|
|
2620
|
-
- (1 - tireConfidence) * 0.1
|
|
2621
|
-
- patienceDamping,
|
|
2622
|
-
0.08,
|
|
2623
|
-
1,
|
|
2624
|
-
);
|
|
424
|
+
return computeAggressionForSimulation(this, car, orderIndex);
|
|
2625
425
|
}
|
|
2626
426
|
|
|
2627
427
|
updateStartSequence() {
|
|
2628
|
-
|
|
2629
|
-
if (this.raceControl.mode !== 'pre-start' || !start || start.released) return;
|
|
2630
|
-
|
|
2631
|
-
if (this.time >= start.lightsOutAt) {
|
|
2632
|
-
this.releaseRaceStart();
|
|
2633
|
-
this.events.unshift({ type: 'start-lights-out', at: this.time });
|
|
2634
|
-
return;
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
|
-
start.lightsLit = clamp(
|
|
2638
|
-
Math.floor((this.time + Number.EPSILON) / this.rules.startLightInterval),
|
|
2639
|
-
0,
|
|
2640
|
-
start.lightCount,
|
|
2641
|
-
);
|
|
428
|
+
return updateStartSequenceForSimulation(this);
|
|
2642
429
|
}
|
|
2643
430
|
|
|
2644
431
|
releaseRaceStart() {
|
|
2645
|
-
|
|
2646
|
-
this.raceControl.mode = 'green';
|
|
2647
|
-
start.lightsLit = 0;
|
|
2648
|
-
start.released = true;
|
|
2649
|
-
start.releasedAt = this.time;
|
|
2650
|
-
this.cars.forEach((car) => {
|
|
2651
|
-
const wasGridLocked = car.gridLocked;
|
|
2652
|
-
car.gridLocked = false;
|
|
2653
|
-
const state = nearestTrackState(this.track, car, car.gridDistance);
|
|
2654
|
-
car.progress = state.distance;
|
|
2655
|
-
applyWheelSurfaceState(car, this.track);
|
|
2656
|
-
if (wasGridLocked) car.raceDistance = car.gridDistance;
|
|
2657
|
-
if (wasGridLocked) car.desiredOffset = 0;
|
|
2658
|
-
car.previousX = car.x;
|
|
2659
|
-
car.previousY = car.y;
|
|
2660
|
-
car.previousHeading = car.heading;
|
|
2661
|
-
});
|
|
432
|
+
return releaseRaceStartForSimulation(this);
|
|
2662
433
|
}
|
|
2663
434
|
|
|
2664
435
|
holdGridCars() {
|
|
2665
|
-
this
|
|
2666
|
-
if (!car.gridLocked) return;
|
|
2667
|
-
const gridPoint = pointAt(this.track, car.gridDistance);
|
|
2668
|
-
const position = offsetTrackPoint(gridPoint, car.gridOffset);
|
|
2669
|
-
car.previousX = position.x;
|
|
2670
|
-
car.previousY = position.y;
|
|
2671
|
-
car.previousHeading = gridPoint.heading;
|
|
2672
|
-
car.x = position.x;
|
|
2673
|
-
car.y = position.y;
|
|
2674
|
-
car.heading = gridPoint.heading;
|
|
2675
|
-
car.speed = 0;
|
|
2676
|
-
car.throttle = 0;
|
|
2677
|
-
car.brake = 1;
|
|
2678
|
-
car.steeringAngle = 0;
|
|
2679
|
-
car.yawRate = 0;
|
|
2680
|
-
car.turnRadius = Infinity;
|
|
2681
|
-
car.progress = gridPoint.distance;
|
|
2682
|
-
car.raceDistance = car.gridDistance;
|
|
2683
|
-
applyWheelSurfaceState(car, this.track);
|
|
2684
|
-
});
|
|
436
|
+
return holdGridCarsForSimulation(this);
|
|
2685
437
|
}
|
|
2686
438
|
|
|
2687
439
|
updateSafetyCar(dt) {
|
|
2688
|
-
|
|
2689
|
-
const leader = this.orderedCars()[0];
|
|
2690
|
-
const targetProgress = (leader?.raceDistance ?? 0) + this.rules.safetyCarLeadDistance;
|
|
2691
|
-
const progress = this.safetyCar.progress < targetProgress
|
|
2692
|
-
? Math.min(this.safetyCar.progress + this.safetyCar.speed * dt, targetProgress)
|
|
2693
|
-
: targetProgress;
|
|
2694
|
-
this.moveSafetyCarTo(progress);
|
|
440
|
+
updateSafetyCarState(this, dt);
|
|
2695
441
|
}
|
|
2696
442
|
|
|
2697
443
|
moveSafetyCarTo(progress) {
|
|
2698
|
-
|
|
2699
|
-
this.safetyCar.previousX = this.safetyCar.x;
|
|
2700
|
-
this.safetyCar.previousY = this.safetyCar.y;
|
|
2701
|
-
this.safetyCar.previousHeading = this.safetyCar.heading;
|
|
2702
|
-
this.safetyCar.progress = progress;
|
|
2703
|
-
this.safetyCar.x = point.x;
|
|
2704
|
-
this.safetyCar.y = point.y;
|
|
2705
|
-
this.safetyCar.heading = point.heading;
|
|
444
|
+
moveSafetyCarToState(this, progress);
|
|
2706
445
|
}
|
|
2707
446
|
|
|
2708
447
|
applyRunoffResponse(car) {
|
|
2709
|
-
|
|
2710
|
-
if (state.inPitLane) {
|
|
2711
|
-
applyWheelSurfaceState(car, this.track, { centerState: state });
|
|
2712
|
-
return;
|
|
2713
|
-
}
|
|
2714
|
-
const signedLimit = this.track.width / 2 + this.track.gravelWidth + this.track.runoffWidth;
|
|
2715
|
-
const overshoot = Math.abs(state.signedOffset) - signedLimit;
|
|
2716
|
-
if (overshoot <= 0) {
|
|
2717
|
-
applyWheelSurfaceState(car, this.track, { centerState: state });
|
|
2718
|
-
return;
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
const side = Math.sign(state.signedOffset) || 1;
|
|
2722
|
-
car.x -= state.normalX * side * overshoot;
|
|
2723
|
-
car.y -= state.normalY * side * overshoot;
|
|
2724
|
-
car.speed = clamp(car.speed * clamp(1 - overshoot * 0.012, 0.22, 0.86), 0, VEHICLE_LIMITS.maxSpeed);
|
|
2725
|
-
car.heading = normalizeAngle(car.heading - side * clamp(overshoot * 0.0028, 0.018, 0.08));
|
|
2726
|
-
applyWheelSurfaceState(car, this.track);
|
|
448
|
+
applyRunoffResponseForSimulation(this, car);
|
|
2727
449
|
}
|
|
2728
450
|
|
|
2729
451
|
recalculateRaceState({ updateDrs = true } = {}) {
|
|
2730
|
-
this
|
|
2731
|
-
const previousRaceDistance = car.raceDistance;
|
|
2732
|
-
if (car.gridLocked) {
|
|
2733
|
-
const gridPoint = pointAt(this.track, car.gridDistance);
|
|
2734
|
-
applyWheelSurfaceState(car, this.track);
|
|
2735
|
-
car.progress = gridPoint.distance;
|
|
2736
|
-
car.raceDistance = car.gridDistance;
|
|
2737
|
-
car.lap = 1;
|
|
2738
|
-
resetLapTelemetry(car, this.time, this.track, this.totalLaps);
|
|
2739
|
-
return;
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
applyWheelSurfaceState(car, this.track);
|
|
2743
|
-
const previousProgress = car.progress ?? car.trackState.distance;
|
|
2744
|
-
const delta = progressDelta(car.trackState.distance, previousProgress, this.track.length);
|
|
2745
|
-
car.raceDistance = (car.raceDistance ?? previousProgress) + delta;
|
|
2746
|
-
car.progress = car.trackState.distance;
|
|
2747
|
-
car.lap = this.computeLap(car.raceDistance);
|
|
2748
|
-
updateLapTelemetry(car, previousRaceDistance, this.time, this.track, this.totalLaps);
|
|
2749
|
-
});
|
|
2750
|
-
|
|
2751
|
-
updateSectorPerformance(this.cars);
|
|
2752
|
-
this.cars.forEach((car) => {
|
|
2753
|
-
recordTimingSample(car, this.time);
|
|
2754
|
-
recordTimingLineCrossings(car, car.previousRaceDistanceForTiming, this.time, this.track);
|
|
2755
|
-
car.previousRaceDistanceForTiming = car.raceDistance;
|
|
2756
|
-
});
|
|
2757
|
-
|
|
2758
|
-
const ordered = this.orderedCars();
|
|
2759
|
-
const leader = ordered[0];
|
|
2760
|
-
ordered.forEach((car, index) => {
|
|
2761
|
-
const ahead = ordered[index - 1];
|
|
2762
|
-
const drsReference = this.getDrsReferenceCar(car);
|
|
2763
|
-
const gap = ahead ? ahead.raceDistance - car.raceDistance : Infinity;
|
|
2764
|
-
const intervalAheadLaps = ahead ? wholeLapGap(ahead.raceDistance, car.raceDistance, this.track.length) : 0;
|
|
2765
|
-
const leaderGapLaps = leader ? wholeLapGap(leader.raceDistance, car.raceDistance, this.track.length) : 0;
|
|
2766
|
-
const activePitStop = this.isCarInActivePitStop(car);
|
|
2767
|
-
car.rank = index + 1;
|
|
2768
|
-
car.gapAhead = gap;
|
|
2769
|
-
car.gapAheadLaps = intervalAheadLaps;
|
|
2770
|
-
car.intervalAheadLaps = intervalAheadLaps;
|
|
2771
|
-
car.leaderGapLaps = leaderGapLaps;
|
|
2772
|
-
car.gapAheadSeconds = Number.isFinite(gap) && intervalAheadLaps === 0
|
|
2773
|
-
? estimateGapAheadSeconds(ahead, car, this.time, this.track)
|
|
2774
|
-
: Infinity;
|
|
2775
|
-
car.intervalAheadSeconds = car.gapAheadSeconds;
|
|
2776
|
-
car.leaderGapSeconds = leaderGapLaps > 0
|
|
2777
|
-
? Infinity
|
|
2778
|
-
: (leader && leader !== car ? estimateGapAheadSeconds(leader, car, this.time, this.track) : 0);
|
|
2779
|
-
car.canAttack = !this.safetyCar.deployed && !car.finished && !activePitStop;
|
|
2780
|
-
car.aggression = this.computeAggression(car, index);
|
|
2781
|
-
if (activePitStop) {
|
|
2782
|
-
car.drsEligible = false;
|
|
2783
|
-
car.drsActive = false;
|
|
2784
|
-
car.drsZoneId = null;
|
|
2785
|
-
car.drsZoneEnabled = false;
|
|
2786
|
-
} else if (updateDrs) this.updateDrsLatch(car, drsReference, Boolean(drsReference));
|
|
2787
|
-
});
|
|
2788
|
-
this.evaluateRaceFinish();
|
|
452
|
+
return recalculateRaceStateForSimulation(this, { updateDrs });
|
|
2789
453
|
}
|
|
2790
454
|
|
|
2791
455
|
evaluateRaceFinish() {
|
|
2792
|
-
|
|
2793
|
-
if (this.raceControl.mode === 'pre-start') return;
|
|
2794
|
-
|
|
2795
|
-
const ordered = this.orderedCars();
|
|
2796
|
-
const newlyFinished = ordered.filter((car) => !car.finished && car.raceDistance >= this.finishDistance);
|
|
2797
|
-
if (newlyFinished.length === 0) return;
|
|
2798
|
-
|
|
2799
|
-
newlyFinished.forEach((car) => {
|
|
2800
|
-
car.finished = true;
|
|
2801
|
-
car.finishTime = this.time;
|
|
2802
|
-
car.finishRank = this.raceControl.finishOrder.length + 1;
|
|
2803
|
-
car.classifiedRank = car.finishRank;
|
|
2804
|
-
this.raceControl.finishOrder.push(car.id);
|
|
2805
|
-
if (!this.raceControl.winnerId) this.raceControl.winnerId = car.id;
|
|
2806
|
-
car.drsActive = false;
|
|
2807
|
-
car.drsEligible = false;
|
|
2808
|
-
car.drsZoneId = null;
|
|
2809
|
-
car.drsZoneEnabled = false;
|
|
2810
|
-
car.canAttack = false;
|
|
2811
|
-
this.events.unshift({
|
|
2812
|
-
type: 'car-finish',
|
|
2813
|
-
at: this.time,
|
|
2814
|
-
carId: car.id,
|
|
2815
|
-
rank: car.finishRank,
|
|
2816
|
-
winnerId: this.raceControl.winnerId,
|
|
2817
|
-
});
|
|
2818
|
-
this.reviewTireRequirement(car);
|
|
2819
|
-
});
|
|
2820
|
-
|
|
2821
|
-
if (!this.cars.every((car) => car.finished)) return;
|
|
2822
|
-
|
|
2823
|
-
this.applyOutstandingServicePenalties();
|
|
2824
|
-
const classification = this.buildClassificationFromFinishOrder();
|
|
2825
|
-
this.raceControl.winnerId = classification[0]?.id ?? this.raceControl.winnerId;
|
|
2826
|
-
this.raceControl.mode = 'safety-car';
|
|
2827
|
-
this.raceControl.finished = true;
|
|
2828
|
-
this.raceControl.finishedAt = this.time;
|
|
2829
|
-
this.raceControl.classification = classification;
|
|
2830
|
-
this.raceControl.frozenOrder = classification.map((entry) => entry.id);
|
|
2831
|
-
this.safetyCar.deployed = true;
|
|
2832
|
-
const leader = this.cars.find((car) => car.id === this.raceControl.winnerId) ?? ordered[0];
|
|
2833
|
-
const safetyCarProgress = (leader?.raceDistance ?? 0) + this.rules.safetyCarLeadDistance;
|
|
2834
|
-
if (this.safetyCar.progress < safetyCarProgress) {
|
|
2835
|
-
this.moveSafetyCarTo(safetyCarProgress);
|
|
2836
|
-
}
|
|
2837
|
-
this.cars.forEach((car) => {
|
|
2838
|
-
const classified = classification.find((entry) => entry.id === car.id);
|
|
2839
|
-
car.classifiedRank = classified?.rank ?? car.rank;
|
|
2840
|
-
car.desiredOffset = 0;
|
|
2841
|
-
car.drsActive = false;
|
|
2842
|
-
car.drsEligible = false;
|
|
2843
|
-
car.drsZoneId = null;
|
|
2844
|
-
car.drsZoneEnabled = false;
|
|
2845
|
-
car.canAttack = false;
|
|
2846
|
-
});
|
|
2847
|
-
this.events.unshift({
|
|
2848
|
-
type: 'race-finish',
|
|
2849
|
-
at: this.time,
|
|
2850
|
-
winnerId: this.raceControl.winnerId,
|
|
2851
|
-
classification: classification.map((entry) => ({ id: entry.id, rank: entry.rank })),
|
|
2852
|
-
});
|
|
456
|
+
evaluateRaceFinishForSimulation(this);
|
|
2853
457
|
}
|
|
2854
458
|
|
|
2855
459
|
applyOutstandingServicePenalties() {
|
|
2856
|
-
this
|
|
2857
|
-
const beforeStatus = penalty.status;
|
|
2858
|
-
applyUnservedServicePenalty(penalty, this.time);
|
|
2859
|
-
if (beforeStatus !== penalty.status && penalty.unserved) {
|
|
2860
|
-
this.events.unshift({
|
|
2861
|
-
type: 'penalty-applied',
|
|
2862
|
-
at: this.time,
|
|
2863
|
-
penaltyId: penalty.id,
|
|
2864
|
-
driverId: penalty.driverId,
|
|
2865
|
-
serviceType: penalty.serviceType,
|
|
2866
|
-
penaltySeconds: penalty.penaltySeconds,
|
|
2867
|
-
});
|
|
2868
|
-
}
|
|
2869
|
-
});
|
|
460
|
+
applyOutstandingServicePenaltiesState(this, applyUnservedServicePenalty);
|
|
2870
461
|
}
|
|
2871
462
|
|
|
2872
463
|
buildClassificationFromFinishOrder() {
|
|
2873
|
-
|
|
2874
|
-
const penaltyStats = this.getPenaltyStatsByDriver();
|
|
2875
|
-
const orderedByAdjustedTime = this.raceControl.finishOrder
|
|
2876
|
-
.map((id, finishOrderIndex) => ({ car: byId.get(id), finishOrderIndex }))
|
|
2877
|
-
.filter((entry) => Boolean(entry.car))
|
|
2878
|
-
.sort((left, right) => {
|
|
2879
|
-
const leftTime = (left.car.finishTime ?? Infinity) + getPenaltyStats(penaltyStats, left.car.id).seconds;
|
|
2880
|
-
const rightTime = (right.car.finishTime ?? Infinity) + getPenaltyStats(penaltyStats, right.car.id).seconds;
|
|
2881
|
-
return leftTime === rightTime ? left.finishOrderIndex - right.finishOrderIndex : leftTime - rightTime;
|
|
2882
|
-
})
|
|
2883
|
-
.map((entry) => entry.car);
|
|
2884
|
-
const ordered = this.applyClassificationConsequences(orderedByAdjustedTime, penaltyStats);
|
|
2885
|
-
return this.buildClassification(ordered, penaltyStats);
|
|
464
|
+
return buildClassificationFromFinishOrderState(this);
|
|
2886
465
|
}
|
|
2887
466
|
|
|
2888
467
|
applyClassificationConsequences(ordered, penaltyStats = this.getPenaltyStatsByDriver()) {
|
|
2889
|
-
|
|
2890
|
-
ordered.forEach((car) => {
|
|
2891
|
-
const stats = getPenaltyStats(penaltyStats, car.id);
|
|
2892
|
-
if (stats.disqualified) return;
|
|
2893
|
-
const drop = stats.positionDrop;
|
|
2894
|
-
if (drop <= 0) return;
|
|
2895
|
-
const currentIndex = classified.findIndex((entry) => entry.id === car.id);
|
|
2896
|
-
if (currentIndex < 0) return;
|
|
2897
|
-
const [entry] = classified.splice(currentIndex, 1);
|
|
2898
|
-
classified.splice(Math.min(classified.length, currentIndex + drop), 0, entry);
|
|
2899
|
-
});
|
|
2900
|
-
return [
|
|
2901
|
-
...classified,
|
|
2902
|
-
...ordered.filter((car) => getPenaltyStats(penaltyStats, car.id).disqualified),
|
|
2903
|
-
];
|
|
468
|
+
return applyClassificationConsequencesState(this, ordered, penaltyStats);
|
|
2904
469
|
}
|
|
2905
470
|
|
|
2906
471
|
buildClassification(ordered = this.orderedCars(), penaltyStats = this.getPenaltyStatsByDriver()) {
|
|
2907
|
-
|
|
2908
|
-
return ordered.map((car, index) => {
|
|
2909
|
-
const finishTime = car.finishTime ?? (car.raceDistance >= this.finishDistance ? this.time : null);
|
|
2910
|
-
const stats = getPenaltyStats(penaltyStats, car.id);
|
|
2911
|
-
const penaltySeconds = stats.seconds;
|
|
2912
|
-
const positionDrop = stats.positionDrop;
|
|
2913
|
-
const disqualified = stats.disqualified;
|
|
2914
|
-
return {
|
|
2915
|
-
id: car.id,
|
|
2916
|
-
code: car.code,
|
|
2917
|
-
timingCode: car.timingCode,
|
|
2918
|
-
name: car.name,
|
|
2919
|
-
rank: index + 1,
|
|
2920
|
-
raceDistance: car.raceDistance,
|
|
2921
|
-
distanceMeters: simUnitsToMeters(car.raceDistance),
|
|
2922
|
-
lap: this.computeLap(car.raceDistance),
|
|
2923
|
-
lapsCompleted: clamp(Math.floor(Math.max(0, car.raceDistance) / this.track.length), 0, this.totalLaps),
|
|
2924
|
-
gapMeters: simUnitsToMeters(Math.max(0, leaderDistance - car.raceDistance)),
|
|
2925
|
-
gapSeconds: index === 0 ? 0 : car.leaderGapSeconds,
|
|
2926
|
-
intervalSeconds: index === 0 ? 0 : car.intervalAheadSeconds,
|
|
2927
|
-
gapLaps: index === 0 ? 0 : car.leaderGapLaps,
|
|
2928
|
-
intervalLaps: index === 0 ? 0 : car.intervalAheadLaps,
|
|
2929
|
-
finished: car.raceDistance >= this.finishDistance,
|
|
2930
|
-
finishTime,
|
|
2931
|
-
penaltySeconds,
|
|
2932
|
-
adjustedFinishTime: finishTime == null ? null : finishTime + penaltySeconds,
|
|
2933
|
-
positionDrop,
|
|
2934
|
-
disqualified,
|
|
2935
|
-
};
|
|
2936
|
-
});
|
|
472
|
+
return buildClassificationState(this, ordered, penaltyStats);
|
|
2937
473
|
}
|
|
2938
474
|
|
|
2939
475
|
getRaceWinnerSnapshot() {
|
|
2940
|
-
|
|
2941
|
-
const car = this.cars.find((item) => item.id === this.raceControl.winnerId);
|
|
2942
|
-
if (!car) return null;
|
|
2943
|
-
const rank = car.classifiedRank ?? car.rank ?? 1;
|
|
2944
|
-
return serializeCar(car, rank, getPenaltyStats(this.getPenaltyStatsByDriver(), car.id).seconds);
|
|
476
|
+
return getRaceWinnerSnapshotForSimulation(this);
|
|
2945
477
|
}
|
|
2946
478
|
|
|
2947
479
|
getDrsReferenceCar(car) {
|
|
2948
|
-
|
|
2949
|
-
this.cars.forEach((candidate) => {
|
|
2950
|
-
if (candidate === car || candidate.finished || this.isCarInActivePitStop(candidate)) return;
|
|
2951
|
-
const delta = distanceForward(car.progress ?? car.raceDistance ?? 0, candidate.progress ?? candidate.raceDistance ?? 0, this.track.length);
|
|
2952
|
-
if (delta <= 0 || delta > this.track.length / 2) return;
|
|
2953
|
-
if (!closest || delta < closest.delta) closest = { car: candidate, delta };
|
|
2954
|
-
});
|
|
2955
|
-
return closest?.car ?? null;
|
|
480
|
+
return getDrsReferenceCarForSimulation(this, car);
|
|
2956
481
|
}
|
|
2957
482
|
|
|
2958
483
|
updateDrsLatch(car, ahead, hasReference = Boolean(ahead)) {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
const previousProgress = car.previousProgress ?? car.progress;
|
|
2968
|
-
const currentZone = car.drsZoneId
|
|
2969
|
-
? this.track.drsZones.find((zone) => zone.id === car.drsZoneId)
|
|
2970
|
-
: null;
|
|
2971
|
-
|
|
2972
|
-
if (currentZone && !isProgressInZone(this.track, car.progress, currentZone)) {
|
|
2973
|
-
car.drsZoneId = null;
|
|
2974
|
-
car.drsZoneEnabled = false;
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
if (!car.drsZoneId) {
|
|
2978
|
-
const crossedZone = this.track.drsZones.find((zone) => (
|
|
2979
|
-
crossesDistance(previousProgress, car.progress, zone.start, this.track.length)
|
|
2980
|
-
));
|
|
2981
|
-
if (crossedZone) {
|
|
2982
|
-
car.drsZoneId = crossedZone.id;
|
|
2983
|
-
const crossing = recordDrsDetection(car, crossedZone.id, this.time);
|
|
2984
|
-
const aheadCrossing = ahead?.drsDetection?.[crossedZone.id];
|
|
2985
|
-
car.drsZoneEnabled = Boolean(
|
|
2986
|
-
hasReference &&
|
|
2987
|
-
aheadCrossing &&
|
|
2988
|
-
aheadCrossing.passage === crossing.passage &&
|
|
2989
|
-
crossing.time >= aheadCrossing.time &&
|
|
2990
|
-
crossing.time - aheadCrossing.time <= this.rules.drsDetectionSeconds + 1e-6
|
|
2991
|
-
);
|
|
2992
|
-
}
|
|
2993
|
-
}
|
|
2994
|
-
|
|
2995
|
-
const activeZone = car.drsZoneId
|
|
2996
|
-
? this.track.drsZones.find((zone) => zone.id === car.drsZoneId)
|
|
2997
|
-
: null;
|
|
2998
|
-
const inLatchedZone = activeZone ? isProgressInZone(this.track, car.progress, activeZone) : false;
|
|
2999
|
-
car.drsEligible = Boolean(car.drsZoneEnabled && inLatchedZone);
|
|
3000
|
-
car.drsActive = car.drsEligible;
|
|
484
|
+
updateDrsLatchState(car, ahead, {
|
|
485
|
+
hasReference,
|
|
486
|
+
safetyCarDeployed: this.safetyCar.deployed,
|
|
487
|
+
time: this.time,
|
|
488
|
+
track: this.track,
|
|
489
|
+
rules: this.rules,
|
|
490
|
+
});
|
|
3001
491
|
}
|
|
3002
492
|
|
|
3003
493
|
resolveCollisions() {
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
for (let pass = 0; pass < 3; pass += 1) {
|
|
3007
|
-
const candidates = buildCollisionCandidatePairs(this.cars, { trackLength: this.track.length });
|
|
3008
|
-
for (const [first, second] of candidates) {
|
|
3009
|
-
const collision = detectVehicleCollision(first, second);
|
|
3010
|
-
if (!collision) continue;
|
|
3011
|
-
const firstPitControlled = isPitPositionControlledCar(first);
|
|
3012
|
-
const secondPitControlled = isPitPositionControlledCar(second);
|
|
3013
|
-
if (firstPitControlled && secondPitControlled) continue;
|
|
3014
|
-
const stewardCollision = createCollisionStewardContext(first, second, {
|
|
3015
|
-
...collision,
|
|
3016
|
-
trackLength: this.track.length,
|
|
3017
|
-
});
|
|
3018
|
-
const oneCarFixed = firstPitControlled || secondPitControlled;
|
|
3019
|
-
|
|
3020
|
-
const correction = Math.min(
|
|
3021
|
-
(oneCarFixed ? collision.depth : collision.depth / 2) + 0.65,
|
|
3022
|
-
MAX_COLLISION_CORRECTION,
|
|
3023
|
-
);
|
|
3024
|
-
const firstCorrectionX = firstPitControlled ? 0 : -collision.axis.x * correction;
|
|
3025
|
-
const firstCorrectionY = firstPitControlled ? 0 : -collision.axis.y * correction;
|
|
3026
|
-
const secondCorrectionX = secondPitControlled ? 0 : collision.axis.x * correction;
|
|
3027
|
-
const secondCorrectionY = secondPitControlled ? 0 : collision.axis.y * correction;
|
|
3028
|
-
if (!firstPitControlled) {
|
|
3029
|
-
first.x += firstCorrectionX;
|
|
3030
|
-
first.y += firstCorrectionY;
|
|
3031
|
-
}
|
|
3032
|
-
if (!secondPitControlled) {
|
|
3033
|
-
second.x += secondCorrectionX;
|
|
3034
|
-
second.y += secondCorrectionY;
|
|
3035
|
-
}
|
|
3036
|
-
|
|
3037
|
-
if (oneCarFixed) {
|
|
3038
|
-
if (!firstPitControlled) first.speed = clamp(first.speed * 0.985, 0, VEHICLE_LIMITS.maxSpeed);
|
|
3039
|
-
if (!secondPitControlled) second.speed = clamp(second.speed * 0.985, 0, VEHICLE_LIMITS.maxSpeed);
|
|
3040
|
-
} else {
|
|
3041
|
-
this.applyContactVelocityResponse(first, second, collision.axis);
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
const yawNudge = clamp(collision.depth * 0.0025, 0.008, 0.035);
|
|
3045
|
-
const freshContact = first.contactCooldown <= 0 && second.contactCooldown <= 0;
|
|
3046
|
-
const firstHeadingCorrection = firstPitControlled ? 0 : -collision.axis.y * yawNudge;
|
|
3047
|
-
const secondHeadingCorrection = secondPitControlled ? 0 : collision.axis.y * yawNudge;
|
|
3048
|
-
if (!firstPitControlled) {
|
|
3049
|
-
first.heading = normalizeAngle(first.heading + firstHeadingCorrection);
|
|
3050
|
-
shiftPreviousRenderPose(first, firstCorrectionX, firstCorrectionY, firstHeadingCorrection);
|
|
3051
|
-
}
|
|
3052
|
-
if (!secondPitControlled) {
|
|
3053
|
-
second.heading = normalizeAngle(second.heading + secondHeadingCorrection);
|
|
3054
|
-
shiftPreviousRenderPose(second, secondCorrectionX, secondCorrectionY, secondHeadingCorrection);
|
|
3055
|
-
}
|
|
3056
|
-
first.contactCooldown = 1;
|
|
3057
|
-
second.contactCooldown = 1;
|
|
3058
|
-
|
|
3059
|
-
const contactKey = `${first.id}:${second.id}`;
|
|
3060
|
-
if (freshContact && pass === 0 && !reportedContacts.has(contactKey)) {
|
|
3061
|
-
reportedContacts.add(contactKey);
|
|
3062
|
-
this.events.unshift({
|
|
3063
|
-
type: 'contact',
|
|
3064
|
-
at: this.time,
|
|
3065
|
-
carId: first.id,
|
|
3066
|
-
otherCarId: second.id,
|
|
3067
|
-
firstShapeId: collision.firstShapeId,
|
|
3068
|
-
secondShapeId: collision.secondShapeId,
|
|
3069
|
-
contactType: collision.contactType,
|
|
3070
|
-
depth: collision.depth,
|
|
3071
|
-
timeOfImpact: collision.timeOfImpact,
|
|
3072
|
-
});
|
|
3073
|
-
this.reviewCollision(first, second, stewardCollision);
|
|
3074
|
-
}
|
|
3075
|
-
}
|
|
3076
|
-
}
|
|
494
|
+
resolveCollisionsForSimulation(this);
|
|
3077
495
|
}
|
|
3078
496
|
|
|
3079
497
|
applyContactVelocityResponse(first, second, axis) {
|
|
3080
|
-
|
|
3081
|
-
const secondForward = forwardVector(second);
|
|
3082
|
-
const firstNormal = dot(firstForward, axis);
|
|
3083
|
-
const secondNormal = dot(secondForward, axis);
|
|
3084
|
-
const relativeNormalVelocity = second.speed * secondNormal - first.speed * firstNormal;
|
|
3085
|
-
|
|
3086
|
-
if (relativeNormalVelocity < 0) {
|
|
3087
|
-
const impulse = clamp(-relativeNormalVelocity * (0.34 + this.rules.collisionRestitution), 0, 16);
|
|
3088
|
-
if (firstNormal > 0) first.speed = clamp(first.speed - impulse * firstNormal, 0, VEHICLE_LIMITS.maxSpeed);
|
|
3089
|
-
if (secondNormal < 0) second.speed = clamp(second.speed + impulse * secondNormal, 0, VEHICLE_LIMITS.maxSpeed);
|
|
3090
|
-
}
|
|
3091
|
-
|
|
3092
|
-
first.speed = clamp(first.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
3093
|
-
second.speed = clamp(second.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
498
|
+
applyContactVelocityResponseForSimulation(this, first, second, axis);
|
|
3094
499
|
}
|
|
3095
500
|
|
|
3096
501
|
computeLap(raceDistance) {
|
|
3097
|
-
return
|
|
502
|
+
return computeLapForDistance(raceDistance, this.track.length, this.totalLaps);
|
|
3098
503
|
}
|
|
3099
504
|
|
|
3100
505
|
get finishDistance() {
|
|
3101
|
-
return this.track.length
|
|
506
|
+
return finishDistanceForRace(this.track.length, this.totalLaps);
|
|
3102
507
|
}
|
|
3103
508
|
}
|
|
3104
509
|
|
|
3105
510
|
export function createRaceSimulation(options = {}) {
|
|
3106
|
-
|
|
511
|
+
const resolvedOptions = withWarmupSurfaceOptions(options, options?.warmup?.surface ?? 'simulation');
|
|
512
|
+
runWarmupWithGuard({
|
|
513
|
+
options: resolvedOptions,
|
|
514
|
+
surface: resolvedOptions.warmup.surface,
|
|
515
|
+
execute: ({ warmup }) => {
|
|
516
|
+
const warmupSimulation = new F1RaceSimulation(disableWarmupOptions(resolvedOptions, warmup.surface));
|
|
517
|
+
for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
return new F1RaceSimulation(resolvedOptions);
|
|
3107
521
|
}
|