@inventure71/paddockjs 3.0.0 → 4.1.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 +152 -1
- package/INSTALL_AND_UPDATE.md +21 -4
- package/README.md +103 -551
- package/docs/architecture.md +28 -18
- package/docs/component_inventory.md +5 -4
- package/docs/composable-layouts.md +79 -0
- package/docs/custom_model_controller.md +4 -4
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +13 -7
- package/docs/getting-started.md +105 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +17 -4
- package/docs/rules.md +6 -2
- package/docs/system_specs.md +60 -7
- package/docs/theming.md +64 -0
- package/docs/training.md +20 -14
- 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 +21 -5
- package/src/api/PaddockSimulatorController.js +78 -22
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +24 -3
- package/src/app/readouts/timingTowerRenderer.js +203 -42
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/config/defaultOptions.js +8 -1
- package/src/config/themeOptions.js +5 -0
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/episode.js +11 -1
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/runtime.js +41 -0
- 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 +130 -89
- package/src/environment/sensors/trackRays.js +115 -1
- package/src/index.d.ts +43 -0
- package/src/index.js +27 -2
- package/src/placeholder/index.d.ts +11 -0
- package/src/placeholder/index.js +61 -0
- package/src/placeholder.css +72 -0
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +77 -42
- 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 +2 -0
- package/src/simulation/race/raceStep.js +25 -13
- 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 +115 -36
- 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 +23 -0
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +67 -11
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1334 -100
- 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 -1
- package/src/simulation/vehicle/contactResolution.js +96 -59
- 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/vehiclePhysics.js +42 -27
- package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
- package/src/simulation/vehicle/vehicleState.js +6 -0
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/styles.css +497 -497
- package/src/ui/raceControlsTemplate.js +1 -0
- package/src/ui/telemetryDrawerTemplate.js +1 -0
- package/src/ui/telemetryTemplates.js +1 -0
|
@@ -125,6 +125,195 @@ export function syncTimingGapModeControls({ readouts, buttons, timingGapMode })
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function timingGapForCar(car, raceMode, timingGapMode) {
|
|
129
|
+
if (isDnfCar(car)) return 'DNF';
|
|
130
|
+
if (isWavedFlagCar(car) && raceMode !== 'finished') return 'WAVED';
|
|
131
|
+
if (raceMode === 'finished') return car.rank === 1 ? 'Winner' : 'FIN';
|
|
132
|
+
if (raceMode === 'safety-car' && car.rank > 1) return 'SC';
|
|
133
|
+
if (raceMode === 'pre-start') return car.rank === 1 ? 'Pole' : 'Grid';
|
|
134
|
+
if (car.rank > 1) return formatRaceGap(car, timingGapMode);
|
|
135
|
+
return 'Leader';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function timingRowModel({ car, driver, raceMode, selectedId, timingGapMode, penalty }) {
|
|
139
|
+
const dnf = isDnfCar(car);
|
|
140
|
+
const tire = car.tire ?? driver?.tire ?? 'M';
|
|
141
|
+
const timingCode = car.timingCode ?? driver?.timingCode ?? car.code;
|
|
142
|
+
const team = car.team ?? driver?.team ?? null;
|
|
143
|
+
const icon = team?.icon ?? car.icon ?? driver?.icon ?? timingCode;
|
|
144
|
+
const driverColor = formatCssColor(car.color);
|
|
145
|
+
const iconColor = formatCssColor(team?.color ?? car.color, driverColor);
|
|
146
|
+
const penaltyLabel = penalty ? formatPenaltyBadgeLabel(penalty) : '';
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
id: car.id,
|
|
150
|
+
rank: car.rank,
|
|
151
|
+
name: car.name,
|
|
152
|
+
selected: car.id === selectedId,
|
|
153
|
+
dnf,
|
|
154
|
+
gap: timingGapForCar(car, raceMode, timingGapMode),
|
|
155
|
+
tire,
|
|
156
|
+
tireClass: getTireClass(tire),
|
|
157
|
+
timingCode,
|
|
158
|
+
icon,
|
|
159
|
+
driverColor,
|
|
160
|
+
iconColor,
|
|
161
|
+
penaltyLabel,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function timingRowKey(row) {
|
|
166
|
+
return [
|
|
167
|
+
row.id,
|
|
168
|
+
row.rank,
|
|
169
|
+
row.name,
|
|
170
|
+
row.selected ? 1 : 0,
|
|
171
|
+
row.dnf ? 1 : 0,
|
|
172
|
+
row.gap,
|
|
173
|
+
row.tire,
|
|
174
|
+
row.tireClass,
|
|
175
|
+
row.timingCode,
|
|
176
|
+
row.icon,
|
|
177
|
+
row.driverColor,
|
|
178
|
+
row.iconColor,
|
|
179
|
+
row.penaltyLabel,
|
|
180
|
+
].join('\u001f');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function timingRowsKey(rows) {
|
|
184
|
+
return rows.map(timingRowKey).join('\u001e');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderTimingRowMarkup(row) {
|
|
188
|
+
const penaltyBadge = row.penaltyLabel
|
|
189
|
+
? `<span class="timing-penalty-badge" aria-label="${escapeHtml(row.penaltyLabel)}" title="${escapeHtml(row.penaltyLabel)}">!</span>`
|
|
190
|
+
: '';
|
|
191
|
+
|
|
192
|
+
return `
|
|
193
|
+
<li>
|
|
194
|
+
<button class="timing-row ${row.selected ? 'is-selected' : ''} ${row.dnf ? 'is-dnf' : ''}" type="button"
|
|
195
|
+
data-driver-id="${escapeHtml(row.id)}" aria-label="Select ${escapeHtml(row.name)}"
|
|
196
|
+
style="--driver-color: ${escapeHtml(row.driverColor)}">
|
|
197
|
+
<span class="timing-position">${row.rank}</span>
|
|
198
|
+
<span class="timing-icon timing-team-icon" aria-hidden="true" style="--team-color: ${escapeHtml(row.iconColor)}">${escapeHtml(row.icon)}</span>
|
|
199
|
+
<span class="timing-name" title="${escapeHtml(row.name)}"><span>${escapeHtml(row.timingCode)}</span>${penaltyBadge}</span>
|
|
200
|
+
<span class="timing-gap">${escapeHtml(row.gap)}</span>
|
|
201
|
+
<span class="timing-tire timing-tire--${row.tireClass}">${escapeHtml(row.tire)}</span>
|
|
202
|
+
</button>
|
|
203
|
+
</li>
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function canPatchTimingRows(timingList) {
|
|
208
|
+
return Boolean(
|
|
209
|
+
timingList?.ownerDocument?.createElement &&
|
|
210
|
+
typeof timingList.appendChild === 'function' &&
|
|
211
|
+
timingList.children,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function createTimingRowView(document) {
|
|
216
|
+
const item = document.createElement('li');
|
|
217
|
+
const button = document.createElement('button');
|
|
218
|
+
const position = document.createElement('span');
|
|
219
|
+
const icon = document.createElement('span');
|
|
220
|
+
const name = document.createElement('span');
|
|
221
|
+
const code = document.createElement('span');
|
|
222
|
+
const gap = document.createElement('span');
|
|
223
|
+
const tire = document.createElement('span');
|
|
224
|
+
|
|
225
|
+
item.appendChild(button);
|
|
226
|
+
button.appendChild(position);
|
|
227
|
+
button.appendChild(icon);
|
|
228
|
+
button.appendChild(name);
|
|
229
|
+
name.appendChild(code);
|
|
230
|
+
button.appendChild(gap);
|
|
231
|
+
button.appendChild(tire);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
item,
|
|
235
|
+
button,
|
|
236
|
+
position,
|
|
237
|
+
icon,
|
|
238
|
+
name,
|
|
239
|
+
code,
|
|
240
|
+
gap,
|
|
241
|
+
tire,
|
|
242
|
+
penaltyBadge: null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function setStyleProperty(element, name, value) {
|
|
247
|
+
if (typeof element.style?.setProperty === 'function') {
|
|
248
|
+
element.style.setProperty(name, value);
|
|
249
|
+
} else {
|
|
250
|
+
element.setAttribute?.('style', `${name}: ${value}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function updateTimingRowView(view, row) {
|
|
255
|
+
view.button.className = `timing-row ${row.selected ? 'is-selected' : ''} ${row.dnf ? 'is-dnf' : ''}`;
|
|
256
|
+
view.button.setAttribute('type', 'button');
|
|
257
|
+
view.button.setAttribute('data-driver-id', row.id);
|
|
258
|
+
view.button.setAttribute('aria-label', `Select ${row.name}`);
|
|
259
|
+
setStyleProperty(view.button, '--driver-color', row.driverColor);
|
|
260
|
+
|
|
261
|
+
view.position.className = 'timing-position';
|
|
262
|
+
view.position.textContent = String(row.rank);
|
|
263
|
+
|
|
264
|
+
view.icon.className = 'timing-icon timing-team-icon';
|
|
265
|
+
view.icon.setAttribute('aria-hidden', 'true');
|
|
266
|
+
setStyleProperty(view.icon, '--team-color', row.iconColor);
|
|
267
|
+
view.icon.textContent = String(row.icon ?? '');
|
|
268
|
+
|
|
269
|
+
view.name.className = 'timing-name';
|
|
270
|
+
view.name.setAttribute('title', row.name);
|
|
271
|
+
view.code.textContent = String(row.timingCode ?? '');
|
|
272
|
+
|
|
273
|
+
if (row.penaltyLabel) {
|
|
274
|
+
if (!view.penaltyBadge) {
|
|
275
|
+
view.penaltyBadge = view.name.ownerDocument.createElement('span');
|
|
276
|
+
view.name.appendChild(view.penaltyBadge);
|
|
277
|
+
}
|
|
278
|
+
view.penaltyBadge.className = 'timing-penalty-badge';
|
|
279
|
+
view.penaltyBadge.setAttribute('aria-label', row.penaltyLabel);
|
|
280
|
+
view.penaltyBadge.setAttribute('title', row.penaltyLabel);
|
|
281
|
+
view.penaltyBadge.textContent = '!';
|
|
282
|
+
} else if (view.penaltyBadge) {
|
|
283
|
+
view.penaltyBadge.remove();
|
|
284
|
+
view.penaltyBadge = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
view.gap.className = 'timing-gap';
|
|
288
|
+
view.gap.textContent = String(row.gap);
|
|
289
|
+
|
|
290
|
+
view.tire.className = `timing-tire timing-tire--${row.tireClass}`;
|
|
291
|
+
view.tire.textContent = String(row.tire ?? '');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function patchTimingRows(timingList, rows) {
|
|
295
|
+
const rowViews = timingList.__paddockTimingRowViews ?? new Map();
|
|
296
|
+
timingList.__paddockTimingRowViews = rowViews;
|
|
297
|
+
const seen = new Set();
|
|
298
|
+
|
|
299
|
+
rows.forEach((row) => {
|
|
300
|
+
let view = rowViews.get(row.id);
|
|
301
|
+
if (!view) {
|
|
302
|
+
view = createTimingRowView(timingList.ownerDocument);
|
|
303
|
+
rowViews.set(row.id, view);
|
|
304
|
+
}
|
|
305
|
+
seen.add(row.id);
|
|
306
|
+
updateTimingRowView(view, row);
|
|
307
|
+
timingList.appendChild(view.item);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
rowViews.forEach((view, id) => {
|
|
311
|
+
if (seen.has(id)) return;
|
|
312
|
+
view.item.remove();
|
|
313
|
+
rowViews.delete(id);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
128
317
|
export function renderTimingTower({
|
|
129
318
|
timingList,
|
|
130
319
|
cars,
|
|
@@ -138,48 +327,20 @@ export function renderTimingTower({
|
|
|
138
327
|
}) {
|
|
139
328
|
if (!timingList) return lastTimingMarkup;
|
|
140
329
|
const penaltyByDriver = timingPenaltyBadgesEnabled ? getPenaltyByDriver(penalties) : new Map();
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
} else if (car.rank > 1) {
|
|
156
|
-
gap = formatRaceGap(car, timingGapMode);
|
|
157
|
-
}
|
|
158
|
-
const tire = car.tire ?? driver?.tire ?? 'M';
|
|
159
|
-
const timingCode = car.timingCode ?? driver?.timingCode ?? car.code;
|
|
160
|
-
const team = car.team ?? driver?.team ?? null;
|
|
161
|
-
const icon = team?.icon ?? car.icon ?? driver?.icon ?? timingCode;
|
|
162
|
-
const driverColor = formatCssColor(car.color);
|
|
163
|
-
const iconColor = formatCssColor(team?.color ?? car.color, driverColor);
|
|
164
|
-
const penalty = penaltyByDriver.get(car.id);
|
|
165
|
-
const penaltyBadge = penalty
|
|
166
|
-
? `<span class="timing-penalty-badge" aria-label="${escapeHtml(formatPenaltyBadgeLabel(penalty))}" title="${escapeHtml(formatPenaltyBadgeLabel(penalty))}">!</span>`
|
|
167
|
-
: '';
|
|
168
|
-
|
|
169
|
-
return `
|
|
170
|
-
<li>
|
|
171
|
-
<button class="timing-row ${car.id === selectedId ? 'is-selected' : ''} ${dnf ? 'is-dnf' : ''}" type="button"
|
|
172
|
-
data-driver-id="${escapeHtml(car.id)}" aria-label="Select ${escapeHtml(car.name)}"
|
|
173
|
-
style="--driver-color: ${escapeHtml(driverColor)}">
|
|
174
|
-
<span class="timing-position">${car.rank}</span>
|
|
175
|
-
<span class="timing-icon timing-team-icon" aria-hidden="true" style="--team-color: ${escapeHtml(iconColor)}">${escapeHtml(icon)}</span>
|
|
176
|
-
<span class="timing-name" title="${escapeHtml(car.name)}"><span>${escapeHtml(timingCode)}</span>${penaltyBadge}</span>
|
|
177
|
-
<span class="timing-gap">${escapeHtml(gap)}</span>
|
|
178
|
-
<span class="timing-tire timing-tire--${getTireClass(tire)}">${escapeHtml(tire)}</span>
|
|
179
|
-
</button>
|
|
180
|
-
</li>
|
|
181
|
-
`;
|
|
182
|
-
}).join('');
|
|
330
|
+
const rows = cars.map((car) => timingRowModel({
|
|
331
|
+
car,
|
|
332
|
+
driver: driverById.get(car.id),
|
|
333
|
+
raceMode,
|
|
334
|
+
selectedId,
|
|
335
|
+
timingGapMode,
|
|
336
|
+
penalty: penaltyByDriver.get(car.id),
|
|
337
|
+
}));
|
|
338
|
+
if (canPatchTimingRows(timingList)) {
|
|
339
|
+
patchTimingRows(timingList, rows);
|
|
340
|
+
return timingRowsKey(rows);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const timingMarkup = rows.map(renderTimingRowMarkup).join('');
|
|
183
344
|
if (timingMarkup !== lastTimingMarkup || timingList.innerHTML !== timingMarkup) {
|
|
184
345
|
timingList.innerHTML = timingMarkup;
|
|
185
346
|
return timingMarkup;
|
|
@@ -48,7 +48,11 @@ export function runFrameLoopTick(app) {
|
|
|
48
48
|
const shouldUpdateDom = now - app.lastDomUpdateTime >= domUpdateIntervalForSpeed(app.simulationSpeed);
|
|
49
49
|
const fullSnapshot = stepEvents.length > 0 || shouldUpdateDom ? app.sim.snapshot() : null;
|
|
50
50
|
if (stepEvents.length > 0) app.emitRaceEvents(stepEvents, fullSnapshot);
|
|
51
|
-
const renderSource = fullSnapshot ??
|
|
51
|
+
const renderSource = fullSnapshot ??
|
|
52
|
+
app.sim.snapshotRenderInto?.(app.renderSourceSnapshotBuffer ?? {}) ??
|
|
53
|
+
app.sim.snapshotRender?.() ??
|
|
54
|
+
app.sim.snapshot();
|
|
55
|
+
if (!fullSnapshot && app.sim.snapshotRenderInto) app.renderSourceSnapshotBuffer = renderSource;
|
|
52
56
|
const renderSnapshot = interpolateRenderSnapshotInto(
|
|
53
57
|
app.renderSnapshotBuffer ?? {},
|
|
54
58
|
renderSource,
|
|
@@ -119,7 +119,14 @@ export const PADDOCK_SIMULATOR_PRESETS = {
|
|
|
119
119
|
},
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
export {
|
|
122
|
+
export {
|
|
123
|
+
DEFAULT_PADDOCK_THEME,
|
|
124
|
+
PADDOCK_THEME_CSS_VARIABLES,
|
|
125
|
+
PADDOCK_THEME_TOKEN_KEYS,
|
|
126
|
+
applyPaddockTheme,
|
|
127
|
+
applyPaddockThemeCssVariables,
|
|
128
|
+
resolvePaddockTheme,
|
|
129
|
+
} from './themeOptions.js';
|
|
123
130
|
|
|
124
131
|
const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'driver', 'show-all', 'pit']);
|
|
125
132
|
|
|
@@ -34,6 +34,7 @@ export const THEME_SIZE_TOKEN_KEYS = [
|
|
|
34
34
|
];
|
|
35
35
|
|
|
36
36
|
const THEME_TOKEN_KEYS = [...THEME_COLOR_TOKEN_KEYS, ...THEME_SIZE_TOKEN_KEYS];
|
|
37
|
+
export const PADDOCK_THEME_TOKEN_KEYS = THEME_TOKEN_KEYS;
|
|
37
38
|
const THEME_TOKEN_SET = new Set(THEME_TOKEN_KEYS);
|
|
38
39
|
const COLOR_TOKEN_SET = new Set(THEME_COLOR_TOKEN_KEYS);
|
|
39
40
|
const SIZE_TOKEN_SET = new Set(THEME_SIZE_TOKEN_KEYS);
|
|
@@ -248,6 +249,7 @@ export const DEFAULT_PADDOCK_THEME_INPUT = {
|
|
|
248
249
|
mode: 'dark',
|
|
249
250
|
use: 'default',
|
|
250
251
|
};
|
|
252
|
+
export const DEFAULT_PADDOCK_THEME = DEFAULT_PADDOCK_THEME_INPUT;
|
|
251
253
|
|
|
252
254
|
const DEFAULT_THEME_PACKAGE = {
|
|
253
255
|
tokens: {
|
|
@@ -346,6 +348,9 @@ export function applyPaddockThemeCssVariables(root, theme = DEFAULT_PADDOCK_THEM
|
|
|
346
348
|
});
|
|
347
349
|
}
|
|
348
350
|
|
|
351
|
+
export const resolvePaddockTheme = normalizePaddockTheme;
|
|
352
|
+
export const applyPaddockTheme = applyPaddockThemeCssVariables;
|
|
353
|
+
|
|
349
354
|
export function normalizeCssColorToken(value, fallback = null) {
|
|
350
355
|
if (typeof value !== 'string') return fallback;
|
|
351
356
|
const candidate = value.trim();
|
|
@@ -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';
|
|
@@ -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;
|
|
@@ -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
|
+
}
|