@inventure71/paddockjs 4.2.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/INSTALL_AND_UPDATE.md +24 -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 +84 -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 +38 -11
- package/docs/testing-reliability-guide.md +5 -0
- package/docs/v11_improvements.md +108 -0
- package/package.json +24 -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 +106 -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
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { disableWarmupOptions, runWarmupWithGuard } from '../simulation/warmup/runtimeWarmup.js';
|
|
1
|
+
import { FIXED_STEP } from '../simulation/raceSimulation.js';
|
|
3
2
|
import { handleActionError, resolveActionMap } from './actions.js';
|
|
4
3
|
import { collectStepEvents } from './events.js';
|
|
5
|
-
import {
|
|
6
|
-
advanceDriverEpisodes,
|
|
7
|
-
buildDriverEpisodeInfo,
|
|
8
|
-
createEpisodeState,
|
|
9
|
-
evaluateEpisode,
|
|
10
|
-
initializeDriverEpisodes,
|
|
11
|
-
markDestroyedDriverEpisodes,
|
|
12
|
-
resetDriverEpisodes,
|
|
13
|
-
} from './episode.js';
|
|
14
|
-
import { buildDriverMetrics } from './metrics.js';
|
|
15
|
-
import { buildEnvironmentObservation } from './observations.js';
|
|
4
|
+
import { advanceDriverEpisodes, createEpisodeState, initializeDriverEpisodes, resetDriverEpisodes } from './episode.js';
|
|
16
5
|
import { resolveEnvironmentOptions } from './options.js';
|
|
17
|
-
import { applyEnvironmentPlacements,
|
|
6
|
+
import { applyEnvironmentPlacements, normalizeEnvironmentPlacements } from './scenarios.js';
|
|
18
7
|
import { buildActionSpec, buildObservationSpec } from './specs.js';
|
|
8
|
+
import { initializeControlledPitIntent, resetControlledDriverState } from './driverReset.js';
|
|
9
|
+
import { createSimulationWithEnvironmentScenario } from './simulationSetup.js';
|
|
10
|
+
import { mergeEnvironmentResetOptions } from './resetOptions.js';
|
|
11
|
+
import { buildEnvironmentResult, snapshotForResult } from './results.js';
|
|
12
|
+
import { emitExternalRenderFrame } from './renderFrames.js';
|
|
13
|
+
import { clearMetricScratch, clearObservationScratch } from './scratch.js';
|
|
19
14
|
|
|
20
15
|
export function createPaddockEnvironment(options = {}) {
|
|
21
16
|
let resolvedOptions = resolveEnvironmentOptions(options);
|
|
@@ -37,31 +32,6 @@ export function createPaddockEnvironment(options = {}) {
|
|
|
37
32
|
});
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
function createSimulationWithEnvironmentScenario(options) {
|
|
41
|
-
runWarmupWithGuard({
|
|
42
|
-
options,
|
|
43
|
-
surface: 'environment',
|
|
44
|
-
execute: ({ warmup }) => {
|
|
45
|
-
const warmupSimulation = createRaceSimulation(disableWarmupOptions(options, warmup.surface));
|
|
46
|
-
initializeControlledPitIntentForSimulation(warmupSimulation, options);
|
|
47
|
-
applyEnvironmentScenario(warmupSimulation, options);
|
|
48
|
-
applyControlledRunoffResponse(warmupSimulation, options);
|
|
49
|
-
for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
|
|
50
|
-
const warmupSnapshot = warmupSimulation.snapshotObservation?.() ?? warmupSimulation.snapshot();
|
|
51
|
-
buildEnvironmentObservation({
|
|
52
|
-
snapshot: warmupSnapshot,
|
|
53
|
-
options,
|
|
54
|
-
events: [],
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
const sim = createRaceSimulation(disableWarmupOptions(options, 'environment'));
|
|
59
|
-
initializeControlledPitIntentForSimulation(sim, options);
|
|
60
|
-
applyEnvironmentScenario(sim, options);
|
|
61
|
-
applyControlledRunoffResponse(sim, options);
|
|
62
|
-
return sim;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
35
|
export function createEnvironmentRuntime(host) {
|
|
66
36
|
const episodeState = createEpisodeState();
|
|
67
37
|
|
|
@@ -76,9 +46,9 @@ export function createEnvironmentRuntime(host) {
|
|
|
76
46
|
episodeState.step = 0;
|
|
77
47
|
episodeState.previousSnapshot = null;
|
|
78
48
|
initializeDriverEpisodes(episodeState, options.controlledDrivers);
|
|
79
|
-
const result =
|
|
49
|
+
const result = buildEnvironmentResult({ host, episodeState, events: [], actionErrors: [] });
|
|
80
50
|
episodeState.lastResult = result;
|
|
81
|
-
emitExternalRenderFrame(host, result, { source: 'reset' });
|
|
51
|
+
emitExternalRenderFrame(host, result, { source: 'reset' }, episodeState.lastObservationSnapshot);
|
|
82
52
|
host.afterReset(result);
|
|
83
53
|
return result;
|
|
84
54
|
}
|
|
@@ -116,7 +86,7 @@ export function createEnvironmentRuntime(host) {
|
|
|
116
86
|
}
|
|
117
87
|
episodeState.step += 1;
|
|
118
88
|
advanceDriverEpisodes(episodeState, options.controlledDrivers);
|
|
119
|
-
const result =
|
|
89
|
+
const result = buildEnvironmentResult({
|
|
120
90
|
host,
|
|
121
91
|
episodeState,
|
|
122
92
|
events: stepEvents,
|
|
@@ -125,14 +95,14 @@ export function createEnvironmentRuntime(host) {
|
|
|
125
95
|
rewardEnabled: true,
|
|
126
96
|
});
|
|
127
97
|
episodeState.lastResult = result;
|
|
128
|
-
emitExternalRenderFrame(host, result, { source: 'step', actions });
|
|
98
|
+
emitExternalRenderFrame(host, result, { source: 'step', actions }, episodeState.lastObservationSnapshot);
|
|
129
99
|
host.afterStep(result);
|
|
130
100
|
return result;
|
|
131
101
|
}
|
|
132
102
|
|
|
133
103
|
function getObservation() {
|
|
134
104
|
return episodeState.lastResult?.observation ??
|
|
135
|
-
|
|
105
|
+
buildEnvironmentResult({ host, episodeState, events: [], actionErrors: [] }).observation;
|
|
136
106
|
}
|
|
137
107
|
|
|
138
108
|
function getState({ output = 'full' } = {}) {
|
|
@@ -150,21 +120,14 @@ export function createEnvironmentRuntime(host) {
|
|
|
150
120
|
const snapshot = sim.snapshotObservation?.() ?? sim.snapshot();
|
|
151
121
|
const carsById = new Map(snapshot.cars.map((car) => [car.id, car]));
|
|
152
122
|
applyEnvironmentPlacements(sim, snapshot.track, carsById, normalizedPlacements);
|
|
153
|
-
Object.keys(normalizedPlacements)
|
|
154
|
-
sim.clearCarControls?.(driverId);
|
|
155
|
-
sim.setAutomaticPitIntentEnabled?.(driverId, false);
|
|
156
|
-
const car = sim.cars?.find?.((item) => item.id === driverId);
|
|
157
|
-
resetEnvironmentPitStop(car);
|
|
158
|
-
sim.setPitIntent?.(driverId, 0);
|
|
159
|
-
if (car) sim.applyRunoffResponse?.(car);
|
|
160
|
-
});
|
|
123
|
+
resetControlledDriverState(sim, Object.keys(normalizedPlacements));
|
|
161
124
|
resetDriverEpisodes(episodeState, Object.keys(normalizedPlacements));
|
|
162
125
|
episodeState.previousSnapshot = null;
|
|
163
126
|
const resetDriverIds = Object.keys(normalizedPlacements);
|
|
164
127
|
const observationScope = resultOptions.observationScope ??
|
|
165
128
|
resultOptions.resetDriversObservationScope ??
|
|
166
129
|
options.result.resetDriversObservationScope;
|
|
167
|
-
const result =
|
|
130
|
+
const result = buildEnvironmentResult({
|
|
168
131
|
host,
|
|
169
132
|
episodeState,
|
|
170
133
|
events: [],
|
|
@@ -177,7 +140,7 @@ export function createEnvironmentRuntime(host) {
|
|
|
177
140
|
source: 'resetDrivers',
|
|
178
141
|
resetDriverIds,
|
|
179
142
|
observationScope,
|
|
180
|
-
});
|
|
143
|
+
}, episodeState.lastObservationSnapshot);
|
|
181
144
|
return result;
|
|
182
145
|
}
|
|
183
146
|
|
|
@@ -203,255 +166,9 @@ export function createEnvironmentRuntime(host) {
|
|
|
203
166
|
return { reset, step, resetDrivers, getObservation, getState, getActionSpec, getObservationSpec, destroy };
|
|
204
167
|
}
|
|
205
168
|
|
|
206
|
-
function mergeEnvironmentResetOptions(currentOptions, nextOptions = {}) {
|
|
207
|
-
return mergePlainObjects(currentOptions, nextOptions);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const REPLACE_OBJECT_PATHS = new Set([
|
|
211
|
-
'scenario.placements',
|
|
212
|
-
]);
|
|
213
|
-
|
|
214
|
-
function mergePlainObjects(base, overrides, path = []) {
|
|
215
|
-
if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
|
|
216
|
-
const merged = { ...base };
|
|
217
|
-
Object.entries(overrides).forEach(([key, value]) => {
|
|
218
|
-
if (value === undefined) return;
|
|
219
|
-
const nextPath = [...path, key];
|
|
220
|
-
merged[key] = isPlainObject(value) &&
|
|
221
|
-
isPlainObject(base[key]) &&
|
|
222
|
-
!REPLACE_OBJECT_PATHS.has(nextPath.join('.'))
|
|
223
|
-
? mergePlainObjects(base[key], value, nextPath)
|
|
224
|
-
: value;
|
|
225
|
-
});
|
|
226
|
-
return merged;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function isPlainObject(value) {
|
|
230
|
-
return value != null &&
|
|
231
|
-
typeof value === 'object' &&
|
|
232
|
-
!Array.isArray(value) &&
|
|
233
|
-
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
169
|
function isNoopPitIntent(pitIntent) {
|
|
237
170
|
if (pitIntent && typeof pitIntent === 'object') {
|
|
238
171
|
return Number(pitIntent.intent ?? pitIntent.pitIntent) === 0;
|
|
239
172
|
}
|
|
240
173
|
return Number(pitIntent) === 0;
|
|
241
174
|
}
|
|
242
|
-
|
|
243
|
-
function initializeControlledPitIntent(host) {
|
|
244
|
-
const sim = host.getSimulation?.();
|
|
245
|
-
const options = host.getOptions?.();
|
|
246
|
-
initializeControlledPitIntentForSimulation(sim, options);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function initializeControlledPitIntentForSimulation(sim, options) {
|
|
250
|
-
options?.controlledDrivers?.forEach?.((driverId) => {
|
|
251
|
-
sim?.setAutomaticPitIntentEnabled?.(driverId, false);
|
|
252
|
-
sim?.setPitIntent?.(driverId, 0);
|
|
253
|
-
const car = sim?.cars?.find?.((item) => item.id === driverId);
|
|
254
|
-
if (car) car.environmentControlled = true;
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function resetEnvironmentPitStop(car) {
|
|
259
|
-
const stop = car?.pitStop;
|
|
260
|
-
if (!stop) return;
|
|
261
|
-
stop.status = 'pending';
|
|
262
|
-
stop.phase = null;
|
|
263
|
-
stop.intent = 0;
|
|
264
|
-
stop.queueingForService = false;
|
|
265
|
-
stop.route = null;
|
|
266
|
-
stop.routeProgress = 0;
|
|
267
|
-
stop.routeStartRaceDistance = null;
|
|
268
|
-
stop.routeEndRaceDistance = null;
|
|
269
|
-
stop.serviceRemaining = 0;
|
|
270
|
-
stop.penaltyServiceRemaining = 0;
|
|
271
|
-
stop.penaltyServiceTotal = 0;
|
|
272
|
-
stop.servingPenaltyIds = [];
|
|
273
|
-
stop.serviceProfile = null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function buildResult({
|
|
277
|
-
host,
|
|
278
|
-
episodeState,
|
|
279
|
-
events,
|
|
280
|
-
actionErrors,
|
|
281
|
-
actions = {},
|
|
282
|
-
controlledDrivers = null,
|
|
283
|
-
stateOutput = null,
|
|
284
|
-
rewardEnabled = false,
|
|
285
|
-
}) {
|
|
286
|
-
const options = host.getOptions();
|
|
287
|
-
const sim = host.getSimulation();
|
|
288
|
-
const resultDrivers = controlledDrivers ?? options.controlledDrivers;
|
|
289
|
-
const resolvedStateOutput = stateOutput ?? options.result.stateOutput;
|
|
290
|
-
const observationSnapshot = snapshotForResult(sim, options, resolvedStateOutput);
|
|
291
|
-
markDestroyedDriverEpisodes(episodeState, options.controlledDrivers, observationSnapshot);
|
|
292
|
-
const observation = buildEnvironmentObservation({
|
|
293
|
-
snapshot: observationSnapshot,
|
|
294
|
-
previousSnapshot: episodeState.previousSnapshot,
|
|
295
|
-
options,
|
|
296
|
-
events,
|
|
297
|
-
controlledDrivers: resultDrivers,
|
|
298
|
-
scratch: episodeState.observationScratch,
|
|
299
|
-
});
|
|
300
|
-
const episode = evaluateEpisode(observationSnapshot, options, episodeState, resultDrivers);
|
|
301
|
-
const metrics = buildDriverMetrics({
|
|
302
|
-
snapshot: observationSnapshot,
|
|
303
|
-
previousSnapshot: episodeState.previousSnapshot,
|
|
304
|
-
options: { ...options, controlledDrivers: resultDrivers },
|
|
305
|
-
events,
|
|
306
|
-
scratch: episodeState.metricScratch,
|
|
307
|
-
});
|
|
308
|
-
const rewardEpisodeInfo = buildDriverEpisodeInfo(episodeState, {
|
|
309
|
-
...options,
|
|
310
|
-
controlledDrivers: resultDrivers,
|
|
311
|
-
}, episode);
|
|
312
|
-
const driverEpisode = resultDrivers === options.controlledDrivers
|
|
313
|
-
? episode
|
|
314
|
-
: evaluateEpisode(observationSnapshot, options, episodeState, options.controlledDrivers);
|
|
315
|
-
const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, driverEpisode);
|
|
316
|
-
const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
|
|
317
|
-
const reward = rewardEnabled
|
|
318
|
-
? computeReward({
|
|
319
|
-
options: { ...options, controlledDrivers: resultDrivers },
|
|
320
|
-
observation,
|
|
321
|
-
events,
|
|
322
|
-
snapshot: rewardSnapshot,
|
|
323
|
-
actions,
|
|
324
|
-
previousSnapshot: episodeState.previousSnapshot,
|
|
325
|
-
metrics,
|
|
326
|
-
driverEpisodeInfo: rewardEpisodeInfo,
|
|
327
|
-
})
|
|
328
|
-
: null;
|
|
329
|
-
episodeState.lastObservationSnapshot = observationSnapshot;
|
|
330
|
-
episodeState.lastRewardSnapshot = rewardSnapshot;
|
|
331
|
-
return {
|
|
332
|
-
observation,
|
|
333
|
-
reward,
|
|
334
|
-
metrics,
|
|
335
|
-
terminated: episode.terminated,
|
|
336
|
-
truncated: episode.truncated,
|
|
337
|
-
done: episode.terminated || episode.truncated,
|
|
338
|
-
events,
|
|
339
|
-
state,
|
|
340
|
-
info: {
|
|
341
|
-
step: episodeState.step,
|
|
342
|
-
elapsedSeconds: observationSnapshot.time,
|
|
343
|
-
seed: options.seed,
|
|
344
|
-
trackSeed: options.trackSeed,
|
|
345
|
-
controlledDrivers: [...resultDrivers],
|
|
346
|
-
actionErrors,
|
|
347
|
-
endReason: episode.endReason,
|
|
348
|
-
drivers: driverEpisodeInfo,
|
|
349
|
-
},
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function clearObservationScratch(scratch) {
|
|
354
|
-
if (!scratch) return;
|
|
355
|
-
scratch.carsById?.clear?.();
|
|
356
|
-
scratch.eventsByDriver?.clear?.();
|
|
357
|
-
scratch.controlledEventDrivers?.clear?.();
|
|
358
|
-
clearRayBatchContextScratch(scratch.rayBatchContextScratch);
|
|
359
|
-
if (Array.isArray(scratch.eventDriverIds)) scratch.eventDriverIds.length = 0;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function clearRayBatchContextScratch(scratch) {
|
|
363
|
-
if (!scratch) return;
|
|
364
|
-
const arrayKeys = [
|
|
365
|
-
'rayTargets',
|
|
366
|
-
'rayTargetPool',
|
|
367
|
-
'carTargets',
|
|
368
|
-
'rayVectors',
|
|
369
|
-
'sharedRayQueries',
|
|
370
|
-
'rays',
|
|
371
|
-
'rayPool',
|
|
372
|
-
'rayVectorValues',
|
|
373
|
-
'rayVectorValuePool',
|
|
374
|
-
];
|
|
375
|
-
for (let index = 0; index < arrayKeys.length; index += 1) {
|
|
376
|
-
const value = scratch[arrayKeys[index]];
|
|
377
|
-
if (Array.isArray(value)) value.length = 0;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function clearMetricScratch(scratch) {
|
|
382
|
-
if (!scratch) return;
|
|
383
|
-
scratch.previousCarsById?.clear?.();
|
|
384
|
-
scratch.currentCarsById?.clear?.();
|
|
385
|
-
scratch.contactCounts?.clear?.();
|
|
386
|
-
if (Array.isArray(scratch.eventDriverIds)) scratch.eventDriverIds.length = 0;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function applyControlledRunoffResponse(sim, options) {
|
|
390
|
-
options?.controlledDrivers?.forEach?.((driverId) => {
|
|
391
|
-
const car = sim?.cars?.find?.((item) => item.id === driverId);
|
|
392
|
-
if (car) sim?.applyRunoffResponse?.(car);
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function buildResultState(sim, observationSnapshot, stateOutput) {
|
|
397
|
-
if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
|
|
398
|
-
if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };
|
|
399
|
-
const snapshot = sim.snapshot();
|
|
400
|
-
return { state: { snapshot }, rewardSnapshot: snapshot };
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function snapshotForResult(sim, options, stateOutput) {
|
|
404
|
-
if (canUseTrainingSnapshot(options, stateOutput)) {
|
|
405
|
-
return sim.snapshotTraining?.() ?? sim.snapshotObservation?.() ?? sim.snapshot();
|
|
406
|
-
}
|
|
407
|
-
return sim.snapshotObservation?.() ?? sim.snapshot();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function canUseTrainingSnapshot(options, stateOutput) {
|
|
411
|
-
return stateOutput === 'none' &&
|
|
412
|
-
options.observation?.output === 'vector' &&
|
|
413
|
-
options.observation?.includeSchema === false;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function computeReward({ options, observation, events, snapshot, actions, previousSnapshot, metrics, driverEpisodeInfo }) {
|
|
417
|
-
if (!options.reward) return null;
|
|
418
|
-
return Object.fromEntries(options.controlledDrivers.map((driverId) => [
|
|
419
|
-
driverId,
|
|
420
|
-
normalizeRewardValue(options.reward({
|
|
421
|
-
driverId,
|
|
422
|
-
previous: previousSnapshot,
|
|
423
|
-
current: observation[driverId],
|
|
424
|
-
action: actions?.[driverId],
|
|
425
|
-
events: observation[driverId]?.events ?? events,
|
|
426
|
-
state: { snapshot },
|
|
427
|
-
metrics: metrics?.[driverId] ?? null,
|
|
428
|
-
episode: driverEpisodeInfo?.[driverId] ?? null,
|
|
429
|
-
})),
|
|
430
|
-
]));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function emitExternalRenderFrame(host, result, meta = {}) {
|
|
434
|
-
const options = host.getOptions?.();
|
|
435
|
-
const onFrame = options?.externalRenderer?.onFrame;
|
|
436
|
-
if (typeof onFrame !== 'function') return;
|
|
437
|
-
try {
|
|
438
|
-
onFrame({
|
|
439
|
-
snapshot: result?.state?.snapshot ??
|
|
440
|
-
host.getSimulation?.()?.snapshotObservation?.() ??
|
|
441
|
-
host.getSimulation?.()?.snapshot?.(),
|
|
442
|
-
observation: result?.observation ?? {},
|
|
443
|
-
meta: {
|
|
444
|
-
...meta,
|
|
445
|
-
step: result?.info?.step ?? null,
|
|
446
|
-
elapsedSeconds: result?.info?.elapsedSeconds ?? null,
|
|
447
|
-
},
|
|
448
|
-
});
|
|
449
|
-
} catch {
|
|
450
|
-
// External rendering hooks are observer-only and must not break stepping.
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function normalizeRewardValue(value) {
|
|
455
|
-
const reward = Number(value ?? 0);
|
|
456
|
-
return Number.isFinite(reward) ? reward : 0;
|
|
457
|
-
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ENVIRONMENT_SCENARIO_PRESETS } from './scenarioPresets.js';
|
|
2
|
+
|
|
3
|
+
export function resolveScenarioPlacementConfig(scenario = {}, driverIds = new Set()) {
|
|
4
|
+
const preset = scenario.preset == null ? null : String(scenario.preset);
|
|
5
|
+
if (preset && !ENVIRONMENT_SCENARIO_PRESETS.includes(preset)) {
|
|
6
|
+
throw new Error(`PaddockJS environment scenario.preset must be one of: ${ENVIRONMENT_SCENARIO_PRESETS.join(', ')}.`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const placements = normalizePlacementMap(scenario.placements, driverIds);
|
|
10
|
+
const traffic = normalizeTrafficLayout(scenario.traffic, driverIds);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
preset,
|
|
14
|
+
placements,
|
|
15
|
+
traffic,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeEnvironmentPlacements(placements = {}, driverIds = new Set()) {
|
|
20
|
+
return normalizePlacementMap(placements, driverIds);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizePlacementMap(value, driverIds) {
|
|
24
|
+
if (value == null) return {};
|
|
25
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
26
|
+
throw new Error('PaddockJS environment scenario.placements must be an object keyed by driver id.');
|
|
27
|
+
}
|
|
28
|
+
return Object.fromEntries(Object.entries(value).map(([driverId, placement]) => {
|
|
29
|
+
assertKnownDriver(driverId, driverIds, 'scenario placement');
|
|
30
|
+
return [driverId, normalizePlacement(placement, `scenario.placements.${driverId}`)];
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeTrafficLayout(value, driverIds) {
|
|
35
|
+
if (value == null) return [];
|
|
36
|
+
if (!Array.isArray(value)) {
|
|
37
|
+
throw new Error('PaddockJS environment scenario.traffic must be an array.');
|
|
38
|
+
}
|
|
39
|
+
return value.map((entry, index) => {
|
|
40
|
+
if (!entry || typeof entry !== 'object') {
|
|
41
|
+
throw new Error(`PaddockJS environment scenario.traffic[${index}] must be an object.`);
|
|
42
|
+
}
|
|
43
|
+
assertKnownDriver(entry.driverId, driverIds, `scenario.traffic[${index}].driverId`);
|
|
44
|
+
assertKnownDriver(entry.relativeTo, driverIds, `scenario.traffic[${index}].relativeTo`);
|
|
45
|
+
return {
|
|
46
|
+
driverId: entry.driverId,
|
|
47
|
+
relativeTo: entry.relativeTo,
|
|
48
|
+
deltaDistanceMeters: finiteNumber(entry.deltaDistanceMeters, 0, `scenario.traffic[${index}].deltaDistanceMeters`),
|
|
49
|
+
offsetMeters: finiteNumber(entry.offsetMeters, 0, `scenario.traffic[${index}].offsetMeters`),
|
|
50
|
+
speedKph: optionalFiniteNumber(entry.speedKph, `scenario.traffic[${index}].speedKph`),
|
|
51
|
+
headingErrorRadians: finiteNumber(entry.headingErrorRadians, 0, `scenario.traffic[${index}].headingErrorRadians`),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizePlacement(placement, label) {
|
|
57
|
+
if (!placement || typeof placement !== 'object') {
|
|
58
|
+
throw new Error(`PaddockJS environment ${label} must be an object.`);
|
|
59
|
+
}
|
|
60
|
+
const distanceMeters = placement.distanceMeters ?? placement.startDistanceMeters;
|
|
61
|
+
return {
|
|
62
|
+
distanceMeters: optionalFiniteNumber(distanceMeters, `${label}.distanceMeters`),
|
|
63
|
+
offsetMeters: finiteNumber(placement.offsetMeters, 0, `${label}.offsetMeters`),
|
|
64
|
+
speedKph: optionalFiniteNumber(placement.speedKph, `${label}.speedKph`),
|
|
65
|
+
headingErrorRadians: finiteNumber(placement.headingErrorRadians, 0, `${label}.headingErrorRadians`),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertKnownDriver(driverId, driverIds, label) {
|
|
70
|
+
if (typeof driverId !== 'string' || driverId.length === 0) {
|
|
71
|
+
throw new Error(`PaddockJS environment ${label} must be a driver id.`);
|
|
72
|
+
}
|
|
73
|
+
if (!driverIds.has(driverId)) {
|
|
74
|
+
throw new Error(`PaddockJS environment ${label} does not exist: ${driverId}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function finiteNumber(value, fallback, label) {
|
|
79
|
+
if (value == null) return fallback;
|
|
80
|
+
const number = Number(value);
|
|
81
|
+
if (!Number.isFinite(number)) {
|
|
82
|
+
throw new Error(`PaddockJS environment ${label} must be a finite number.`);
|
|
83
|
+
}
|
|
84
|
+
return number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function optionalFiniteNumber(value, label) {
|
|
88
|
+
if (value == null) return undefined;
|
|
89
|
+
return finiteNumber(value, undefined, label);
|
|
90
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { normalizeAngle } from '../simulation/simMath.js';
|
|
2
|
+
import { offsetTrackPoint, pointAt } from '../simulation/track/trackModel.js';
|
|
3
|
+
import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
|
|
4
|
+
|
|
5
|
+
export function applyEnvironmentPlacements(sim, track, carsById, placements = {}) {
|
|
6
|
+
const states = {};
|
|
7
|
+
Object.entries(placements).forEach(([driverId, placement]) => {
|
|
8
|
+
const currentCar = carsById.get(driverId);
|
|
9
|
+
if (!currentCar) return;
|
|
10
|
+
states[driverId] = placementToCarState(track, currentCar, placement, {
|
|
11
|
+
physicsMode: sim.physicsMode,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
if (typeof sim.setCarStates === 'function') {
|
|
15
|
+
sim.setCarStates(states);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
Object.entries(states).forEach(([driverId, partial]) => {
|
|
19
|
+
sim.setCarState(driverId, partial);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function placementToCarState(track, currentCar, placement, { physicsMode = 'arcade' } = {}) {
|
|
24
|
+
const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
|
|
25
|
+
const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
|
|
26
|
+
const offset = metersToSimUnits(placement.offsetMeters ?? 0);
|
|
27
|
+
const base = pointAt(track, raceDistance);
|
|
28
|
+
const position = offsetTrackPoint(base, offset);
|
|
29
|
+
const heading = normalizeAngle(base.heading + (placement.headingErrorRadians ?? 0));
|
|
30
|
+
const speed = placement.speedKph == null ? null : kphToSimSpeed(placement.speedKph);
|
|
31
|
+
const partial = {
|
|
32
|
+
x: position.x,
|
|
33
|
+
y: position.y,
|
|
34
|
+
previousX: position.x,
|
|
35
|
+
previousY: position.y,
|
|
36
|
+
progress: base.distance,
|
|
37
|
+
raceDistance,
|
|
38
|
+
desiredOffset: offset,
|
|
39
|
+
heading,
|
|
40
|
+
previousHeading: heading,
|
|
41
|
+
destroyed: false,
|
|
42
|
+
destroyReason: null,
|
|
43
|
+
destroyedAt: null,
|
|
44
|
+
outOfRace: false,
|
|
45
|
+
canAttack: true,
|
|
46
|
+
throttle: 0,
|
|
47
|
+
brake: 0,
|
|
48
|
+
longitudinalAcceleration: 0,
|
|
49
|
+
lateralAcceleration: 0,
|
|
50
|
+
lateralG: 0,
|
|
51
|
+
longitudinalG: 0,
|
|
52
|
+
gripUsage: 0,
|
|
53
|
+
slipAngleRadians: 0,
|
|
54
|
+
tractionLimited: false,
|
|
55
|
+
steeringAngle: 0,
|
|
56
|
+
yawRate: 0,
|
|
57
|
+
stabilityState: 'stable',
|
|
58
|
+
};
|
|
59
|
+
if (speed != null) {
|
|
60
|
+
partial.speed = speed;
|
|
61
|
+
if (physicsMode === 'advanced') {
|
|
62
|
+
partial.velocityX = Math.cos(heading) * speed;
|
|
63
|
+
partial.velocityY = Math.sin(heading) * speed;
|
|
64
|
+
} else {
|
|
65
|
+
partial.velocityX = null;
|
|
66
|
+
partial.velocityY = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return partial;
|
|
70
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
|
|
2
|
+
|
|
3
|
+
export const ENVIRONMENT_SCENARIO_PRESETS = Object.freeze([
|
|
4
|
+
'cornering',
|
|
5
|
+
'off-track-recovery',
|
|
6
|
+
'overtaking-pack',
|
|
7
|
+
'pit-entry',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export function buildPresetPlacements(snapshot, options) {
|
|
11
|
+
const preset = options.scenario?.preset;
|
|
12
|
+
if (!preset) return {};
|
|
13
|
+
const controlledDriverId = options.controlledDrivers[0];
|
|
14
|
+
const cars = snapshot.cars;
|
|
15
|
+
const track = snapshot.track;
|
|
16
|
+
const primaryDistance = presetBaseDistance(track, preset);
|
|
17
|
+
const placements = {};
|
|
18
|
+
|
|
19
|
+
if (preset === 'cornering') {
|
|
20
|
+
placements[controlledDriverId] = {
|
|
21
|
+
distanceMeters: simUnitsToMeters(primaryDistance),
|
|
22
|
+
offsetMeters: 0,
|
|
23
|
+
speedKph: 135,
|
|
24
|
+
headingErrorRadians: 0,
|
|
25
|
+
};
|
|
26
|
+
} else if (preset === 'off-track-recovery') {
|
|
27
|
+
placements[controlledDriverId] = {
|
|
28
|
+
distanceMeters: simUnitsToMeters(primaryDistance),
|
|
29
|
+
offsetMeters: simUnitsToMeters(track.width / 2) + 7,
|
|
30
|
+
speedKph: 52,
|
|
31
|
+
headingErrorRadians: -0.45,
|
|
32
|
+
};
|
|
33
|
+
} else if (preset === 'pit-entry') {
|
|
34
|
+
placements[controlledDriverId] = {
|
|
35
|
+
distanceMeters: simUnitsToMeters(primaryDistance),
|
|
36
|
+
offsetMeters: 0,
|
|
37
|
+
speedKph: 120,
|
|
38
|
+
headingErrorRadians: 0,
|
|
39
|
+
};
|
|
40
|
+
} else if (preset === 'overtaking-pack') {
|
|
41
|
+
const baseMeters = simUnitsToMeters(primaryDistance);
|
|
42
|
+
cars.slice(0, 4).forEach((car, index) => {
|
|
43
|
+
placements[car.id] = {
|
|
44
|
+
distanceMeters: baseMeters + (index - 1) * 18,
|
|
45
|
+
offsetMeters: index % 2 === 0 ? -2.2 : 2.2,
|
|
46
|
+
speedKph: 135 - index * 3,
|
|
47
|
+
headingErrorRadians: 0,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return placements;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function presetBaseDistance(track, preset) {
|
|
56
|
+
if (preset === 'cornering') return findHighCurvatureDistance(track) - metersToSimUnits(70);
|
|
57
|
+
if (preset === 'off-track-recovery') return track.length * 0.42;
|
|
58
|
+
if (preset === 'overtaking-pack') return track.length * 0.35;
|
|
59
|
+
if (preset === 'pit-entry') {
|
|
60
|
+
const pitDistance = track.pitLane?.entry?.trackDistance ??
|
|
61
|
+
track.pitLane?.entry?.distanceFromStart ??
|
|
62
|
+
track.length * 0.92;
|
|
63
|
+
return pitDistance - metersToSimUnits(170);
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findHighCurvatureDistance(track) {
|
|
69
|
+
const samples = Array.isArray(track.samples) ? track.samples : [];
|
|
70
|
+
const fallback = track.length * 0.25;
|
|
71
|
+
const best = samples
|
|
72
|
+
.filter((sample) => sample.distance > metersToSimUnits(350))
|
|
73
|
+
.reduce((selected, sample) => {
|
|
74
|
+
if (!selected || (sample.curvature ?? 0) > (selected.curvature ?? 0)) return sample;
|
|
75
|
+
return selected;
|
|
76
|
+
}, null);
|
|
77
|
+
return best?.distance ?? fallback;
|
|
78
|
+
}
|