@inventure71/paddockjs 4.1.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 +76 -0
- package/INSTALL_AND_UPDATE.md +31 -5
- package/README.md +34 -3
- package/docs/advanced_ai_pace.md +131 -0
- package/docs/advanced_physics_rebuild.md +166 -0
- package/docs/architecture.md +111 -30
- package/docs/cleanup_coverage.md +284 -0
- package/docs/component_inventory.md +32 -16
- 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/getting-started.md +3 -0
- package/docs/grid_walk.md +80 -0
- package/docs/index.md +5 -2
- package/docs/keyboard_driving_fix.md +118 -0
- package/docs/limit_driving_2026-10-14.md +88 -0
- package/docs/loading.md +141 -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 +40 -12
- package/docs/testing-reliability-guide.md +67 -0
- package/docs/v11_improvements.md +108 -0
- package/package.json +23 -15
- 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 +12 -195
- 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 +35 -91
- 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 +43 -202
- 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 +12 -20
- 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 +16 -5
- package/src/simulation/driver/edgeRecovery.js +2 -3
- package/src/simulation/driver/racingControls.js +10 -396
- package/src/simulation/driver/racingDriverProfile.js +48 -0
- package/src/simulation/driver/racingLinePlan.js +54 -20
- package/src/simulation/driver/rejoinControls.js +59 -112
- package/src/simulation/driver/safetyCarControls.js +12 -0
- package/src/simulation/driver/trafficScan.js +136 -27
- package/src/simulation/participants/participantInteractions.js +24 -15
- 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 +20 -11
- package/src/simulation/race/redFlag.js +2 -4
- package/src/simulation/race/safetyCar.js +5 -9
- 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/pitLaneSpeedingSteward.js +1 -1
- package/src/simulation/rules/rulesReview.js +20 -16
- package/src/simulation/rules/trackLimitsSteward.js +2 -2
- 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 +14 -6
- 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 +76 -31
- 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 +12 -240
- 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 +23 -9
- 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 +85 -103
- package/src/simulation/vehicle/vehicleKinematics.js +10 -0
- package/src/simulation/vehicle/vehiclePhysics.js +42 -196
- package/src/simulation/vehicle/vehicleSnapshots.js +18 -6
- package/src/simulation/vehicle/vehicleState.js +5 -3
- 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/raceCanvasTemplate.js +3 -2
- 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
- package/examples/train-basic-policy.mjs +0 -276
- package/examples/trainingData.mjs +0 -133
|
@@ -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,23 +10,35 @@ 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;
|
|
20
28
|
|
|
21
|
-
LANE_OFFSETS.
|
|
22
|
-
const offset = clamp(
|
|
29
|
+
for (let laneIndex = 0; laneIndex < LANE_OFFSETS.length; laneIndex += 1) {
|
|
30
|
+
const offset = clamp(LANE_OFFSETS[laneIndex], -trackLimit, trackLimit);
|
|
23
31
|
const edgeClearance = trackLimit - Math.abs(offset);
|
|
24
32
|
let score = 80;
|
|
25
33
|
score -= Math.abs(offset - preferred) * 0.18;
|
|
26
34
|
score -= Math.abs(offset - currentOffset) * (0.09 - aggression * 0.025);
|
|
27
35
|
score -= Math.max(0, LANE_EDGE_CLEARANCE_TARGET - edgeClearance) * (0.92 - aggression * 0.42);
|
|
28
36
|
|
|
29
|
-
traffic.
|
|
37
|
+
for (let trafficIndex = 0; trafficIndex < traffic.length; trafficIndex += 1) {
|
|
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;
|
|
30
42
|
const lateral = Math.abs(entry.signedOffset - offset);
|
|
31
43
|
if (entry.gap > 0 && entry.gap < TRAFFIC_GAP_AHEAD) {
|
|
32
44
|
const overlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
|
|
@@ -38,24 +50,26 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
38
50
|
const sideOverlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
|
|
39
51
|
score -= sideOverlapRisk * (TRAFFIC_SIDE_OVERLAP + entry.gap) * 0.052 * (1 - riskTolerance * 0.22);
|
|
40
52
|
}
|
|
41
|
-
}
|
|
53
|
+
}
|
|
42
54
|
|
|
43
|
-
if (ahead &&
|
|
55
|
+
if (ahead && aheadGap < TRAFFIC_GAP_AHEAD) {
|
|
44
56
|
const passSide = attackPlan?.targetOffset ?? clamp(
|
|
45
|
-
(ahead
|
|
57
|
+
(mainTrackCenterOffset(ahead) * -0.65) + (car.index % 2 === 0 ? -1 : 1) * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION),
|
|
46
58
|
-trackLimit,
|
|
47
59
|
trackLimit,
|
|
48
60
|
);
|
|
49
61
|
const attackPressure = attackPlan?.pressure ?? 0.45;
|
|
50
62
|
score -= Math.abs(offset - passSide) * (0.12 + aggression * 0.16) * attackPressure;
|
|
51
|
-
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) {
|
|
52
64
|
score += metersToSimUnits(4 + aggression * 5) * attackPressure;
|
|
53
65
|
}
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
const defenseIntensity = clamp((aggression - 0.5) / 0.3, 0, 1) *
|
|
57
69
|
clamp((Math.max(riskTolerance, aggression) - 0.42) / 0.4, 0, 1);
|
|
58
|
-
|
|
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) {
|
|
59
73
|
const threatSide = Math.sign(rearThreat.signedOffset || currentOffset || (car.index % 2 === 0 ? 1 : -1));
|
|
60
74
|
const defendOffset = clamp(
|
|
61
75
|
threatSide * Math.max(Math.abs(rearThreat.signedOffset) + metersToSimUnits(0.8), DEFEND_MIN_OFFSET),
|
|
@@ -73,17 +87,21 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
73
87
|
bestScore = score;
|
|
74
88
|
bestOffset = offset;
|
|
75
89
|
}
|
|
76
|
-
}
|
|
90
|
+
}
|
|
77
91
|
|
|
78
92
|
const laneChangeRate = metersToSimUnits(0.95 + car.racecraft * 0.32 + aggression * 0.78);
|
|
79
93
|
car.desiredOffset = currentOffset + clamp(bestOffset - currentOffset, -laneChangeRate, laneChangeRate);
|
|
80
94
|
|
|
95
|
+
const movement = laneMovementBounds(traffic, mainTrackCenterOffset(car));
|
|
81
96
|
return {
|
|
82
97
|
offset: car.desiredOffset,
|
|
98
|
+
minimumOffset: movement.minimum,
|
|
99
|
+
maximumOffset: movement.maximum,
|
|
83
100
|
sameLaneAhead: findLaneTrafficAhead(traffic, car.desiredOffset, TRAFFIC_GAP_AHEAD),
|
|
84
|
-
actualLaneAhead: findLaneTrafficAhead(traffic, car
|
|
101
|
+
actualLaneAhead: findLaneTrafficAhead(traffic, mainTrackCenterOffset(car), TRAFFIC_GAP_AHEAD),
|
|
85
102
|
sideRisk: findLaneTrafficBeside(traffic, car.desiredOffset),
|
|
86
103
|
attackCommitted: Boolean(attackPlan),
|
|
104
|
+
followingSpeed: followingSpeedFromTraffic(car, traffic),
|
|
87
105
|
};
|
|
88
106
|
}
|
|
89
107
|
|
|
@@ -105,20 +123,20 @@ export function calculateActualOverlapPenalty(car, entry) {
|
|
|
105
123
|
return clamp(urgent, 0, 72);
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression,
|
|
126
|
+
export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression, aheadGap = car.gapAhead) {
|
|
109
127
|
const existingFrames = Math.max(0, Math.floor(car.attackCommitmentFrames ?? 0));
|
|
110
128
|
const relativeSpeed = ahead ? car.speed - ahead.speed : 0;
|
|
111
129
|
const closeEnough = ahead &&
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
aheadGap > 0 &&
|
|
131
|
+
aheadGap < OVERTAKE_GAP_BASE + aggression * OVERTAKE_GAP_AGGRESSION + metersToSimUnits(28);
|
|
114
132
|
const speedPressure = clamp(relativeSpeed / kphToSimSpeed(34), -0.4, 1);
|
|
115
133
|
|
|
116
134
|
if (closeEnough && speedPressure > -0.15) {
|
|
117
|
-
const aheadOffset = ahead
|
|
135
|
+
const aheadOffset = mainTrackCenterOffset(ahead);
|
|
118
136
|
const currentSide = Math.sign(car.attackSide ?? 0);
|
|
119
137
|
const blockedCommitment = currentSide &&
|
|
120
138
|
Math.sign(aheadOffset || currentSide) === currentSide &&
|
|
121
|
-
Math.abs(aheadOffset - (car
|
|
139
|
+
Math.abs(aheadOffset - mainTrackCenterOffset(car)) < TRAFFIC_SIDE_GAP &&
|
|
122
140
|
Math.abs(aheadOffset) > trackLimit * 0.55;
|
|
123
141
|
|
|
124
142
|
if (!existingFrames || !currentSide || blockedCommitment) {
|
|
@@ -140,7 +158,7 @@ export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression
|
|
|
140
158
|
if (!side || !ahead) return null;
|
|
141
159
|
|
|
142
160
|
const targetOffset = clamp(
|
|
143
|
-
(ahead
|
|
161
|
+
mainTrackCenterOffset(ahead) + side * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION + metersToSimUnits(2.4)),
|
|
144
162
|
-trackLimit,
|
|
145
163
|
trackLimit,
|
|
146
164
|
);
|
|
@@ -164,3 +182,19 @@ export function chooseFreerAttackSide(traffic, preferredSide) {
|
|
|
164
182
|
}, side === preferredSide ? 0.18 : 0);
|
|
165
183
|
return sideScore(1) >= sideScore(-1) ? 1 : -1;
|
|
166
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,116 +1,78 @@
|
|
|
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 } 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) {
|
|
11
|
-
if (race.physicsMode
|
|
13
|
+
if (isSimulatorPhysicsMode(race.physicsMode)) {
|
|
12
14
|
return decideSimulatorRejoinControls(car, race);
|
|
13
15
|
}
|
|
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,26 +1,50 @@
|
|
|
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';
|
|
6
|
+
|
|
7
|
+
const TRAFFIC_REAR_SCAN_WINDOW = metersToSimUnits(39);
|
|
8
|
+
const TRAFFIC_FORWARD_SCAN_WINDOW = metersToSimUnits(134);
|
|
9
|
+
const TRAFFIC_BESIDE_WINDOW = metersToSimUnits(28);
|
|
10
|
+
|
|
11
|
+
// Entries are reused across calls: drivers consume the scan synchronously
|
|
12
|
+
// within a single control decision, so a module-level pool avoids re-allocating
|
|
13
|
+
// the candidate list for every car on every step.
|
|
14
|
+
const TRAFFIC_SCRATCH = [];
|
|
15
|
+
const TRAFFIC_ENTRY_POOL = [];
|
|
4
16
|
|
|
5
17
|
export function scanNearbyTraffic(car, race) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
const traffic = TRAFFIC_SCRATCH;
|
|
19
|
+
traffic.length = 0;
|
|
20
|
+
const cars = race.cars;
|
|
21
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
22
|
+
const other = cars[index];
|
|
23
|
+
if (other === car || !canCollide(car, other)) continue;
|
|
24
|
+
const gap = physicalTrafficGap(car, other, race.track?.length);
|
|
25
|
+
if (gap <= -TRAFFIC_REAR_SCAN_WINDOW || gap >= TRAFFIC_FORWARD_SCAN_WINDOW) continue;
|
|
26
|
+
const entry = TRAFFIC_ENTRY_POOL[traffic.length] ??= { car: null, gap: 0, signedOffset: 0, speed: 0 };
|
|
27
|
+
entry.car = other;
|
|
28
|
+
entry.gap = gap;
|
|
29
|
+
entry.signedOffset = mainTrackCenterOffset(other);
|
|
30
|
+
entry.speed = other.speed;
|
|
31
|
+
traffic.push(entry);
|
|
32
|
+
}
|
|
33
|
+
return traffic;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function findDefensiveThreat(traffic, corridorLimit = Infinity, car = null) {
|
|
18
37
|
let closest = null;
|
|
19
38
|
|
|
20
|
-
traffic.
|
|
21
|
-
|
|
39
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
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;
|
|
45
|
+
if (entry.gap >= -VEHICLE_LIMITS.carLength * 1.1 || entry.gap < -DEFEND_REAR_GAP) continue;
|
|
22
46
|
if (!closest || entry.gap > closest.gap) closest = entry;
|
|
23
|
-
}
|
|
47
|
+
}
|
|
24
48
|
|
|
25
49
|
return closest;
|
|
26
50
|
}
|
|
@@ -28,24 +52,109 @@ export function findDefensiveThreat(traffic) {
|
|
|
28
52
|
export function findLaneTrafficAhead(traffic, offset, maxDistance) {
|
|
29
53
|
let closest = null;
|
|
30
54
|
|
|
31
|
-
traffic.
|
|
32
|
-
|
|
33
|
-
if (
|
|
55
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
56
|
+
const entry = traffic[index];
|
|
57
|
+
if (entry.gap <= 0 || entry.gap > maxDistance) continue;
|
|
58
|
+
if (Math.abs(entry.signedOffset - offset) > TRAFFIC_SIDE_GAP) continue;
|
|
34
59
|
if (!closest || entry.gap < closest.gap) closest = entry;
|
|
35
|
-
}
|
|
60
|
+
}
|
|
36
61
|
|
|
37
62
|
return closest;
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
export function findLaneTrafficBeside(traffic, offset) {
|
|
41
66
|
let closest = null;
|
|
67
|
+
let closestLateral = 0;
|
|
68
|
+
let closestRisk = -Infinity;
|
|
42
69
|
|
|
43
|
-
traffic.
|
|
70
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
71
|
+
const entry = traffic[index];
|
|
44
72
|
const lateral = Math.abs(entry.signedOffset - offset);
|
|
45
|
-
if (Math.abs(entry.gap) >
|
|
46
|
-
const risk = (
|
|
47
|
-
if (!closest || risk >
|
|
48
|
-
|
|
73
|
+
if (Math.abs(entry.gap) > TRAFFIC_BESIDE_WINDOW || lateral > TRAFFIC_SIDE_GAP) continue;
|
|
74
|
+
const risk = (TRAFFIC_BESIDE_WINDOW - Math.abs(entry.gap)) + (TRAFFIC_SIDE_GAP - lateral);
|
|
75
|
+
if (!closest || risk > closestRisk) {
|
|
76
|
+
closest = entry;
|
|
77
|
+
closestLateral = lateral;
|
|
78
|
+
closestRisk = risk;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
49
81
|
|
|
50
|
-
return closest;
|
|
82
|
+
return closest ? { ...closest, lateral: closestLateral, risk: closestRisk } : null;
|
|
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;
|
|
51
160
|
}
|