@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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { metersToSimUnits } from '../units.js';
|
|
2
|
+
import { firstDifferentCompound } from './pitIntent.js';
|
|
3
|
+
|
|
4
|
+
const PIT_ENTRY_APPROACH_DISTANCE = metersToSimUnits(250);
|
|
5
|
+
|
|
6
|
+
export function normalizePitCrewStats(value) {
|
|
7
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
8
|
+
const rating = (entry) => {
|
|
9
|
+
const numeric = Number(entry);
|
|
10
|
+
return Number.isFinite(numeric) ? Math.max(0, Math.min(1, numeric)) : 0.5;
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
speed: rating(source.speed ?? source.pace),
|
|
14
|
+
consistency: rating(source.consistency),
|
|
15
|
+
reliability: rating(source.reliability),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function assignPitLaneTeams({ cars, pitLane }) {
|
|
20
|
+
if (!pitLane?.enabled || !Array.isArray(pitLane.boxes)) return;
|
|
21
|
+
const boxesPerTeam = pitLane.boxesPerTeam ?? 2;
|
|
22
|
+
const teamCount = pitLane.teamCount ?? Math.ceil(pitLane.boxes.length / boxesPerTeam);
|
|
23
|
+
|
|
24
|
+
pitLane.teams = Array.from({ length: teamCount }, (_, teamIndex) => {
|
|
25
|
+
const primary = cars[teamIndex * boxesPerTeam] ?? cars[teamIndex] ?? null;
|
|
26
|
+
const secondary = cars[teamIndex * boxesPerTeam + 1] ?? null;
|
|
27
|
+
const team = primary?.team ?? secondary?.team ?? null;
|
|
28
|
+
const id = team?.id ?? `team-${teamIndex + 1}`;
|
|
29
|
+
const name = team?.name ?? (primary ? `${primary.name} Team` : `Team ${teamIndex + 1}`);
|
|
30
|
+
const color = team?.color ?? primary?.color ?? secondary?.color ?? '#f8fafc';
|
|
31
|
+
const pitCrew = normalizePitCrewStats(
|
|
32
|
+
team?.pitCrew ?? team?.pitCrewStats ?? primary?.team?.pitCrew ?? secondary?.team?.pitCrew,
|
|
33
|
+
);
|
|
34
|
+
const boxIds = pitLane.boxes
|
|
35
|
+
.filter((box) => box.teamIndex === teamIndex)
|
|
36
|
+
.map((box) => box.id);
|
|
37
|
+
|
|
38
|
+
pitLane.boxes
|
|
39
|
+
.filter((box) => box.teamIndex === teamIndex)
|
|
40
|
+
.forEach((box) => {
|
|
41
|
+
box.teamId = id;
|
|
42
|
+
box.teamName = name;
|
|
43
|
+
box.teamColor = color;
|
|
44
|
+
});
|
|
45
|
+
const serviceArea = pitLane.serviceAreas?.[teamIndex] ?? null;
|
|
46
|
+
if (serviceArea) {
|
|
47
|
+
serviceArea.teamId = id;
|
|
48
|
+
serviceArea.teamName = name;
|
|
49
|
+
serviceArea.teamColor = color;
|
|
50
|
+
serviceArea.pitCrew = pitCrew;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
name,
|
|
56
|
+
color,
|
|
57
|
+
index: teamIndex,
|
|
58
|
+
boxIds,
|
|
59
|
+
serviceAreaId: serviceArea?.id ?? null,
|
|
60
|
+
pitCrew,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function initializePitStops({ cars, pitLane, pitStops, totalLaps, trackLength, tireCompounds, PIT_INTENT_NONE }) {
|
|
66
|
+
if (!pitStops?.enabled || !pitLane?.enabled || !Array.isArray(pitLane.boxes) || totalLaps < 2) return;
|
|
67
|
+
const boxesPerTeam = pitLane.boxesPerTeam ?? 2;
|
|
68
|
+
const maxConcurrentPitLaneCars = Math.max(1, Math.floor(pitStops.maxConcurrentPitLaneCars ?? 3));
|
|
69
|
+
const pitWindowLapCount = Math.max(1, Math.min(
|
|
70
|
+
totalLaps - 1,
|
|
71
|
+
Math.ceil(cars.length / maxConcurrentPitLaneCars),
|
|
72
|
+
));
|
|
73
|
+
|
|
74
|
+
cars.forEach((car, index) => {
|
|
75
|
+
const stopLapBase = trackLength * (1 + (index % pitWindowLapCount));
|
|
76
|
+
const trainPosition = Math.floor(index / pitWindowLapCount);
|
|
77
|
+
const teamIndex = Math.floor(index / boxesPerTeam);
|
|
78
|
+
const teamBoxIndex = index % boxesPerTeam;
|
|
79
|
+
const garageBoxIndex = Math.min(pitLane.boxes.length - 1, teamIndex * boxesPerTeam + teamBoxIndex);
|
|
80
|
+
const garageBox = pitLane.boxes[garageBoxIndex] ?? pitLane.boxes[index % pitLane.boxes.length];
|
|
81
|
+
const serviceArea = pitLane.serviceAreas?.[teamIndex] ??
|
|
82
|
+
pitLane.serviceAreas?.[index % pitLane.serviceAreas.length] ??
|
|
83
|
+
garageBox;
|
|
84
|
+
car.pitStop = {
|
|
85
|
+
status: 'pending',
|
|
86
|
+
phase: null,
|
|
87
|
+
boxIndex: serviceArea.index,
|
|
88
|
+
boxId: serviceArea.id,
|
|
89
|
+
garageBoxIndex,
|
|
90
|
+
garageBoxId: garageBox.id,
|
|
91
|
+
teamId: serviceArea.teamId ?? garageBox.teamId ?? null,
|
|
92
|
+
teamColor: serviceArea.teamColor ?? garageBox.teamColor ?? null,
|
|
93
|
+
stopsCompleted: 0,
|
|
94
|
+
entryRaceDistance: stopLapBase + pitLane.entry.distanceFromStart,
|
|
95
|
+
plannedRaceDistance: stopLapBase + pitLane.entry.distanceFromStart -
|
|
96
|
+
PIT_ENTRY_APPROACH_DISTANCE,
|
|
97
|
+
trainPosition,
|
|
98
|
+
lapBase: stopLapBase,
|
|
99
|
+
serviceRemaining: 0,
|
|
100
|
+
penaltyServiceRemaining: 0,
|
|
101
|
+
penaltyServiceTotal: 0,
|
|
102
|
+
servingPenaltyIds: [],
|
|
103
|
+
serviceProfile: null,
|
|
104
|
+
queueingForService: false,
|
|
105
|
+
route: null,
|
|
106
|
+
routeProgress: 0,
|
|
107
|
+
routeStartRaceDistance: null,
|
|
108
|
+
routeEndRaceDistance: null,
|
|
109
|
+
targetTire: firstDifferentCompound(car.tire, tireCompounds),
|
|
110
|
+
intent: PIT_INTENT_NONE,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { kphToSimSpeed } from '../units.js';
|
|
2
|
+
import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
3
|
+
import {
|
|
4
|
+
createRoute,
|
|
5
|
+
pitDriveLaneOffset,
|
|
6
|
+
pitMainLanePointAt,
|
|
7
|
+
routePoint,
|
|
8
|
+
} from './pitRouting.js';
|
|
9
|
+
import { getPitBoxRaceDistance, getPitStopBox } from './pitOccupancy.js';
|
|
10
|
+
import { calculatePitServiceProfile } from './pitServiceProfile.js';
|
|
11
|
+
import { PIT_BOX_APPROACH_DISTANCE, PIT_EXIT_RELEASE_SPEED_KPH } from './pitServiceConstants.js';
|
|
12
|
+
|
|
13
|
+
export function beginTireService(sim, car) {
|
|
14
|
+
const stop = car.pitStop;
|
|
15
|
+
if (!stop) return;
|
|
16
|
+
const serviceProfile = calculatePitServiceProfile(sim, car);
|
|
17
|
+
stop.phase = 'service';
|
|
18
|
+
stop.penaltyServiceRemaining = 0;
|
|
19
|
+
stop.serviceProfile = serviceProfile;
|
|
20
|
+
stop.serviceRemaining = serviceProfile.seconds;
|
|
21
|
+
sim.events.unshift({
|
|
22
|
+
type: 'pit-stop-start',
|
|
23
|
+
at: sim.time,
|
|
24
|
+
carId: car.id,
|
|
25
|
+
boxId: stop.boxId,
|
|
26
|
+
teamId: stop.teamId ?? null,
|
|
27
|
+
targetTire: stop.targetTire,
|
|
28
|
+
serviceSeconds: serviceProfile.seconds,
|
|
29
|
+
serviceIssue: serviceProfile.issue,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function completePitService(sim, car) {
|
|
34
|
+
const stop = car.pitStop;
|
|
35
|
+
const box = getPitStopBox(sim, stop);
|
|
36
|
+
const pitLane = sim.track.pitLane;
|
|
37
|
+
if (!stop || !box || !pitLane?.enabled) return;
|
|
38
|
+
|
|
39
|
+
car.tire = stop.targetTire ?? car.tire;
|
|
40
|
+
car.tireEnergy = 100;
|
|
41
|
+
if (car.tire && !car.usedTireCompounds.includes(car.tire)) car.usedTireCompounds.push(car.tire);
|
|
42
|
+
sim.events.unshift({
|
|
43
|
+
type: 'pit-stop-complete',
|
|
44
|
+
at: sim.time,
|
|
45
|
+
carId: car.id,
|
|
46
|
+
boxId: box.id,
|
|
47
|
+
teamId: box.teamId ?? null,
|
|
48
|
+
tire: car.tire,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const driveLaneOffset = pitDriveLaneOffset(pitLane);
|
|
52
|
+
const boxReleaseDistance = Math.min(pitLane.mainLane.length, box.distanceAlongLane + PIT_BOX_APPROACH_DISTANCE);
|
|
53
|
+
const route = createRoute([
|
|
54
|
+
routePoint(box.center, pitLane.mainLane.heading, { limiterActive: true }),
|
|
55
|
+
routePoint(box.laneTarget, pitLane.mainLane.heading, { limiterActive: true }),
|
|
56
|
+
routePoint(pitMainLanePointAt(pitLane, boxReleaseDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
57
|
+
routePoint(pitMainLanePointAt(pitLane, pitLane.mainLane.length, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
58
|
+
...(pitLane.exit.roadCenterline ?? []).map((point) => routePoint(point)),
|
|
59
|
+
]);
|
|
60
|
+
stop.status = 'exiting';
|
|
61
|
+
stop.phase = 'exit';
|
|
62
|
+
stop.route = route;
|
|
63
|
+
stop.routeProgress = 0;
|
|
64
|
+
stop.routeStartRaceDistance = getPitBoxRaceDistance(sim, stop, box);
|
|
65
|
+
stop.routeEndRaceDistance = stop.lapBase + pitLane.exit.distanceFromStart;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function finishPitExit(sim, car) {
|
|
69
|
+
const stop = car.pitStop;
|
|
70
|
+
if (!stop) return;
|
|
71
|
+
stop.status = 'completed';
|
|
72
|
+
stop.phase = null;
|
|
73
|
+
stop.route = null;
|
|
74
|
+
stop.routeProgress = 0;
|
|
75
|
+
stop.serviceRemaining = 0;
|
|
76
|
+
stop.queueingForService = false;
|
|
77
|
+
stop.stopsCompleted = (stop.stopsCompleted ?? 0) + 1;
|
|
78
|
+
stop.intent = 0;
|
|
79
|
+
car.speed = Math.max(car.speed, kphToSimSpeed(PIT_EXIT_RELEASE_SPEED_KPH));
|
|
80
|
+
car.throttle = 0.55;
|
|
81
|
+
car.brake = 0;
|
|
82
|
+
applyWheelSurfaceState(car, sim.track);
|
|
83
|
+
car.progress = car.trackState.distance;
|
|
84
|
+
car.raceDistance = Math.max(car.raceDistance ?? 0, stop.routeEndRaceDistance ?? car.raceDistance ?? 0);
|
|
85
|
+
sim.events.unshift({
|
|
86
|
+
type: 'pit-exit',
|
|
87
|
+
at: sim.time,
|
|
88
|
+
carId: car.id,
|
|
89
|
+
boxId: stop.boxId,
|
|
90
|
+
teamId: stop.teamId ?? null,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { clamp } from '../simMath.js';
|
|
2
|
+
import { simUnitsToMeters } from '../units.js';
|
|
3
|
+
import { isRaceDnf, raceDnfCars } from './retirements.js';
|
|
4
|
+
|
|
5
|
+
function getPenaltyStats(statsByDriver, driverId) {
|
|
6
|
+
return statsByDriver.get(driverId) ?? {
|
|
7
|
+
seconds: 0,
|
|
8
|
+
positionDrop: 0,
|
|
9
|
+
disqualified: false,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function applyOutstandingServicePenalties(sim, applyUnservedServicePenalty) {
|
|
14
|
+
sim.penalties.forEach((penalty) => {
|
|
15
|
+
const beforeStatus = penalty.status;
|
|
16
|
+
applyUnservedServicePenalty(penalty, sim.time);
|
|
17
|
+
if (beforeStatus !== penalty.status && penalty.unserved) {
|
|
18
|
+
sim.events.unshift({
|
|
19
|
+
type: 'penalty-applied',
|
|
20
|
+
at: sim.time,
|
|
21
|
+
penaltyId: penalty.id,
|
|
22
|
+
driverId: penalty.driverId,
|
|
23
|
+
serviceType: penalty.serviceType,
|
|
24
|
+
penaltySeconds: penalty.penaltySeconds,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildClassificationFromFinishOrder(sim) {
|
|
31
|
+
const byId = new Map(sim.cars.map((car) => [car.id, car]));
|
|
32
|
+
const penaltyStats = sim.getPenaltyStatsByDriver();
|
|
33
|
+
const finishOrderIds = new Set(sim.raceControl.finishOrder);
|
|
34
|
+
const orderedByAdjustedTime = sim.raceControl.finishOrder
|
|
35
|
+
.map((id, finishOrderIndex) => ({ car: byId.get(id), finishOrderIndex }))
|
|
36
|
+
.filter((entry) => Boolean(entry.car))
|
|
37
|
+
.sort((left, right) => {
|
|
38
|
+
const leftTime = (left.car.finishTime ?? Infinity) + getPenaltyStats(penaltyStats, left.car.id).seconds;
|
|
39
|
+
const rightTime = (right.car.finishTime ?? Infinity) + getPenaltyStats(penaltyStats, right.car.id).seconds;
|
|
40
|
+
return leftTime === rightTime ? left.finishOrderIndex - right.finishOrderIndex : leftTime - rightTime;
|
|
41
|
+
})
|
|
42
|
+
.map((entry) => entry.car);
|
|
43
|
+
const finishedOrder = applyClassificationConsequences(sim, orderedByAdjustedTime, penaltyStats);
|
|
44
|
+
const dnfOrder = raceDnfCars(sim.cars.filter((car) => (
|
|
45
|
+
!finishOrderIds.has(car.id) && car.interaction?.affectsRaceOrder !== false
|
|
46
|
+
)));
|
|
47
|
+
return buildClassification(sim, [...finishedOrder, ...dnfOrder], penaltyStats);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function applyClassificationConsequences(sim, ordered, penaltyStats = sim.getPenaltyStatsByDriver()) {
|
|
51
|
+
const classified = ordered.filter((car) => !getPenaltyStats(penaltyStats, car.id).disqualified);
|
|
52
|
+
ordered.forEach((car) => {
|
|
53
|
+
const stats = getPenaltyStats(penaltyStats, car.id);
|
|
54
|
+
if (stats.disqualified) return;
|
|
55
|
+
const drop = stats.positionDrop;
|
|
56
|
+
if (drop <= 0) return;
|
|
57
|
+
const currentIndex = classified.findIndex((entry) => entry.id === car.id);
|
|
58
|
+
if (currentIndex < 0) return;
|
|
59
|
+
const [entry] = classified.splice(currentIndex, 1);
|
|
60
|
+
classified.splice(Math.min(classified.length, currentIndex + drop), 0, entry);
|
|
61
|
+
});
|
|
62
|
+
return [
|
|
63
|
+
...classified,
|
|
64
|
+
...ordered.filter((car) => getPenaltyStats(penaltyStats, car.id).disqualified),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildClassification(sim, ordered = sim.orderedCars(), penaltyStats = sim.getPenaltyStatsByDriver()) {
|
|
69
|
+
const leaderDistance = ordered[0]?.raceDistance ?? 0;
|
|
70
|
+
return ordered.map((car, index) => {
|
|
71
|
+
const dnf = isRaceDnf(car);
|
|
72
|
+
const finishTime = dnf ? null : (car.finishTime ?? (car.raceDistance >= sim.finishDistance ? sim.time : null));
|
|
73
|
+
const stats = getPenaltyStats(penaltyStats, car.id);
|
|
74
|
+
const penaltySeconds = stats.seconds;
|
|
75
|
+
const positionDrop = stats.positionDrop;
|
|
76
|
+
const disqualified = stats.disqualified;
|
|
77
|
+
return {
|
|
78
|
+
id: car.id,
|
|
79
|
+
code: car.code,
|
|
80
|
+
timingCode: car.timingCode,
|
|
81
|
+
name: car.name,
|
|
82
|
+
rank: index + 1,
|
|
83
|
+
raceDistance: car.raceDistance,
|
|
84
|
+
distanceMeters: simUnitsToMeters(car.raceDistance),
|
|
85
|
+
lap: sim.computeLap(car.raceDistance),
|
|
86
|
+
lapsCompleted: clamp(Math.floor(Math.max(0, car.raceDistance) / sim.track.length), 0, sim.totalLaps),
|
|
87
|
+
gapMeters: simUnitsToMeters(Math.max(0, leaderDistance - car.raceDistance)),
|
|
88
|
+
gapSeconds: index === 0 ? 0 : car.leaderGapSeconds,
|
|
89
|
+
intervalSeconds: index === 0 ? 0 : car.intervalAheadSeconds,
|
|
90
|
+
gapLaps: index === 0 ? 0 : car.leaderGapLaps,
|
|
91
|
+
intervalLaps: index === 0 ? 0 : car.intervalAheadLaps,
|
|
92
|
+
finished: !dnf && Boolean(car.finished || car.raceDistance >= sim.finishDistance),
|
|
93
|
+
finishTime,
|
|
94
|
+
dnf,
|
|
95
|
+
dnfReason: car.dnfReason ?? car.destroyReason ?? null,
|
|
96
|
+
dnfAt: car.dnfAt ?? car.destroyedAt ?? null,
|
|
97
|
+
dnfOrder: car.dnfOrder ?? null,
|
|
98
|
+
penaltySeconds,
|
|
99
|
+
adjustedFinishTime: finishTime == null ? null : finishTime + penaltySeconds,
|
|
100
|
+
positionDrop,
|
|
101
|
+
disqualified,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { pointAt, offsetTrackPoint } from '../track/trackModel.js';
|
|
2
|
+
import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
3
|
+
import { getStartGridSlot } from '../vehicle/vehicleState.js';
|
|
4
|
+
|
|
5
|
+
export function applyGridDropForSimulation(simulation, driverId, positions) {
|
|
6
|
+
const drop = Math.max(0, Math.floor(Number(positions) || 0));
|
|
7
|
+
if (drop <= 0) return;
|
|
8
|
+
|
|
9
|
+
const ordered = [...simulation.cars].sort((left, right) => {
|
|
10
|
+
const delta = right.gridDistance - left.gridDistance;
|
|
11
|
+
return delta === 0 ? left.index - right.index : delta;
|
|
12
|
+
});
|
|
13
|
+
const currentIndex = ordered.findIndex((car) => car.id === driverId);
|
|
14
|
+
if (currentIndex < 0) return;
|
|
15
|
+
const [car] = ordered.splice(currentIndex, 1);
|
|
16
|
+
ordered.splice(Math.min(ordered.length, currentIndex + drop), 0, car);
|
|
17
|
+
|
|
18
|
+
ordered.forEach((entry, index) => {
|
|
19
|
+
const { gridDistance, gridOffset } = getStartGridSlot(index, { standingStart: true });
|
|
20
|
+
const gridPoint = pointAt(simulation.track, gridDistance);
|
|
21
|
+
const position = offsetTrackPoint(gridPoint, gridOffset);
|
|
22
|
+
entry.gridDistance = gridDistance;
|
|
23
|
+
entry.gridOffset = gridOffset;
|
|
24
|
+
entry.rank = index + 1;
|
|
25
|
+
if (entry.gridLocked) {
|
|
26
|
+
entry.x = position.x;
|
|
27
|
+
entry.y = position.y;
|
|
28
|
+
entry.previousX = position.x;
|
|
29
|
+
entry.previousY = position.y;
|
|
30
|
+
entry.heading = gridPoint.heading;
|
|
31
|
+
entry.previousHeading = gridPoint.heading;
|
|
32
|
+
entry.progress = gridPoint.distance;
|
|
33
|
+
entry.raceDistance = gridDistance;
|
|
34
|
+
applyWheelSurfaceState(entry, simulation.track);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { pointAt } from '../track/trackModel.js';
|
|
2
|
+
|
|
3
|
+
export function createRaceControlState(rules, startLightsOutAt) {
|
|
4
|
+
return {
|
|
5
|
+
mode: rules.standingStart === false ? 'green' : 'pre-start',
|
|
6
|
+
frozenOrder: null,
|
|
7
|
+
redFlag: false,
|
|
8
|
+
pitLaneOpen: true,
|
|
9
|
+
finished: false,
|
|
10
|
+
finishedAt: null,
|
|
11
|
+
winnerId: null,
|
|
12
|
+
classification: [],
|
|
13
|
+
finishOrder: [],
|
|
14
|
+
nextDnfOrder: 1,
|
|
15
|
+
start: {
|
|
16
|
+
lightCount: rules.startLightCount,
|
|
17
|
+
lightsLit: 0,
|
|
18
|
+
lightsOutAt: startLightsOutAt,
|
|
19
|
+
released: rules.standingStart === false,
|
|
20
|
+
releasedAt: rules.standingStart === false ? 0 : null,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createSafetyCarState(track, rules) {
|
|
26
|
+
const safetyCarStart = pointAt(track, rules.safetyCarLeadDistance);
|
|
27
|
+
return {
|
|
28
|
+
deployed: false,
|
|
29
|
+
progress: rules.safetyCarLeadDistance,
|
|
30
|
+
speed: rules.safetyCarSpeed,
|
|
31
|
+
previousX: safetyCarStart.x,
|
|
32
|
+
previousY: safetyCarStart.y,
|
|
33
|
+
previousHeading: safetyCarStart.heading,
|
|
34
|
+
x: safetyCarStart.x,
|
|
35
|
+
y: safetyCarStart.y,
|
|
36
|
+
heading: safetyCarStart.heading,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { clamp, wrapDistance } from '../simMath.js';
|
|
2
|
+
|
|
3
|
+
const MIN_TOTAL_LAPS = 1;
|
|
4
|
+
|
|
5
|
+
export function normalizeTotalLaps(value, { minimum = MIN_TOTAL_LAPS } = {}) {
|
|
6
|
+
const numeric = Number(value);
|
|
7
|
+
if (!Number.isFinite(numeric)) return minimum;
|
|
8
|
+
return Math.max(minimum, Math.floor(numeric));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function wrapProgress(value, length) {
|
|
12
|
+
return wrapDistance(value, length);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function distanceForward(from, to, length) {
|
|
16
|
+
return wrapProgress(to - from, length);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function progressDelta(a, b, trackLength) {
|
|
20
|
+
let delta = a - b;
|
|
21
|
+
if (delta < -trackLength / 2) delta += trackLength;
|
|
22
|
+
if (delta > trackLength / 2) delta -= trackLength;
|
|
23
|
+
return delta;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function computeLapForDistance(raceDistance, trackLength, totalLaps) {
|
|
27
|
+
return clamp(Math.floor(Math.max(0, raceDistance) / trackLength) + 1, 1, totalLaps);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function finishDistanceForRace(trackLength, totalLaps) {
|
|
31
|
+
return trackLength * totalLaps;
|
|
32
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isRaceDnf } from './retirements.js';
|
|
2
|
+
|
|
3
|
+
export function evaluateRaceFinishForSimulation(sim) {
|
|
4
|
+
if (sim.raceControl.finished) return;
|
|
5
|
+
if (sim.raceControl.mode === 'pre-start') return;
|
|
6
|
+
|
|
7
|
+
const ordered = sim.orderedCars();
|
|
8
|
+
const newlyFinished = ordered.filter((car) => !isRaceDnf(car) && !car.finished && car.raceDistance >= sim.finishDistance);
|
|
9
|
+
|
|
10
|
+
newlyFinished.forEach((car) => {
|
|
11
|
+
car.finished = true;
|
|
12
|
+
car.finishTime = sim.time;
|
|
13
|
+
car.finishRank = sim.raceControl.finishOrder.length + 1;
|
|
14
|
+
car.classifiedRank = car.finishRank;
|
|
15
|
+
sim.raceControl.finishOrder.push(car.id);
|
|
16
|
+
if (!sim.raceControl.winnerId) sim.raceControl.winnerId = car.id;
|
|
17
|
+
car.drsActive = false;
|
|
18
|
+
car.drsEligible = false;
|
|
19
|
+
car.drsZoneId = null;
|
|
20
|
+
car.drsZoneEnabled = false;
|
|
21
|
+
car.canAttack = false;
|
|
22
|
+
sim.events.unshift({
|
|
23
|
+
type: 'car-finish',
|
|
24
|
+
at: sim.time,
|
|
25
|
+
carId: car.id,
|
|
26
|
+
rank: car.finishRank,
|
|
27
|
+
winnerId: sim.raceControl.winnerId,
|
|
28
|
+
});
|
|
29
|
+
sim.reviewTireRequirement(car);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!ordered.length || !ordered.every((car) => car.finished || isRaceDnf(car))) return;
|
|
33
|
+
|
|
34
|
+
sim.applyOutstandingServicePenalties();
|
|
35
|
+
const classification = sim.buildClassificationFromFinishOrder();
|
|
36
|
+
const winningClassification = classification.find((entry) => !entry.dnf && entry.finished);
|
|
37
|
+
sim.raceControl.winnerId = winningClassification?.id ?? null;
|
|
38
|
+
sim.raceControl.mode = 'safety-car';
|
|
39
|
+
sim.raceControl.finished = true;
|
|
40
|
+
sim.raceControl.finishedAt = sim.time;
|
|
41
|
+
sim.raceControl.classification = classification;
|
|
42
|
+
sim.raceControl.frozenOrder = classification.map((entry) => entry.id);
|
|
43
|
+
sim.safetyCar.deployed = true;
|
|
44
|
+
const leader = sim.cars.find((car) => car.id === sim.raceControl.winnerId) ?? ordered[0];
|
|
45
|
+
const safetyCarProgress = (leader?.raceDistance ?? 0) + sim.rules.safetyCarLeadDistance;
|
|
46
|
+
if (sim.safetyCar.progress < safetyCarProgress) {
|
|
47
|
+
sim.moveSafetyCarTo(safetyCarProgress);
|
|
48
|
+
}
|
|
49
|
+
sim.cars.forEach((car) => {
|
|
50
|
+
const classified = classification.find((entry) => entry.id === car.id);
|
|
51
|
+
car.classifiedRank = classified?.rank ?? null;
|
|
52
|
+
car.desiredOffset = 0;
|
|
53
|
+
car.drsActive = false;
|
|
54
|
+
car.drsEligible = false;
|
|
55
|
+
car.drsZoneId = null;
|
|
56
|
+
car.drsZoneEnabled = false;
|
|
57
|
+
car.canAttack = false;
|
|
58
|
+
});
|
|
59
|
+
sim.events.unshift({
|
|
60
|
+
type: 'race-finish',
|
|
61
|
+
at: sim.time,
|
|
62
|
+
winnerId: sim.raceControl.winnerId,
|
|
63
|
+
classification: classification.map((entry) => ({ id: entry.id, rank: entry.rank })),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
|
|
2
|
+
import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
3
|
+
import { clamp } from '../simMath.js';
|
|
4
|
+
import { nearestTrackStateForCar } from '../track/trackStatePolicy.js';
|
|
5
|
+
|
|
6
|
+
export function setPitLaneOpenState(sim, open) {
|
|
7
|
+
const next = Boolean(open);
|
|
8
|
+
if (next === Boolean(sim.raceControl.pitLaneOpen)) return;
|
|
9
|
+
sim.raceControl.pitLaneOpen = next;
|
|
10
|
+
sim.events.unshift({ type: next ? 'pit-lane-open' : 'pit-lane-closed', at: sim.time });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function updateStartSequence(sim) {
|
|
14
|
+
const start = sim.raceControl.start;
|
|
15
|
+
if (sim.raceControl.mode !== 'pre-start' || !start || start.released) return;
|
|
16
|
+
|
|
17
|
+
if (sim.time >= start.lightsOutAt) {
|
|
18
|
+
releaseRaceStart(sim);
|
|
19
|
+
sim.events.unshift({ type: 'start-lights-out', at: sim.time });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
start.lightsLit = clamp(
|
|
24
|
+
Math.floor((sim.time + Number.EPSILON) / sim.rules.startLightInterval),
|
|
25
|
+
0,
|
|
26
|
+
start.lightCount,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function releaseRaceStart(sim) {
|
|
31
|
+
const start = sim.raceControl.start;
|
|
32
|
+
sim.raceControl.mode = 'green';
|
|
33
|
+
start.lightsLit = 0;
|
|
34
|
+
start.released = true;
|
|
35
|
+
start.releasedAt = sim.time;
|
|
36
|
+
sim.cars.forEach((car) => {
|
|
37
|
+
const wasGridLocked = car.gridLocked;
|
|
38
|
+
car.gridLocked = false;
|
|
39
|
+
const state = nearestTrackStateForCar(sim.track, car, car, car.gridDistance);
|
|
40
|
+
car.progress = state.distance;
|
|
41
|
+
applyWheelSurfaceState(car, sim.track);
|
|
42
|
+
if (wasGridLocked) car.raceDistance = car.gridDistance;
|
|
43
|
+
if (wasGridLocked) car.desiredOffset = 0;
|
|
44
|
+
car.previousX = car.x;
|
|
45
|
+
car.previousY = car.y;
|
|
46
|
+
car.previousHeading = car.heading;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function holdGridCars(sim) {
|
|
51
|
+
sim.cars.forEach((car) => {
|
|
52
|
+
if (!car.gridLocked) return;
|
|
53
|
+
const gridPoint = pointAt(sim.track, car.gridDistance);
|
|
54
|
+
const position = offsetTrackPoint(gridPoint, car.gridOffset);
|
|
55
|
+
car.previousX = position.x;
|
|
56
|
+
car.previousY = position.y;
|
|
57
|
+
car.previousHeading = gridPoint.heading;
|
|
58
|
+
car.x = position.x;
|
|
59
|
+
car.y = position.y;
|
|
60
|
+
car.heading = gridPoint.heading;
|
|
61
|
+
car.speed = 0;
|
|
62
|
+
car.throttle = 0;
|
|
63
|
+
car.brake = 1;
|
|
64
|
+
car.steeringAngle = 0;
|
|
65
|
+
car.yawRate = 0;
|
|
66
|
+
car.turnRadius = Infinity;
|
|
67
|
+
car.progress = gridPoint.distance;
|
|
68
|
+
car.raceDistance = car.gridDistance;
|
|
69
|
+
applyWheelSurfaceState(car, sim.track);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { clamp } from '../simMath.js';
|
|
2
|
+
import { affectsRaceOrder } from '../participants/participantInteractions.js';
|
|
3
|
+
import { distanceForward } from './raceDistance.js';
|
|
4
|
+
import { isRaceDnf, raceDnfCars } from './retirements.js';
|
|
5
|
+
|
|
6
|
+
function canAppearInRaceOrder(car) {
|
|
7
|
+
return car?.interaction?.affectsRaceOrder !== false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function orderedCarsForSimulation(sim) {
|
|
11
|
+
const raceCars = raceOrderCarsForSimulation(sim);
|
|
12
|
+
const dnfCars = raceDnfCars(sim.cars.filter(canAppearInRaceOrder));
|
|
13
|
+
const sortLive = (cars) => [...cars].sort((a, b) => {
|
|
14
|
+
const delta = b.raceDistance - a.raceDistance;
|
|
15
|
+
return delta === 0 ? a.index - b.index : delta;
|
|
16
|
+
});
|
|
17
|
+
const byId = new Map(sim.cars.map((car) => [car.id, car]));
|
|
18
|
+
|
|
19
|
+
if (sim.raceControl.finished && sim.raceControl.classification?.length) {
|
|
20
|
+
const classified = sim.raceControl.classification.map((entry) => byId.get(entry.id)).filter(Boolean);
|
|
21
|
+
const classifiedIds = new Set(classified.map((car) => car.id));
|
|
22
|
+
return [
|
|
23
|
+
...classified,
|
|
24
|
+
...sortLive(raceCars.filter((car) => !classifiedIds.has(car.id))),
|
|
25
|
+
...dnfCars.filter((car) => !classifiedIds.has(car.id)),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (sim.raceControl.finishOrder?.length) {
|
|
30
|
+
const finished = sim.raceControl.finishOrder.map((id) => byId.get(id)).filter(Boolean);
|
|
31
|
+
const finishedIds = new Set(finished.map((car) => car.id));
|
|
32
|
+
const missedFinished = sim.cars
|
|
33
|
+
.filter((car) => canAppearInRaceOrder(car) && !isRaceDnf(car) && car.finished && !finishedIds.has(car.id))
|
|
34
|
+
.sort((a, b) => {
|
|
35
|
+
const delta = (a.finishRank ?? Infinity) - (b.finishRank ?? Infinity);
|
|
36
|
+
return delta === 0 ? a.index - b.index : delta;
|
|
37
|
+
});
|
|
38
|
+
const running = sortLive(raceCars.filter((car) => !finishedIds.has(car.id) && !car.finished && !isRaceDnf(car)));
|
|
39
|
+
return [...finished, ...missedFinished, ...running, ...dnfCars.filter((car) => !finishedIds.has(car.id))];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (sim.safetyCar.deployed && sim.raceControl.frozenOrder?.length) {
|
|
43
|
+
const frozen = sim.raceControl.frozenOrder.map((id) => byId.get(id)).filter(Boolean);
|
|
44
|
+
const activeFrozen = frozen.filter((car) => !isRaceDnf(car));
|
|
45
|
+
const activeFrozenIds = new Set(activeFrozen.map((car) => car.id));
|
|
46
|
+
return [
|
|
47
|
+
...activeFrozen,
|
|
48
|
+
...dnfCars.filter((car) => !activeFrozenIds.has(car.id)),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return [...sortLive(raceCars.filter((car) => !isRaceDnf(car))), ...dnfCars];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function raceOrderCarsForSimulation(sim) {
|
|
56
|
+
return sim.cars.filter((car) => affectsRaceOrder(car) && !isRaceDnf(car));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function driverRaceContextForSimulation(sim, orderedCars = orderedCarsForSimulation(sim)) {
|
|
60
|
+
const raceCars = raceOrderCarsForSimulation(sim);
|
|
61
|
+
return {
|
|
62
|
+
physicsMode: sim.physicsMode,
|
|
63
|
+
track: sim.track,
|
|
64
|
+
cars: raceCars,
|
|
65
|
+
orderedCars,
|
|
66
|
+
safetyCar: sim.safetyCar,
|
|
67
|
+
rules: sim.rules,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function computeAggressionForSimulation(sim, car, orderIndex = Math.max(0, (car.rank ?? 1) - 1)) {
|
|
72
|
+
const personality = car.personality ?? { baseAggression: 0.5, riskTolerance: 0.5, patience: 0.5 };
|
|
73
|
+
if (sim.safetyCar.deployed || car.canAttack === false) {
|
|
74
|
+
return clamp(personality.baseAggression * 0.62, 0.08, 0.62);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fieldDepth = Math.max(1, raceOrderCarsForSimulation(sim).length - 1);
|
|
78
|
+
const positionPressure = clamp(orderIndex / fieldDepth, 0, 1);
|
|
79
|
+
const gapPressure = Number.isFinite(car.gapAhead) ? clamp((230 - car.gapAhead) / 230, 0, 1) : 0;
|
|
80
|
+
const tireConfidence = clamp(((car.tireEnergy ?? 100) - 42) / 58, 0, 1);
|
|
81
|
+
const patienceDamping = (1 - gapPressure) * personality.patience * 0.08;
|
|
82
|
+
|
|
83
|
+
return clamp(
|
|
84
|
+
personality.baseAggression
|
|
85
|
+
+ positionPressure * 0.26
|
|
86
|
+
+ gapPressure * (0.1 + personality.riskTolerance * 0.08)
|
|
87
|
+
- (1 - tireConfidence) * 0.1
|
|
88
|
+
- patienceDamping,
|
|
89
|
+
0.08,
|
|
90
|
+
1,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getDrsReferenceCarForSimulation(sim, car) {
|
|
95
|
+
let closest = null;
|
|
96
|
+
sim.cars.forEach((candidate) => {
|
|
97
|
+
if (
|
|
98
|
+
candidate === car ||
|
|
99
|
+
!affectsRaceOrder(candidate) ||
|
|
100
|
+
isRaceDnf(candidate) ||
|
|
101
|
+
candidate.finished ||
|
|
102
|
+
sim.isCarInActivePitStop(candidate)
|
|
103
|
+
) return;
|
|
104
|
+
const delta = distanceForward(
|
|
105
|
+
car.progress ?? car.raceDistance ?? 0,
|
|
106
|
+
candidate.progress ?? candidate.raceDistance ?? 0,
|
|
107
|
+
sim.track.length,
|
|
108
|
+
);
|
|
109
|
+
if (delta <= 0 || delta > sim.track.length / 2) return;
|
|
110
|
+
if (!closest || delta < closest.delta) closest = { car: candidate, delta };
|
|
111
|
+
});
|
|
112
|
+
return closest?.car ?? null;
|
|
113
|
+
}
|