@inventure71/paddockjs 2.0.1 → 3.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 +31 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +65 -15
- package/docs/architecture.md +17 -13
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +96 -21
- package/docs/index.md +2 -1
- package/docs/learnings.md +6 -6
- package/docs/rules.md +7 -7
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +25 -18
- package/docs/training.md +13 -12
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +74 -4
- package/src/app/F1SimulatorApp.js +191 -28
- 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 +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +17 -3
- package/src/environment/index.d.ts +1 -2
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +3 -11
- package/src/environment/scenarios.js +1 -1
- package/src/environment/sensors/surfaceRays.js +41 -33
- package/src/environment/sensors/trackRays.js +44 -49
- package/src/index.d.ts +110 -9
- package/src/index.js +25 -1
- 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/pitKinematics.js +2 -2
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- package/src/simulation/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryIndex.js +180 -135
- package/src/simulation/track/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +3 -3
- package/src/simulation/vehicle/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +3 -3
- package/src/simulation/vehicle/vehicleSnapshots.js +12 -1
- package/src/simulation/vehicle/vehicleState.js +1 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- 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
package/src/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
PaddockActionSpec,
|
|
3
3
|
PaddockDriverControllerLoop,
|
|
4
4
|
PaddockDriverControllerLoopOptions,
|
|
5
|
+
PaddockEnvironmentOptions,
|
|
5
6
|
PaddockObservationSpec,
|
|
6
7
|
PaddockParticipantInteraction as EnvPaddockParticipantInteraction,
|
|
7
8
|
PaddockParticipantInteractionsOptions as EnvPaddockParticipantInteractionsOptions,
|
|
@@ -23,7 +24,7 @@ export type {
|
|
|
23
24
|
} from './environment/index.js';
|
|
24
25
|
|
|
25
26
|
export type TireCompound = 'S' | 'M' | 'H';
|
|
26
|
-
export type PaddockPhysicsMode = 'arcade' | '
|
|
27
|
+
export type PaddockPhysicsMode = 'arcade' | 'advanced';
|
|
27
28
|
export type PaddockWarmupPolicy = 'config-change' | 'always' | 'never';
|
|
28
29
|
export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
|
|
29
30
|
|
|
@@ -81,11 +82,12 @@ export type PaddockPitIntentRequest = PaddockPitIntent | {
|
|
|
81
82
|
pitTargetCompound?: TireCompound | string;
|
|
82
83
|
targetTire?: TireCompound | string;
|
|
83
84
|
};
|
|
84
|
-
export type CameraMode = 'overview' | 'leader' | 'selected' | 'show-all' | 'pit';
|
|
85
|
+
export type CameraMode = 'overview' | 'leader' | 'selected' | 'driver' | 'show-all' | 'pit';
|
|
85
86
|
export type RaceBannerMode = 'project' | 'radio' | 'hidden';
|
|
86
87
|
export type RaceBannerEnabledMode = 'project' | 'radio';
|
|
87
88
|
export type RaceDataBannerSize = 'auto' | 'custom';
|
|
88
89
|
export type TimingTowerVerticalFit = 'expand-race-view' | 'scroll';
|
|
90
|
+
export type TimingGapMode = 'interval' | 'leader';
|
|
89
91
|
export type LayoutPreset = 'standard' | 'left-tower-overlay';
|
|
90
92
|
export type CameraControlsMode = 'embedded' | 'external' | false;
|
|
91
93
|
export type PaddockPresetName = 'dashboard' | 'timing-overlay' | 'compact-race' | 'full-dashboard';
|
|
@@ -111,6 +113,7 @@ export interface TeamData {
|
|
|
111
113
|
name?: string;
|
|
112
114
|
color?: string;
|
|
113
115
|
icon?: string;
|
|
116
|
+
theme?: PaddockThemeSelector;
|
|
114
117
|
pitCrew?: PaddockPitCrewStats;
|
|
115
118
|
pitCrewStats?: PaddockPitCrewStats;
|
|
116
119
|
}
|
|
@@ -254,12 +257,84 @@ export interface NormalizedSimulatorDriver extends SimulatorDriver {
|
|
|
254
257
|
};
|
|
255
258
|
}
|
|
256
259
|
|
|
257
|
-
export
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
export type PaddockThemeMode = 'dark' | 'light' | 'system';
|
|
261
|
+
export type PaddockThemeSelector = 'default' | 'active' | 'selectedTeam' | 'team' | `team:${string}` | string;
|
|
262
|
+
export type PaddockThemeTokenValue = string | { light?: string; dark?: string };
|
|
263
|
+
export type PaddockThemeTokenName =
|
|
264
|
+
| 'primary'
|
|
265
|
+
| 'primaryText'
|
|
266
|
+
| 'secondary'
|
|
267
|
+
| 'secondaryText'
|
|
268
|
+
| 'surface'
|
|
269
|
+
| 'surfaceRaised'
|
|
270
|
+
| 'surfacePanel'
|
|
271
|
+
| 'text'
|
|
272
|
+
| 'mutedText'
|
|
273
|
+
| 'border'
|
|
274
|
+
| 'success'
|
|
275
|
+
| 'warning'
|
|
276
|
+
| 'danger'
|
|
277
|
+
| 'info'
|
|
278
|
+
| 'yellowFlag'
|
|
279
|
+
| 'greenFlag'
|
|
280
|
+
| 'redFlag'
|
|
281
|
+
| 'safetyCar'
|
|
282
|
+
| 'drsActive'
|
|
283
|
+
| 'pitLane'
|
|
284
|
+
| 'track'
|
|
285
|
+
| 'trackEdge'
|
|
286
|
+
| 'timingTowerMaxWidth'
|
|
287
|
+
| 'raceViewMinHeight';
|
|
288
|
+
|
|
289
|
+
export interface F1SimulatorThemeTokens extends Partial<Record<PaddockThemeTokenName, PaddockThemeTokenValue>> {
|
|
290
|
+
/** Compatibility alias for `primary`. Prefer `tokens.primary` in new 3.0.0 code. */
|
|
291
|
+
accentColor?: PaddockThemeTokenValue;
|
|
292
|
+
/** Compatibility alias for `greenFlag`. Prefer `tokens.greenFlag` or `tokens.success`. */
|
|
293
|
+
greenColor?: PaddockThemeTokenValue;
|
|
294
|
+
/** Compatibility alias for `yellowFlag`. Prefer `tokens.yellowFlag` or `tokens.warning`. */
|
|
295
|
+
yellowColor?: PaddockThemeTokenValue;
|
|
296
|
+
/** Compatibility alias for `info`. */
|
|
297
|
+
blueColor?: PaddockThemeTokenValue;
|
|
298
|
+
/** Compatibility alias for `redFlag`. Prefer `tokens.redFlag` or `tokens.danger`. */
|
|
299
|
+
raceControlRedColor?: PaddockThemeTokenValue;
|
|
300
|
+
/** Compatibility alias for `surface`. */
|
|
301
|
+
surfaceColor?: PaddockThemeTokenValue;
|
|
302
|
+
/** Compatibility alias for `surfaceRaised`. */
|
|
303
|
+
surfaceRaisedColor?: PaddockThemeTokenValue;
|
|
304
|
+
/** Compatibility alias for `surfacePanel`. */
|
|
305
|
+
surfacePanelColor?: PaddockThemeTokenValue;
|
|
306
|
+
/** Compatibility alias for `border`. */
|
|
307
|
+
lineColor?: PaddockThemeTokenValue;
|
|
308
|
+
/** Compatibility alias for `border`. */
|
|
309
|
+
lineStrongColor?: PaddockThemeTokenValue;
|
|
310
|
+
/** Compatibility alias for `text`. */
|
|
311
|
+
textColor?: PaddockThemeTokenValue;
|
|
312
|
+
/** Compatibility alias for `mutedText`. */
|
|
313
|
+
mutedTextColor?: PaddockThemeTokenValue;
|
|
314
|
+
/** Compatibility alias for `track`. */
|
|
315
|
+
trackColor?: PaddockThemeTokenValue;
|
|
316
|
+
/** Compatibility alias for `trackEdge`. */
|
|
317
|
+
trackEdgeColor?: PaddockThemeTokenValue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface F1SimulatorThemePackage extends F1SimulatorThemeTokens {
|
|
321
|
+
extends?: string;
|
|
322
|
+
tokens?: F1SimulatorThemeTokens;
|
|
323
|
+
components?: Record<string, Partial<Record<string, PaddockThemeTokenName>>>;
|
|
324
|
+
light?: F1SimulatorThemeTokens;
|
|
325
|
+
dark?: F1SimulatorThemeTokens;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface F1SimulatorTheme extends Omit<F1SimulatorThemePackage, 'components'> {
|
|
329
|
+
mode?: PaddockThemeMode;
|
|
330
|
+
use?: string;
|
|
331
|
+
themes?: Record<string, F1SimulatorThemePackage>;
|
|
332
|
+
componentThemes?: Record<string, PaddockThemeSelector | { theme: PaddockThemeSelector }>;
|
|
333
|
+
teamThemes?: Record<string, PaddockThemeSelector | { theme: PaddockThemeSelector }>;
|
|
334
|
+
components?: Record<
|
|
335
|
+
string,
|
|
336
|
+
PaddockThemeSelector | { theme: PaddockThemeSelector } | Partial<Record<string, PaddockThemeTokenName>>
|
|
337
|
+
>;
|
|
263
338
|
}
|
|
264
339
|
|
|
265
340
|
export interface F1SimulatorUiOptions {
|
|
@@ -277,10 +352,15 @@ export interface F1SimulatorUiOptions {
|
|
|
277
352
|
};
|
|
278
353
|
raceDataBannerSize?: RaceDataBannerSize;
|
|
279
354
|
raceDataTelemetryDetail?: boolean;
|
|
355
|
+
driverCamera?: boolean;
|
|
280
356
|
penaltyBanners?: boolean;
|
|
281
357
|
timingPenaltyBadges?: boolean;
|
|
282
358
|
simulationSpeedControl?: boolean;
|
|
283
359
|
timingTowerVerticalFit?: TimingTowerVerticalFit;
|
|
360
|
+
timingGapMode?: TimingGapMode;
|
|
361
|
+
timingGapModeToggle?: boolean;
|
|
362
|
+
timingEntryVerticalPadding?: number;
|
|
363
|
+
responsiveNarrowLayout?: boolean;
|
|
284
364
|
}
|
|
285
365
|
|
|
286
366
|
export interface F1SimulatorDebugOptions {
|
|
@@ -638,6 +718,18 @@ export interface WheelSurfaceSnapshot {
|
|
|
638
718
|
fullyOutsideWhiteLine: boolean;
|
|
639
719
|
}
|
|
640
720
|
|
|
721
|
+
export interface CarTrackStateSnapshot {
|
|
722
|
+
distance: number;
|
|
723
|
+
signedOffset: number;
|
|
724
|
+
crossTrackError: number;
|
|
725
|
+
surface: string;
|
|
726
|
+
inPitLane: boolean;
|
|
727
|
+
pitLanePart: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
|
|
728
|
+
pitBoxId: string | null;
|
|
729
|
+
curvature: number;
|
|
730
|
+
heading: number;
|
|
731
|
+
}
|
|
732
|
+
|
|
641
733
|
export interface CarSnapshot {
|
|
642
734
|
id: string;
|
|
643
735
|
rank: number | null;
|
|
@@ -683,6 +775,7 @@ export interface CarSnapshot {
|
|
|
683
775
|
stabilityState?: PaddockStabilityState;
|
|
684
776
|
signedOffset?: number;
|
|
685
777
|
crossTrackError?: number;
|
|
778
|
+
trackState?: CarTrackStateSnapshot | null;
|
|
686
779
|
inPitLane?: boolean;
|
|
687
780
|
wheels?: WheelSurfaceSnapshot[];
|
|
688
781
|
pitLanePart?: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
|
|
@@ -791,6 +884,7 @@ export interface F1SimulatorExpertOptions {
|
|
|
791
884
|
enabled: boolean;
|
|
792
885
|
controlledDrivers: string[];
|
|
793
886
|
frameSkip?: number;
|
|
887
|
+
episode?: PaddockEnvironmentOptions['episode'];
|
|
794
888
|
visualizeSensors?: boolean | {
|
|
795
889
|
rays?: boolean;
|
|
796
890
|
drivers?: 'selected' | 'all' | string[];
|
|
@@ -862,7 +956,6 @@ export interface F1SimulatorOptions extends F1SimulatorCallbacks {
|
|
|
862
956
|
seed?: number;
|
|
863
957
|
trackSeed?: number;
|
|
864
958
|
trackGeneration?: PaddockProceduralTrackOptions;
|
|
865
|
-
trackQueryIndex?: boolean;
|
|
866
959
|
warmup?: PaddockWarmupOptions | boolean;
|
|
867
960
|
totalLaps?: number;
|
|
868
961
|
physicsMode?: PaddockPhysicsMode;
|
|
@@ -889,6 +982,7 @@ export interface MountRaceCanvasOptions {
|
|
|
889
982
|
includeTimingTower?: boolean;
|
|
890
983
|
includeTelemetrySectorBanner?: boolean;
|
|
891
984
|
timingTowerVerticalFit?: TimingTowerVerticalFit;
|
|
985
|
+
responsiveNarrowLayout?: boolean;
|
|
892
986
|
}
|
|
893
987
|
|
|
894
988
|
export interface MountTelemetryPanelOptions {
|
|
@@ -899,6 +993,7 @@ export interface MountRaceTelemetryDrawerOptions {
|
|
|
899
993
|
timingTowerVerticalFit?: TimingTowerVerticalFit;
|
|
900
994
|
drawerInitiallyOpen?: boolean;
|
|
901
995
|
raceDataTelemetryDetail?: boolean;
|
|
996
|
+
responsiveNarrowLayout?: boolean;
|
|
902
997
|
}
|
|
903
998
|
|
|
904
999
|
export interface F1MountedSimulator {
|
|
@@ -916,6 +1011,9 @@ export interface F1MountedSimulator {
|
|
|
916
1011
|
getPitIntent(driverId: string): PaddockPitIntent;
|
|
917
1012
|
getPitTargetCompound(driverId: string): TireCompound | string | null;
|
|
918
1013
|
getSimulationSpeed(): number;
|
|
1014
|
+
setTimingGapMode(mode: TimingGapMode): TimingGapMode;
|
|
1015
|
+
getTimingGapMode(): TimingGapMode;
|
|
1016
|
+
toggleTimingGapMode(): TimingGapMode;
|
|
919
1017
|
servePenalty(penaltyId: string): PaddockPenaltyEntry | null;
|
|
920
1018
|
cancelPenalty(penaltyId: string): PaddockPenaltyEntry | null;
|
|
921
1019
|
getSnapshot(): RaceSnapshot | null;
|
|
@@ -953,6 +1051,9 @@ export interface PaddockSimulatorController {
|
|
|
953
1051
|
getPitIntent(driverId: string): PaddockPitIntent;
|
|
954
1052
|
getPitTargetCompound(driverId: string): TireCompound | string | null;
|
|
955
1053
|
getSimulationSpeed(): number;
|
|
1054
|
+
setTimingGapMode(mode: TimingGapMode): TimingGapMode;
|
|
1055
|
+
getTimingGapMode(): TimingGapMode;
|
|
1056
|
+
toggleTimingGapMode(): TimingGapMode;
|
|
956
1057
|
servePenalty(penaltyId: string): PaddockPenaltyEntry | null;
|
|
957
1058
|
cancelPenalty(penaltyId: string): PaddockPenaltyEntry | null;
|
|
958
1059
|
getSnapshot(): RaceSnapshot | null;
|
package/src/index.js
CHANGED
|
@@ -61,6 +61,21 @@ export async function mountF1Simulator(root, options = {}) {
|
|
|
61
61
|
const app = new F1SimulatorApp(shell, resolvedOptions);
|
|
62
62
|
await app.init();
|
|
63
63
|
|
|
64
|
+
const syncResolvedTimingGapMode = (timingGapMode) => {
|
|
65
|
+
resolvedOptions = {
|
|
66
|
+
...resolvedOptions,
|
|
67
|
+
ui: {
|
|
68
|
+
...resolvedOptions.ui,
|
|
69
|
+
timingGapMode,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
return timingGapMode;
|
|
73
|
+
};
|
|
74
|
+
const syncResolvedRuntimeOptions = () => {
|
|
75
|
+
syncResolvedTimingGapMode(app.getTimingGapMode());
|
|
76
|
+
return resolvedOptions;
|
|
77
|
+
};
|
|
78
|
+
|
|
64
79
|
return {
|
|
65
80
|
get expert() {
|
|
66
81
|
return app.expert ?? null;
|
|
@@ -70,7 +85,7 @@ export async function mountF1Simulator(root, options = {}) {
|
|
|
70
85
|
root.innerHTML = '';
|
|
71
86
|
},
|
|
72
87
|
restart(nextOptions = {}) {
|
|
73
|
-
const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(
|
|
88
|
+
const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(syncResolvedRuntimeOptions(), nextOptions));
|
|
74
89
|
app.restart(nextResolvedOptions);
|
|
75
90
|
resolvedOptions = nextResolvedOptions;
|
|
76
91
|
},
|
|
@@ -108,6 +123,15 @@ export async function mountF1Simulator(root, options = {}) {
|
|
|
108
123
|
getSimulationSpeed() {
|
|
109
124
|
return app.simulationSpeed;
|
|
110
125
|
},
|
|
126
|
+
setTimingGapMode(mode) {
|
|
127
|
+
return syncResolvedTimingGapMode(app.setTimingGapMode(mode));
|
|
128
|
+
},
|
|
129
|
+
getTimingGapMode() {
|
|
130
|
+
return app.getTimingGapMode();
|
|
131
|
+
},
|
|
132
|
+
toggleTimingGapMode() {
|
|
133
|
+
return syncResolvedTimingGapMode(app.toggleTimingGapMode());
|
|
134
|
+
},
|
|
111
135
|
servePenalty(penaltyId) {
|
|
112
136
|
return app.servePenalty(penaltyId);
|
|
113
137
|
},
|
|
@@ -37,7 +37,7 @@ export function decideDriverControls({ car, orderIndex, race }) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function shouldStabilizeSimulatorCar(car, race) {
|
|
40
|
-
if (race.physicsMode !== '
|
|
40
|
+
if (race.physicsMode !== 'advanced') return false;
|
|
41
41
|
const gripUsage = car.gripUsage ?? 0;
|
|
42
42
|
const slipAngle = Math.abs(car.slipAngleRadians ?? 0);
|
|
43
43
|
const edgeDistance = race.track.width / 2 - VEHICLE_LIMITS.carWidth * 2.25;
|
|
@@ -5,7 +5,7 @@ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
|
5
5
|
import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
|
|
6
6
|
|
|
7
7
|
export function calculateTrackEdgeGuard(car, race) {
|
|
8
|
-
const simulatorMode = race.physicsMode === '
|
|
8
|
+
const simulatorMode = race.physicsMode === 'advanced';
|
|
9
9
|
const trackLimit = race.track.width / 2;
|
|
10
10
|
const halfCarWidth = VEHICLE_LIMITS.carWidth / 2;
|
|
11
11
|
const wholeCarOutsideLimit = trackLimit + halfCarWidth;
|
|
@@ -10,7 +10,7 @@ import { calculateActualOverlapPenalty, calculatePlannedTrafficPenalty, planRaci
|
|
|
10
10
|
import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
|
|
11
11
|
|
|
12
12
|
export function decideRacingControls(car, orderIndex, race) {
|
|
13
|
-
if (race.physicsMode === '
|
|
13
|
+
if (race.physicsMode === 'advanced') {
|
|
14
14
|
return decideSimulatorRacingControls(car, orderIndex, race);
|
|
15
15
|
}
|
|
16
16
|
return decideArcadeRacingControls(car, orderIndex, race);
|
|
@@ -389,7 +389,7 @@ export function calculateRacingLineOffset(car, race, lookahead, curvature, edgeG
|
|
|
389
389
|
if (turnDirection === 0) return 0;
|
|
390
390
|
|
|
391
391
|
const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
|
|
392
|
-
const simulatorMode = race.physicsMode === '
|
|
392
|
+
const simulatorMode = race.physicsMode === 'advanced';
|
|
393
393
|
const safeEdge = race.track.width / 2 - VEHICLE_LIMITS.carWidth *
|
|
394
394
|
(simulatorMode ? 2.05 - aggression * 0.08 : 1.15 - aggression * 0.12);
|
|
395
395
|
const apexOffset = turnDirection * safeEdge *
|
|
@@ -8,7 +8,7 @@ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
|
8
8
|
import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
|
|
9
9
|
|
|
10
10
|
export function decideRejoinControls(car, race) {
|
|
11
|
-
if (race.physicsMode === '
|
|
11
|
+
if (race.physicsMode === 'advanced') {
|
|
12
12
|
return decideSimulatorRejoinControls(car, race);
|
|
13
13
|
}
|
|
14
14
|
return decideArcadeRejoinControls(car, race);
|
|
@@ -103,7 +103,7 @@ export function decideSimulatorRejoinControls(car, race) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function decideRejoinControlsForMode(car, race, profile) {
|
|
106
|
-
const simulatorMode = race.physicsMode === '
|
|
106
|
+
const simulatorMode = race.physicsMode === 'advanced';
|
|
107
107
|
const lookahead = clamp(car.speed * (profile.simulatorMode ? 0.58 : 0.72) + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
|
|
108
108
|
const edgeMotion = analyzeTrackEdgeMotion(car, race);
|
|
109
109
|
const lowSpeedOffTrack = !car.trackState.onTrack && car.speed < kphToSimSpeed(24);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export function syncPitCarKinematics(car, { physicsMode = '
|
|
1
|
+
export function syncPitCarKinematics(car, { physicsMode = 'arcade' } = {}) {
|
|
2
2
|
const speed = Number.isFinite(car.speed) ? car.speed : 0;
|
|
3
3
|
const heading = Number.isFinite(car.heading) ? car.heading : 0;
|
|
4
|
-
if (physicsMode === '
|
|
4
|
+
if (physicsMode === 'advanced') {
|
|
5
5
|
car.velocityX = Math.cos(heading) * speed;
|
|
6
6
|
car.velocityY = Math.sin(heading) * speed;
|
|
7
7
|
} else {
|
|
@@ -285,11 +285,9 @@ export function nearestDistanceOnRoute(route, position, previousDistance = 0) {
|
|
|
285
285
|
export function createPitApproachPoints(track, car, pitLane, entryRaceDistance) {
|
|
286
286
|
const currentDistance = car.raceDistance ?? entryRaceDistance - PIT_ENTRY_APPROACH_DISTANCE;
|
|
287
287
|
const remaining = Math.max(0, entryRaceDistance - currentDistance);
|
|
288
|
-
const currentState = nearestTrackState(track, car, car.progress
|
|
288
|
+
const currentState = nearestTrackState(track, car, car.progress);
|
|
289
289
|
const startOffset = currentState.inPitLane ? 0 : currentState.signedOffset ?? 0;
|
|
290
|
-
const entryConnectState = nearestTrackState(track, pitLane.entry.trackConnectPoint, pitLane.entry.trackDistance
|
|
291
|
-
indexMode: 'legacy',
|
|
292
|
-
});
|
|
290
|
+
const entryConnectState = nearestTrackState(track, pitLane.entry.trackConnectPoint, pitLane.entry.trackDistance);
|
|
293
291
|
const targetOffset = entryConnectState.signedOffset ?? 0;
|
|
294
292
|
const points = [routePoint(car, car.heading)];
|
|
295
293
|
const straightDistance = remaining > metersToSimUnits(14)
|
|
@@ -29,7 +29,6 @@ export function initializeRaceSimulation(simulation, {
|
|
|
29
29
|
track = null,
|
|
30
30
|
trackSeed = null,
|
|
31
31
|
trackGeneration = {},
|
|
32
|
-
trackQueryIndex = true,
|
|
33
32
|
physicsMode = 'arcade',
|
|
34
33
|
participantInteractions = {},
|
|
35
34
|
replayGhosts = [],
|
|
@@ -43,11 +42,8 @@ export function initializeRaceSimulation(simulation, {
|
|
|
43
42
|
...builtTrack,
|
|
44
43
|
pitLane: clonePitLaneModel(builtTrack.pitLane),
|
|
45
44
|
};
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const forkedCachedIndex = forkTrackQueryIndex(builtTrack.queryIndex);
|
|
49
|
-
attachTrackQueryIndex(simulation.track, forkedCachedIndex ?? createTrackQueryIndex(simulation.track));
|
|
50
|
-
}
|
|
45
|
+
const forkedCachedIndex = forkTrackQueryIndex(builtTrack.queryIndex, simulation.track);
|
|
46
|
+
attachTrackQueryIndex(simulation.track, forkedCachedIndex ?? createTrackQueryIndex(simulation.track));
|
|
51
47
|
simulation.track.timingLines = createTimingLines(simulation.track);
|
|
52
48
|
simulation.trackSeed = simulation.track.seed ?? trackSeed;
|
|
53
49
|
simulation.rules = normalizeRaceRules(rules);
|
|
@@ -59,7 +59,7 @@ export function runRaceStep(simulation, dt) {
|
|
|
59
59
|
throttle: controls.throttle ?? 0,
|
|
60
60
|
brake: controls.brake ?? 0,
|
|
61
61
|
};
|
|
62
|
-
if (simulation.physicsMode === '
|
|
62
|
+
if (simulation.physicsMode === 'advanced') {
|
|
63
63
|
applyWheelSurfaceState(car, simulation.track);
|
|
64
64
|
}
|
|
65
65
|
integrateVehiclePhysics(car, controls, delta, {
|
|
@@ -82,7 +82,7 @@ export function setRedFlagState(sim, deployed) {
|
|
|
82
82
|
});
|
|
83
83
|
} else if (!car.finished) {
|
|
84
84
|
car.speed = Math.max(car.speed, kphToSimSpeed(60));
|
|
85
|
-
if (sim.physicsMode === '
|
|
85
|
+
if (sim.physicsMode === 'advanced') syncVehicleVelocityToHeading(car);
|
|
86
86
|
car.brake = 0;
|
|
87
87
|
car.throttle = Math.max(car.throttle ?? 0, 0.35);
|
|
88
88
|
}
|
|
@@ -59,7 +59,7 @@ function retireStalledCar(sim, car) {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export function freezeRetiredCar(car, { physicsMode = '
|
|
62
|
+
export function freezeRetiredCar(car, { physicsMode = 'arcade' } = {}) {
|
|
63
63
|
freezeVehicleMotion(car, {
|
|
64
64
|
clearManualControls: true,
|
|
65
65
|
physicsMode,
|
|
@@ -33,9 +33,10 @@ function getLapTelemetryPosition(track, raceDistance, totalLaps = Infinity) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function createSectorProgress(position, currentSectors = null) {
|
|
36
|
+
function createSectorProgress(position, currentSectors = null, { preserveCompletedSectors = false } = {}) {
|
|
37
37
|
const activeIndex = clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
38
38
|
return createEmptySectorTimes().map((_, index) => {
|
|
39
|
+
if (preserveCompletedSectors && Number.isFinite(currentSectors?.[index])) return 1;
|
|
39
40
|
if (index < activeIndex && Number.isFinite(currentSectors?.[index])) return 1;
|
|
40
41
|
if (index === activeIndex) return clamp(position.currentSectorProgress ?? 0, 0, 1);
|
|
41
42
|
return 0;
|
|
@@ -99,11 +100,23 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
|
|
|
99
100
|
const previousUpdateTime = Number.isFinite(telemetry.lastUpdatedAt) ? telemetry.lastUpdatedAt : currentTime;
|
|
100
101
|
const elapsedTime = Math.max(0, currentTime - previousUpdateTime);
|
|
101
102
|
|
|
102
|
-
if (
|
|
103
|
+
if (
|
|
104
|
+
!Number.isFinite(previousDistance) ||
|
|
105
|
+
!Number.isFinite(currentDistance) ||
|
|
106
|
+
Math.abs(travelled) > track.length / 2
|
|
107
|
+
) {
|
|
103
108
|
resetLapTelemetry(car, currentTime, track, totalLaps);
|
|
104
109
|
return;
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
if (travelled < -1e-3) {
|
|
113
|
+
syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
|
|
114
|
+
preserveCompletedSectors: true,
|
|
115
|
+
});
|
|
116
|
+
telemetry.lastUpdatedAt = currentTime;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
107
120
|
const sectorLength = getSectorLength(track);
|
|
108
121
|
if (travelled > 1e-6) {
|
|
109
122
|
const firstBoundary = Math.floor(previousDistance / sectorLength) + 1;
|
|
@@ -155,28 +168,34 @@ function clearFutureSectorTelemetry(telemetry) {
|
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
function syncLiveSectorTelemetry(telemetry) {
|
|
171
|
+
function syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors = false } = {}) {
|
|
159
172
|
const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
160
173
|
|
|
161
174
|
telemetry.liveSectors = createEmptySectorTimes();
|
|
162
175
|
telemetry.currentSectors.forEach((time, index) => {
|
|
163
|
-
if (index >= activeIndex) return;
|
|
176
|
+
if (!preserveCompletedSectors && index >= activeIndex) return;
|
|
164
177
|
if (!Number.isFinite(time)) return;
|
|
165
178
|
telemetry.liveSectors[index] = time;
|
|
166
179
|
});
|
|
167
|
-
telemetry.liveSectors[activeIndex]
|
|
180
|
+
if (!Number.isFinite(telemetry.liveSectors[activeIndex])) {
|
|
181
|
+
telemetry.liveSectors[activeIndex] = telemetry.currentSectorElapsed;
|
|
182
|
+
}
|
|
168
183
|
}
|
|
169
184
|
|
|
170
|
-
function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps
|
|
185
|
+
function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
|
|
186
|
+
preserveCompletedSectors = false,
|
|
187
|
+
} = {}) {
|
|
171
188
|
const position = getLapTelemetryPosition(track, currentRaceDistance, totalLaps);
|
|
172
189
|
telemetry.completedLaps = position.completedLaps;
|
|
173
190
|
telemetry.currentLap = position.currentLap;
|
|
174
191
|
telemetry.currentSector = position.currentSector;
|
|
175
192
|
telemetry.currentSectorProgress = position.currentSectorProgress;
|
|
176
|
-
clearFutureSectorTelemetry(telemetry);
|
|
177
|
-
telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors
|
|
193
|
+
if (!preserveCompletedSectors) clearFutureSectorTelemetry(telemetry);
|
|
194
|
+
telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors, {
|
|
195
|
+
preserveCompletedSectors,
|
|
196
|
+
});
|
|
178
197
|
telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(position.completedLaps, totalLaps));
|
|
179
198
|
telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
|
|
180
199
|
telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
|
|
181
|
-
syncLiveSectorTelemetry(telemetry);
|
|
200
|
+
syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors });
|
|
182
201
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { clamp } from '../simMath.js';
|
|
2
2
|
import { metersToSimUnits } from '../units.js';
|
|
3
3
|
import { WORLD, PIT_BOX_COUNT, PIT_BOX_DEPTH, PIT_BOX_LENGTH, PIT_BOX_PAIR_GAP, PIT_BOX_TO_LANE_GAP, PIT_ENTRY_SEARCH_AFTER, PIT_ENTRY_SEARCH_BEFORE, PIT_EXIT_SEARCH_AFTER, PIT_EXIT_SEARCH_BEFORE, PIT_LANE_EDGE_GAP, PIT_LANE_ENTRY_BUFFER, PIT_LANE_EXIT_BUFFER, PIT_LANE_FINISH_RATIO, PIT_LANE_OFFSET_SEARCH_STEP, PIT_LANE_WIDTH, PIT_SERVICE_AREA_DEPTH, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_QUEUE_GAP, PIT_TEAM_COUNT, PIT_TEAM_GAP, PIT_BOXES_PER_TEAM, PIT_WORKING_LANE_GAP, PIT_WORKING_LANE_WIDTH, PIT_ACCESS_MAX_LENGTH, PIT_ACCESS_MIN_LENGTH, PIT_ACCESS_TANGENT_RATIO, PIT_TRACK_CLEARANCE_MARGIN } from './trackConstants.js';
|
|
4
|
-
import { clonePoint, distance, headingVector, interpolatePitPoint, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
|
|
4
|
+
import { clonePoint, distance, headingVector, interpolatePitPoint, nearestPointOnPolyline, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
|
|
5
5
|
import { findPitAccessConnection, createPitAccessRoadCenterline } from './pitLaneAccess.js';
|
|
6
|
-
import {
|
|
6
|
+
import { pointAt } from './spatialQueries.js';
|
|
7
7
|
|
|
8
8
|
export function createStartStraightBasis(track) {
|
|
9
9
|
const finish = pointAt(track, 0);
|
|
@@ -90,7 +90,7 @@ export function scorePitLanePlacement(track, side, laneOffset, layout) {
|
|
|
90
90
|
minimumClearance = Math.min(minimumClearance, pointWorldClearance(lanePoint), pointWorldClearance(boxPoint));
|
|
91
91
|
minimumTrackClearance = Math.min(
|
|
92
92
|
minimumTrackClearance,
|
|
93
|
-
|
|
93
|
+
nearestMainTrackProjection(track, lanePoint).crossTrackError -
|
|
94
94
|
(track.width / 2 + (track.kerbWidth ?? 0) + PIT_TRACK_CLEARANCE_MARGIN),
|
|
95
95
|
);
|
|
96
96
|
}
|
|
@@ -105,6 +105,10 @@ export function scorePitLanePlacement(track, side, laneOffset, layout) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function nearestMainTrackProjection(track, position) {
|
|
109
|
+
return nearestPointOnPolyline(track.samples, position) ?? { crossTrackError: Infinity };
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
export function choosePitLanePlacement(track, baseLaneOffset, layout) {
|
|
109
113
|
const candidates = [];
|
|
110
114
|
for (let offsetStep = 0; offsetStep <= 9; offsetStep += 1) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clamp, normalizeAngle, wrapDistance } from '../simMath.js';
|
|
2
|
-
import {
|
|
2
|
+
import { PIT_LANE_WIDTH } from './trackConstants.js';
|
|
3
3
|
import { nearestPitLaneState } from './pitLaneState.js';
|
|
4
4
|
import { queryNearestTrackProjection } from './trackQueryIndex.js';
|
|
5
5
|
|
|
@@ -91,13 +91,10 @@ export function createTrackState(track, position, best) {
|
|
|
91
91
|
|
|
92
92
|
export function nearestTrackState(track, position, progressHint = null, options = {}) {
|
|
93
93
|
const allowPitOverride = options.allowPitOverride !== false;
|
|
94
|
-
const best =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
hintMaxDistance: options.hintMaxDistance,
|
|
99
|
-
}) ??
|
|
100
|
-
nearestTrackSampleLegacy(track, position, progressHint);
|
|
94
|
+
const best = queryNearestTrackProjection(track, position, progressHint);
|
|
95
|
+
if (!best) {
|
|
96
|
+
throw new Error('nearestTrackState requires a finite position and an indexed track model. Build tracks with buildTrackModel() or createRaceSimulation().');
|
|
97
|
+
}
|
|
101
98
|
const trackState = createTrackState(track, position, best);
|
|
102
99
|
if (!allowPitOverride) return trackState;
|
|
103
100
|
const pitState = nearestPitLaneState(track, position);
|
|
@@ -125,26 +122,6 @@ export function nearestTrackState(track, position, progressHint = null, options
|
|
|
125
122
|
};
|
|
126
123
|
}
|
|
127
124
|
|
|
128
|
-
function nearestTrackSampleLegacy(track, position, progressHint = null) {
|
|
129
|
-
let nearest = null;
|
|
130
|
-
|
|
131
|
-
if (Number.isFinite(progressHint)) {
|
|
132
|
-
const centerIndex = sampleIndexAtDistance(track, progressHint);
|
|
133
|
-
nearest = nearestSampleInRange(
|
|
134
|
-
track,
|
|
135
|
-
position,
|
|
136
|
-
centerIndex - NEAREST_HINT_WINDOW_SAMPLES,
|
|
137
|
-
centerIndex + NEAREST_HINT_WINDOW_SAMPLES,
|
|
138
|
-
);
|
|
139
|
-
const fallbackDistance = track.width / 2 + (track.kerbWidth ?? 0) + track.gravelWidth + track.runoffWidth + 180;
|
|
140
|
-
if (!nearest.best || nearest.bestDistance > fallbackDistance * fallbackDistance) {
|
|
141
|
-
nearest = null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return nearest?.best ?? nearestSampleGlobal(track, position).best;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
125
|
export function offsetTrackPoint(point, offset) {
|
|
149
126
|
return {
|
|
150
127
|
x: point.x + point.normalX * offset,
|