@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.
Files changed (44) hide show
  1. package/AGENTS.md +86 -0
  2. package/CHANGELOG.md +5 -0
  3. package/LICENSE +176 -0
  4. package/README.md +165 -0
  5. package/assets/f1-broadcast-panel-surface.png +0 -0
  6. package/assets/f1-car-sprite-game.png +0 -0
  7. package/assets/f1-driver-helmet.png +0 -0
  8. package/assets/f1-safety-car-sprite.png +0 -0
  9. package/assets/f1-texture-asphalt.png +0 -0
  10. package/assets/f1Logo.png +0 -0
  11. package/docs/architecture.md +209 -0
  12. package/docs/concepts.md +178 -0
  13. package/docs/data_contract.md +360 -0
  14. package/docs/data_model.md +167 -0
  15. package/docs/index.md +15 -0
  16. package/docs/learnings.md +47 -0
  17. package/docs/rules.md +181 -0
  18. package/docs/system_specs.md +211 -0
  19. package/package.json +85 -0
  20. package/src/api/PaddockSimulatorController.js +233 -0
  21. package/src/app/F1SimulatorApp.js +1293 -0
  22. package/src/app/domBindings.js +64 -0
  23. package/src/config/defaultAssets.js +29 -0
  24. package/src/config/defaultOptions.js +184 -0
  25. package/src/data/championship.js +225 -0
  26. package/src/data/customFields.js +14 -0
  27. package/src/data/demoDrivers.js +134 -0
  28. package/src/data/driverData.js +71 -0
  29. package/src/data/normalizeDrivers.js +21 -0
  30. package/src/data/vehicleData.js +81 -0
  31. package/src/index.d.ts +399 -0
  32. package/src/index.js +116 -0
  33. package/src/rendering/proceduralTrackAsset.js +517 -0
  34. package/src/rendering/renderSnapshot.js +23 -0
  35. package/src/simulation/driverController.js +253 -0
  36. package/src/simulation/raceSimulation.js +933 -0
  37. package/src/simulation/simMath.js +34 -0
  38. package/src/simulation/trackModel.js +607 -0
  39. package/src/simulation/units.js +30 -0
  40. package/src/simulation/vehiclePhysics.js +104 -0
  41. package/src/standalone.js +15 -0
  42. package/src/styles.css +1818 -0
  43. package/src/ui/componentTemplates.js +229 -0
  44. package/src/ui/shellTemplate.js +43 -0
