@inventure71/paddockjs 2.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +176 -1
- package/INSTALL_AND_UPDATE.md +24 -4
- package/README.md +84 -512
- package/docs/architecture.md +38 -24
- package/docs/component_inventory.md +72 -0
- package/docs/composable-layouts.md +77 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +6 -6
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +104 -27
- package/docs/getting-started.md +75 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +18 -4
- package/docs/learnings.md +6 -6
- package/docs/rules.md +12 -8
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +71 -21
- package/docs/theming.md +64 -0
- package/docs/training.md +26 -19
- package/docs/troubleshooting.md +76 -0
- package/docs/upgrading-hosts-to-3.md +42 -0
- package/examples/python/README.md +6 -0
- package/examples/python/base_policy_server.py +19 -0
- package/package.json +10 -3
- package/src/api/PaddockSimulatorController.js +150 -24
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +213 -29
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +262 -49
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +58 -38
- package/src/config/themeOptions.js +830 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/controllerLoop.js +17 -3
- package/src/environment/episode.js +11 -1
- package/src/environment/index.d.ts +1 -2
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +44 -11
- package/src/environment/scenarios.js +1 -1
- package/src/environment/sensors/carRays.js +48 -37
- package/src/environment/sensors/index.js +501 -97
- package/src/environment/sensors/indexedRayBands.js +134 -46
- package/src/environment/sensors/rayBandTrace.js +1171 -0
- package/src/environment/sensors/rayChannels.js +108 -0
- package/src/environment/sensors/rayConfig.js +71 -25
- package/src/environment/sensors/rayGeometry.js +23 -17
- package/src/environment/sensors/sensorTargets.js +67 -1
- package/src/environment/sensors/surfaceRays.js +167 -118
- package/src/environment/sensors/trackRays.js +157 -48
- package/src/index.d.ts +153 -9
- package/src/index.js +52 -3
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitKinematics.js +2 -2
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +79 -46
- package/src/simulation/pit/pitState.js +1 -0
- package/src/simulation/pit/pitTireService.js +7 -3
- package/src/simulation/race/raceFinish.js +25 -8
- package/src/simulation/race/raceOrder.js +189 -32
- package/src/simulation/race/raceProgress.js +129 -37
- package/src/simulation/race/raceSetup.js +4 -6
- package/src/simulation/race/raceStep.js +26 -14
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- package/src/simulation/raceSimulation.js +20 -17
- package/src/simulation/rules/collisionSteward.js +40 -23
- package/src/simulation/rules/rulesReview.js +77 -56
- package/src/simulation/snapshots/raceSnapshots.js +37 -0
- package/src/simulation/timing/drsTiming.js +143 -30
- package/src/simulation/timing/gapEstimation.js +25 -3
- package/src/simulation/timing/lapTelemetry.js +138 -40
- package/src/simulation/timing/raceTiming.js +5 -1
- package/src/simulation/timing/sectorPerformance.js +104 -26
- package/src/simulation/timing/timingHistory.js +229 -14
- package/src/simulation/timing/timingLines.js +65 -7
- package/src/simulation/track/pitLaneLayout.js +30 -3
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +66 -33
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1468 -189
- package/src/simulation/track/trackQueryPitIndex.js +138 -14
- package/src/simulation/track/trackQueryScratch.js +7 -0
- package/src/simulation/track/trackQueryStats.js +25 -0
- package/src/simulation/track/trackSnapshotJson.js +139 -0
- package/src/simulation/track/trackStatePolicy.js +249 -2
- package/src/simulation/vehicle/contactResolution.js +99 -62
- package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
- package/src/simulation/vehicle/pitWheelSurface.js +81 -28
- package/src/simulation/vehicle/runoffResponse.js +54 -29
- package/src/simulation/vehicle/vehicleGeometry.js +245 -51
- package/src/simulation/vehicle/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +45 -30
- package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
- package/src/simulation/vehicle/vehicleState.js +7 -1
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1568 -519
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const TIMING_GAP_MODES = new Set(['interval', 'leader']);
|
|
2
|
+
|
|
3
|
+
export function normalizeTimingGapMode(value) {
|
|
4
|
+
return value === 'leader' ? 'leader' : 'interval';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getTimingGapModeLabel(mode) {
|
|
8
|
+
return normalizeTimingGapMode(mode) === 'leader' ? 'Gap' : 'Int';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getNextTimingGapMode(mode) {
|
|
12
|
+
return normalizeTimingGapMode(mode) === 'leader' ? 'interval' : 'leader';
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const SAFE_ABSOLUTE_URL_PROTOCOLS = new Set(['http:', 'https:']);
|
|
2
|
+
|
|
3
|
+
export function normalizePublicUrlOption(value, fallback) {
|
|
4
|
+
const fallbackValue = typeof fallback === 'string' && fallback.trim() !== ''
|
|
5
|
+
? fallback.trim()
|
|
6
|
+
: '';
|
|
7
|
+
if (typeof value !== 'string') return fallbackValue;
|
|
8
|
+
|
|
9
|
+
const candidate = value.trim();
|
|
10
|
+
if (candidate === '') return fallbackValue;
|
|
11
|
+
if (/[\u0000-\u001F\u007F]/.test(candidate)) return fallbackValue;
|
|
12
|
+
if (candidate.includes('\\')) return fallbackValue;
|
|
13
|
+
if (candidate.startsWith('#')) return candidate;
|
|
14
|
+
if (candidate.startsWith('//')) return fallbackValue;
|
|
15
|
+
|
|
16
|
+
const schemeMatch = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(candidate);
|
|
17
|
+
if (!schemeMatch) return candidate;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(candidate);
|
|
21
|
+
return SAFE_ABSOLUTE_URL_PROTOCOLS.has(url.protocol) ? candidate : fallbackValue;
|
|
22
|
+
} catch {
|
|
23
|
+
return fallbackValue;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/data/championship.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DriverData } from './driverData.js';
|
|
2
2
|
import { DEMO_PROJECT_DRIVERS } from './demoDrivers.js';
|
|
3
3
|
import { VehicleData } from './vehicleData.js';
|
|
4
|
+
import { DEFAULT_DARK_THEME_TOKENS, normalizeCssColorToken } from '../config/themeOptions.js';
|
|
4
5
|
|
|
5
6
|
export { DriverData, DRIVER_STAT_DEFINITIONS } from './driverData.js';
|
|
6
7
|
export { VehicleData, VEHICLE_STAT_DEFINITIONS } from './vehicleData.js';
|
|
@@ -187,7 +188,7 @@ function normalizeTeam(team, driver, timingCode) {
|
|
|
187
188
|
...source,
|
|
188
189
|
id,
|
|
189
190
|
name,
|
|
190
|
-
color: source.color ?? driver.color,
|
|
191
|
+
color: normalizeCssColorToken(source.color ?? driver.color, driver.color),
|
|
191
192
|
icon: source.icon ?? normalizeTeamIcon(source.name ?? source.id ?? timingCode ?? driver.name),
|
|
192
193
|
};
|
|
193
194
|
}
|
|
@@ -201,22 +202,26 @@ export function buildChampionshipDriverGrid(drivers = DEMO_PROJECT_DRIVERS, entr
|
|
|
201
202
|
const usedTimingCodes = new Set();
|
|
202
203
|
|
|
203
204
|
return drivers.map((driver, index) => {
|
|
205
|
+
const safeDriver = {
|
|
206
|
+
...driver,
|
|
207
|
+
color: normalizeCssColorToken(driver.color, DEFAULT_DARK_THEME_TOKENS.primary),
|
|
208
|
+
};
|
|
204
209
|
const entry = {
|
|
205
|
-
...(blueprintByDriverId.get(
|
|
206
|
-
...(entryByDriverId.get(
|
|
210
|
+
...(blueprintByDriverId.get(safeDriver.id) ?? {}),
|
|
211
|
+
...(entryByDriverId.get(safeDriver.id) ?? {}),
|
|
207
212
|
};
|
|
208
|
-
const timingCode = pickUniqueTimingCode(entry.timingName ??
|
|
209
|
-
const driverNumber = entry.driverNumber ??
|
|
210
|
-
const driverArgs = buildDriverConstructorArgs({ driverId:
|
|
211
|
-
const vehicleArgs = buildVehicleConstructorArgs({ driverId:
|
|
213
|
+
const timingCode = pickUniqueTimingCode(entry.timingName ?? safeDriver.timingName ?? safeDriver.name ?? safeDriver.code, usedTimingCodes);
|
|
214
|
+
const driverNumber = entry.driverNumber ?? safeDriver.driverNumber ?? index + 1;
|
|
215
|
+
const driverArgs = buildDriverConstructorArgs({ driverId: safeDriver.id, ...entry });
|
|
216
|
+
const vehicleArgs = buildVehicleConstructorArgs({ driverId: safeDriver.id, ...entry });
|
|
212
217
|
|
|
213
218
|
return {
|
|
214
|
-
...
|
|
219
|
+
...safeDriver,
|
|
215
220
|
code: timingCode,
|
|
216
221
|
timingCode,
|
|
217
222
|
raceName: timingCode,
|
|
218
223
|
driverNumber,
|
|
219
|
-
team: normalizeTeam(entry.team,
|
|
224
|
+
team: normalizeTeam(entry.team, safeDriver, timingCode),
|
|
220
225
|
pace: driverArgs.pace,
|
|
221
226
|
racecraft: driverArgs.racecraft,
|
|
222
227
|
consistency: driverArgs.consistency,
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export type TireCompound = 'S' | 'M' | 'H';
|
|
2
|
+
export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
|
|
3
|
+
|
|
4
|
+
export interface PaddockProceduralTrackOptions {
|
|
5
|
+
profile?: PaddockProceduralTrackProfile;
|
|
6
|
+
minLengthMeters?: number;
|
|
7
|
+
maxLengthMeters?: number;
|
|
8
|
+
startStraightMeters?: number;
|
|
9
|
+
includePitLane?: boolean;
|
|
10
|
+
length?: {
|
|
11
|
+
minMeters?: number;
|
|
12
|
+
maxMeters?: number;
|
|
13
|
+
};
|
|
14
|
+
startStraight?: {
|
|
15
|
+
gridMeters?: number;
|
|
16
|
+
exitMeters?: number;
|
|
17
|
+
blendMeters?: number;
|
|
18
|
+
lockExtraMeters?: number;
|
|
19
|
+
};
|
|
20
|
+
pitLane?: {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
};
|
|
23
|
+
shape?: {
|
|
24
|
+
scale?: number;
|
|
25
|
+
cornerDensity?: number;
|
|
26
|
+
variation?: number;
|
|
27
|
+
};
|
|
28
|
+
validation?: {
|
|
29
|
+
minClearanceMultiplier?: number;
|
|
30
|
+
minShapeVariation?: number;
|
|
31
|
+
minNonAdjacentArcMeters?: number;
|
|
32
|
+
maxLocalTurnRadians?: number;
|
|
33
|
+
maxSampleHeadingDeltaRadians?: number;
|
|
34
|
+
};
|
|
35
|
+
attempts?: {
|
|
36
|
+
primary?: number;
|
|
37
|
+
fallback?: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CustomField {
|
|
42
|
+
label: string;
|
|
43
|
+
value: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type CustomFieldInput = CustomField[] | Record<string, string>;
|
|
47
|
+
export type PaddockThemeSelector = 'default' | 'active' | 'selectedTeam' | 'team' | `team:${string}` | string;
|
|
48
|
+
|
|
49
|
+
export interface PaddockPitCrewStats {
|
|
50
|
+
speed?: number;
|
|
51
|
+
consistency?: number;
|
|
52
|
+
reliability?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TeamData {
|
|
56
|
+
id?: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
color?: string;
|
|
59
|
+
icon?: string;
|
|
60
|
+
theme?: PaddockThemeSelector;
|
|
61
|
+
pitCrew?: PaddockPitCrewStats;
|
|
62
|
+
pitCrewStats?: PaddockPitCrewStats;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SimulatorDriver {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
color: string;
|
|
69
|
+
link?: string;
|
|
70
|
+
icon?: string;
|
|
71
|
+
code?: string;
|
|
72
|
+
timingName?: string;
|
|
73
|
+
tire?: TireCompound;
|
|
74
|
+
raceData?: string[];
|
|
75
|
+
customFields?: CustomFieldInput;
|
|
76
|
+
team?: TeamData | null;
|
|
77
|
+
driverNumber?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DriverRatings {
|
|
81
|
+
pace: number;
|
|
82
|
+
racecraft: number;
|
|
83
|
+
aggression: number;
|
|
84
|
+
riskTolerance: number;
|
|
85
|
+
patience: number;
|
|
86
|
+
consistency: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface VehicleRatings {
|
|
90
|
+
power: number;
|
|
91
|
+
braking: number;
|
|
92
|
+
aero: number;
|
|
93
|
+
dragEfficiency: number;
|
|
94
|
+
mechanicalGrip: number;
|
|
95
|
+
weightControl: number;
|
|
96
|
+
tireCare: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface DriverBlueprint extends Partial<DriverRatings> {
|
|
100
|
+
customFields?: CustomFieldInput;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface VehicleBlueprint extends Partial<VehicleRatings> {
|
|
104
|
+
id?: string | null;
|
|
105
|
+
name?: string | null;
|
|
106
|
+
customFields?: CustomFieldInput;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DriverConstructorArgs {
|
|
110
|
+
ratings: DriverRatings;
|
|
111
|
+
customFields: CustomField[];
|
|
112
|
+
pace: number;
|
|
113
|
+
racecraft: number;
|
|
114
|
+
consistency: number;
|
|
115
|
+
personality: {
|
|
116
|
+
aggression: number;
|
|
117
|
+
riskTolerance: number;
|
|
118
|
+
patience: number;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface VehicleConstructorArgs {
|
|
123
|
+
id: string | null;
|
|
124
|
+
name: string | null;
|
|
125
|
+
ratings: VehicleRatings;
|
|
126
|
+
customFields: CustomField[];
|
|
127
|
+
powerNewtons: number;
|
|
128
|
+
brakeNewtons: number;
|
|
129
|
+
downforceCoefficient: number;
|
|
130
|
+
dragCoefficient: number;
|
|
131
|
+
tireGrip: number;
|
|
132
|
+
mass: number;
|
|
133
|
+
tireCare: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class DriverData {
|
|
137
|
+
constructor(input?: DriverBlueprint);
|
|
138
|
+
readonly pace: number;
|
|
139
|
+
readonly racecraft: number;
|
|
140
|
+
readonly aggression: number;
|
|
141
|
+
readonly riskTolerance: number;
|
|
142
|
+
readonly patience: number;
|
|
143
|
+
readonly consistency: number;
|
|
144
|
+
readonly customFields: CustomField[];
|
|
145
|
+
ratings(): DriverRatings;
|
|
146
|
+
toConstructorArgs(): DriverConstructorArgs;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class VehicleData {
|
|
150
|
+
constructor(input?: VehicleBlueprint);
|
|
151
|
+
readonly id: string | null;
|
|
152
|
+
readonly name: string | null;
|
|
153
|
+
readonly power: number;
|
|
154
|
+
readonly braking: number;
|
|
155
|
+
readonly aero: number;
|
|
156
|
+
readonly dragEfficiency: number;
|
|
157
|
+
readonly mechanicalGrip: number;
|
|
158
|
+
readonly weightControl: number;
|
|
159
|
+
readonly tireCare: number;
|
|
160
|
+
readonly customFields: CustomField[];
|
|
161
|
+
ratings(): VehicleRatings;
|
|
162
|
+
toConstructorArgs(): VehicleConstructorArgs;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ChampionshipEntryBlueprint {
|
|
166
|
+
driverId: string;
|
|
167
|
+
driverNumber?: number;
|
|
168
|
+
timingName?: string;
|
|
169
|
+
driver?: DriverData | DriverBlueprint;
|
|
170
|
+
vehicle?: VehicleData | VehicleBlueprint;
|
|
171
|
+
team?: TeamData;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface NormalizedSimulatorDriver extends SimulatorDriver {
|
|
175
|
+
code: string;
|
|
176
|
+
timingCode: string;
|
|
177
|
+
raceName: string;
|
|
178
|
+
driverNumber: number;
|
|
179
|
+
team: TeamData | null;
|
|
180
|
+
pace: number;
|
|
181
|
+
racecraft: number;
|
|
182
|
+
consistency: number;
|
|
183
|
+
personality: DriverConstructorArgs['personality'];
|
|
184
|
+
vehicle: VehicleConstructorArgs;
|
|
185
|
+
constructorArgs: {
|
|
186
|
+
driver: DriverConstructorArgs;
|
|
187
|
+
vehicle: VehicleConstructorArgs;
|
|
188
|
+
};
|
|
189
|
+
championship: {
|
|
190
|
+
id: string;
|
|
191
|
+
name: string;
|
|
192
|
+
season: number;
|
|
193
|
+
entryIndex: number;
|
|
194
|
+
vehicleId: string | null;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const CHAMPIONSHIP_ENTRY_BLUEPRINTS: ChampionshipEntryBlueprint[];
|
|
199
|
+
export const DEMO_PROJECT_DRIVERS: SimulatorDriver[];
|
|
200
|
+
|
|
201
|
+
export function buildChampionshipDriverGrid(
|
|
202
|
+
drivers?: SimulatorDriver[],
|
|
203
|
+
entries?: ChampionshipEntryBlueprint[],
|
|
204
|
+
): NormalizedSimulatorDriver[];
|
|
205
|
+
export function formatDriverNumber(driverNumber: number | string | null | undefined): string;
|
|
206
|
+
export function normalizeSimulatorDrivers(
|
|
207
|
+
drivers: SimulatorDriver[],
|
|
208
|
+
options?: { entries?: ChampionshipEntryBlueprint[]; caller?: string },
|
|
209
|
+
): NormalizedSimulatorDriver[];
|
|
210
|
+
export function createProceduralTrack(seed?: number | string, options?: PaddockProceduralTrackOptions): unknown;
|
|
211
|
+
export function kphToSimSpeed(kph: number): number;
|
|
212
|
+
export function simSpeedToKph(simSpeed: number): number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CHAMPIONSHIP_ENTRY_BLUEPRINTS,
|
|
3
|
+
DriverData,
|
|
4
|
+
VehicleData,
|
|
5
|
+
buildChampionshipDriverGrid,
|
|
6
|
+
formatDriverNumber,
|
|
7
|
+
} from './championship.js';
|
|
8
|
+
export { DEMO_PROJECT_DRIVERS } from './demoDrivers.js';
|
|
9
|
+
export { normalizeSimulatorDrivers } from './normalizeDrivers.js';
|
|
10
|
+
export { createProceduralTrack } from '../simulation/track/trackModel.js';
|
|
11
|
+
export {
|
|
12
|
+
kphToSimSpeed,
|
|
13
|
+
simSpeedToKph,
|
|
14
|
+
} from '../simulation/units.js';
|
|
@@ -168,8 +168,7 @@ export function createPaddockDriverControllerLoop({
|
|
|
168
168
|
|
|
169
169
|
function stop() {
|
|
170
170
|
running = false;
|
|
171
|
-
|
|
172
|
-
else if (typeof scheduled === 'number' && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scheduled);
|
|
171
|
+
cancelScheduled(scheduled);
|
|
173
172
|
scheduled = null;
|
|
174
173
|
}
|
|
175
174
|
|
|
@@ -250,9 +249,24 @@ function now() {
|
|
|
250
249
|
: Date.now();
|
|
251
250
|
}
|
|
252
251
|
|
|
252
|
+
function cancelScheduled(handle) {
|
|
253
|
+
if (handle == null) return;
|
|
254
|
+
if (typeof handle.cancel === 'function') {
|
|
255
|
+
handle.cancel();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (typeof handle === 'number') {
|
|
259
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(handle);
|
|
260
|
+
if (typeof clearTimeout === 'function') clearTimeout(handle);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
253
264
|
function defaultScheduler() {
|
|
254
265
|
if (typeof requestAnimationFrame === 'function') {
|
|
255
|
-
return (callback) =>
|
|
266
|
+
return (callback) => {
|
|
267
|
+
const id = requestAnimationFrame(callback);
|
|
268
|
+
return { cancel: () => cancelAnimationFrame(id) };
|
|
269
|
+
};
|
|
256
270
|
}
|
|
257
271
|
return (callback) => {
|
|
258
272
|
const id = setTimeout(callback, 0);
|
|
@@ -2,6 +2,9 @@ export function createEpisodeState() {
|
|
|
2
2
|
return {
|
|
3
3
|
step: 0,
|
|
4
4
|
drivers: new Map(),
|
|
5
|
+
observationScratch: {},
|
|
6
|
+
metricScratch: {},
|
|
7
|
+
destroyedCarsById: new Map(),
|
|
5
8
|
previousSnapshot: null,
|
|
6
9
|
lastResult: null,
|
|
7
10
|
};
|
|
@@ -30,7 +33,14 @@ export function resetDriverEpisodes(episodeState, driverIds = []) {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function markDestroyedDriverEpisodes(episodeState, driverIds = [], snapshot = null) {
|
|
33
|
-
const carsById = new Map(
|
|
36
|
+
const carsById = episodeState.destroyedCarsById ?? new Map();
|
|
37
|
+
episodeState.destroyedCarsById = carsById;
|
|
38
|
+
carsById.clear();
|
|
39
|
+
const cars = snapshot?.cars ?? [];
|
|
40
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
41
|
+
const car = cars[index];
|
|
42
|
+
carsById.set(car.id, car);
|
|
43
|
+
}
|
|
34
44
|
driverIds.forEach((driverId) => {
|
|
35
45
|
const car = carsById.get(driverId);
|
|
36
46
|
if (!car?.destroyed && !car?.dnf && !car?.outOfRace) return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type TireCompound = 'S' | 'M' | 'H';
|
|
2
|
-
export type PaddockPhysicsMode = 'arcade' | '
|
|
2
|
+
export type PaddockPhysicsMode = 'arcade' | 'advanced';
|
|
3
3
|
export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
|
|
4
4
|
|
|
5
5
|
export interface PaddockProceduralTrackOptions {
|
|
@@ -478,7 +478,6 @@ export interface PaddockEnvironmentOptions {
|
|
|
478
478
|
seed?: number;
|
|
479
479
|
trackSeed?: number;
|
|
480
480
|
trackGeneration?: PaddockProceduralTrackOptions;
|
|
481
|
-
trackQueryIndex?: boolean;
|
|
482
481
|
totalLaps?: number;
|
|
483
482
|
frameSkip?: number;
|
|
484
483
|
physicsMode?: PaddockPhysicsMode;
|
|
@@ -5,6 +5,21 @@ import { simUnitsToMeters } from '../simulation/units.js';
|
|
|
5
5
|
const LEGAL_SURFACES = new Set(['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box']);
|
|
6
6
|
const ILLEGAL_SURFACES = new Set(['grass', 'gravel', 'runoff', 'barrier']);
|
|
7
7
|
const UNSTABLE_STATES = new Set(['spun', 'backwards', 'spin-risk', 'understeer', 'oversteer', 'destroyed']);
|
|
8
|
+
export const ENVIRONMENT_METRIC_FIELDS = Object.freeze([
|
|
9
|
+
'progressDeltaMeters',
|
|
10
|
+
'legalProgressDeltaMeters',
|
|
11
|
+
'offTrack',
|
|
12
|
+
'kerb',
|
|
13
|
+
'fullyOutsideWhiteLine',
|
|
14
|
+
'severeCut',
|
|
15
|
+
'destroyed',
|
|
16
|
+
'destroyReason',
|
|
17
|
+
'under30kph',
|
|
18
|
+
'spinOrBackwards',
|
|
19
|
+
'completedLap',
|
|
20
|
+
'lapTimeSeconds',
|
|
21
|
+
'contactCount',
|
|
22
|
+
]);
|
|
8
23
|
|
|
9
24
|
export function isEnvironmentCarOffTrack(car) {
|
|
10
25
|
if (!car) return false;
|
|
@@ -15,19 +30,32 @@ export function isEnvironmentContactEvent(event) {
|
|
|
15
30
|
return ['collision', 'contact', 'car-contact'].includes(event?.type);
|
|
16
31
|
}
|
|
17
32
|
|
|
18
|
-
export function buildDriverMetrics({ snapshot, previousSnapshot = null, options, events = [] }) {
|
|
19
|
-
const previousById =
|
|
20
|
-
const currentById =
|
|
21
|
-
const contactCounts = contactCountsByDriver(events);
|
|
33
|
+
export function buildDriverMetrics({ snapshot, previousSnapshot = null, options, events = [], scratch = null }) {
|
|
34
|
+
const previousById = carsByIdForMetrics(previousSnapshot, scratch, 'previousCarsById');
|
|
35
|
+
const currentById = carsByIdForMetrics(snapshot, scratch, 'currentCarsById');
|
|
36
|
+
const contactCounts = contactCountsByDriver(events, scratch);
|
|
22
37
|
const metrics = {};
|
|
23
38
|
options.controlledDrivers.forEach((driverId) => {
|
|
24
39
|
const car = currentById.get(driverId);
|
|
25
40
|
const previous = previousById.get(driverId);
|
|
26
|
-
metrics[driverId] = car ? buildMetricForDriver(car, previous, snapshot, contactCounts
|
|
41
|
+
metrics[driverId] = car ? buildMetricForDriver(car, previous, snapshot, contactCounts?.get(driverId) ?? 0) : emptyMetrics();
|
|
27
42
|
});
|
|
28
43
|
return metrics;
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
function carsByIdForMetrics(snapshot, scratch, key) {
|
|
47
|
+
if (!scratch) return new Map((snapshot?.cars ?? []).map((car) => [car.id, car]));
|
|
48
|
+
const carsById = scratch[key] ?? new Map();
|
|
49
|
+
scratch[key] = carsById;
|
|
50
|
+
carsById.clear();
|
|
51
|
+
const cars = snapshot?.cars ?? [];
|
|
52
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
53
|
+
const car = cars[index];
|
|
54
|
+
carsById.set(car.id, car);
|
|
55
|
+
}
|
|
56
|
+
return carsById;
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
function buildMetricForDriver(car, previous, snapshot, contactCount) {
|
|
32
60
|
const progressDeltaMeters = progressDelta(car, previous);
|
|
33
61
|
const wheelState = classifyWheels(car);
|
|
@@ -140,20 +168,35 @@ function lastLapTimeSeconds(car) {
|
|
|
140
168
|
return car.lapTelemetry?.lastLapTime ?? car.lapTelemetry?.bestLapTime ?? null;
|
|
141
169
|
}
|
|
142
170
|
|
|
143
|
-
function contactCountsByDriver(events) {
|
|
144
|
-
|
|
145
|
-
|
|
171
|
+
function contactCountsByDriver(events, scratch = null) {
|
|
172
|
+
if (!events.length) return null;
|
|
173
|
+
const counts = scratch ? (scratch.contactCounts ?? new Map()) : new Map();
|
|
174
|
+
const eventDriverIds = scratch ? (scratch.eventDriverIds ?? []) : [];
|
|
175
|
+
if (scratch) {
|
|
176
|
+
scratch.contactCounts = counts;
|
|
177
|
+
scratch.eventDriverIds = eventDriverIds;
|
|
178
|
+
}
|
|
179
|
+
counts.clear();
|
|
146
180
|
events.forEach((event) => {
|
|
147
181
|
if (!isEnvironmentContactEvent(event)) return;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
eventDriverIds.length = 0;
|
|
183
|
+
pushEventDriverId(eventDriverIds, event.driverId);
|
|
184
|
+
pushEventDriverId(eventDriverIds, event.carId);
|
|
185
|
+
pushEventDriverId(eventDriverIds, event.otherCarId);
|
|
186
|
+
const extraDriverIds = event.driverIds ?? [];
|
|
187
|
+
for (let index = 0; index < extraDriverIds.length; index += 1) {
|
|
188
|
+
pushEventDriverId(eventDriverIds, extraDriverIds[index]);
|
|
189
|
+
}
|
|
190
|
+
for (let index = 0; index < eventDriverIds.length; index += 1) {
|
|
191
|
+
const driverId = eventDriverIds[index];
|
|
155
192
|
counts.set(driverId, (counts.get(driverId) ?? 0) + 1);
|
|
156
|
-
}
|
|
193
|
+
}
|
|
157
194
|
});
|
|
195
|
+
eventDriverIds.length = 0;
|
|
158
196
|
return counts;
|
|
159
197
|
}
|
|
198
|
+
|
|
199
|
+
function pushEventDriverId(target, driverId) {
|
|
200
|
+
if (!driverId || target.includes(driverId)) return;
|
|
201
|
+
target.push(driverId);
|
|
202
|
+
}
|
|
@@ -91,40 +91,36 @@ export function buildObservationVector(source, sensors, { includeSchema = true,
|
|
|
91
91
|
);
|
|
92
92
|
vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
|
|
93
93
|
});
|
|
94
|
-
source.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{ name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
|
|
101
|
-
{ name: `rays[${index}].car.hit`, scale: 'boolean' },
|
|
102
|
-
{ name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
103
|
-
{ name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
|
|
104
|
-
);
|
|
105
|
-
vector.push(
|
|
106
|
-
ratio(ray.track.distanceMeters, ray.lengthMeters),
|
|
107
|
-
ray.track.hit ? 1 : 0,
|
|
108
|
-
ray.track.kind === 'exit' ? 1 : 0,
|
|
109
|
-
ray.track.kind === 'entry' ? 1 : 0,
|
|
110
|
-
ratio(ray.car.distanceMeters, ray.lengthMeters),
|
|
111
|
-
ray.car.hit ? 1 : 0,
|
|
112
|
-
ray.car.relativeSpeedKph / 200,
|
|
113
|
-
ray.car.targetType === 'replayGhost' ? 1 : 0,
|
|
114
|
-
);
|
|
115
|
-
MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
|
|
116
|
-
if (sensors.rays.channels?.includes?.(channel)) {
|
|
117
|
-
pushSchema(schema,
|
|
118
|
-
{ name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
|
|
119
|
-
{ name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
|
|
120
|
-
);
|
|
121
|
-
vector.push(
|
|
122
|
-
ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
|
|
123
|
-
ray[channel]?.hit ? 1 : 0,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
94
|
+
if (typeof source.appendRayVectorValues === 'function') {
|
|
95
|
+
source.appendRayVectorValues(vector);
|
|
96
|
+
} else if (Array.isArray(source.rayVectorValues)) {
|
|
97
|
+
source.rayVectorValues.forEach((values, index) => {
|
|
98
|
+
pushRaySchema(schema, index, sensors);
|
|
99
|
+
vector.push(...values);
|
|
126
100
|
});
|
|
127
|
-
}
|
|
101
|
+
} else {
|
|
102
|
+
source.rays.forEach((ray, index) => {
|
|
103
|
+
pushRaySchema(schema, index, sensors);
|
|
104
|
+
vector.push(
|
|
105
|
+
ratio(ray.track.distanceMeters, ray.lengthMeters),
|
|
106
|
+
ray.track.hit ? 1 : 0,
|
|
107
|
+
ray.track.kind === 'exit' ? 1 : 0,
|
|
108
|
+
ray.track.kind === 'entry' ? 1 : 0,
|
|
109
|
+
ratio(ray.car.distanceMeters, ray.lengthMeters),
|
|
110
|
+
ray.car.hit ? 1 : 0,
|
|
111
|
+
ray.car.relativeSpeedKph / 200,
|
|
112
|
+
ray.car.targetType === 'replayGhost' ? 1 : 0,
|
|
113
|
+
);
|
|
114
|
+
MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
|
|
115
|
+
if (sensors.rays.channels?.includes?.(channel)) {
|
|
116
|
+
vector.push(
|
|
117
|
+
ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
|
|
118
|
+
ray[channel]?.hit ? 1 : 0,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
128
124
|
const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? source.nearbyCars.length) : 0;
|
|
129
125
|
const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
|
|
130
126
|
for (let index = 0; index < nearbyLimit; index += 1) {
|
|
@@ -179,6 +175,27 @@ function pushSchema(schema, ...entries) {
|
|
|
179
175
|
if (schema) schema.push(...entries);
|
|
180
176
|
}
|
|
181
177
|
|
|
178
|
+
function pushRaySchema(schema, index, sensors) {
|
|
179
|
+
pushSchema(schema,
|
|
180
|
+
{ name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
|
|
181
|
+
{ name: `rays[${index}].track.hit`, scale: 'boolean' },
|
|
182
|
+
{ name: `rays[${index}].track.kindExit`, scale: 'boolean' },
|
|
183
|
+
{ name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
|
|
184
|
+
{ name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
|
|
185
|
+
{ name: `rays[${index}].car.hit`, scale: 'boolean' },
|
|
186
|
+
{ name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
187
|
+
{ name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
|
|
188
|
+
);
|
|
189
|
+
MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
|
|
190
|
+
if (sensors.rays.channels?.includes?.(channel)) {
|
|
191
|
+
pushSchema(schema,
|
|
192
|
+
{ name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
|
|
193
|
+
{ name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
182
199
|
function normalizeLapProgress(source) {
|
|
183
200
|
return ratio(source.self.lapProgressMeters, source.track.lengthMeters || 1);
|
|
184
201
|
}
|