@inventure71/paddockjs 0.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/AGENTS.md +86 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +176 -0
- package/README.md +165 -0
- package/assets/f1-broadcast-panel-surface.png +0 -0
- package/assets/f1-car-sprite-game.png +0 -0
- package/assets/f1-driver-helmet.png +0 -0
- package/assets/f1-safety-car-sprite.png +0 -0
- package/assets/f1-texture-asphalt.png +0 -0
- package/assets/f1Logo.png +0 -0
- package/docs/architecture.md +209 -0
- package/docs/concepts.md +178 -0
- package/docs/data_contract.md +360 -0
- package/docs/data_model.md +167 -0
- package/docs/index.md +15 -0
- package/docs/learnings.md +47 -0
- package/docs/rules.md +181 -0
- package/docs/system_specs.md +211 -0
- package/package.json +85 -0
- package/src/api/PaddockSimulatorController.js +233 -0
- package/src/app/F1SimulatorApp.js +1293 -0
- package/src/app/domBindings.js +64 -0
- package/src/config/defaultAssets.js +29 -0
- package/src/config/defaultOptions.js +184 -0
- package/src/data/championship.js +225 -0
- package/src/data/customFields.js +14 -0
- package/src/data/demoDrivers.js +134 -0
- package/src/data/driverData.js +71 -0
- package/src/data/normalizeDrivers.js +21 -0
- package/src/data/vehicleData.js +81 -0
- package/src/index.d.ts +399 -0
- package/src/index.js +116 -0
- package/src/rendering/proceduralTrackAsset.js +517 -0
- package/src/rendering/renderSnapshot.js +23 -0
- package/src/simulation/driverController.js +253 -0
- package/src/simulation/raceSimulation.js +933 -0
- package/src/simulation/simMath.js +34 -0
- package/src/simulation/trackModel.js +607 -0
- package/src/simulation/units.js +30 -0
- package/src/simulation/vehiclePhysics.js +104 -0
- package/src/standalone.js +15 -0
- package/src/styles.css +1818 -0
- package/src/ui/componentTemplates.js +229 -0
- package/src/ui/shellTemplate.js +43 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { normalizeCustomFields } from './customFields.js';
|
|
2
|
+
|
|
3
|
+
const RATING_MINIMUM = 0;
|
|
4
|
+
const RATING_NEUTRAL = 50;
|
|
5
|
+
const RATING_MAXIMUM = 100;
|
|
6
|
+
|
|
7
|
+
export const DRIVER_STAT_DEFINITIONS = {
|
|
8
|
+
pace: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 1, variance: 0.08 },
|
|
9
|
+
racecraft: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 0.78, variance: 0.16 },
|
|
10
|
+
aggression: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 0.5, variance: 0.34 },
|
|
11
|
+
riskTolerance: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 0.5, variance: 0.28 },
|
|
12
|
+
patience: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 0.5, variance: 0.28 },
|
|
13
|
+
consistency: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 0.75, variance: 0.16 },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function clampRating(value, label) {
|
|
17
|
+
const rating = Number(value);
|
|
18
|
+
if (!Number.isFinite(rating)) throw new Error(`Invalid driver rating for ${label}: ${value}`);
|
|
19
|
+
return Math.min(Math.max(rating, RATING_MINIMUM), RATING_MAXIMUM);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function applyRating(definition, value) {
|
|
23
|
+
const normalized = (value - definition.neutral) / (definition.maximum - definition.neutral);
|
|
24
|
+
return definition.base + normalized * definition.variance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class DriverData {
|
|
28
|
+
constructor({
|
|
29
|
+
pace = RATING_NEUTRAL,
|
|
30
|
+
racecraft = RATING_NEUTRAL,
|
|
31
|
+
aggression = RATING_NEUTRAL,
|
|
32
|
+
riskTolerance = RATING_NEUTRAL,
|
|
33
|
+
patience = RATING_NEUTRAL,
|
|
34
|
+
consistency = RATING_NEUTRAL,
|
|
35
|
+
customFields = [],
|
|
36
|
+
} = {}) {
|
|
37
|
+
this.pace = clampRating(pace, 'pace');
|
|
38
|
+
this.racecraft = clampRating(racecraft, 'racecraft');
|
|
39
|
+
this.aggression = clampRating(aggression, 'aggression');
|
|
40
|
+
this.riskTolerance = clampRating(riskTolerance, 'riskTolerance');
|
|
41
|
+
this.patience = clampRating(patience, 'patience');
|
|
42
|
+
this.consistency = clampRating(consistency, 'consistency');
|
|
43
|
+
this.customFields = normalizeCustomFields(customFields);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ratings() {
|
|
47
|
+
return {
|
|
48
|
+
pace: this.pace,
|
|
49
|
+
racecraft: this.racecraft,
|
|
50
|
+
aggression: this.aggression,
|
|
51
|
+
riskTolerance: this.riskTolerance,
|
|
52
|
+
patience: this.patience,
|
|
53
|
+
consistency: this.consistency,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toConstructorArgs() {
|
|
58
|
+
return {
|
|
59
|
+
ratings: this.ratings(),
|
|
60
|
+
customFields: this.customFields,
|
|
61
|
+
pace: applyRating(DRIVER_STAT_DEFINITIONS.pace, this.pace),
|
|
62
|
+
racecraft: applyRating(DRIVER_STAT_DEFINITIONS.racecraft, this.racecraft),
|
|
63
|
+
consistency: applyRating(DRIVER_STAT_DEFINITIONS.consistency, this.consistency),
|
|
64
|
+
personality: {
|
|
65
|
+
aggression: applyRating(DRIVER_STAT_DEFINITIONS.aggression, this.aggression),
|
|
66
|
+
riskTolerance: applyRating(DRIVER_STAT_DEFINITIONS.riskTolerance, this.riskTolerance),
|
|
67
|
+
patience: applyRating(DRIVER_STAT_DEFINITIONS.patience, this.patience),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { buildChampionshipDriverGrid } from './championship.js';
|
|
2
|
+
|
|
3
|
+
export function normalizeSimulatorDrivers(drivers, { entries } = {}) {
|
|
4
|
+
if (!Array.isArray(drivers) || drivers.length === 0) {
|
|
5
|
+
throw new Error('mountF1Simulator requires a non-empty drivers array.');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
drivers.forEach((driver, index) => {
|
|
9
|
+
if (!driver || typeof driver !== 'object') {
|
|
10
|
+
throw new Error(`Invalid simulator driver at index ${index}.`);
|
|
11
|
+
}
|
|
12
|
+
if (!driver.id) {
|
|
13
|
+
throw new Error(`Simulator driver at index ${index} is missing an id.`);
|
|
14
|
+
}
|
|
15
|
+
if (!driver.name) {
|
|
16
|
+
throw new Error(`Simulator driver "${driver.id}" is missing a name.`);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return buildChampionshipDriverGrid(drivers, entries);
|
|
21
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { normalizeCustomFields } from './customFields.js';
|
|
2
|
+
|
|
3
|
+
const RATING_MINIMUM = 0;
|
|
4
|
+
const RATING_NEUTRAL = 50;
|
|
5
|
+
const RATING_MAXIMUM = 100;
|
|
6
|
+
|
|
7
|
+
export const VEHICLE_STAT_DEFINITIONS = {
|
|
8
|
+
power: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 43000, variance: 3200, output: 'powerNewtons' },
|
|
9
|
+
braking: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 59000, variance: 4200, output: 'brakeNewtons' },
|
|
10
|
+
aero: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 6.1, variance: 0.35, output: 'downforceCoefficient' },
|
|
11
|
+
dragEfficiency: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 0.33, variance: 0.035, direction: -1, output: 'dragCoefficient' },
|
|
12
|
+
mechanicalGrip: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 2.35, variance: 0.18, output: 'tireGrip' },
|
|
13
|
+
weightControl: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 798, variance: 10, direction: -1, output: 'mass' },
|
|
14
|
+
tireCare: { minimum: RATING_MINIMUM, neutral: RATING_NEUTRAL, maximum: RATING_MAXIMUM, base: 1, variance: 0.12, output: 'tireCare' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function clampRating(value, label) {
|
|
18
|
+
const rating = Number(value);
|
|
19
|
+
if (!Number.isFinite(rating)) throw new Error(`Invalid vehicle rating for ${label}: ${value}`);
|
|
20
|
+
return Math.min(Math.max(rating, RATING_MINIMUM), RATING_MAXIMUM);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function applyRating(definition, value) {
|
|
24
|
+
const normalized = (value - definition.neutral) / (definition.maximum - definition.neutral);
|
|
25
|
+
const direction = definition.direction ?? 1;
|
|
26
|
+
return definition.base + normalized * definition.variance * direction;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class VehicleData {
|
|
30
|
+
constructor({
|
|
31
|
+
id = null,
|
|
32
|
+
name = null,
|
|
33
|
+
power = RATING_NEUTRAL,
|
|
34
|
+
braking = RATING_NEUTRAL,
|
|
35
|
+
aero = RATING_NEUTRAL,
|
|
36
|
+
dragEfficiency = RATING_NEUTRAL,
|
|
37
|
+
mechanicalGrip = RATING_NEUTRAL,
|
|
38
|
+
weightControl = RATING_NEUTRAL,
|
|
39
|
+
tireCare = RATING_NEUTRAL,
|
|
40
|
+
customFields = [],
|
|
41
|
+
} = {}) {
|
|
42
|
+
this.id = id;
|
|
43
|
+
this.name = name;
|
|
44
|
+
this.power = clampRating(power, 'power');
|
|
45
|
+
this.braking = clampRating(braking, 'braking');
|
|
46
|
+
this.aero = clampRating(aero, 'aero');
|
|
47
|
+
this.dragEfficiency = clampRating(dragEfficiency, 'dragEfficiency');
|
|
48
|
+
this.mechanicalGrip = clampRating(mechanicalGrip, 'mechanicalGrip');
|
|
49
|
+
this.weightControl = clampRating(weightControl, 'weightControl');
|
|
50
|
+
this.tireCare = clampRating(tireCare, 'tireCare');
|
|
51
|
+
this.customFields = normalizeCustomFields(customFields);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ratings() {
|
|
55
|
+
return {
|
|
56
|
+
power: this.power,
|
|
57
|
+
braking: this.braking,
|
|
58
|
+
aero: this.aero,
|
|
59
|
+
dragEfficiency: this.dragEfficiency,
|
|
60
|
+
mechanicalGrip: this.mechanicalGrip,
|
|
61
|
+
weightControl: this.weightControl,
|
|
62
|
+
tireCare: this.tireCare,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toConstructorArgs() {
|
|
67
|
+
const ratings = this.ratings();
|
|
68
|
+
const values = Object.fromEntries(Object.entries(VEHICLE_STAT_DEFINITIONS).map(([key, definition]) => [
|
|
69
|
+
definition.output,
|
|
70
|
+
applyRating(definition, ratings[key]),
|
|
71
|
+
]));
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
id: this.id,
|
|
75
|
+
name: this.name,
|
|
76
|
+
ratings,
|
|
77
|
+
customFields: this.customFields,
|
|
78
|
+
...values,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
export type TireCompound = 'S' | 'M' | 'H';
|
|
2
|
+
export type CameraMode = 'overview' | 'leader' | 'selected';
|
|
3
|
+
export type RaceBannerMode = 'project' | 'radio' | 'hidden';
|
|
4
|
+
export type RaceBannerEnabledMode = 'project' | 'radio';
|
|
5
|
+
export type RaceDataBannerSize = 'auto' | 'custom';
|
|
6
|
+
export type TimingTowerVerticalFit = 'expand-race-view' | 'scroll';
|
|
7
|
+
export type LayoutPreset = 'standard' | 'left-tower-overlay';
|
|
8
|
+
export type CameraControlsMode = 'embedded' | 'external' | false;
|
|
9
|
+
export type PaddockPresetName = 'dashboard' | 'timing-overlay' | 'compact-race' | 'full-dashboard';
|
|
10
|
+
|
|
11
|
+
export interface CustomField {
|
|
12
|
+
label: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CustomFieldInput = CustomField[] | Record<string, string>;
|
|
17
|
+
|
|
18
|
+
export interface TeamData {
|
|
19
|
+
id?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
color?: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SimulatorDriver {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
color: string;
|
|
29
|
+
link?: string;
|
|
30
|
+
icon?: string;
|
|
31
|
+
code?: string;
|
|
32
|
+
timingName?: string;
|
|
33
|
+
tire?: TireCompound;
|
|
34
|
+
raceData?: string[];
|
|
35
|
+
customFields?: CustomFieldInput;
|
|
36
|
+
team?: TeamData | null;
|
|
37
|
+
driverNumber?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DriverRatings {
|
|
41
|
+
pace: number;
|
|
42
|
+
racecraft: number;
|
|
43
|
+
aggression: number;
|
|
44
|
+
riskTolerance: number;
|
|
45
|
+
patience: number;
|
|
46
|
+
consistency: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface VehicleRatings {
|
|
50
|
+
power: number;
|
|
51
|
+
braking: number;
|
|
52
|
+
aero: number;
|
|
53
|
+
dragEfficiency: number;
|
|
54
|
+
mechanicalGrip: number;
|
|
55
|
+
weightControl: number;
|
|
56
|
+
tireCare: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DriverBlueprint extends Partial<DriverRatings> {
|
|
60
|
+
customFields?: CustomFieldInput;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VehicleBlueprint extends Partial<VehicleRatings> {
|
|
64
|
+
id?: string | null;
|
|
65
|
+
name?: string | null;
|
|
66
|
+
customFields?: CustomFieldInput;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DriverConstructorArgs {
|
|
70
|
+
ratings: DriverRatings;
|
|
71
|
+
customFields: CustomField[];
|
|
72
|
+
pace: number;
|
|
73
|
+
racecraft: number;
|
|
74
|
+
consistency: number;
|
|
75
|
+
personality: {
|
|
76
|
+
aggression: number;
|
|
77
|
+
riskTolerance: number;
|
|
78
|
+
patience: number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface VehicleConstructorArgs {
|
|
83
|
+
id: string | null;
|
|
84
|
+
name: string | null;
|
|
85
|
+
ratings: VehicleRatings;
|
|
86
|
+
customFields: CustomField[];
|
|
87
|
+
powerNewtons: number;
|
|
88
|
+
brakeNewtons: number;
|
|
89
|
+
downforceCoefficient: number;
|
|
90
|
+
dragCoefficient: number;
|
|
91
|
+
tireGrip: number;
|
|
92
|
+
mass: number;
|
|
93
|
+
tireCare: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class DriverData {
|
|
97
|
+
constructor(input?: DriverBlueprint);
|
|
98
|
+
readonly pace: number;
|
|
99
|
+
readonly racecraft: number;
|
|
100
|
+
readonly aggression: number;
|
|
101
|
+
readonly riskTolerance: number;
|
|
102
|
+
readonly patience: number;
|
|
103
|
+
readonly consistency: number;
|
|
104
|
+
readonly customFields: CustomField[];
|
|
105
|
+
ratings(): DriverRatings;
|
|
106
|
+
toConstructorArgs(): DriverConstructorArgs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class VehicleData {
|
|
110
|
+
constructor(input?: VehicleBlueprint);
|
|
111
|
+
readonly id: string | null;
|
|
112
|
+
readonly name: string | null;
|
|
113
|
+
readonly power: number;
|
|
114
|
+
readonly braking: number;
|
|
115
|
+
readonly aero: number;
|
|
116
|
+
readonly dragEfficiency: number;
|
|
117
|
+
readonly mechanicalGrip: number;
|
|
118
|
+
readonly weightControl: number;
|
|
119
|
+
readonly tireCare: number;
|
|
120
|
+
readonly customFields: CustomField[];
|
|
121
|
+
ratings(): VehicleRatings;
|
|
122
|
+
toConstructorArgs(): VehicleConstructorArgs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ChampionshipEntryBlueprint {
|
|
126
|
+
driverId: string;
|
|
127
|
+
driverNumber?: number;
|
|
128
|
+
timingName?: string;
|
|
129
|
+
driver?: DriverData | DriverBlueprint;
|
|
130
|
+
vehicle?: VehicleData | VehicleBlueprint;
|
|
131
|
+
team?: TeamData;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface NormalizedSimulatorDriver extends SimulatorDriver {
|
|
135
|
+
code: string;
|
|
136
|
+
timingCode: string;
|
|
137
|
+
raceName: string;
|
|
138
|
+
driverNumber: number;
|
|
139
|
+
team: TeamData | null;
|
|
140
|
+
pace: number;
|
|
141
|
+
racecraft: number;
|
|
142
|
+
consistency: number;
|
|
143
|
+
personality: DriverConstructorArgs['personality'];
|
|
144
|
+
vehicle: VehicleConstructorArgs;
|
|
145
|
+
constructorArgs: {
|
|
146
|
+
driver: DriverConstructorArgs;
|
|
147
|
+
vehicle: VehicleConstructorArgs;
|
|
148
|
+
};
|
|
149
|
+
championship: {
|
|
150
|
+
id: string;
|
|
151
|
+
name: string;
|
|
152
|
+
season: number;
|
|
153
|
+
entryIndex: number;
|
|
154
|
+
vehicleId: string | null;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface F1SimulatorTheme {
|
|
159
|
+
accentColor?: string;
|
|
160
|
+
greenColor?: string;
|
|
161
|
+
yellowColor?: string;
|
|
162
|
+
timingTowerMaxWidth?: string;
|
|
163
|
+
raceViewMinHeight?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface F1SimulatorUiOptions {
|
|
167
|
+
layoutPreset?: LayoutPreset;
|
|
168
|
+
cameraControls?: CameraControlsMode;
|
|
169
|
+
showFps?: boolean;
|
|
170
|
+
showTimingTower?: boolean;
|
|
171
|
+
showTelemetry?: boolean;
|
|
172
|
+
telemetryIncludesOverview?: boolean;
|
|
173
|
+
showRaceDataPanel?: boolean;
|
|
174
|
+
raceDataBanners?: {
|
|
175
|
+
initial?: RaceBannerMode;
|
|
176
|
+
enabled?: true | false | RaceBannerEnabledMode[];
|
|
177
|
+
};
|
|
178
|
+
raceDataBannerSize?: RaceDataBannerSize;
|
|
179
|
+
timingTowerVerticalFit?: TimingTowerVerticalFit;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface TrackTextureAssets {
|
|
183
|
+
asphalt?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface F1SimulatorAssets {
|
|
187
|
+
car?: string;
|
|
188
|
+
carOverview?: string;
|
|
189
|
+
driverHelmet?: string;
|
|
190
|
+
safetyCar?: string;
|
|
191
|
+
broadcastPanel?: string;
|
|
192
|
+
f1Logo?: string;
|
|
193
|
+
trackTextures?: TrackTextureAssets;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface TrackSnapshot {
|
|
197
|
+
seed?: number | null;
|
|
198
|
+
width?: number;
|
|
199
|
+
length?: number;
|
|
200
|
+
[key: string]: unknown;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface RaceEvent {
|
|
204
|
+
type: string;
|
|
205
|
+
at?: number;
|
|
206
|
+
[key: string]: unknown;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface CarSnapshot {
|
|
210
|
+
id: string;
|
|
211
|
+
rank: number;
|
|
212
|
+
code: string;
|
|
213
|
+
timingCode: string;
|
|
214
|
+
name: string;
|
|
215
|
+
color: string;
|
|
216
|
+
tire: TireCompound;
|
|
217
|
+
lap: number;
|
|
218
|
+
speedKph: number;
|
|
219
|
+
finished?: boolean;
|
|
220
|
+
finishTime?: number | null;
|
|
221
|
+
classifiedRank?: number | null;
|
|
222
|
+
intervalAheadSeconds?: number | null;
|
|
223
|
+
leaderGapSeconds?: number | null;
|
|
224
|
+
[key: string]: unknown;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface RaceClassificationEntry {
|
|
228
|
+
id: string;
|
|
229
|
+
rank: number;
|
|
230
|
+
code: string;
|
|
231
|
+
lap: number;
|
|
232
|
+
finished: boolean;
|
|
233
|
+
finishTime?: number | null;
|
|
234
|
+
[key: string]: unknown;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface RaceSnapshot {
|
|
238
|
+
time: number;
|
|
239
|
+
world: {
|
|
240
|
+
width: number;
|
|
241
|
+
height: number;
|
|
242
|
+
[key: string]: unknown;
|
|
243
|
+
};
|
|
244
|
+
track: TrackSnapshot;
|
|
245
|
+
totalLaps: number;
|
|
246
|
+
raceControl: {
|
|
247
|
+
mode: string;
|
|
248
|
+
finished: boolean;
|
|
249
|
+
finishedAt: number | null;
|
|
250
|
+
winner: CarSnapshot | null;
|
|
251
|
+
classification: RaceClassificationEntry[];
|
|
252
|
+
start: {
|
|
253
|
+
visible: boolean;
|
|
254
|
+
[key: string]: unknown;
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
safetyCar: {
|
|
258
|
+
deployed: boolean;
|
|
259
|
+
[key: string]: unknown;
|
|
260
|
+
};
|
|
261
|
+
rules: Record<string, unknown>;
|
|
262
|
+
events: RaceEvent[];
|
|
263
|
+
cars: CarSnapshot[];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface LifecycleErrorContext {
|
|
267
|
+
phase?: string;
|
|
268
|
+
callback?: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface F1SimulatorCallbacks {
|
|
272
|
+
onDriverOpen?: (driver: NormalizedSimulatorDriver) => void;
|
|
273
|
+
onLoadingChange?: (state: { loading: boolean; phase: string }) => void;
|
|
274
|
+
onReady?: (payload: { snapshot: RaceSnapshot }) => void;
|
|
275
|
+
onError?: (error: unknown, context: LifecycleErrorContext) => void;
|
|
276
|
+
onDriverSelect?: (driver: CarSnapshot, snapshot: RaceSnapshot) => void;
|
|
277
|
+
onRaceEvent?: (event: RaceEvent, snapshot: RaceSnapshot) => void;
|
|
278
|
+
onLapChange?: (payload: {
|
|
279
|
+
previousLeaderLap: number | null;
|
|
280
|
+
leaderLap: number;
|
|
281
|
+
leader: CarSnapshot | null;
|
|
282
|
+
snapshot: RaceSnapshot;
|
|
283
|
+
}) => void;
|
|
284
|
+
onRaceFinish?: (payload: {
|
|
285
|
+
winner: CarSnapshot | null;
|
|
286
|
+
classification: RaceClassificationEntry[];
|
|
287
|
+
snapshot: RaceSnapshot;
|
|
288
|
+
}) => void;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface F1SimulatorOptions extends F1SimulatorCallbacks {
|
|
292
|
+
preset?: PaddockPresetName;
|
|
293
|
+
drivers: SimulatorDriver[];
|
|
294
|
+
entries?: ChampionshipEntryBlueprint[];
|
|
295
|
+
seed?: number;
|
|
296
|
+
trackSeed?: number;
|
|
297
|
+
totalLaps?: number;
|
|
298
|
+
initialCameraMode?: CameraMode;
|
|
299
|
+
theme?: F1SimulatorTheme;
|
|
300
|
+
title?: string;
|
|
301
|
+
kicker?: string;
|
|
302
|
+
backLinkHref?: string;
|
|
303
|
+
backLinkLabel?: string;
|
|
304
|
+
showBackLink?: boolean;
|
|
305
|
+
ui?: F1SimulatorUiOptions;
|
|
306
|
+
assets?: F1SimulatorAssets;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface MountRaceCanvasOptions {
|
|
310
|
+
includeRaceDataPanel?: boolean;
|
|
311
|
+
includeTimingTower?: boolean;
|
|
312
|
+
timingTowerVerticalFit?: TimingTowerVerticalFit;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface MountTelemetryPanelOptions {
|
|
316
|
+
includeOverview?: boolean;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface F1MountedSimulator {
|
|
320
|
+
destroy(): void;
|
|
321
|
+
restart(nextOptions?: Partial<F1SimulatorOptions>): void;
|
|
322
|
+
selectDriver(driverId: string): void;
|
|
323
|
+
setSafetyCarDeployed(deployed: boolean): void;
|
|
324
|
+
callSafetyCar(): void;
|
|
325
|
+
clearSafetyCar(): void;
|
|
326
|
+
toggleSafetyCar(): void;
|
|
327
|
+
getSnapshot(): RaceSnapshot | null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface PaddockSimulatorController {
|
|
331
|
+
mountRaceControls<T extends Element>(root: T): T;
|
|
332
|
+
mountCameraControls<T extends Element>(root: T): T;
|
|
333
|
+
mountSafetyCarControl<T extends Element>(root: T): T;
|
|
334
|
+
mountTimingTower<T extends Element>(root: T): T;
|
|
335
|
+
mountRaceCanvas<T extends Element>(root: T, options?: MountRaceCanvasOptions): T;
|
|
336
|
+
mountTelemetryPanel<T extends Element>(root: T, options?: MountTelemetryPanelOptions): T;
|
|
337
|
+
mountCarDriverOverview<T extends Element>(root: T): T;
|
|
338
|
+
mountRaceDataPanel<T extends Element>(root: T): T;
|
|
339
|
+
querySelector(selector: string): Element | null;
|
|
340
|
+
querySelectorAll(selector: string): Element[];
|
|
341
|
+
start(): Promise<PaddockSimulatorController>;
|
|
342
|
+
destroy(): void;
|
|
343
|
+
restart(nextOptions?: Partial<F1SimulatorOptions>): void;
|
|
344
|
+
selectDriver(driverId: string): void;
|
|
345
|
+
setSafetyCarDeployed(deployed: boolean): void;
|
|
346
|
+
callSafetyCar(): void;
|
|
347
|
+
clearSafetyCar(): void;
|
|
348
|
+
toggleSafetyCar(): void;
|
|
349
|
+
getSnapshot(): RaceSnapshot | null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export const CHAMPIONSHIP_ENTRY_BLUEPRINTS: ChampionshipEntryBlueprint[];
|
|
353
|
+
export const DEMO_PROJECT_DRIVERS: SimulatorDriver[];
|
|
354
|
+
export const DEFAULT_F1_SIMULATOR_ASSETS: Required<F1SimulatorAssets>;
|
|
355
|
+
export const PADDOCK_SIMULATOR_PRESETS: Record<PaddockPresetName, {
|
|
356
|
+
ui?: F1SimulatorUiOptions;
|
|
357
|
+
theme?: F1SimulatorTheme;
|
|
358
|
+
}>;
|
|
359
|
+
|
|
360
|
+
export const REAL_F1_CAR_LENGTH_METERS: number;
|
|
361
|
+
export const SIM_UNITS_PER_METER: number;
|
|
362
|
+
export const TARGET_F1_TOP_SPEED_KPH: number;
|
|
363
|
+
export const VISUAL_CAR_LENGTH_METERS: number;
|
|
364
|
+
|
|
365
|
+
export function buildChampionshipDriverGrid(
|
|
366
|
+
drivers?: SimulatorDriver[],
|
|
367
|
+
entries?: ChampionshipEntryBlueprint[],
|
|
368
|
+
): NormalizedSimulatorDriver[];
|
|
369
|
+
export function formatDriverNumber(driverNumber: number | string | null | undefined): string;
|
|
370
|
+
export function normalizeSimulatorDrivers(
|
|
371
|
+
drivers: SimulatorDriver[],
|
|
372
|
+
options?: { entries?: ChampionshipEntryBlueprint[] },
|
|
373
|
+
): NormalizedSimulatorDriver[];
|
|
374
|
+
|
|
375
|
+
export function metersToSimUnits(meters: number): number;
|
|
376
|
+
export function simUnitsToMeters(simUnits: number): number;
|
|
377
|
+
export function kphToSimSpeed(kph: number): number;
|
|
378
|
+
export function simSpeedToKph(simSpeed: number): number;
|
|
379
|
+
export function simSpeedToMetersPerSecond(simSpeed: number): number;
|
|
380
|
+
|
|
381
|
+
export function createPaddockSimulator(options: F1SimulatorOptions): PaddockSimulatorController;
|
|
382
|
+
export function mountF1Simulator(root: Element, options: F1SimulatorOptions): Promise<F1MountedSimulator>;
|
|
383
|
+
|
|
384
|
+
export function mountRaceControls<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
385
|
+
export function mountCameraControls<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
386
|
+
export function mountSafetyCarControl<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
387
|
+
export function mountTimingTower<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
388
|
+
export function mountRaceCanvas<T extends Element>(
|
|
389
|
+
root: T,
|
|
390
|
+
simulator: PaddockSimulatorController,
|
|
391
|
+
options?: MountRaceCanvasOptions,
|
|
392
|
+
): T;
|
|
393
|
+
export function mountTelemetryPanel<T extends Element>(
|
|
394
|
+
root: T,
|
|
395
|
+
simulator: PaddockSimulatorController,
|
|
396
|
+
options?: MountTelemetryPanelOptions,
|
|
397
|
+
): T;
|
|
398
|
+
export function mountCarDriverOverview<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
399
|
+
export function mountRaceDataPanel<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import './styles.css';
|
|
2
|
+
import { F1SimulatorApp } from './app/F1SimulatorApp.js';
|
|
3
|
+
import { resolveF1SimulatorOptions } from './config/defaultOptions.js';
|
|
4
|
+
import { createF1SimulatorShell } from './ui/shellTemplate.js';
|
|
5
|
+
export {
|
|
6
|
+
createPaddockSimulator,
|
|
7
|
+
mountCarDriverOverview,
|
|
8
|
+
mountCameraControls,
|
|
9
|
+
mountRaceCanvas,
|
|
10
|
+
mountRaceControls,
|
|
11
|
+
mountRaceDataPanel,
|
|
12
|
+
mountSafetyCarControl,
|
|
13
|
+
mountTelemetryPanel,
|
|
14
|
+
mountTimingTower,
|
|
15
|
+
} from './api/PaddockSimulatorController.js';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
CHAMPIONSHIP_ENTRY_BLUEPRINTS,
|
|
19
|
+
DriverData,
|
|
20
|
+
VehicleData,
|
|
21
|
+
buildChampionshipDriverGrid,
|
|
22
|
+
formatDriverNumber,
|
|
23
|
+
} from './data/championship.js';
|
|
24
|
+
export { DEMO_PROJECT_DRIVERS } from './data/demoDrivers.js';
|
|
25
|
+
export { DEFAULT_F1_SIMULATOR_ASSETS } from './config/defaultAssets.js';
|
|
26
|
+
export { PADDOCK_SIMULATOR_PRESETS } from './config/defaultOptions.js';
|
|
27
|
+
export { normalizeSimulatorDrivers } from './data/normalizeDrivers.js';
|
|
28
|
+
export {
|
|
29
|
+
REAL_F1_CAR_LENGTH_METERS,
|
|
30
|
+
SIM_UNITS_PER_METER,
|
|
31
|
+
TARGET_F1_TOP_SPEED_KPH,
|
|
32
|
+
VISUAL_CAR_LENGTH_METERS,
|
|
33
|
+
kphToSimSpeed,
|
|
34
|
+
metersToSimUnits,
|
|
35
|
+
simSpeedToKph,
|
|
36
|
+
simSpeedToMetersPerSecond,
|
|
37
|
+
simUnitsToMeters,
|
|
38
|
+
} from './simulation/units.js';
|
|
39
|
+
|
|
40
|
+
function assertMountRoot(root) {
|
|
41
|
+
if (!(root instanceof Element)) {
|
|
42
|
+
throw new Error('mountF1Simulator requires a DOM element root.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mergeRestartOptions(previousOptions, nextOptions) {
|
|
47
|
+
const resetFromPreset = Object.hasOwn(nextOptions, 'preset');
|
|
48
|
+
const previousUi = resetFromPreset ? {} : previousOptions.ui;
|
|
49
|
+
const previousTheme = resetFromPreset ? {} : previousOptions.theme;
|
|
50
|
+
return {
|
|
51
|
+
...previousOptions,
|
|
52
|
+
...nextOptions,
|
|
53
|
+
ui: {
|
|
54
|
+
...previousUi,
|
|
55
|
+
...(nextOptions.ui ?? {}),
|
|
56
|
+
raceDataBanners: {
|
|
57
|
+
...(previousUi.raceDataBanners ?? {}),
|
|
58
|
+
...(nextOptions.ui?.raceDataBanners ?? {}),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
theme: {
|
|
62
|
+
...previousTheme,
|
|
63
|
+
...(nextOptions.theme ?? {}),
|
|
64
|
+
},
|
|
65
|
+
assets: {
|
|
66
|
+
...previousOptions.assets,
|
|
67
|
+
...(nextOptions.assets ?? {}),
|
|
68
|
+
trackTextures: {
|
|
69
|
+
...previousOptions.assets.trackTextures,
|
|
70
|
+
...(nextOptions.assets?.trackTextures ?? {}),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
drivers: nextOptions.drivers ?? previousOptions.drivers,
|
|
74
|
+
entries: nextOptions.entries ?? previousOptions.entries,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function mountF1Simulator(root, options = {}) {
|
|
79
|
+
assertMountRoot(root);
|
|
80
|
+
|
|
81
|
+
let resolvedOptions = resolveF1SimulatorOptions(options);
|
|
82
|
+
root.innerHTML = createF1SimulatorShell(resolvedOptions);
|
|
83
|
+
const shell = root.querySelector('[data-f1-simulator-shell]');
|
|
84
|
+
const app = new F1SimulatorApp(shell, resolvedOptions);
|
|
85
|
+
await app.init();
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
destroy() {
|
|
89
|
+
app.destroy();
|
|
90
|
+
root.innerHTML = '';
|
|
91
|
+
},
|
|
92
|
+
restart(nextOptions = {}) {
|
|
93
|
+
resolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(resolvedOptions, nextOptions));
|
|
94
|
+
app.restart(resolvedOptions);
|
|
95
|
+
},
|
|
96
|
+
selectDriver(driverId) {
|
|
97
|
+
app.selectCar(driverId, { focus: true });
|
|
98
|
+
},
|
|
99
|
+
setSafetyCarDeployed(deployed) {
|
|
100
|
+
app.setSafetyCarDeployed(deployed);
|
|
101
|
+
},
|
|
102
|
+
callSafetyCar() {
|
|
103
|
+
app.setSafetyCarDeployed(true);
|
|
104
|
+
},
|
|
105
|
+
clearSafetyCar() {
|
|
106
|
+
app.setSafetyCarDeployed(false);
|
|
107
|
+
},
|
|
108
|
+
toggleSafetyCar() {
|
|
109
|
+
const active = app.getSnapshot()?.raceControl.mode === 'safety-car';
|
|
110
|
+
app.setSafetyCarDeployed(!active);
|
|
111
|
+
},
|
|
112
|
+
getSnapshot() {
|
|
113
|
+
return app.getSnapshot();
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|