@@ -0,0 +1,253 @@
1
+ import { pointAt, offsetTrackPoint } from './trackModel.js';
2
+ import { clamp, normalizeAngle, seededRange, TWO_PI } from './simMath.js';
3
+ import { VEHICLE_LIMITS } from './vehiclePhysics.js';
4
+
5
+ const LANE_OFFSETS = [-78, -52, -26, 0, 26, 52, 78];
6
+
7
+ function angleToPoint(car, target) {
8
+ const angle = Math.atan2(target.y - car.y, target.x - car.x);
9
+ return normalizeAngle(angle - car.heading);
10
+ }
11
+
12
+ export function buildDriverPersonality(driver, index, racecraft, random) {
13
+ const numberSeed = Number(driver.driverNumber ?? index + 1);
14
+ const numberBias = (((Number.isFinite(numberSeed) ? numberSeed : index + 1) % 11) - 5) * 0.014;
15
+ const baseAggression = clamp(
16
+ driver.personality?.aggression ?? 0.36 + racecraft * 0.34 + numberBias + seededRange(random, -0.045, 0.045),
17
+ 0.18,
18
+ 0.88,
19
+ );
20
+
21
+ return {
22
+ baseAggression,
23
+ riskTolerance: clamp(
24
+ driver.personality?.riskTolerance ?? baseAggression * 0.72 + racecraft * 0.24 + seededRange(random, -0.04, 0.04),
25
+ 0.12,
26
+ 0.95,
27
+ ),
28
+ patience: clamp(
29
+ driver.personality?.patience ?? 0.72 - baseAggression * 0.36 + racecraft * 0.12 + seededRange(random, -0.05, 0.05),
30
+ 0.18,
31
+ 0.9,
32
+ ),
33
+ };
34
+ }
35
+
36
+ export function createDriverInput() {
37
+ const state = {
38
+ steering: 0,
39
+ throttle: 0,
40
+ brake: 0,
41
+ };
42
+
43
+ return {
44
+ steer(amount) {
45
+ state.steering = clamp(amount, -VEHICLE_LIMITS.maxSteer, VEHICLE_LIMITS.maxSteer);
46
+ return this;
47
+ },
48
+ accelerate(amount) {
49
+ state.throttle = clamp(amount, 0, 1);
50
+ return this;
51
+ },
52
+ brake(amount) {
53
+ state.brake = clamp(amount, 0, 1);
54
+ return this;
55
+ },
56
+ controls() {
57
+ return { ...state };
58
+ },
59
+ };
60
+ }
61
+
62
+ export function decideDriverControls({ car, orderIndex, race }) {
63
+ if (car.manualControls) return car.manualControls;
64
+
65
+ if (car.gridLocked) {
66
+ return createDriverInput().brake(1).controls();
67
+ }
68
+
69
+ if (race.safetyCar.deployed) {
70
+ return decideSafetyCarControls(car, orderIndex, race);
71
+ }
72
+
73
+ if (!car.trackState.onTrack) {
74
+ return decideRejoinControls(car, race);
75
+ }
76
+
77
+ return decideRacingControls(car, orderIndex, race);
78
+ }
79
+
80
+ function decideRacingControls(car, orderIndex, race) {
81
+ const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
82
+ const lookahead = clamp(car.speed * (1.12 - aggression * 0.08) + 160, 160, 360);
83
+ const targetBase = pointAt(race.track, car.progress + lookahead);
84
+ const lanePlan = planRacingLine(car, orderIndex, race);
85
+ const recoveryBias = car.trackState.crossTrackError > race.track.width * 0.46 ? 0 : 1;
86
+ const target = offsetTrackPoint(targetBase, lanePlan.offset * recoveryBias);
87
+ const angleError = angleToPoint(car, target);
88
+ const curvature = Math.max(car.trackState.curvature, targetBase.curvature);
89
+ const gripBudget = 54 + car.racecraft * 11 + (car.tireEnergy ?? 100) * 0.05 + aggression * 4.5;
90
+ const cornerTarget = clamp(
91
+ Math.sqrt(gripBudget / Math.max(curvature, 0.0001)) + (car.pace - 1) * 20 + aggression * 7,
92
+ 72,
93
+ 168,
94
+ );
95
+ const edgeTolerance = race.track.width * (0.36 + aggression * 0.1);
96
+ const edgePenalty = Math.max(0, car.trackState.crossTrackError - edgeTolerance) * (0.16 - aggression * 0.05);
97
+ const trafficPenalty = Math.max(
98
+ lanePlan.sameLaneAhead ? clamp((230 - lanePlan.sameLaneAhead.gap) * 0.16, 0, 32) : 0,
99
+ lanePlan.sideRisk ? clamp((44 - lanePlan.sideRisk.lateral) * 0.42, 0, 16) : 0,
100
+ ) * (1 - aggression * 0.28);
101
+ const desiredSpeed = clamp(
102
+ (car.drsActive ? cornerTarget + 22 : cornerTarget) - edgePenalty - trafficPenalty,
103
+ 58,
104
+ VEHICLE_LIMITS.maxSpeed,
105
+ );
106
+ const speedError = desiredSpeed - car.speed;
107
+
108
+ return createDriverInput()
109
+ .steer(angleError * (0.82 + car.racecraft * 0.1))
110
+ .accelerate(speedError > 1 ? clamp(speedError / 16, 0.1 + aggression * 0.08, 1) : 0)
111
+ .brake(speedError < -2 ? clamp(Math.abs(speedError) / (22 + aggression * 8), 0, 1) : 0)
112
+ .controls();
113
+ }
114
+
115
+ function decideRejoinControls(car, race) {
116
+ const lookahead = clamp(car.speed * 0.72 + 118, 118, 220);
117
+ const targetBase = pointAt(race.track, car.progress + lookahead);
118
+ const target = offsetTrackPoint(targetBase, 0);
119
+ const angleError = angleToPoint(car, target);
120
+ const distanceFromRoad = Math.max(0, car.trackState.crossTrackError - race.track.width / 2);
121
+ const surfaceTargetSpeed = car.trackState.surface === 'gravel'
122
+ ? 54
123
+ : car.trackState.surface === 'grass'
124
+ ? 44
125
+ : 30;
126
+ const desiredSpeed = clamp(surfaceTargetSpeed - distanceFromRoad * 0.045, 20, surfaceTargetSpeed);
127
+ const speedError = desiredSpeed - car.speed;
128
+ const alignment = clamp(1 - Math.abs(angleError) / Math.PI, 0.28, 1);
129
+ const lowSpeedRecovery = car.speed < 18 ? 0.16 : 0;
130
+ const throttleLimit = car.trackState.surface === 'gravel' ? 0.62 : 0.48;
131
+
132
+ return createDriverInput()
133
+ .steer(angleError * 0.92)
134
+ .accelerate(speedError > 0 ? clamp((speedError / 24) * alignment + lowSpeedRecovery, 0.08, throttleLimit) : lowSpeedRecovery)
135
+ .brake(speedError < -5 ? clamp(Math.abs(speedError) / 30, 0.04, 0.74) : 0)
136
+ .controls();
137
+ }
138
+
139
+ function decideSafetyCarControls(car, orderIndex, race) {
140
+ const queueSlot = race.safetyCar.progress - race.rules.safetyCarLeadDistance - orderIndex * race.rules.safetyCarGap;
141
+ const lookahead = clamp(car.speed * 0.8 + 74, 84, 148);
142
+ const targetBase = pointAt(race.track, car.progress + lookahead);
143
+ const target = offsetTrackPoint(targetBase, 0);
144
+ const slotError = queueSlot - car.raceDistance;
145
+ const desiredSpeed = clamp(
146
+ race.rules.safetyCarSpeed + slotError * 0.24,
147
+ 22,
148
+ race.rules.safetyCarSpeed + 32,
149
+ );
150
+ const speedError = desiredSpeed - car.speed;
151
+
152
+ return createDriverInput()
153
+ .steer(angleToPoint(car, target) * 1.04)
154
+ .accelerate(speedError > 1 ? clamp(speedError / 16, 0, 0.5) : 0)
155
+ .brake(speedError < -0.5 ? clamp(Math.abs(speedError) / 14, 0, 1) : 0)
156
+ .controls();
157
+ }
158
+
159
+ export function planRacingLine(car, orderIndex, race) {
160
+ const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
161
+ const riskTolerance = car.personality?.riskTolerance ?? aggression;
162
+ const trackLimit = race.track.width / 2 - VEHICLE_LIMITS.carWidth * clamp(1.22 - aggression * 0.34, 0.84, 1.22);
163
+ const preferred = Math.sin((car.index / Math.max(1, race.cars.length)) * TWO_PI) * 26;
164
+ const currentOffset = clamp(car.desiredOffset ?? preferred, -trackLimit, trackLimit);
165
+ const ahead = race.orderedCars[orderIndex - 1];
166
+ const traffic = scanNearbyTraffic(car, race);
167
+
168
+ let bestOffset = currentOffset;
169
+ let bestScore = -Infinity;
170
+
171
+ LANE_OFFSETS.forEach((rawOffset) => {
172
+ const offset = clamp(rawOffset, -trackLimit, trackLimit);
173
+ const edgeClearance = trackLimit - Math.abs(offset);
174
+ let score = 80;
175
+ score -= Math.abs(offset - preferred) * 0.18;
176
+ score -= Math.abs(offset - currentOffset) * (0.09 - aggression * 0.025);
177
+ score -= Math.max(0, 18 - edgeClearance) * (0.92 - aggression * 0.42);
178
+
179
+ traffic.forEach((entry) => {
180
+ const lateral = Math.abs(entry.signedOffset - offset);
181
+ if (entry.gap > 0 && entry.gap < 260) {
182
+ const overlapRisk = clamp(58 - lateral, 0, 58);
183
+ score -= overlapRisk * (260 - entry.gap) * 0.038 * (1 - riskTolerance * 0.34);
184
+ if (entry.gap < 190 + aggression * 70 && lateral > 34 - aggression * 8) {
185
+ score += Math.min(30 + aggression * 18, lateral - (28 - aggression * 6)) * (0.62 + aggression * 0.82);
186
+ }
187
+ } else if (entry.gap <= 0 && entry.gap > -74) {
188
+ const sideOverlapRisk = clamp(52 - lateral, 0, 52);
189
+ score -= sideOverlapRisk * (74 + entry.gap) * 0.052 * (1 - riskTolerance * 0.22);
190
+ }
191
+ });
192
+
193
+ if (ahead && car.gapAhead < 230) {
194
+ const side = car.index % 2 === 0 ? -1 : 1;
195
+ const passSide = clamp(
196
+ (ahead.trackState.signedOffset * -0.65) + side * (48 + aggression * 42),
197
+ -trackLimit,
198
+ trackLimit,
199
+ );
200
+ score -= Math.abs(offset - passSide) * (0.09 + aggression * 0.08);
201
+ }
202
+
203
+ if (score > bestScore) {
204
+ bestScore = score;
205
+ bestOffset = offset;
206
+ }
207
+ });
208
+
209
+ const laneChangeRate = 0.72 + car.racecraft * 0.3 + aggression * 0.58;
210
+ car.desiredOffset = currentOffset + clamp(bestOffset - currentOffset, -laneChangeRate, laneChangeRate);
211
+
212
+ return {
213
+ offset: car.desiredOffset,
214
+ sameLaneAhead: findLaneTrafficAhead(traffic, car.desiredOffset, 230),
215
+ sideRisk: findLaneTrafficBeside(traffic, car.desiredOffset),
216
+ };
217
+ }
218
+
219
+ function scanNearbyTraffic(car, race) {
220
+ return race.cars
221
+ .filter((other) => other !== car)
222
+ .map((other) => ({
223
+ car: other,
224
+ gap: other.raceDistance - car.raceDistance,
225
+ signedOffset: other.trackState?.signedOffset ?? 0,
226
+ }))
227
+ .filter((entry) => entry.gap > -82 && entry.gap < 280);
228
+ }
229
+
230
+ function findLaneTrafficAhead(traffic, offset, maxDistance) {
231
+ let closest = null;
232
+
233
+ traffic.forEach((entry) => {
234
+ if (entry.gap <= 0 || entry.gap > maxDistance) return;
235
+ if (Math.abs(entry.signedOffset - offset) > 42) return;
236
+ if (!closest || entry.gap < closest.gap) closest = entry;
237
+ });
238
+
239
+ return closest;
240
+ }
241
+
242
+ function findLaneTrafficBeside(traffic, offset) {
243
+ let closest = null;
244
+
245
+ traffic.forEach((entry) => {
246
+ const lateral = Math.abs(entry.signedOffset - offset);
247
+ if (Math.abs(entry.gap) > 58 || lateral > 44) return;
248
+ const risk = (58 - Math.abs(entry.gap)) + (44 - lateral);
249
+ if (!closest || risk > closest.risk) closest = { ...entry, lateral, risk };
250
+ });
251
+
252
+ return closest;
253
+ }