@inventure71/paddockjs 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +194 -12
- package/docs/architecture.md +119 -44
- package/docs/concepts.md +4 -2
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +299 -17
- package/docs/index.md +1 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +37 -10
- package/docs/system_specs.md +23 -10
- package/docs/training.md +272 -30
- 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 +4 -0
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +305 -1603
- 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/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +361 -9
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observations.js +325 -40
- package/src/environment/options.js +37 -5
- package/src/environment/runtime.js +222 -21
- package/src/environment/scenarios.js +37 -8
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +65 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +100 -5
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +169 -6
- package/src/index.js +5 -0
- 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 +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +92 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +230 -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 +141 -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 +192 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryIndex.js +945 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -1706
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -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,175 @@
|
|
|
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
|
-
|
|
1524
|
-
}
|
|
1525
|
-
if (
|
|
1526
|
-
car.pitStop.routeProgress = nearestDistanceOnRoute(car.pitStop.route, car, car.pitStop.routeProgress ?? 0);
|
|
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
|
+
progressDelta,
|
|
156
|
+
raceControl: this.raceControl,
|
|
157
|
+
releaseRaceStart: () => this.releaseRaceStart(),
|
|
158
|
+
resetLapTelemetry,
|
|
159
|
+
resetTimingHistory,
|
|
160
|
+
resetTimingLineCrossings,
|
|
161
|
+
time: this.time,
|
|
162
|
+
totalLaps: this.totalLaps,
|
|
163
|
+
track: this.track,
|
|
164
|
+
};
|
|
165
|
+
let applied = false;
|
|
166
|
+
Object.entries(partialsById).forEach(([id, partial]) => {
|
|
167
|
+
const car = this.cars.find((item) => item.id === id);
|
|
168
|
+
if (!car) return;
|
|
169
|
+
applyExternalCarState(car, partial, context);
|
|
170
|
+
applied = true;
|
|
171
|
+
});
|
|
172
|
+
if (!applied) return;
|
|
1542
173
|
this.recalculateRaceState({ updateDrs: false });
|
|
1543
174
|
this.evaluateRaceFinish();
|
|
1544
175
|
}
|
|
@@ -1563,7 +194,7 @@ export class F1RaceSimulation {
|
|
|
1563
194
|
|
|
1564
195
|
getPitIntent(id) {
|
|
1565
196
|
const car = this.cars.find((item) => item.id === id);
|
|
1566
|
-
return
|
|
197
|
+
return normalizePitIntentForRace(car);
|
|
1567
198
|
}
|
|
1568
199
|
|
|
1569
200
|
getPitTargetCompound(id) {
|
|
@@ -1572,749 +203,115 @@ export class F1RaceSimulation {
|
|
|
1572
203
|
}
|
|
1573
204
|
|
|
1574
205
|
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;
|
|
206
|
+
return setPitIntentForSimulation(this, id, intent, targetCompound);
|
|
1609
207
|
}
|
|
1610
208
|
|
|
1611
209
|
isCarInActivePitStop(car) {
|
|
1612
|
-
return
|
|
210
|
+
return isCarInActivePitStopForSimulation(this, car);
|
|
1613
211
|
}
|
|
1614
212
|
|
|
1615
213
|
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;
|
|
214
|
+
return schedulePitStopAtNextEntryForSimulation(this, car, stop);
|
|
1632
215
|
}
|
|
1633
216
|
|
|
1634
217
|
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);
|
|
218
|
+
return rearmCompletedPitStopForSimulation(this, car, stop);
|
|
1650
219
|
}
|
|
1651
220
|
|
|
1652
221
|
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
|
-
});
|
|
222
|
+
return canStartPitStopForSimulation(this, car);
|
|
1671
223
|
}
|
|
1672
224
|
|
|
1673
225
|
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));
|
|
226
|
+
return shouldStartPitStopForSimulation(this, car);
|
|
1686
227
|
}
|
|
1687
228
|
|
|
1688
229
|
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;
|
|
230
|
+
return updateAutomaticPitIntentForSimulation(this, car);
|
|
231
|
+
}
|
|
1697
232
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
if (nextIntent === PIT_INTENT_NONE || nextIntent <= (normalizePitIntent(stop.intent) ?? PIT_INTENT_NONE)) return;
|
|
233
|
+
isPitLaneOpenForStops() {
|
|
234
|
+
return isPitLaneOpenForStopsForSimulation(this);
|
|
235
|
+
}
|
|
1702
236
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
else this.schedulePitStopAtNextEntry(car, stop);
|
|
237
|
+
pitEntryLateThresholdDistance() {
|
|
238
|
+
return pitEntryLateThresholdDistanceForSimulation();
|
|
1706
239
|
}
|
|
1707
240
|
|
|
1708
241
|
getPitStopBox(stop) {
|
|
1709
|
-
return this
|
|
1710
|
-
this.track.pitLane?.boxes?.find((box) => box.id === stop?.boxId) ??
|
|
1711
|
-
null;
|
|
242
|
+
return getPitStopBoxForSimulation(this, stop);
|
|
1712
243
|
}
|
|
1713
244
|
|
|
1714
245
|
isPitServiceBusy(car, box) {
|
|
1715
|
-
return this
|
|
1716
|
-
candidate !== car &&
|
|
1717
|
-
candidate.pitStop?.boxId === box?.id &&
|
|
1718
|
-
this.isPitServiceAreaOccupied(candidate, box)
|
|
1719
|
-
));
|
|
246
|
+
return isPitServiceBusyForSimulation(this, car, box);
|
|
1720
247
|
}
|
|
1721
248
|
|
|
1722
249
|
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;
|
|
250
|
+
return isPitServiceAreaOccupiedForSimulation(this, candidate, box);
|
|
1730
251
|
}
|
|
1731
252
|
|
|
1732
253
|
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
|
-
));
|
|
254
|
+
return isPitServiceQueueOccupiedForSimulation(this, car, box);
|
|
1741
255
|
}
|
|
1742
256
|
|
|
1743
257
|
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;
|
|
258
|
+
return getPitBoxRaceDistanceForSimulation(this, stop, box);
|
|
1748
259
|
}
|
|
1749
260
|
|
|
1750
261
|
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;
|
|
262
|
+
return startPitStopForSimulation(this, car);
|
|
1809
263
|
}
|
|
1810
264
|
|
|
1811
265
|
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;
|
|
266
|
+
return beginPitQueueForSimulation(this, car, box);
|
|
1832
267
|
}
|
|
1833
268
|
|
|
1834
269
|
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;
|
|
270
|
+
return releasePitQueueForSimulation(this, car, box, { fromCurrent });
|
|
1858
271
|
}
|
|
1859
272
|
|
|
1860
273
|
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);
|
|
274
|
+
return beginPitServiceForSimulation(this, car, box);
|
|
1880
275
|
}
|
|
1881
276
|
|
|
1882
277
|
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;
|
|
278
|
+
return beginPitPenaltyServiceForSimulation(this, car);
|
|
1909
279
|
}
|
|
1910
280
|
|
|
1911
281
|
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;
|
|
282
|
+
return calculatePitServiceProfileForSimulation(this, car);
|
|
1951
283
|
}
|
|
1952
284
|
|
|
1953
285
|
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
|
-
});
|
|
286
|
+
return beginTireServiceForSimulation(this, car);
|
|
1971
287
|
}
|
|
1972
288
|
|
|
1973
289
|
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);
|
|
290
|
+
return completePitPenaltyServiceForSimulation(this, car);
|
|
2001
291
|
}
|
|
2002
292
|
|
|
2003
293
|
getPitServicePenalties(driverId) {
|
|
2004
|
-
return this
|
|
2005
|
-
penalty.driverId === driverId && isPenaltyPitServiceable(penalty)
|
|
2006
|
-
));
|
|
294
|
+
return getPitServicePenaltiesForSimulation(this, driverId);
|
|
2007
295
|
}
|
|
2008
296
|
|
|
2009
297
|
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;
|
|
298
|
+
return completePitServiceForSimulation(this, car);
|
|
2042
299
|
}
|
|
2043
300
|
|
|
2044
301
|
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
|
-
});
|
|
302
|
+
return finishPitExitForSimulation(this, car);
|
|
2068
303
|
}
|
|
2069
304
|
|
|
2070
305
|
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;
|
|
306
|
+
return applyPitRoutePositionForSimulation(this, car, delta);
|
|
2172
307
|
}
|
|
2173
308
|
|
|
2174
309
|
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;
|
|
310
|
+
return advancePitStopCarForSimulation(this, car, delta);
|
|
2259
311
|
}
|
|
2260
312
|
|
|
2261
313
|
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();
|
|
314
|
+
runRaceStep(this, dt);
|
|
2318
315
|
}
|
|
2319
316
|
|
|
2320
317
|
recordPenalty(penalty) {
|
|
@@ -2338,13 +335,7 @@ export class F1RaceSimulation {
|
|
|
2338
335
|
const penalty = this.penalties.find((entry) => entry.id === penaltyId);
|
|
2339
336
|
const result = servePenaltyRecord(penalty, this.time);
|
|
2340
337
|
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
|
-
});
|
|
338
|
+
this.events.unshift(createPenaltyServedEvent(result, this.time));
|
|
2348
339
|
}
|
|
2349
340
|
return result;
|
|
2350
341
|
}
|
|
@@ -2353,12 +344,7 @@ export class F1RaceSimulation {
|
|
|
2353
344
|
const penalty = this.penalties.find((entry) => entry.id === penaltyId);
|
|
2354
345
|
const result = cancelPenaltyRecord(penalty, this.time);
|
|
2355
346
|
if (result) {
|
|
2356
|
-
this.events.unshift(
|
|
2357
|
-
type: 'penalty-cancelled',
|
|
2358
|
-
at: this.time,
|
|
2359
|
-
penaltyId: result.id,
|
|
2360
|
-
driverId: result.driverId,
|
|
2361
|
-
});
|
|
347
|
+
this.events.unshift(createPenaltyCancelledEvent(result, this.time));
|
|
2362
348
|
}
|
|
2363
349
|
return result;
|
|
2364
350
|
}
|
|
@@ -2380,175 +366,45 @@ export class F1RaceSimulation {
|
|
|
2380
366
|
}
|
|
2381
367
|
|
|
2382
368
|
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
|
-
});
|
|
369
|
+
applyGridDropForSimulation(this, driverId, positions);
|
|
2414
370
|
}
|
|
2415
371
|
|
|
2416
372
|
reviewCollision(first, second, collision) {
|
|
2417
|
-
|
|
2418
|
-
calculateCollisionPenalties({ first, second, collision, rule })
|
|
2419
|
-
.forEach((penalty) => this.recordPenalty(penalty));
|
|
373
|
+
reviewCollisionForSimulation(this, first, second, collision);
|
|
2420
374
|
}
|
|
2421
375
|
|
|
2422
376
|
reviewTireRequirement(car) {
|
|
2423
377
|
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
378
|
this.stewardState.tireRequirement[car.id] = true;
|
|
2431
|
-
|
|
379
|
+
reviewTireRequirementForSimulation(this, car);
|
|
2432
380
|
}
|
|
2433
381
|
|
|
2434
382
|
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
|
-
});
|
|
383
|
+
reviewTrackLimitsForSimulation(this);
|
|
2453
384
|
}
|
|
2454
385
|
|
|
2455
386
|
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
|
-
});
|
|
387
|
+
reviewPitLaneSpeedingForSimulation(this);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
vehicleSnapshotDependencies() {
|
|
391
|
+
return createVehicleSnapshotDependencies(this);
|
|
2473
392
|
}
|
|
2474
393
|
|
|
2475
394
|
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
|
-
};
|
|
395
|
+
return snapshotRace(this);
|
|
2506
396
|
}
|
|
2507
397
|
|
|
2508
398
|
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
|
-
};
|
|
399
|
+
return snapshotRaceRender(this);
|
|
2531
400
|
}
|
|
2532
401
|
|
|
2533
402
|
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
|
-
};
|
|
403
|
+
return snapshotRaceObservation(this);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
snapshotTraining() {
|
|
407
|
+
return snapshotRaceTraining(this);
|
|
2552
408
|
}
|
|
2553
409
|
|
|
2554
410
|
consumeStepEvents() {
|
|
@@ -2556,552 +412,109 @@ export class F1RaceSimulation {
|
|
|
2556
412
|
}
|
|
2557
413
|
|
|
2558
414
|
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);
|
|
415
|
+
return orderedCarsForSimulation(this);
|
|
2592
416
|
}
|
|
2593
417
|
|
|
2594
418
|
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
|
-
};
|
|
419
|
+
return driverRaceContextForSimulation(this, orderedCars);
|
|
2602
420
|
}
|
|
2603
421
|
|
|
2604
422
|
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
|
-
);
|
|
423
|
+
return computeAggressionForSimulation(this, car, orderIndex);
|
|
2625
424
|
}
|
|
2626
425
|
|
|
2627
426
|
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
|
-
);
|
|
427
|
+
return updateStartSequenceForSimulation(this);
|
|
2642
428
|
}
|
|
2643
429
|
|
|
2644
430
|
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
|
-
});
|
|
431
|
+
return releaseRaceStartForSimulation(this);
|
|
2662
432
|
}
|
|
2663
433
|
|
|
2664
434
|
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
|
-
});
|
|
435
|
+
return holdGridCarsForSimulation(this);
|
|
2685
436
|
}
|
|
2686
437
|
|
|
2687
438
|
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);
|
|
439
|
+
updateSafetyCarState(this, dt);
|
|
2695
440
|
}
|
|
2696
441
|
|
|
2697
442
|
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;
|
|
443
|
+
moveSafetyCarToState(this, progress);
|
|
2706
444
|
}
|
|
2707
445
|
|
|
2708
446
|
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);
|
|
447
|
+
applyRunoffResponseForSimulation(this, car);
|
|
2727
448
|
}
|
|
2728
449
|
|
|
2729
450
|
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();
|
|
451
|
+
return recalculateRaceStateForSimulation(this, { updateDrs });
|
|
2789
452
|
}
|
|
2790
453
|
|
|
2791
454
|
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
|
-
});
|
|
455
|
+
evaluateRaceFinishForSimulation(this);
|
|
2853
456
|
}
|
|
2854
457
|
|
|
2855
458
|
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
|
-
});
|
|
459
|
+
applyOutstandingServicePenaltiesState(this, applyUnservedServicePenalty);
|
|
2870
460
|
}
|
|
2871
461
|
|
|
2872
462
|
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);
|
|
463
|
+
return buildClassificationFromFinishOrderState(this);
|
|
2886
464
|
}
|
|
2887
465
|
|
|
2888
466
|
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
|
-
];
|
|
467
|
+
return applyClassificationConsequencesState(this, ordered, penaltyStats);
|
|
2904
468
|
}
|
|
2905
469
|
|
|
2906
470
|
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
|
-
});
|
|
471
|
+
return buildClassificationState(this, ordered, penaltyStats);
|
|
2937
472
|
}
|
|
2938
473
|
|
|
2939
474
|
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);
|
|
475
|
+
return getRaceWinnerSnapshotForSimulation(this);
|
|
2945
476
|
}
|
|
2946
477
|
|
|
2947
478
|
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;
|
|
479
|
+
return getDrsReferenceCarForSimulation(this, car);
|
|
2956
480
|
}
|
|
2957
481
|
|
|
2958
482
|
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;
|
|
483
|
+
updateDrsLatchState(car, ahead, {
|
|
484
|
+
hasReference,
|
|
485
|
+
safetyCarDeployed: this.safetyCar.deployed,
|
|
486
|
+
time: this.time,
|
|
487
|
+
track: this.track,
|
|
488
|
+
rules: this.rules,
|
|
489
|
+
});
|
|
3001
490
|
}
|
|
3002
491
|
|
|
3003
492
|
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
|
-
}
|
|
493
|
+
resolveCollisionsForSimulation(this);
|
|
3077
494
|
}
|
|
3078
495
|
|
|
3079
496
|
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);
|
|
497
|
+
applyContactVelocityResponseForSimulation(this, first, second, axis);
|
|
3094
498
|
}
|
|
3095
499
|
|
|
3096
500
|
computeLap(raceDistance) {
|
|
3097
|
-
return
|
|
501
|
+
return computeLapForDistance(raceDistance, this.track.length, this.totalLaps);
|
|
3098
502
|
}
|
|
3099
503
|
|
|
3100
504
|
get finishDistance() {
|
|
3101
|
-
return this.track.length
|
|
505
|
+
return finishDistanceForRace(this.track.length, this.totalLaps);
|
|
3102
506
|
}
|
|
3103
507
|
}
|
|
3104
508
|
|
|
3105
509
|
export function createRaceSimulation(options = {}) {
|
|
3106
|
-
|
|
510
|
+
const resolvedOptions = withWarmupSurfaceOptions(options, options?.warmup?.surface ?? 'simulation');
|
|
511
|
+
runWarmupWithGuard({
|
|
512
|
+
options: resolvedOptions,
|
|
513
|
+
surface: resolvedOptions.warmup.surface,
|
|
514
|
+
execute: ({ warmup }) => {
|
|
515
|
+
const warmupSimulation = new F1RaceSimulation(disableWarmupOptions(resolvedOptions, warmup.surface));
|
|
516
|
+
for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
return new F1RaceSimulation(resolvedOptions);
|
|
3107
520
|
}
|