@inventure71/paddockjs 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/INSTALL_AND_UPDATE.md +163 -0
- package/README.md +96 -16
- package/docs/architecture.md +55 -11
- package/docs/concepts.md +5 -1
- package/docs/data_contract.md +324 -8
- package/docs/data_model.md +1 -0
- package/docs/index.md +1 -0
- package/docs/rules.md +2 -0
- package/docs/system_specs.md +66 -11
- package/docs/training.md +104 -0
- package/examples/train-basic-policy.mjs +277 -0
- package/package.json +14 -7
- package/src/api/PaddockSimulatorController.js +64 -3
- package/src/app/BrowserExpertAdapter.js +32 -0
- package/src/app/F1SimulatorApp.js +308 -38
- package/src/app/domBindings.js +35 -11
- package/src/config/defaultOptions.js +33 -0
- package/src/data/championship.js +13 -0
- package/src/data/normalizeDrivers.js +7 -2
- package/src/environment/actions.js +45 -0
- package/src/environment/episode.js +17 -0
- package/src/environment/events.js +17 -0
- package/src/environment/index.d.ts +241 -0
- package/src/environment/index.js +2 -0
- package/src/environment/observations.js +112 -0
- package/src/environment/options.js +150 -0
- package/src/environment/rewards.js +69 -0
- package/src/environment/runtime.js +140 -0
- package/src/environment/sensors.js +257 -0
- package/src/environment/specs.js +79 -0
- package/src/index.d.ts +97 -4
- package/src/index.js +12 -2
- package/src/rendering/proceduralTrackAsset.js +7 -1
- package/src/simulation/raceSimulation.js +214 -1
- package/src/simulation/trackModel.js +21 -0
- package/src/styles.css +495 -2
- package/src/ui/componentTemplates.js +201 -4
- package/AGENTS.md +0 -86
- package/src/standalone.js +0 -15
|
@@ -2,6 +2,13 @@ function buttonHiddenAttribute(isVisible) {
|
|
|
2
2
|
return isVisible ? '' : ' hidden';
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
let telemetryDrawerIdSequence = 0;
|
|
6
|
+
|
|
7
|
+
function createTelemetryDrawerId() {
|
|
8
|
+
telemetryDrawerIdSequence += 1;
|
|
9
|
+
return `paddock-telemetry-drawer-${telemetryDrawerIdSequence}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
function createLoadingMarkup(label = 'Loading') {
|
|
6
13
|
return `
|
|
7
14
|
<div class="paddock-loading" data-paddock-loading aria-label="${escapeHtml(label)} loading">
|
|
@@ -110,6 +117,7 @@ export function createCameraControlsMarkup({ embedded = false } = {}) {
|
|
|
110
117
|
export function createRaceCanvasMarkup({
|
|
111
118
|
includeRaceDataPanel = false,
|
|
112
119
|
includeTimingTower = false,
|
|
120
|
+
includeTelemetrySectorBanner = false,
|
|
113
121
|
timingTowerVerticalFit,
|
|
114
122
|
assets,
|
|
115
123
|
totalLaps,
|
|
@@ -147,6 +155,7 @@ export function createRaceCanvasMarkup({
|
|
|
147
155
|
</div>
|
|
148
156
|
${showEmbeddedCameraControls ? createCameraControlsMarkup({ embedded: true }) : ''}
|
|
149
157
|
${includeRaceDataPanel ? createRaceDataPanelMarkup({ assets, ui }) : ''}
|
|
158
|
+
${includeTelemetrySectorBanner ? createTelemetrySectorBannerMarkup({ ui }) : ''}
|
|
150
159
|
<div class="race-finish-panel" data-race-finish-panel hidden aria-live="polite">
|
|
151
160
|
<span>Race winner</span>
|
|
152
161
|
<strong data-race-finish-winner>--</strong>
|
|
@@ -159,13 +168,17 @@ export function createRaceCanvasMarkup({
|
|
|
159
168
|
|
|
160
169
|
export function createRaceDataPanelMarkup({ ui = {} } = {}) {
|
|
161
170
|
const sizeMode = ui.raceDataBannerSize === 'auto' ? 'auto' : 'custom';
|
|
171
|
+
const telemetryDetail = Boolean(ui.raceDataTelemetryDetail);
|
|
172
|
+
const classNames = ['race-data-panel', `race-data-panel--${sizeMode}`];
|
|
173
|
+
if (telemetryDetail) classNames.push('race-data-panel--with-telemetry');
|
|
162
174
|
return `
|
|
163
|
-
<div class="
|
|
175
|
+
<div class="${classNames.join(' ')}" data-paddock-component="race-data-panel" data-race-data-panel aria-live="polite">
|
|
164
176
|
<div class="race-data-copy">
|
|
165
177
|
<span class="race-data-kicker" data-race-data-kicker>Project</span>
|
|
166
178
|
<strong data-race-data-title>Select driver</strong>
|
|
167
179
|
<span class="race-data-subtitle" data-race-data-subtitle>Race entry</span>
|
|
168
180
|
</div>
|
|
181
|
+
${telemetryDetail ? createRaceDataTelemetryMarkup() : ''}
|
|
169
182
|
<strong class="race-data-number" data-race-data-number>--</strong>
|
|
170
183
|
<button class="race-data-link" type="button" data-race-data-open>Open project</button>
|
|
171
184
|
${createLoadingMarkup('Race data')}
|
|
@@ -173,6 +186,22 @@ export function createRaceDataPanelMarkup({ ui = {} } = {}) {
|
|
|
173
186
|
`;
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
function createRaceDataTelemetryMarkup() {
|
|
190
|
+
return `
|
|
191
|
+
<div class="race-data-telemetry" data-race-data-telemetry aria-label="Project sector telemetry">
|
|
192
|
+
<span class="race-data-telemetry__label">Sectors</span>
|
|
193
|
+
<div class="race-data-telemetry__bars">
|
|
194
|
+
${[1, 2, 3].map((sector) => `
|
|
195
|
+
<div class="telemetry-sector-bar race-data-sector-bar" data-telemetry-sector-bar="${sector}" style="--sector-fill: 0%">
|
|
196
|
+
<span>S${sector}</span>
|
|
197
|
+
<strong data-telemetry-sector-time="${sector}">--</strong>
|
|
198
|
+
</div>
|
|
199
|
+
`).join('')}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
|
|
176
205
|
export function createCarDriverOverviewMarkup({ assets }) {
|
|
177
206
|
const cells = Array.from({ length: 7 }, (_, index) => `
|
|
178
207
|
<div class="car-overview-cell car-overview-cell--slot-${index + 1}" data-overview-field data-overview-slot="${index}">
|
|
@@ -205,9 +234,33 @@ export function createCarDriverOverviewMarkup({ assets }) {
|
|
|
205
234
|
`;
|
|
206
235
|
}
|
|
207
236
|
|
|
208
|
-
|
|
237
|
+
function getTelemetryModules(ui = {}) {
|
|
238
|
+
const defaults = {
|
|
239
|
+
core: true,
|
|
240
|
+
sectors: true,
|
|
241
|
+
lapTimes: true,
|
|
242
|
+
sectorTimes: true,
|
|
243
|
+
};
|
|
244
|
+
const modules = ui.telemetryModules;
|
|
245
|
+
if (modules === false) return Object.fromEntries(Object.keys(defaults).map((name) => [name, false]));
|
|
246
|
+
if (Array.isArray(modules)) {
|
|
247
|
+
const requested = new Set(modules);
|
|
248
|
+
return Object.fromEntries(Object.keys(defaults).map((name) => [name, requested.has(name)]));
|
|
249
|
+
}
|
|
250
|
+
if (!modules || typeof modules !== 'object') return defaults;
|
|
251
|
+
return Object.fromEntries(Object.entries(defaults).map(([name, fallback]) => [
|
|
252
|
+
name,
|
|
253
|
+
modules[name] == null ? fallback : Boolean(modules[name]),
|
|
254
|
+
]));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getTelemetryModuleClass(componentName) {
|
|
258
|
+
return `sim-telemetry telemetry-component telemetry-component--${componentName}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createTelemetryCoreMarkup() {
|
|
209
262
|
return `
|
|
210
|
-
<
|
|
263
|
+
<section class="${getTelemetryModuleClass('core')}" data-paddock-component="telemetry-core" aria-label="Selected car core telemetry">
|
|
211
264
|
<div class="telemetry-header">
|
|
212
265
|
<span data-selected-code>---</span>
|
|
213
266
|
<strong data-selected-name>Select car</strong>
|
|
@@ -222,8 +275,152 @@ export function createTelemetryPanelMarkup(options, { includeOverview = options.
|
|
|
222
275
|
<div><dt>Interval</dt><dd data-telemetry-gap>--</dd></div>
|
|
223
276
|
<div><dt>Leader</dt><dd data-telemetry-leader-gap>--</dd></div>
|
|
224
277
|
</dl>
|
|
278
|
+
${createLoadingMarkup('Core telemetry')}
|
|
279
|
+
</section>
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function createTelemetrySectorsMarkup() {
|
|
284
|
+
return `
|
|
285
|
+
<section class="${getTelemetryModuleClass('sectors')} telemetry-sector-strip" data-paddock-component="telemetry-sectors" data-telemetry-sector-strip aria-label="Sector progress">
|
|
286
|
+
<div class="telemetry-module-header">
|
|
287
|
+
<span>Sector map</span>
|
|
288
|
+
<strong data-telemetry-current-sector>S1</strong>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="telemetry-sector-bars">
|
|
291
|
+
${[1, 2, 3].map((sector) => `
|
|
292
|
+
<div class="telemetry-sector-bar" data-telemetry-sector-bar="${sector}" style="--sector-fill: 0%">
|
|
293
|
+
<span>S${sector}</span>
|
|
294
|
+
<strong data-telemetry-sector-time="${sector}">--</strong>
|
|
295
|
+
</div>
|
|
296
|
+
`).join('')}
|
|
297
|
+
</div>
|
|
298
|
+
${createLoadingMarkup('Sector telemetry')}
|
|
299
|
+
</section>
|
|
300
|
+
`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function createTelemetrySectorBannerMarkup() {
|
|
304
|
+
return `
|
|
305
|
+
<section class="telemetry-sector-banner" data-paddock-component="telemetry-sector-banner" data-telemetry-sector-banner aria-label="Broadcast sector telemetry">
|
|
306
|
+
<div class="telemetry-sector-banner__copy">
|
|
307
|
+
<span><b data-selected-code>--</b> sector telemetry</span>
|
|
308
|
+
<strong data-selected-name>Select driver</strong>
|
|
309
|
+
<em data-telemetry-current-sector>S1</em>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="telemetry-sector-banner__bars">
|
|
312
|
+
${[1, 2, 3].map((sector) => `
|
|
313
|
+
<div class="telemetry-sector-bar" data-telemetry-sector-bar="${sector}" style="--sector-fill: 0%">
|
|
314
|
+
<span>S${sector}</span>
|
|
315
|
+
<strong data-telemetry-sector-time="${sector}">--</strong>
|
|
316
|
+
</div>
|
|
317
|
+
`).join('')}
|
|
318
|
+
</div>
|
|
319
|
+
${createLoadingMarkup('Sector banner')}
|
|
320
|
+
</section>
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function createTelemetryLapTimesMarkup() {
|
|
325
|
+
return `
|
|
326
|
+
<section class="${getTelemetryModuleClass('lap-times')} telemetry-lap-module" data-paddock-component="telemetry-lap-times" aria-label="Lap timing">
|
|
327
|
+
<div class="telemetry-module-header">
|
|
328
|
+
<span>Lap timing</span>
|
|
329
|
+
<strong data-telemetry-completed-laps>0 laps</strong>
|
|
330
|
+
</div>
|
|
331
|
+
<table class="telemetry-lap-table" data-telemetry-lap-table>
|
|
332
|
+
<tbody>
|
|
333
|
+
<tr><th scope="row">Current</th><td data-telemetry-current-lap-time>--</td></tr>
|
|
334
|
+
<tr><th scope="row">Last</th><td data-telemetry-last-lap-time>--</td></tr>
|
|
335
|
+
<tr><th scope="row">Best</th><td data-telemetry-best-lap-time>--</td></tr>
|
|
336
|
+
</tbody>
|
|
337
|
+
</table>
|
|
338
|
+
${createLoadingMarkup('Lap telemetry')}
|
|
339
|
+
</section>
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function createTelemetrySectorTimesMarkup() {
|
|
344
|
+
return `
|
|
345
|
+
<section class="${getTelemetryModuleClass('sector-times')} telemetry-sector-table-module" data-paddock-component="telemetry-sector-times" aria-label="Sector timing table">
|
|
346
|
+
<div class="telemetry-module-header">
|
|
347
|
+
<span>Sector timing</span>
|
|
348
|
+
<strong>Last / Best</strong>
|
|
349
|
+
</div>
|
|
350
|
+
<table class="telemetry-sector-table" data-telemetry-sector-table>
|
|
351
|
+
<thead>
|
|
352
|
+
<tr><th scope="col">Sector</th><th scope="col">Last</th><th scope="col">Best</th></tr>
|
|
353
|
+
</thead>
|
|
354
|
+
<tbody>
|
|
355
|
+
${[1, 2, 3].map((sector) => `
|
|
356
|
+
<tr data-telemetry-sector-row="${sector}">
|
|
357
|
+
<th scope="row">S${sector}</th>
|
|
358
|
+
<td data-telemetry-sector-last="${sector}">--</td>
|
|
359
|
+
<td data-telemetry-sector-best="${sector}">--</td>
|
|
360
|
+
</tr>
|
|
361
|
+
`).join('')}
|
|
362
|
+
</tbody>
|
|
363
|
+
</table>
|
|
364
|
+
${createLoadingMarkup('Sector table')}
|
|
365
|
+
</section>
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function createTelemetryComponentMarkup(options, modules = getTelemetryModules(options.ui)) {
|
|
370
|
+
return `
|
|
371
|
+
${modules.core ? createTelemetryCoreMarkup(options) : ''}
|
|
372
|
+
${modules.sectors ? createTelemetrySectorsMarkup(options) : ''}
|
|
373
|
+
${modules.lapTimes ? createTelemetryLapTimesMarkup(options) : ''}
|
|
374
|
+
${modules.sectorTimes ? createTelemetrySectorTimesMarkup(options) : ''}
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function createTelemetryPanelMarkup(options, { includeOverview = options.ui?.telemetryIncludesOverview !== false } = {}) {
|
|
379
|
+
const modules = getTelemetryModules(options.ui);
|
|
380
|
+
return `
|
|
381
|
+
<aside class="telemetry-stack" data-paddock-component="telemetry-stack" aria-label="Selected car telemetry stack">
|
|
382
|
+
${createTelemetryComponentMarkup(options, modules)}
|
|
225
383
|
${includeOverview ? createCarDriverOverviewMarkup(options) : ''}
|
|
226
|
-
${createLoadingMarkup('Telemetry')}
|
|
227
384
|
</aside>
|
|
228
385
|
`;
|
|
229
386
|
}
|
|
387
|
+
|
|
388
|
+
export function createRaceTelemetryDrawerMarkup(options, {
|
|
389
|
+
timingTowerVerticalFit,
|
|
390
|
+
drawerInitiallyOpen = false,
|
|
391
|
+
raceDataTelemetryDetail = options.ui?.raceDataTelemetryDetail,
|
|
392
|
+
} = {}) {
|
|
393
|
+
const openClass = drawerInitiallyOpen ? ' is-telemetry-open' : '';
|
|
394
|
+
const drawerId = createTelemetryDrawerId();
|
|
395
|
+
const drawerOptions = {
|
|
396
|
+
...options,
|
|
397
|
+
ui: {
|
|
398
|
+
...(options.ui ?? {}),
|
|
399
|
+
raceDataTelemetryDetail: Boolean(raceDataTelemetryDetail),
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
return `
|
|
403
|
+
<section class="race-telemetry-drawer${openClass}" data-paddock-component="race-telemetry-drawer" data-race-telemetry-drawer aria-label="Race view with telemetry drawer">
|
|
404
|
+
<div class="race-telemetry-drawer__race">
|
|
405
|
+
${createRaceCanvasMarkup({
|
|
406
|
+
...drawerOptions,
|
|
407
|
+
includeRaceDataPanel: true,
|
|
408
|
+
includeTimingTower: true,
|
|
409
|
+
timingTowerVerticalFit,
|
|
410
|
+
})}
|
|
411
|
+
</div>
|
|
412
|
+
<div class="race-telemetry-drawer__controls" aria-label="Race workbench controls">
|
|
413
|
+
${createSafetyCarControlMarkup({ compact: true })}
|
|
414
|
+
<button class="telemetry-drawer-toggle" type="button" data-telemetry-drawer-toggle aria-expanded="${drawerInitiallyOpen ? 'true' : 'false'}" aria-controls="${drawerId}">
|
|
415
|
+
${drawerInitiallyOpen ? 'Close telemetry' : 'Telemetry'}
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
<aside id="${drawerId}" class="telemetry-drawer" data-telemetry-drawer aria-label="Telemetry drawer" aria-hidden="${drawerInitiallyOpen ? 'false' : 'true'}"${drawerInitiallyOpen ? '' : ' inert'}>
|
|
419
|
+
<div class="telemetry-drawer__header" aria-hidden="true"></div>
|
|
420
|
+
<div class="telemetry-drawer__content">
|
|
421
|
+
${createTelemetryComponentMarkup(options)}
|
|
422
|
+
</div>
|
|
423
|
+
</aside>
|
|
424
|
+
</section>
|
|
425
|
+
`;
|
|
426
|
+
}
|
package/AGENTS.md
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# AGENTS.md
|
|
2
|
-
|
|
3
|
-
## Project
|
|
4
|
-
|
|
5
|
-
PaddockJS is an installable browser component package for an F1-style project race simulator.
|
|
6
|
-
|
|
7
|
-
The package lives independently from the portfolio website. Host websites install it and mount it with:
|
|
8
|
-
|
|
9
|
-
```js
|
|
10
|
-
import { mountF1Simulator } from '@inventure71/paddockjs';
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
It can also be mounted as rearrangeable package-owned pieces with:
|
|
14
|
-
|
|
15
|
-
```js
|
|
16
|
-
import { createPaddockSimulator } from '@inventure71/paddockjs';
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## What Is In This Repo
|
|
20
|
-
|
|
21
|
-
- `src/index.js`: public package API.
|
|
22
|
-
- `src/api/`: public composable mounting controller.
|
|
23
|
-
- `src/app/`: browser runtime, DOM bindings, PixiJS app lifecycle, controls, readouts, and camera orchestration.
|
|
24
|
-
- `src/config/`: default options and bundled default asset mapping.
|
|
25
|
-
- `src/data/`: driver/project data normalization, driver/car pairing, rating conversion, and demo data.
|
|
26
|
-
- `src/rendering/`: PixiJS track rendering helpers and render interpolation.
|
|
27
|
-
- `src/simulation/`: race rules, driver AI, vehicle physics, track model, and math utilities.
|
|
28
|
-
- `src/ui/`: generated markup for the all-in-one shell and individually mounted UI surfaces.
|
|
29
|
-
- `src/__tests__/`: package tests.
|
|
30
|
-
- `assets/`: simulator-owned default assets.
|
|
31
|
-
- `docs/`: package specs and design documentation.
|
|
32
|
-
- `INSTALL_AND_UPDATE.md`: install/update workflow.
|
|
33
|
-
|
|
34
|
-
## Required Documentation Discipline
|
|
35
|
-
|
|
36
|
-
When changing simulator behavior, update docs in the same change.
|
|
37
|
-
|
|
38
|
-
Use this mapping:
|
|
39
|
-
|
|
40
|
-
- Public API, lifecycle, install assumptions: update `docs/system_specs.md` and `README.md` if needed.
|
|
41
|
-
- Race behavior, DRS, safety car, starts, physics, collisions: update `docs/rules.md`.
|
|
42
|
-
- Vocabulary or domain model: update `docs/concepts.md`.
|
|
43
|
-
- Driver/entry/options/assets callback shapes: update `docs/data_contract.md`.
|
|
44
|
-
- Module ownership or data/control flow: update `docs/architecture.md`.
|
|
45
|
-
- Local install/update workflow: update `INSTALL_AND_UPDATE.md`.
|
|
46
|
-
|
|
47
|
-
Do not leave docs describing old behavior after code changes.
|
|
48
|
-
|
|
49
|
-
## Linear Work Logging
|
|
50
|
-
|
|
51
|
-
For any implementation that is more than a trivial/no-op change, log the work in the PaddockJS project on Linear.
|
|
52
|
-
|
|
53
|
-
The Linear update must include:
|
|
54
|
-
|
|
55
|
-
- what task was done
|
|
56
|
-
- how it was solved
|
|
57
|
-
- any important verification or remaining follow-up
|
|
58
|
-
|
|
59
|
-
## Engineering Rules
|
|
60
|
-
|
|
61
|
-
- Preserve the package boundary: PaddockJS should stay reusable and host-agnostic.
|
|
62
|
-
- Host-specific routing belongs in `onDriverOpen(driver)`, not package internals.
|
|
63
|
-
- Host-specific project data should be passed as `drivers` and `entries`, not hardcoded into the runtime.
|
|
64
|
-
- The package should own default simulator assets so hosts do not need to copy them.
|
|
65
|
-
- Treat the timing board width and broadcast proportions as package-owned. Do not resize or retune timing-board width as a casual fix; preserve its intended width and solve layout bugs through internal column/content constraints unless the user explicitly asks to redesign the timing board sizing.
|
|
66
|
-
- Prefer small, focused modules when extracting from `F1SimulatorApp.js`.
|
|
67
|
-
- Keep simulation logic deterministic for the same seed, track seed, drivers, entries, and rules.
|
|
68
|
-
- Do not claim completion without running verification.
|
|
69
|
-
|
|
70
|
-
## Verification
|
|
71
|
-
|
|
72
|
-
Run before claiming package changes are complete:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
npm run check
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
For portfolio integration changes, also run from the portfolio repo:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
cd /Users/inventure71/VSProjects/Inventure71.github.io
|
|
82
|
-
npm install ../PaddockJS
|
|
83
|
-
npm run check
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Browser behavior changes need a browser smoke test against the host page.
|
package/src/standalone.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { DEMO_PROJECT_DRIVERS, mountF1Simulator } from './index.js';
|
|
2
|
-
|
|
3
|
-
const root = document.getElementById('f1-simulator-root');
|
|
4
|
-
|
|
5
|
-
if (root) {
|
|
6
|
-
mountF1Simulator(root, {
|
|
7
|
-
drivers: DEMO_PROJECT_DRIVERS,
|
|
8
|
-
onDriverOpen(driver) {
|
|
9
|
-
if (driver.link) window.location.href = driver.link;
|
|
10
|
-
},
|
|
11
|
-
}).catch((error) => {
|
|
12
|
-
root.dataset.simulatorState = 'error';
|
|
13
|
-
console.error('F1 simulator failed to initialize.', error);
|
|
14
|
-
});
|
|
15
|
-
}
|