@inventure71/paddockjs 4.2.0 → 4.3.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 +34 -0
- package/INSTALL_AND_UPDATE.md +23 -4
- package/README.md +31 -2
- package/docs/advanced_ai_pace.md +131 -0
- package/docs/advanced_physics_rebuild.md +166 -0
- package/docs/architecture.md +109 -30
- package/docs/cleanup_coverage.md +284 -0
- package/docs/component_inventory.md +31 -15
- package/docs/composable-layouts.md +2 -0
- package/docs/concepts.md +12 -1
- package/docs/custom_model_controller.md +1 -1
- package/docs/data_contract.md +33 -10
- package/docs/data_model.md +4 -2
- package/docs/efficiency_cleanup_2026-10-13.md +80 -0
- package/docs/grid_walk.md +80 -0
- package/docs/index.md +1 -0
- package/docs/keyboard_driving_fix.md +118 -0
- package/docs/limit_driving_2026-10-14.md +88 -0
- package/docs/maintainability_audit_2026-09-08.md +146 -0
- package/docs/racing_ai_improvements.md +145 -0
- package/docs/rules.md +43 -13
- package/docs/sense_contract.md +4 -1
- package/docs/system_specs.md +36 -11
- package/docs/testing-reliability-guide.md +5 -0
- package/docs/v11_improvements.md +108 -0
- package/package.json +23 -13
- package/src/api/MountedSimulatorComponents.js +101 -0
- package/src/api/PaddockSimulatorController.js +56 -81
- package/src/app/BrowserExpertAdapter.js +0 -6
- package/src/app/BrowserExpertExternalRenderer.js +72 -50
- package/src/app/ExternalTrackSnapshot.js +50 -0
- package/src/app/F1SimulatorApp.js +242 -297
- package/src/app/HostLifecycleEmitter.js +93 -0
- package/src/app/banners/raceDataBannerController.js +9 -4
- package/src/app/camera/cameraConstants.js +1 -1
- package/src/app/camera/cameraController.js +10 -13
- package/src/app/camera/cameraGeometry.js +1 -1
- package/src/app/controls/SimulatorViewControls.js +45 -0
- package/src/app/controls/workbenchBindings.js +64 -0
- package/src/app/controls/workbenchDisclosure.js +41 -0
- package/src/app/domBindings.js +21 -13
- package/src/app/gridWalk/GridWalkController.js +180 -0
- package/src/app/gridWalk/gridWalkCamera.js +88 -0
- package/src/app/gridWalk/gridWalkControls.js +24 -0
- package/src/app/gridWalk/gridWalkCrew.js +246 -0
- package/src/app/gridWalk/gridWalkLabels.js +104 -0
- package/src/app/gridWalk/gridWalkLayout.js +46 -0
- package/src/app/gridWalk/gridWalkNavigation.js +99 -0
- package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
- package/src/app/gridWalk/gridWalkSession.js +50 -0
- package/src/app/layoutSupport.js +2 -0
- package/src/app/raceOverlayClearanceSupport.js +21 -7
- package/src/app/readouts/carOverviewRenderer.js +9 -4
- package/src/app/readouts/readoutFormatters.js +1 -8
- package/src/app/readouts/telemetryRenderer.js +1 -3
- package/src/app/readouts/timingTowerRenderer.js +48 -50
- package/src/app/rendering/appAssets.js +8 -10
- package/src/app/rendering/carRenderer.js +27 -79
- package/src/app/rendering/displayUtils.js +0 -6
- package/src/app/rendering/expertSensorRenderer.js +1 -1
- package/src/app/rendering/replayGhostRenderer.js +4 -1
- package/src/app/rendering/trackRenderer.js +2 -2
- package/src/app/runtime/frameLoop.js +18 -17
- package/src/app/runtime/pixiStartupLifecycle.js +94 -0
- package/src/app/runtime/runtimeTiming.js +4 -1
- package/src/app/runtime/runtimeVisibility.js +7 -2
- package/src/config/cssValues.js +1 -1
- package/src/config/defaultOptions.js +12 -173
- package/src/config/restartOptions.js +1 -0
- package/src/config/runtimeSeed.js +10 -0
- package/src/config/simulatorDefaults.js +106 -0
- package/src/config/telemetryModules.js +20 -0
- package/src/config/theme/cssTokens.js +85 -0
- package/src/config/theme/themeCss.js +72 -0
- package/src/config/theme/themeInputs.js +155 -0
- package/src/config/theme/themeResolver.js +172 -0
- package/src/config/theme/themeSelectors.js +69 -0
- package/src/config/theme/themeTokens.js +257 -0
- package/src/config/theme/themeUtils.js +15 -0
- package/src/config/themeOptions.js +14 -830
- package/src/config/timingGapMode.js +0 -2
- package/src/config/uiOptions.js +53 -0
- package/src/data/championship.js +16 -158
- package/src/data/championshipDefaults.js +87 -0
- package/src/data/driverData.js +10 -20
- package/src/data/entryIdentity.js +67 -0
- package/src/data/entryPresentation.js +23 -0
- package/src/data/entryTypes.d.ts +160 -0
- package/src/data/index.d.ts +13 -259
- package/src/data/ratingScale.js +14 -0
- package/src/data/teamTypes.d.ts +23 -0
- package/src/data/trackTypes.d.ts +101 -0
- package/src/data/vehicleData.js +12 -23
- package/src/environment/actions.js +3 -2
- package/src/environment/controllerLoop.js +57 -44
- package/src/environment/controllerScheduler.js +30 -0
- package/src/environment/driverObservations.js +260 -0
- package/src/environment/driverReset.js +51 -0
- package/src/environment/episode.js +0 -1
- package/src/environment/evaluation.js +6 -90
- package/src/environment/evaluationTracker.js +100 -0
- package/src/environment/events.js +23 -0
- package/src/environment/index.d.ts +21 -140
- package/src/environment/metrics.js +3 -17
- package/src/environment/observationSchema.js +104 -0
- package/src/environment/observationVector.js +16 -115
- package/src/environment/observations.js +16 -316
- package/src/environment/options.js +5 -24
- package/src/environment/renderFrames.js +21 -0
- package/src/environment/resetOptions.js +41 -0
- package/src/environment/results.js +125 -0
- package/src/environment/rewards.js +10 -16
- package/src/environment/runtime.js +17 -300
- package/src/environment/scenarioOptions.js +90 -0
- package/src/environment/scenarioPlacements.js +70 -0
- package/src/environment/scenarioPresets.js +78 -0
- package/src/environment/scenarios.js +5 -236
- package/src/environment/scratch.js +18 -0
- package/src/environment/sensorOptions.js +20 -0
- package/src/environment/sensors/index.js +2 -556
- package/src/environment/sensors/indexedRayBands.js +0 -18
- package/src/environment/sensors/nearbyCars.js +18 -15
- package/src/environment/sensors/opponentRadar.js +3 -2
- package/src/environment/sensors/pitConnectorProximity.js +20 -0
- package/src/environment/sensors/rayBandTrace.js +1 -37
- package/src/environment/sensors/rayBatch.js +81 -0
- package/src/environment/sensors/rayConfig.js +26 -0
- package/src/environment/sensors/rayGeometry.js +0 -8
- package/src/environment/sensors/rayGuards.js +0 -6
- package/src/environment/sensors/rayOutputs.js +75 -0
- package/src/environment/sensors/rayQuery.js +110 -0
- package/src/environment/sensors/rayScratch.js +146 -0
- package/src/environment/sensors/sensorTargets.js +12 -4
- package/src/environment/sensors/surfaceRays.js +6 -61
- package/src/environment/sensors/trackRays.js +7 -80
- package/src/environment/simulationSetup.js +30 -0
- package/src/environment/specs.js +15 -109
- package/src/index.d.ts +34 -259
- package/src/index.js +28 -6
- package/src/placeholder/index.js +1 -8
- package/src/rendering/displayLifecycle.js +6 -0
- package/src/rendering/proceduralTrackAsset.js +1 -1
- package/src/rendering/renderSnapshot.js +3 -0
- package/src/rendering/track/finishLineRenderer.js +1 -1
- package/src/rendering/track/gridRenderer.js +1 -1
- package/src/rendering/track/offsetStrokeSafety.js +2 -2
- package/src/rendering/track/pitLaneRenderer.js +1 -1
- package/src/rendering/track/trackMaterialRenderer.js +4 -4
- package/src/rendering/track/trackRenderConstants.js +0 -1
- package/src/rendering/track/trackRenderGeometry.js +1 -7
- package/src/simulation/collisionGeometry.js +5 -11
- package/src/simulation/driver/advancedPathControls.js +53 -0
- package/src/simulation/driver/advancedRacePace.js +112 -0
- package/src/simulation/driver/advancedRacingControls.js +61 -0
- package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
- package/src/simulation/driver/advancedRacingSteering.js +76 -0
- package/src/simulation/driver/advancedRacingTiming.js +16 -0
- package/src/simulation/driver/arcadeRacePace.js +83 -0
- package/src/simulation/driver/arcadeRacingControls.js +111 -0
- package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
- package/src/simulation/driver/driverControlConstants.js +0 -2
- package/src/simulation/driver/driverController.js +14 -3
- package/src/simulation/driver/edgeRecovery.js +0 -1
- package/src/simulation/driver/racingControls.js +10 -406
- package/src/simulation/driver/racingDriverProfile.js +48 -0
- package/src/simulation/driver/racingLinePlan.js +48 -15
- package/src/simulation/driver/rejoinControls.js +58 -111
- package/src/simulation/driver/safetyCarControls.js +12 -0
- package/src/simulation/driver/trafficScan.js +89 -6
- package/src/simulation/pit/pitCarPlacement.js +29 -0
- package/src/simulation/pit/pitFlow.js +8 -30
- package/src/simulation/pit/pitIntent.js +4 -3
- package/src/simulation/pit/pitOccupancy.js +2 -2
- package/src/simulation/pit/pitQueue.js +2 -18
- package/src/simulation/pit/pitRouteMovement.js +1 -36
- package/src/simulation/pit/pitRouting.js +5 -81
- package/src/simulation/pit/pitService.js +3 -32
- package/src/simulation/pit/pitServiceConstants.js +1 -0
- package/src/simulation/pit/pitServiceProgress.js +34 -0
- package/src/simulation/pit/pitSnapshots.js +8 -4
- package/src/simulation/pit/pitState.js +1 -5
- package/src/simulation/race/classification.js +1 -8
- package/src/simulation/race/raceFinish.js +3 -8
- package/src/simulation/race/raceOrder.js +2 -13
- package/src/simulation/race/raceProgress.js +6 -20
- package/src/simulation/race/raceSetup.js +1 -1
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/redFlag.js +2 -4
- package/src/simulation/race/safetyCar.js +3 -8
- package/src/simulation/race/stalledDnf.js +2 -4
- package/src/simulation/raceSimulation.js +6 -38
- package/src/simulation/replay/replayGhosts.js +44 -32
- package/src/simulation/rules/collisionSteward.js +0 -14
- package/src/simulation/rules/penaltyActions.js +37 -0
- package/src/simulation/rules/penaltyLedger.js +8 -1
- package/src/simulation/rules/rulesReview.js +1 -7
- package/src/simulation/simMath.js +4 -0
- package/src/simulation/snapshots/raceSnapshots.js +30 -11
- package/src/simulation/timing/drsState.js +8 -0
- package/src/simulation/timing/drsTiming.js +2 -4
- package/src/simulation/timing/lapTelemetry.js +7 -4
- package/src/simulation/timing/sectorPerformance.js +3 -3
- package/src/simulation/timing/timingLines.js +12 -6
- package/src/simulation/track/pitLaneAccess.js +2 -2
- package/src/simulation/track/pitLaneLayout.js +4 -135
- package/src/simulation/track/pitLaneModel.js +70 -0
- package/src/simulation/track/pitLaneState.js +0 -1
- package/src/simulation/track/pitServiceLayout.js +121 -0
- package/src/simulation/track/proceduralTrack.js +77 -0
- package/src/simulation/track/spatialQueries.js +1 -25
- package/src/simulation/track/trackBuilder.js +54 -0
- package/src/simulation/track/trackConstants.js +0 -22
- package/src/simulation/track/trackMath.js +4 -29
- package/src/simulation/track/trackModel.js +4 -241
- package/src/simulation/track/trackModelCache.js +105 -0
- package/src/simulation/track/trackNearestQueries.js +1218 -0
- package/src/simulation/track/trackQueryConstants.js +3 -0
- package/src/simulation/track/trackQueryConstruction.js +368 -0
- package/src/simulation/track/trackQueryIndex.js +12 -1933
- package/src/simulation/track/trackQuerySegments.js +174 -0
- package/src/simulation/track/trackQueryStats.js +12 -0
- package/src/simulation/track/trackRayQueries.js +165 -0
- package/src/simulation/vehicle/advancedContactResponse.js +89 -0
- package/src/simulation/vehicle/advancedTireForces.js +92 -0
- package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
- package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
- package/src/simulation/vehicle/contactResolution.js +20 -6
- package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
- package/src/simulation/vehicle/pitWheelSurface.js +0 -12
- package/src/simulation/vehicle/runoffResponse.js +2 -4
- package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
- package/src/simulation/vehicle/vehicleGeometry.js +0 -56
- package/src/simulation/vehicle/vehicleKinematics.js +10 -0
- package/src/simulation/vehicle/vehiclePhysics.js +35 -194
- package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
- package/src/simulation/vehicle/vehicleState.js +2 -1
- package/src/simulation/vehicle/wheelSurface.js +13 -432
- package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
- package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
- package/src/styles.css +173 -19
- package/src/ui/bannerTemplates.js +4 -2
- package/src/ui/cameraControlsTemplate.js +4 -2
- package/src/ui/gridWalkTemplate.js +29 -0
- package/src/ui/htmlEscaping.js +8 -0
- package/src/ui/shellTemplate.js +3 -0
- package/src/ui/telemetryDrawerTemplate.js +4 -2
- package/src/ui/telemetryTemplates.js +3 -22
- package/src/ui/templateUtils.js +2 -8
- package/src/ui/viewControlsTemplate.js +21 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { clamp } from '../simMath.js';
|
|
2
|
+
import { simUnitsToMeters } from '../units.js';
|
|
3
|
+
|
|
4
|
+
const JUDGEMENT_WAVELENGTH_METERS = 180;
|
|
5
|
+
const DISTANCE_PHASE_RATE = Math.PI * 2 / JUDGEMENT_WAVELENGTH_METERS;
|
|
6
|
+
|
|
7
|
+
function finiteOr(value, fallback) {
|
|
8
|
+
return Number.isFinite(value) ? value : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizedSkill(value, minimum, maximum, neutral) {
|
|
12
|
+
return clamp((finiteOr(value, neutral) - minimum) / (maximum - minimum), 0, 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Input skills are DriverData's constructor values, not its 0..100 ratings.
|
|
16
|
+
// Ability controls available precision/margin; willingness to risk it is a
|
|
17
|
+
// separate personality input. This policy does not change physical grip.
|
|
18
|
+
export function racingDriverProfile(car = {}) {
|
|
19
|
+
const pace = normalizedSkill(car.pace, 0.92, 1.08, 1);
|
|
20
|
+
const racecraft = normalizedSkill(car.racecraft, 0.62, 0.94, 0.78);
|
|
21
|
+
const ability = pace * 0.6 + racecraft * 0.4;
|
|
22
|
+
const consistency = normalizedSkill(car.consistency, 0.59, 0.91, 0.75);
|
|
23
|
+
const aggression = clamp(finiteOr(car.aggression, finiteOr(car.personality?.baseAggression, 0.5)), 0, 1);
|
|
24
|
+
const riskTolerance = clamp(finiteOr(car.personality?.riskTolerance, 0.5), 0, 1);
|
|
25
|
+
const risk = riskTolerance * 0.6 + aggression * 0.4;
|
|
26
|
+
|
|
27
|
+
// A continuous distance phase gives a bounded, zero-mean judgement error.
|
|
28
|
+
// It never advances RNG or introduces abrupt per-frame/sector decisions.
|
|
29
|
+
const distanceMeters = simUnitsToMeters(finiteOr(car.raceDistance, finiteOr(car.progress, 0)));
|
|
30
|
+
const phase = distanceMeters * DISTANCE_PHASE_RATE +
|
|
31
|
+
finiteOr(car.index, 0) * 0.73 + finiteOr(car.driverNumber, 0) * 0.31;
|
|
32
|
+
const precisionError = (1 - ability * 0.7) * (1 - consistency * 0.7);
|
|
33
|
+
const lineErrorMeters = 0.2 * precisionError * Math.sin(phase);
|
|
34
|
+
const judgementError = (1 - ability * 0.65) * (1 - consistency * 0.7) * risk;
|
|
35
|
+
const anticipationVariation = 0.025 * judgementError * Math.sin(phase + 1.7);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
ability,
|
|
39
|
+
consistency,
|
|
40
|
+
risk,
|
|
41
|
+
lateralShare: 0.74 + 0.2 * ability + 0.02 * risk,
|
|
42
|
+
longitudinalShare: 0.86 + 0.09 * ability + 0.03 * risk,
|
|
43
|
+
anticipationSeconds: 0.16 + 0.12 * (1 - ability) +
|
|
44
|
+
0.04 * (1 - consistency) - 0.04 * risk + anticipationVariation,
|
|
45
|
+
lineErrorMeters,
|
|
46
|
+
speedJudgementScale: 1 + 0.012 * judgementError * Math.sin(phase + 0.8),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -2,7 +2,7 @@ import { clamp, TWO_PI } from '../simMath.js';
|
|
|
2
2
|
import { kphToSimSpeed, metersToSimUnits, simSpeedToKph, simUnitsToMeters } from '../units.js';
|
|
3
3
|
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
4
4
|
import { DEFEND_MIN_OFFSET, DEFEND_REAR_GAP, LANE_EDGE_CLEARANCE_TARGET, LANE_OFFSETS, OVERTAKE_COMMIT_FRAMES, OVERTAKE_GAP_AGGRESSION, OVERTAKE_GAP_BASE, OVERTAKE_LATERAL_MIN, OVERTAKE_LATERAL_REWARD, PASS_SIDE_AGGRESSION, PASS_SIDE_BASE, TRAFFIC_GAP_AHEAD, TRAFFIC_REAR_WINDOW, TRAFFIC_SIDE_GAP, TRAFFIC_SIDE_OVERLAP } from './driverControlConstants.js';
|
|
5
|
-
import { findDefensiveThreat, findLaneTrafficAhead, findLaneTrafficBeside, scanNearbyTraffic } from './trafficScan.js';
|
|
5
|
+
import { findDefensiveThreat, findLaneTrafficAhead, findLaneTrafficBeside, scanNearbyTraffic, mainTrackCenterOffset, followingSpeedFromTraffic, laneMovementBounds } from './trafficScan.js';
|
|
6
6
|
|
|
7
7
|
export function planRacingLine(car, orderIndex, race) {
|
|
8
8
|
const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
|
|
@@ -10,10 +10,18 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
10
10
|
const trackLimit = race.track.width / 2 - VEHICLE_LIMITS.carWidth * clamp(1.55 - aggression * 0.28, 1.26, 1.55);
|
|
11
11
|
const preferred = Math.sin((car.index / Math.max(1, race.cars.length)) * TWO_PI) * metersToSimUnits(1.1);
|
|
12
12
|
const currentOffset = clamp(car.desiredOffset ?? preferred, -trackLimit, trackLimit);
|
|
13
|
-
const ahead = race.orderedCars[orderIndex - 1];
|
|
14
13
|
const traffic = scanNearbyTraffic(car, race);
|
|
15
|
-
const
|
|
16
|
-
|
|
14
|
+
const corridorLimit = race.track.width / 2 + VEHICLE_LIMITS.carWidth / 2;
|
|
15
|
+
let aheadEntry = null;
|
|
16
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
17
|
+
const entry = traffic[index];
|
|
18
|
+
if (Math.abs(entry.signedOffset) > corridorLimit) continue;
|
|
19
|
+
if (entry.gap > 0 && (!aheadEntry || entry.gap < aheadEntry.gap)) aheadEntry = entry;
|
|
20
|
+
}
|
|
21
|
+
const ahead = aheadEntry?.car;
|
|
22
|
+
const aheadGap = aheadEntry?.gap ?? Infinity;
|
|
23
|
+
const rearThreat = findDefensiveThreat(traffic, corridorLimit, car);
|
|
24
|
+
const attackPlan = planAttackCommitment(car, ahead, traffic, trackLimit, aggression, aheadGap);
|
|
17
25
|
|
|
18
26
|
let bestOffset = currentOffset;
|
|
19
27
|
let bestScore = -Infinity;
|
|
@@ -28,6 +36,9 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
28
36
|
|
|
29
37
|
for (let trafficIndex = 0; trafficIndex < traffic.length; trafficIndex += 1) {
|
|
30
38
|
const entry = traffic[trafficIndex];
|
|
39
|
+
// Separate pit/runoff traffic cannot be an overtaking opportunity or
|
|
40
|
+
// defensive target. Keep it in the scan for genuine overlapping lanes.
|
|
41
|
+
if (Math.abs(entry.signedOffset) > corridorLimit) continue;
|
|
31
42
|
const lateral = Math.abs(entry.signedOffset - offset);
|
|
32
43
|
if (entry.gap > 0 && entry.gap < TRAFFIC_GAP_AHEAD) {
|
|
33
44
|
const overlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
|
|
@@ -41,22 +52,24 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
if (ahead &&
|
|
55
|
+
if (ahead && aheadGap < TRAFFIC_GAP_AHEAD) {
|
|
45
56
|
const passSide = attackPlan?.targetOffset ?? clamp(
|
|
46
|
-
(ahead
|
|
57
|
+
(mainTrackCenterOffset(ahead) * -0.65) + (car.index % 2 === 0 ? -1 : 1) * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION),
|
|
47
58
|
-trackLimit,
|
|
48
59
|
trackLimit,
|
|
49
60
|
);
|
|
50
61
|
const attackPressure = attackPlan?.pressure ?? 0.45;
|
|
51
62
|
score -= Math.abs(offset - passSide) * (0.12 + aggression * 0.16) * attackPressure;
|
|
52
|
-
if (attackPlan && Math.sign(offset || passSide) === attackPlan.side && Math.abs(offset - ahead
|
|
63
|
+
if (attackPlan && Math.sign(offset || passSide) === attackPlan.side && Math.abs(offset - mainTrackCenterOffset(ahead)) > OVERTAKE_LATERAL_MIN) {
|
|
53
64
|
score += metersToSimUnits(4 + aggression * 5) * attackPressure;
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
const defenseIntensity = clamp((aggression - 0.5) / 0.3, 0, 1) *
|
|
58
69
|
clamp((Math.max(riskTolerance, aggression) - 0.42) / 0.4, 0, 1);
|
|
59
|
-
|
|
70
|
+
// A pass needs a stable lane; reactive defense must not steer across the
|
|
71
|
+
// committed attacking line while the car is still passing its target.
|
|
72
|
+
if (rearThreat && !attackPlan && defenseIntensity > 0.04) {
|
|
60
73
|
const threatSide = Math.sign(rearThreat.signedOffset || currentOffset || (car.index % 2 === 0 ? 1 : -1));
|
|
61
74
|
const defendOffset = clamp(
|
|
62
75
|
threatSide * Math.max(Math.abs(rearThreat.signedOffset) + metersToSimUnits(0.8), DEFEND_MIN_OFFSET),
|
|
@@ -79,12 +92,16 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
79
92
|
const laneChangeRate = metersToSimUnits(0.95 + car.racecraft * 0.32 + aggression * 0.78);
|
|
80
93
|
car.desiredOffset = currentOffset + clamp(bestOffset - currentOffset, -laneChangeRate, laneChangeRate);
|
|
81
94
|
|
|
95
|
+
const movement = laneMovementBounds(traffic, mainTrackCenterOffset(car));
|
|
82
96
|
return {
|
|
83
97
|
offset: car.desiredOffset,
|
|
98
|
+
minimumOffset: movement.minimum,
|
|
99
|
+
maximumOffset: movement.maximum,
|
|
84
100
|
sameLaneAhead: findLaneTrafficAhead(traffic, car.desiredOffset, TRAFFIC_GAP_AHEAD),
|
|
85
|
-
actualLaneAhead: findLaneTrafficAhead(traffic, car
|
|
101
|
+
actualLaneAhead: findLaneTrafficAhead(traffic, mainTrackCenterOffset(car), TRAFFIC_GAP_AHEAD),
|
|
86
102
|
sideRisk: findLaneTrafficBeside(traffic, car.desiredOffset),
|
|
87
103
|
attackCommitted: Boolean(attackPlan),
|
|
104
|
+
followingSpeed: followingSpeedFromTraffic(car, traffic),
|
|
88
105
|
};
|
|
89
106
|
}
|
|
90
107
|
|
|
@@ -106,20 +123,20 @@ export function calculateActualOverlapPenalty(car, entry) {
|
|
|
106
123
|
return clamp(urgent, 0, 72);
|
|
107
124
|
}
|
|
108
125
|
|
|
109
|
-
export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression,
|
|
126
|
+
export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression, aheadGap = car.gapAhead) {
|
|
110
127
|
const existingFrames = Math.max(0, Math.floor(car.attackCommitmentFrames ?? 0));
|
|
111
128
|
const relativeSpeed = ahead ? car.speed - ahead.speed : 0;
|
|
112
129
|
const closeEnough = ahead &&
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
aheadGap > 0 &&
|
|
131
|
+
aheadGap < OVERTAKE_GAP_BASE + aggression * OVERTAKE_GAP_AGGRESSION + metersToSimUnits(28);
|
|
115
132
|
const speedPressure = clamp(relativeSpeed / kphToSimSpeed(34), -0.4, 1);
|
|
116
133
|
|
|
117
134
|
if (closeEnough && speedPressure > -0.15) {
|
|
118
|
-
const aheadOffset = ahead
|
|
135
|
+
const aheadOffset = mainTrackCenterOffset(ahead);
|
|
119
136
|
const currentSide = Math.sign(car.attackSide ?? 0);
|
|
120
137
|
const blockedCommitment = currentSide &&
|
|
121
138
|
Math.sign(aheadOffset || currentSide) === currentSide &&
|
|
122
|
-
Math.abs(aheadOffset - (car
|
|
139
|
+
Math.abs(aheadOffset - mainTrackCenterOffset(car)) < TRAFFIC_SIDE_GAP &&
|
|
123
140
|
Math.abs(aheadOffset) > trackLimit * 0.55;
|
|
124
141
|
|
|
125
142
|
if (!existingFrames || !currentSide || blockedCommitment) {
|
|
@@ -141,7 +158,7 @@ export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression
|
|
|
141
158
|
if (!side || !ahead) return null;
|
|
142
159
|
|
|
143
160
|
const targetOffset = clamp(
|
|
144
|
-
(ahead
|
|
161
|
+
mainTrackCenterOffset(ahead) + side * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION + metersToSimUnits(2.4)),
|
|
145
162
|
-trackLimit,
|
|
146
163
|
trackLimit,
|
|
147
164
|
);
|
|
@@ -165,3 +182,19 @@ export function chooseFreerAttackSide(traffic, preferredSide) {
|
|
|
165
182
|
}, side === preferredSide ? 0.18 : 0);
|
|
166
183
|
return sideScore(1) >= sideScore(-1) ? 1 : -1;
|
|
167
184
|
}
|
|
185
|
+
|
|
186
|
+
export function calculateRacingTrafficPenalty(car, lanePlan) {
|
|
187
|
+
const actualLaneLateral = lanePlan.actualLaneAhead
|
|
188
|
+
? Math.abs(lanePlan.actualLaneAhead.signedOffset - mainTrackCenterOffset(car))
|
|
189
|
+
: Infinity;
|
|
190
|
+
const plannedPassingOverlap = lanePlan.attackCommitted && lanePlan.sameLaneAhead
|
|
191
|
+
? Math.abs(lanePlan.sameLaneAhead.signedOffset - mainTrackCenterOffset(car)) > OVERTAKE_LATERAL_MIN
|
|
192
|
+
: false;
|
|
193
|
+
return Math.max(
|
|
194
|
+
lanePlan.sameLaneAhead ? calculatePlannedTrafficPenalty(lanePlan.sameLaneAhead, plannedPassingOverlap) : 0,
|
|
195
|
+
lanePlan.actualLaneAhead && actualLaneLateral < OVERTAKE_LATERAL_MIN
|
|
196
|
+
? calculateActualOverlapPenalty(car, lanePlan.actualLaneAhead)
|
|
197
|
+
: 0,
|
|
198
|
+
lanePlan.sideRisk ? clamp(simUnitsToMeters(TRAFFIC_SIDE_GAP - lanePlan.sideRisk.lateral) * 0.78, 0, 30) : 0,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { clamp } from '../simMath.js';
|
|
2
|
-
import { kphToSimSpeed, simUnitsToMeters } from '../units.js';
|
|
2
|
+
import { kphToSimSpeed, simUnitsToMeters, metersToSimUnits } from '../units.js';
|
|
3
3
|
import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
|
|
4
4
|
import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
|
|
5
5
|
import { angleToPoint } from './driverMath.js';
|
|
6
6
|
import { createDriverInput } from './driverInput.js';
|
|
7
|
-
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
7
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode, tirePerformanceFactor } from '../vehicle/vehiclePhysics.js';
|
|
8
|
+
import { advancedLateralAccelerationLimit } from '../vehicle/advancedTireForces.js';
|
|
9
|
+
import { advancedSteeringToPoint, advancedSpeedControls } from './advancedPathControls.js';
|
|
8
10
|
import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
|
|
9
11
|
|
|
10
12
|
export function decideRejoinControls(car, race) {
|
|
@@ -14,103 +16,63 @@ export function decideRejoinControls(car, race) {
|
|
|
14
16
|
return decideArcadeRejoinControls(car, race);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
lowSpeedRecoveryThrottle: 0.32,
|
|
50
|
-
lowSpeedOutwardBrakeStartMps: 1.2,
|
|
51
|
-
lowSpeedCrawlOutwardToleranceMps: 0,
|
|
52
|
-
lowSpeedHeadingOutwardThrottleLimit: 1,
|
|
53
|
-
stabilizeLowSpeedOutward: false,
|
|
54
|
-
stabilizeRejoinHoldOutsideRoad: false,
|
|
55
|
-
unsettledScale: () => 1,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
19
|
+
const ARCADE_REJOIN_PROFILE = {
|
|
20
|
+
inwardOffsetRatio: 0.34,
|
|
21
|
+
surfaceTargetSpeeds: {
|
|
22
|
+
track: 88,
|
|
23
|
+
kerb: 72,
|
|
24
|
+
gravel: 56,
|
|
25
|
+
grass: 48,
|
|
26
|
+
barrier: 34,
|
|
27
|
+
},
|
|
28
|
+
lowSpeedRecovery: {
|
|
29
|
+
barrier: 0.34,
|
|
30
|
+
default: 0.18,
|
|
31
|
+
},
|
|
32
|
+
throttleLimits: {
|
|
33
|
+
barrier: 0.68,
|
|
34
|
+
gravel: 0.46,
|
|
35
|
+
grass: 0.42,
|
|
36
|
+
default: 0.42,
|
|
37
|
+
},
|
|
38
|
+
brakeResponseKph: 30,
|
|
39
|
+
onTrackBrakeLimit: 0.46,
|
|
40
|
+
offTrackBrakeLimit: 0.74,
|
|
41
|
+
steerGain: 1.65,
|
|
42
|
+
outwardBrakeStartMps: 1.2,
|
|
43
|
+
outwardBrakeGain: 0.08,
|
|
44
|
+
misalignmentBrakeGain: 0.06,
|
|
45
|
+
slideThrottleDamping: 0.36,
|
|
46
|
+
offTrackForwardTargetScale: 0.42,
|
|
47
|
+
lowSpeedForwardTargetScale: 0,
|
|
48
|
+
lowSpeedOffTrackSteerLimit: 0.52,
|
|
49
|
+
lowSpeedRecoveryThrottle: 0.32,
|
|
50
|
+
};
|
|
58
51
|
|
|
59
52
|
export function decideSimulatorRejoinControls(car, race) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
throttleLimits: {
|
|
75
|
-
barrier: 0.5,
|
|
76
|
-
gravel: 0.82,
|
|
77
|
-
grass: 0.76,
|
|
78
|
-
default: 0.52,
|
|
79
|
-
},
|
|
80
|
-
brakeResponseKph: 24,
|
|
81
|
-
onTrackBrakeLimit: 0.34,
|
|
82
|
-
offTrackBrakeLimit: 0.24,
|
|
83
|
-
steerGain: 0.94,
|
|
84
|
-
outwardBrakeStartMps: 0.45,
|
|
85
|
-
outwardBrakeGain: 0.12,
|
|
86
|
-
misalignmentBrakeGain: 0.08,
|
|
87
|
-
slideThrottleDamping: 0.62,
|
|
88
|
-
offTrackForwardTargetScale: 0.32,
|
|
89
|
-
lowSpeedForwardTargetScale: 0.22,
|
|
90
|
-
lowSpeedOffTrackSteerLimit: 0.68,
|
|
91
|
-
lowSpeedRecoveryThrottle: 0.68,
|
|
92
|
-
lowSpeedOutwardBrakeStartMps: 0.24,
|
|
93
|
-
lowSpeedCrawlOutwardToleranceMps: 0.42,
|
|
94
|
-
lowSpeedHeadingOutwardThrottleLimit: 0.24,
|
|
95
|
-
stabilizeLowSpeedOutward: true,
|
|
96
|
-
stabilizeRejoinHoldOutsideRoad: true,
|
|
97
|
-
unsettledScale: (entry) => clamp(
|
|
98
|
-
1 - Math.max(0, (entry.gripUsage ?? 0) - 0.55) * 0.75 - Math.abs(entry.slipAngleRadians ?? 0) * 1.65,
|
|
99
|
-
0.22,
|
|
100
|
-
1,
|
|
101
|
-
),
|
|
102
|
-
});
|
|
53
|
+
const speed = simUnitsToMeters(car.speed);
|
|
54
|
+
if (Math.abs(car.slipAngleRadians ?? 0) > 0.5 && speed > 2) {
|
|
55
|
+
// Once the car is sliding sideways/backwards, path steering cannot recover
|
|
56
|
+
// its line. Arrest the slide before requesting a forward rejoin.
|
|
57
|
+
return createDriverInput().brake(1).controls();
|
|
58
|
+
}
|
|
59
|
+
const target = pointAt(race.track, car.progress + metersToSimUnits(clamp(10 + speed * 0.5, 10, 30)));
|
|
60
|
+
const { steering, curvature } = advancedSteeringToPoint(car, target);
|
|
61
|
+
const capacity = advancedLateralAccelerationLimit(car, speed, tirePerformanceFactor(car.tireEnergy ?? 100));
|
|
62
|
+
const surfaceSpeed = car.trackState.onTrack ? 22 : 12;
|
|
63
|
+
const targetSpeed = Math.min(surfaceSpeed, Math.sqrt(capacity * 0.55 / Math.max(Math.abs(curvature), 0.001)));
|
|
64
|
+
const { throttle, brake } = advancedSpeedControls(car, targetSpeed);
|
|
65
|
+
return createDriverInput().steer(steering).accelerate(throttle).brake(brake).controls();
|
|
103
66
|
}
|
|
104
67
|
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
const lookahead = clamp(car.speed *
|
|
68
|
+
export function decideArcadeRejoinControls(car, race) {
|
|
69
|
+
const profile = ARCADE_REJOIN_PROFILE;
|
|
70
|
+
const lookahead = clamp(car.speed * 0.72 + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
|
|
108
71
|
const edgeMotion = analyzeTrackEdgeMotion(car, race);
|
|
109
72
|
const lowSpeedOffTrack = !car.trackState.onTrack && car.speed < kphToSimSpeed(24);
|
|
110
73
|
const distanceFromRoadMeters = simUnitsToMeters(Math.max(0, car.trackState.crossTrackError - race.track.width / 2));
|
|
111
|
-
const inRejoinHold = (car.rejoinRecoveryFrames ?? 0) > 0;
|
|
112
74
|
const recoveringOutsideRoad = distanceFromRoadMeters > 0.25 &&
|
|
113
|
-
|
|
75
|
+
!car.trackState.onTrack;
|
|
114
76
|
const offTrackTargetScale = lowSpeedOffTrack
|
|
115
77
|
? profile.lowSpeedForwardTargetScale
|
|
116
78
|
: profile.offTrackForwardTargetScale;
|
|
@@ -145,26 +107,17 @@ function decideRejoinControlsForMode(car, race, profile) {
|
|
|
145
107
|
const lowSpeedRecovery = car.speed < kphToSimSpeed(18)
|
|
146
108
|
? car.trackState.surface === 'barrier' ? profile.lowSpeedRecovery.barrier : profile.lowSpeedRecovery.default
|
|
147
109
|
: 0;
|
|
148
|
-
const outwardBrakeStart =
|
|
149
|
-
? profile.lowSpeedOutwardBrakeStartMps
|
|
150
|
-
: profile.outwardBrakeStartMps;
|
|
151
|
-
const canCrawlSteerRecovery = lowSpeedOffTrack &&
|
|
152
|
-
car.speed < kphToSimSpeed(8) &&
|
|
153
|
-
edgeMotion.outwardSpeedMps < profile.lowSpeedCrawlOutwardToleranceMps;
|
|
110
|
+
const outwardBrakeStart = profile.outwardBrakeStartMps;
|
|
154
111
|
const needsOutwardStabilization = recoveringOutsideRoad &&
|
|
155
|
-
|
|
156
|
-
!canCrawlSteerRecovery &&
|
|
112
|
+
!lowSpeedOffTrack &&
|
|
157
113
|
edgeMotion.outwardSpeedMps > outwardBrakeStart &&
|
|
158
114
|
distanceFromRoadMeters > 0.25;
|
|
159
|
-
const recoveryThrottleFloorBase = profile.simulatorMode && !car.trackState.onTrack
|
|
160
|
-
? (car.trackState.surface === 'barrier' ? profile.lowSpeedRecovery.barrier : profile.lowSpeedRecovery.default)
|
|
161
|
-
: lowSpeedRecovery;
|
|
162
115
|
const lowSpeedRecoveryThrottle = lowSpeedOffTrack && car.trackState.surface !== 'barrier'
|
|
163
116
|
? profile.lowSpeedRecoveryThrottle
|
|
164
117
|
: 0;
|
|
165
118
|
const recoveryThrottleFloor = needsOutwardStabilization
|
|
166
119
|
? 0
|
|
167
|
-
: Math.max(
|
|
120
|
+
: Math.max(lowSpeedRecovery, lowSpeedRecoveryThrottle);
|
|
168
121
|
const surfaceThrottleLimit = car.trackState.surface === 'barrier'
|
|
169
122
|
? profile.throttleLimits.barrier
|
|
170
123
|
: car.trackState.surface === 'gravel'
|
|
@@ -172,9 +125,6 @@ function decideRejoinControlsForMode(car, race, profile) {
|
|
|
172
125
|
: car.trackState.surface === 'grass'
|
|
173
126
|
? profile.throttleLimits.grass
|
|
174
127
|
: profile.throttleLimits.default;
|
|
175
|
-
const throttleLimit = lowSpeedOffTrack && edgeMotion.headingOutward && distanceFromRoadMeters > 0.5
|
|
176
|
-
? Math.min(surfaceThrottleLimit, profile.lowSpeedHeadingOutwardThrottleLimit)
|
|
177
|
-
: surfaceThrottleLimit;
|
|
178
128
|
let brakeAmount = speedError < -kphToSimSpeed(5)
|
|
179
129
|
? clamp(Math.abs(speedError) / kphToSimSpeed(profile.brakeResponseKph), 0.04, car.trackState.onTrack ? profile.onTrackBrakeLimit : profile.offTrackBrakeLimit)
|
|
180
130
|
: 0;
|
|
@@ -189,9 +139,6 @@ function decideRejoinControlsForMode(car, race, profile) {
|
|
|
189
139
|
),
|
|
190
140
|
);
|
|
191
141
|
}
|
|
192
|
-
const unsettledScale = lowSpeedOffTrack
|
|
193
|
-
? Math.max(profile.unsettledScale(car), 0.82)
|
|
194
|
-
: profile.unsettledScale(car);
|
|
195
142
|
const slideThrottleScale = 1 - clamp(
|
|
196
143
|
Math.max(0, edgeMotion.outwardSpeedMps - profile.outwardBrakeStartMps) * profile.slideThrottleDamping +
|
|
197
144
|
Math.max(0, Math.abs(angleError) - 1.25) * 0.22,
|
|
@@ -206,8 +153,8 @@ function decideRejoinControlsForMode(car, race, profile) {
|
|
|
206
153
|
return createDriverInput()
|
|
207
154
|
.steer(steeringRequest)
|
|
208
155
|
.accelerate(brakeAmount > 0.05 ? 0 : speedError > 0
|
|
209
|
-
? clamp(((speedError / kphToSimSpeed(24)) * alignment + recoveryThrottleFloor) *
|
|
210
|
-
: recoveryThrottleFloor *
|
|
156
|
+
? clamp(((speedError / kphToSimSpeed(24)) * alignment + recoveryThrottleFloor) * slideThrottleScale, minimumAcceleration, surfaceThrottleLimit)
|
|
157
|
+
: recoveryThrottleFloor * slideThrottleScale)
|
|
211
158
|
.brake(brakeAmount)
|
|
212
159
|
.controls();
|
|
213
160
|
}
|
|
@@ -4,8 +4,14 @@ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
|
|
|
4
4
|
import { SAFETY_CAR_LOOKAHEAD_MAX, SAFETY_CAR_LOOKAHEAD_MIN } from './driverControlConstants.js';
|
|
5
5
|
import { angleToPoint } from './driverMath.js';
|
|
6
6
|
import { createDriverInput } from './driverInput.js';
|
|
7
|
+
import { isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
8
|
+
import { advancedSteeringToPoint, advancedSpeedControls } from './advancedPathControls.js';
|
|
9
|
+
import { decideSimulatorRejoinControls } from './rejoinControls.js';
|
|
7
10
|
|
|
8
11
|
export function decideSafetyCarControls(car, orderIndex, race) {
|
|
12
|
+
if (isSimulatorPhysicsMode(race.physicsMode) && !car.trackState.onTrack) {
|
|
13
|
+
return decideSimulatorRejoinControls(car, race);
|
|
14
|
+
}
|
|
9
15
|
const queueSlot = race.safetyCar.progress - race.rules.safetyCarLeadDistance - orderIndex * race.rules.safetyCarGap;
|
|
10
16
|
const lookahead = clamp(car.speed * 0.55 + metersToSimUnits(20), SAFETY_CAR_LOOKAHEAD_MIN, SAFETY_CAR_LOOKAHEAD_MAX);
|
|
11
17
|
const targetBase = pointAt(race.track, car.progress + lookahead);
|
|
@@ -27,6 +33,12 @@ export function decideSafetyCarControls(car, orderIndex, race) {
|
|
|
27
33
|
);
|
|
28
34
|
const speedError = desiredSpeed - car.speed;
|
|
29
35
|
|
|
36
|
+
if (isSimulatorPhysicsMode(race.physicsMode)) {
|
|
37
|
+
const { steering } = advancedSteeringToPoint(car, target);
|
|
38
|
+
const { throttle, brake } = advancedSpeedControls(car, simUnitsToMeters(desiredSpeed));
|
|
39
|
+
return createDriverInput().steer(steering).accelerate(throttle).brake(brake).controls();
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
return createDriverInput()
|
|
31
43
|
.steer(angleToPoint(car, target) * 1.18 + headingError * 0.32 + centerlineCorrection)
|
|
32
44
|
.accelerate(speedError > kphToSimSpeed(1) ? clamp(speedError / kphToSimSpeed(16), 0, 0.5) : 0)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { metersToSimUnits } from '../units.js';
|
|
2
|
-
import { DEFEND_REAR_GAP,
|
|
1
|
+
import { metersToSimUnits, simUnitsToMeters, TOP_SPEED_SIM_UNITS_PER_SECOND } from '../units.js';
|
|
2
|
+
import { DEFEND_REAR_GAP, TRAFFIC_SIDE_GAP, TRAFFIC_GAP_AHEAD } from './driverControlConstants.js';
|
|
3
3
|
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
4
|
+
import { canCollide } from '../participants/participantInteractions.js';
|
|
5
|
+
import { signedLateralOffsetToPoint } from '../track/trackMath.js';
|
|
4
6
|
|
|
5
7
|
const TRAFFIC_REAR_SCAN_WINDOW = metersToSimUnits(39);
|
|
6
8
|
const TRAFFIC_FORWARD_SCAN_WINDOW = metersToSimUnits(134);
|
|
@@ -18,24 +20,28 @@ export function scanNearbyTraffic(car, race) {
|
|
|
18
20
|
const cars = race.cars;
|
|
19
21
|
for (let index = 0; index < cars.length; index += 1) {
|
|
20
22
|
const other = cars[index];
|
|
21
|
-
if (other === car) continue;
|
|
22
|
-
const gap = other
|
|
23
|
+
if (other === car || !canCollide(car, other)) continue;
|
|
24
|
+
const gap = physicalTrafficGap(car, other, race.track?.length);
|
|
23
25
|
if (gap <= -TRAFFIC_REAR_SCAN_WINDOW || gap >= TRAFFIC_FORWARD_SCAN_WINDOW) continue;
|
|
24
26
|
const entry = TRAFFIC_ENTRY_POOL[traffic.length] ??= { car: null, gap: 0, signedOffset: 0, speed: 0 };
|
|
25
27
|
entry.car = other;
|
|
26
28
|
entry.gap = gap;
|
|
27
|
-
entry.signedOffset = other
|
|
29
|
+
entry.signedOffset = mainTrackCenterOffset(other);
|
|
28
30
|
entry.speed = other.speed;
|
|
29
31
|
traffic.push(entry);
|
|
30
32
|
}
|
|
31
33
|
return traffic;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
export function findDefensiveThreat(traffic) {
|
|
36
|
+
export function findDefensiveThreat(traffic, corridorLimit = Infinity, car = null) {
|
|
35
37
|
let closest = null;
|
|
36
38
|
|
|
37
39
|
for (let index = 0; index < traffic.length; index += 1) {
|
|
38
40
|
const entry = traffic[index];
|
|
41
|
+
if (Math.abs(entry.signedOffset) > corridorLimit) continue;
|
|
42
|
+
// A physically rearward car with greater race distance is lapping us.
|
|
43
|
+
// Yield ordinary road space instead of treating it as a position threat.
|
|
44
|
+
if (car && entry.car.raceDistance > car.raceDistance) continue;
|
|
39
45
|
if (entry.gap >= -VEHICLE_LIMITS.carLength * 1.1 || entry.gap < -DEFEND_REAR_GAP) continue;
|
|
40
46
|
if (!closest || entry.gap > closest.gap) closest = entry;
|
|
41
47
|
}
|
|
@@ -75,3 +81,80 @@ export function findLaneTrafficBeside(traffic, offset) {
|
|
|
75
81
|
|
|
76
82
|
return closest ? { ...closest, lateral: closestLateral, risk: closestRisk } : null;
|
|
77
83
|
}
|
|
84
|
+
|
|
85
|
+
// Race classification distance can differ by whole laps while the cars occupy
|
|
86
|
+
// the same piece of road. Keep traffic in the local, signed circuit frame.
|
|
87
|
+
export function physicalTrafficGap(car, other, trackLength) {
|
|
88
|
+
if (!(trackLength > 0)) return other.raceDistance - car.raceDistance;
|
|
89
|
+
let gap = ((other.progress ?? other.raceDistance) - (car.progress ?? car.raceDistance)) % trackLength;
|
|
90
|
+
if (gap > trackLength / 2) gap -= trackLength;
|
|
91
|
+
else if (gap < -trackLength / 2) gap += trackLength;
|
|
92
|
+
return gap;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function mainTrackCenterOffset(car) {
|
|
96
|
+
// Pit surfaces retain the chassis offset in the main-track frame. Ordinary
|
|
97
|
+
// signedOffset describes an outer wheel, so project the center instead.
|
|
98
|
+
const state = car.trackState;
|
|
99
|
+
if (state?.mainTrackSignedOffset != null) return state.mainTrackSignedOffset;
|
|
100
|
+
const projected = state ? signedLateralOffsetToPoint(state, car) : NaN;
|
|
101
|
+
return Number.isFinite(projected) ? projected : state?.signedOffset ?? 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function followingSpeedLimit(car, cars, trackLength) {
|
|
105
|
+
const centerOffset = mainTrackCenterOffset(car);
|
|
106
|
+
let leader = null;
|
|
107
|
+
let gap = TRAFFIC_GAP_AHEAD;
|
|
108
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
109
|
+
const other = cars[index];
|
|
110
|
+
if (other === car || !canCollide(car, other)) continue;
|
|
111
|
+
const distance = physicalTrafficGap(car, other, trackLength);
|
|
112
|
+
if (distance <= 0 || distance > gap) continue;
|
|
113
|
+
const lateral = Math.abs(mainTrackCenterOffset(other) - centerOffset);
|
|
114
|
+
if (lateral >= VEHICLE_LIMITS.carWidth + metersToSimUnits(0.4)) continue;
|
|
115
|
+
leader = other;
|
|
116
|
+
gap = distance;
|
|
117
|
+
}
|
|
118
|
+
return followingTargetSpeed(car, leader, gap);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The lane planner already has a filtered scan. Reuse it for pacing instead
|
|
122
|
+
// of rescanning the entire field for every driver decision.
|
|
123
|
+
export function followingSpeedFromTraffic(car, traffic) {
|
|
124
|
+
const centerOffset = mainTrackCenterOffset(car);
|
|
125
|
+
let leader = null;
|
|
126
|
+
let gap = TRAFFIC_GAP_AHEAD;
|
|
127
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
128
|
+
const entry = traffic[index];
|
|
129
|
+
if (entry.gap <= 0 || entry.gap > gap) continue;
|
|
130
|
+
if (Math.abs(entry.signedOffset - centerOffset) >= VEHICLE_LIMITS.carWidth + metersToSimUnits(0.4)) continue;
|
|
131
|
+
leader = entry.car;
|
|
132
|
+
gap = entry.gap;
|
|
133
|
+
}
|
|
134
|
+
return followingTargetSpeed(car, leader, gap);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function followingTargetSpeed(car, leader, gap) {
|
|
138
|
+
if (!leader) return simUnitsToMeters(TOP_SPEED_SIM_UNITS_PER_SECOND);
|
|
139
|
+
const speed = simUnitsToMeters(car.speed);
|
|
140
|
+
const followingGap = simUnitsToMeters(VEHICLE_LIMITS.carLength) + 2 + speed * 0.35;
|
|
141
|
+
return Math.max(0, simUnitsToMeters(leader.speed) + (simUnitsToMeters(gap) - followingGap) * 1.2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Lane intentions and corner offsets compose differently in each mode. Return
|
|
145
|
+
// the available lateral corridor so the final commanded path stays clear of
|
|
146
|
+
// cars alongside, rather than checking only an intermediate lane intention.
|
|
147
|
+
const LANE_MOVEMENT_BOUNDS = { minimum: -Infinity, maximum: Infinity };
|
|
148
|
+
export function laneMovementBounds(traffic, currentOffset) {
|
|
149
|
+
const bounds = LANE_MOVEMENT_BOUNDS;
|
|
150
|
+
bounds.minimum = -Infinity;
|
|
151
|
+
bounds.maximum = Infinity;
|
|
152
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
153
|
+
const entry = traffic[index];
|
|
154
|
+
if (Math.abs(entry.gap) > VEHICLE_LIMITS.carLength) continue;
|
|
155
|
+
const neighbor = entry.signedOffset - currentOffset;
|
|
156
|
+
if (neighbor > 0) bounds.maximum = Math.min(bounds.maximum, Math.max(currentOffset, entry.signedOffset - VEHICLE_LIMITS.carWidth));
|
|
157
|
+
else if (neighbor < 0) bounds.minimum = Math.max(bounds.minimum, Math.min(currentOffset, entry.signedOffset + VEHICLE_LIMITS.carWidth));
|
|
158
|
+
}
|
|
159
|
+
return bounds;
|
|
160
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
2
|
+
import { syncPitCarKinematics } from './pitKinematics.js';
|
|
3
|
+
import { getPitBoxRaceDistance } from './pitOccupancy.js';
|
|
4
|
+
|
|
5
|
+
// Pit control owns this pose while a car waits or receives stationary service.
|
|
6
|
+
export function holdPitCarAtBox(sim, car, box, { queue = false, updatePreviousPose = false } = {}) {
|
|
7
|
+
if (updatePreviousPose) {
|
|
8
|
+
car.previousX = car.x;
|
|
9
|
+
car.previousY = car.y;
|
|
10
|
+
car.previousHeading = car.heading;
|
|
11
|
+
}
|
|
12
|
+
const position = queue ? box.queuePoint : box.center;
|
|
13
|
+
car.x = position.x;
|
|
14
|
+
car.y = position.y;
|
|
15
|
+
car.heading = sim.track.pitLane.mainLane.heading;
|
|
16
|
+
car.speed = 0;
|
|
17
|
+
car.throttle = 0;
|
|
18
|
+
car.brake = 1;
|
|
19
|
+
car.steeringAngle = 0;
|
|
20
|
+
car.yawRate = 0;
|
|
21
|
+
car.turnRadius = Infinity;
|
|
22
|
+
syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
|
|
23
|
+
applyWheelSurfaceState(car, sim.track);
|
|
24
|
+
car.progress = car.trackState.distance;
|
|
25
|
+
car.raceDistance = getPitBoxRaceDistance(sim, car.pitStop, queue ? {
|
|
26
|
+
...box,
|
|
27
|
+
distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
|
|
28
|
+
} : box);
|
|
29
|
+
}
|